From eee809a62949442319115f27c8f7fbd93435d8ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Fri, 25 Oct 2024 14:05:08 +0200 Subject: [PATCH] chore: various cleanups (#965) --- CONTRIBUTING.md | 2 +- bolt_transport.go | 31 +++++++++++++++++++++++-------- bolt_transport_test.go | 4 ++-- config.go | 2 +- demo.go | 2 +- docs/UPGRADE.md | 12 ++++++------ docs/getting-started.md | 5 +++-- docs/hub/cloud.md | 2 +- docs/hub/cluster.md | 40 ++++++++++++++++++++-------------------- docs/hub/config.md | 4 ++-- docs/hub/install.md | 2 +- docs/hub/traefik.md | 2 +- docs/spec/faq.md | 5 +++-- event.go | 6 +++--- hub.go | 4 ++-- jwt_keyfunc_test.go | 2 +- metrics.go | 2 +- publish.go | 7 ++++++- spec/mercure.md | 28 ++++++++++++++-------------- subscribe.go | 25 +++++++++++++++++++------ subscribe_test.go | 4 ++-- subscriber.go | 2 +- subscriber_bench_test.go | 2 +- subscriber_list_test.go | 2 +- subscription.go | 16 ++++++++++++---- topic_selector.go | 4 ++-- topic_selector_lru.go | 2 +- update.go | 2 +- 28 files changed, 132 insertions(+), 89 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2bc2dd93..dc23e4e1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -84,7 +84,7 @@ To debug potential deadlocks: ## Spec The spec is written in Markdown, compatible with [Mmark](https://mmark.miek.nl/). -It is then converted in the [the "xml2rfc" Version 3 Vocabulary](https://tools.ietf.org/html/rfc7991). +It is then converted in [the "xml2rfc" Version 3 Vocabulary](https://tools.ietf.org/html/rfc7991). To contribute to the protocol itself: diff --git a/bolt_transport.go b/bolt_transport.go index 4df7254c..0843eb64 100644 --- a/bolt_transport.go +++ b/bolt_transport.go @@ -98,6 +98,11 @@ func NewBoltTransport( return nil, &TransportError{err: err} } + lastEventID, err := getDBLastEventID(db, bucketName) + if err != nil { + return nil, &TransportError{err: err} + } + return &BoltTransport{ logger: logger, db: db, @@ -107,13 +112,13 @@ func NewBoltTransport( subscribers: NewSubscriberList(1e5), closed: make(chan struct{}), - lastEventID: getDBLastEventID(db, bucketName), + lastEventID: lastEventID, }, nil } -func getDBLastEventID(db *bolt.DB, bucketName string) string { +func getDBLastEventID(db *bolt.DB, bucketName string) (string, error) { lastEventID := EarliestLastEventID - db.View(func(tx *bolt.Tx) error { + err := db.View(func(tx *bolt.Tx) error { b := tx.Bucket([]byte(bucketName)) if b == nil { return nil // No data @@ -125,8 +130,11 @@ func getDBLastEventID(db *bolt.DB, bucketName string) string { return nil }) + if err != nil { + return "", fmt.Errorf("unable to get lastEventID from BoltDB: %w", err) + } - return lastEventID + return lastEventID, nil } // Dispatch dispatches an update to all subscribers and persists it in Bolt DB. @@ -176,7 +184,7 @@ func (t *BoltTransport) persist(updateID string, updateJSON []byte) error { // The sequence value is prepended to the update id to create an ordered list key := bytes.Join([][]byte{prefix, []byte(updateID)}, []byte{}) - // The DB is append only + // The DB is append-only bucket.FillPercent = 1 t.lastSeq = seq @@ -207,7 +215,9 @@ func (t *BoltTransport) AddSubscriber(s *Subscriber) error { t.Unlock() if s.RequestLastEventID != "" { - t.dispatchHistory(s, toSeq) + if err := t.dispatchHistory(s, toSeq); err != nil { + return err + } } s.Ready() @@ -239,8 +249,8 @@ func (t *BoltTransport) GetSubscribers() (string, []*Subscriber, error) { } //nolint:gocognit -func (t *BoltTransport) dispatchHistory(s *Subscriber, toSeq uint64) { - t.db.View(func(tx *bolt.Tx) error { +func (t *BoltTransport) dispatchHistory(s *Subscriber, toSeq uint64) error { + err := t.db.View(func(tx *bolt.Tx) error { b := tx.Bucket([]byte(t.bucketName)) if b == nil { s.HistoryDispatched(EarliestLastEventID) @@ -286,6 +296,11 @@ func (t *BoltTransport) dispatchHistory(s *Subscriber, toSeq uint64) { return nil }) + if err != nil { + return fmt.Errorf("unable to retrieve history from BoltDB: %w", err) + } + + return nil } // Close closes the Transport. diff --git a/bolt_transport_test.go b/bolt_transport_test.go index 0e31a364..ba78612c 100644 --- a/bolt_transport_test.go +++ b/bolt_transport_test.go @@ -203,7 +203,7 @@ func TestNewBoltTransport(t *testing.T) { u, _ = url.Parse("bolt:///test.db") _, err = DeprecatedNewBoltTransport(u, zap.NewNop()) - // The exact error message depends of the OS + // The exact error message depends on the OS assert.Contains(t, err.Error(), "open /test.db:") u, _ = url.Parse("bolt://test.db?cleanup_frequency=invalid") @@ -351,7 +351,7 @@ func TestBoltLastEventID(t *testing.T) { // The sequence value is prepended to the update id to create an ordered list key := bytes.Join([][]byte{prefix, []byte("foo")}, []byte{}) - // The DB is append only + // The DB is append-only bucket.FillPercent = 1 return bucket.Put(key, []byte("invalid")) diff --git a/config.go b/config.go index c90a737f..088cf15b 100644 --- a/config.go +++ b/config.go @@ -244,7 +244,7 @@ func NewHubFromViper(v *viper.Viper) (*Hub, error) { //nolint:funlen,gocognit return h, err } -// Start is an helper method to start the Mercure Hub. +// Start is a helper method to start the Mercure Hub. // // Deprecated: use the Caddy server module or the standalone library instead. func Start() { diff --git a/demo.go b/demo.go index 4789eb62..e7ae2816 100644 --- a/demo.go +++ b/demo.go @@ -22,7 +22,7 @@ var uiContent embed.FS // The Content-Type header will automatically be set according to the URL's extension. func (h *Hub) Demo(w http.ResponseWriter, r *http.Request) { // JSON-LD is the preferred format - mime.AddExtensionType(".jsonld", "application/ld+json") + _ = mime.AddExtensionType(".jsonld", "application/ld+json") url := r.URL.String() mimeType := mime.TypeByExtension(filepath.Ext(r.URL.Path)) diff --git a/docs/UPGRADE.md b/docs/UPGRADE.md index f7582e57..cbd940e6 100644 --- a/docs/UPGRADE.md +++ b/docs/UPGRADE.md @@ -5,11 +5,11 @@ The `MERCURE_TRANSPORT_URL` environment variable and the `transport_url` directive have been deprecated. Use the new `transport` directive instead. -The `MERCURE_TRANSPORT_URL` environement variable has been removed from the default `Caddyfile`s, +The `MERCURE_TRANSPORT_URL` environment variable has been removed from the default `Caddyfile`s, but a backward compatibility layer is provided. If both the `transport` and the deprecated `transport_url` are not explicitly set -and the `MERCURE_TRANSPORT_URL` environement variable is set, the `transport_url` will be automatically populated. +and the `MERCURE_TRANSPORT_URL` environment variable is set, the `transport_url` will be automatically populated. To disable this behavior, unset `MERCURE_TRANSPORT_URL` or set it to an empty string. Before: @@ -30,12 +30,12 @@ To configure the transport using an environment variable, append the `transport To prevent security issues, be sure to not pass credentials such as API tokens or password in `MERCURE_EXTRA_DIRECTIVES` (ex: when using transports [provided by the paid version](hub/cluster.md) such as Redis). -To pass credentials security, create a custom `Caddyfile` an use the `{env.MY_ENV_VAR}` syntax, which is interpreted at runtime. +To pass credentials security, create a custom `Caddyfile` and use the `{env.MY_ENV_VAR}` syntax, which is interpreted at runtime. ## 0.16.2 The `Caddyfile.dev` file has been renamed `dev.Caddyfile` to match new Caddy best practices -and prevent "ambigous adapter" issues. +and prevent "ambiguous adapter" issues. ## 0.14.4 @@ -52,9 +52,9 @@ The default dev key changed from `!ChangeMe!` to `!ChangeThisMercureHubJWTSecret ## 0.14 -The query parameter allowing to fetch past events has been renamed `lastEventID`: in your clients, replace all occurences of the `Last-Event-ID` query parameter by `lastEventID`. +The query parameter allowing to fetch past events has been renamed `lastEventID`: in your clients, replace all occurrences of the `Last-Event-ID` query parameter by `lastEventID`. -Publishing public updates in topics not explictly listed in the `mercure.publish` JWT claim isn't supported anymore. +Publishing public updates in topics not explicitly listed in the `mercure.publish` JWT claim isn't supported anymore. To let your publishers publish (public and private updates) in all topics, use the special `*` topic selector: ```patch diff --git a/docs/getting-started.md b/docs/getting-started.md index c8680282..2c34a74c 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -5,7 +5,8 @@ ## Starting the Hub The easiest way to get started is to [install the official Mercure.rocks -Hub](hub/install.md). When it's done, go directly to the next step. There are also other unofficial [libraries implementing Mercure](ecosystem/awesome.md#hubs-and-server-libraries). In the rest of this tutorial, we'll assume that the hub is running on `https://localhost` and that the `JWT_KEY` is `!ChangeThisMercureHubJWTSecretKey!`. +Hub](hub/install.md). When it's done, go directly to the next step. +There are other unofficial [libraries implementing Mercure](ecosystem/awesome.md#hubs-and-server-libraries). In the rest of this tutorial, we'll assume that the hub is running on `https://localhost` and that the `JWT_KEY` is `!ChangeThisMercureHubJWTSecretKey!`. Please note that the hub is entirely optional when using the Mercure protocol. Your app can also implement the Mercure protocol directly. @@ -47,7 +48,7 @@ Optionally, [the authorization mechanism](../spec/mercure.md#authorization) can ## Discovering the Mercure Hub -Also optionally, the hub URL can be automatically discovered: +Also, optionally, the hub URL can be automatically discovered: ![Discovery Schema](../spec/discovery.png) diff --git a/docs/hub/cloud.md b/docs/hub/cloud.md index 13555666..1c1584de 100644 --- a/docs/hub/cloud.md +++ b/docs/hub/cloud.md @@ -11,7 +11,7 @@ The Cloud service is built on top of the free and open-source hub and helps fund Purchase your managed Mercure.rocks hub [directly online](https://mercure.rocks/pricing)! -After purchase, your hub will be instantly provisionned and available under a `mercure.rocks` subdomain. A TLS certificate is also automatically created. +After purchase, your hub will be instantly provisioned and available under a `mercure.rocks` subdomain. A TLS certificate is also automatically created. You'll have access to an administration interface allowing you to: diff --git a/docs/hub/cluster.md b/docs/hub/cluster.md index 8e64628f..760c253c 100644 --- a/docs/hub/cluster.md +++ b/docs/hub/cluster.md @@ -22,7 +22,7 @@ To use it, just configure your custom domain name (if any) and your secret JWT k ## High Availability On Premise Version -The High Availability On Premise Mercure.rocks Hub is a drop-in replacement for the free Hub which allows to spread the load across as many servers as you want. It is designed to run on your own servers and is fault tolerant by default. +The High Availability On Premise Mercure.rocks Hub is a drop-in replacement for the free Hub which allows to spread the load across as many servers as you want. It is designed to run on your own servers and is fault-tolerant by default. The HA version is shipped with transports having node synchronization capabilities. These transports can rely on: @@ -59,7 +59,7 @@ If you use the Helm chart, set the `license` value and change the Docker image t ### Transports -The clustered mode of the Mercure.rocks Hub requires a transport to work. +The clustered mode of the Mercure.rocks Hub requires transport to work. Supported transports are Apache Pulsar, Apache Kafka and PostgreSQL. #### Redis Transport @@ -71,10 +71,10 @@ To install Redis, [read the documentation](https://redis.io/topics/quickstart). Most Cloud Computing platforms also provide managed versions of Redis. | Feature | Supported | -| --------------- | --------- | -| History | ✅ | -| Presence API | ✅ | -| Custom event ID | ✅ | +|-----------------|-----------| +| History | ✅ | +| Presence API | ✅ | +| Custom event ID | ✅ | ##### Configuration @@ -90,10 +90,10 @@ To use Redis, the `MERCURE_TRANSPORT_URL` environment variable must be set like The following options can be passed as query parameters of the URL set in `transport_url`: -| Parameter | Description | Default | -| ---------------- | ------------------------------------------------------------------------------------------------------ | ------- | -| `tls` | set to `1` to enable TLS support | `0` | -| `max_len_approx` | the approximative maximum number of messages to store in the history, set to `0` to store all messages | `0` | +| Parameter | Description | Default | +|------------------|------------------------------------------------------------------------------------------------------|---------| +| `tls` | set to `1` to enable TLS support | `0` | +| `max_len_approx` | the approximate maximum number of messages to store in the history, set to `0` to store all messages | `0` | #### PostgreSQL Transport @@ -103,8 +103,8 @@ It is mostly useful when using the Mercure.rocks Hub as an event store, or as a To install PostgreSQL, [read the documentation](https://www.postgresql.org/docs/12/tutorial-install.html). Most Cloud Computing platforms also provide managed versions of PostgreSQL. -| Feature | Supported | -| --------------- | ------------ | +| Feature | Supported | +|-----------------|-------------| | History | ✅ | | Presence API | ❌ (planned) | | Custom event ID | ✅ | @@ -138,10 +138,10 @@ The Mercure.rocks hub has been tested with: - Heroku Kafka | Feature | Supported | -| --------------- | --------- | -| History | ✅ | -| Presence API | ❌ | -| Custom event ID | ✅ | +|-----------------|-----------| +| History | ✅ | +| Presence API | ❌ | +| Custom event ID | ✅ | ##### Kafka Configuration @@ -158,7 +158,7 @@ To use Kafka, the `MERCURE_TRANSPORT_URL` environment variable must be set like The following options can be passed as query parameters of the URL set in `transport_url`: | Parameter | Description | -| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | +|------------------|---------------------------------------------------------------------------------------------------------------------------------------------| | `addr` | addresses of the Kafka servers, you can pass several `addr` parameters to use several Kafka servers (ex: `addr=host1:9092&addr=host2:9092`) | | `topic` | the name of the Kafka topic to use (ex: `topic=mercure-ha`), **all Mercure.rocks hub instances must use the same topic** | | `consumer_group` | consumer group, **must be different for every instance of the Mercure.rocks hub** (ex: `consumer_group=`) | @@ -172,8 +172,8 @@ The Pulsar transport should only be used when Pulsar is already part of your sta To install Apache Pulsar, [read the documentation](https://pulsar.apache.org/docs/en/standalone/). -| Feature | Supported | -| --------------- | ------------ | +| Feature | Supported | +|-----------------|-------------| | History | ✅ | | Presence API | ❌ | | Custom event ID | ❌ (planned) | @@ -193,7 +193,7 @@ To use Pulsar, the `MERCURE_TRANSPORT_URL` environment variable must be set like The following options can be passed as query parameters of the URL set in `transport_url`: | Parameters | Description | | -| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | --- | +|---------------------|--------------------------------------------------------------------------------------------------------------------------------------------------|-----| | `topic` | the name of the Pulsar topic to use (ex: `topic=mercure`), **all Mercure.rocks hub instances must use the same topic** | | | `subscription_name` | the subscription name for this node, **must be different for every instance of the Mercure.rocks hub** (ex: `subscription_name=`) | | diff --git a/docs/hub/config.md b/docs/hub/config.md index b7573654..d97f59f3 100644 --- a/docs/hub/config.md +++ b/docs/hub/config.md @@ -98,12 +98,12 @@ services: ## JWT Verification -JWT can validated using HMAC and RSA algorithms. +JWT can be validated using HMAC and RSA algorithms. In addition, it's possible to use JSON Web Key Sets (JWK Sets) (usually provided by OAuth and OIDC providers such as Keycloak or Amazon Cognito) to validate the keys. When using RSA public keys for verification make sure the key is properly formatted and make sure to set the correct algorithm as second parameter of the `publisher_jwt` or `subscriber_jwt` directives (for example `RS256`). -Here is an example of how to use environments variables with a RSA public key. +Here is an example of how to use environments variables with an RSA public key. Generate keys (if you don't already have them): diff --git a/docs/hub/install.md b/docs/hub/install.md index 852dc91c..e1b292ec 100644 --- a/docs/hub/install.md +++ b/docs/hub/install.md @@ -81,7 +81,7 @@ docker run \ dunglas/mercure ``` -HTTPS support is automatically enabled. If you run the Mercure hub behind a reverse proxy [such as NGINX](cookbooks.md#using-nginx-as-an-http-2-reverse-proxy-in-front-of-the-hub), you usually want to use unencrypted HTTP. +HTTPS support is automatically enabled. If you run the Mercure hub behind a reverse proxy [such as NGINX](nginx.md), you usually want to use unencrypted HTTP. This can be done like that: ```console diff --git a/docs/hub/traefik.md b/docs/hub/traefik.md index 9cbea484..59858abc 100644 --- a/docs/hub/traefik.md +++ b/docs/hub/traefik.md @@ -1,6 +1,6 @@ # Use the Mercure.rocks Hub with Traefik Proxy -[Traefik](https://doc.traefik.io/traefik/) is a free and open source _edge router_ poular in the Docker and Kubernetes ecosystems. +[Traefik](https://doc.traefik.io/traefik/) is a free and open source _edge router_ popular in the Docker and Kubernetes ecosystems. The following Docker Compose file exposes a Mercure.rocks hub through Traefik: diff --git a/docs/spec/faq.md b/docs/spec/faq.md index b652f2b9..74251a3d 100644 --- a/docs/spec/faq.md +++ b/docs/spec/faq.md @@ -5,7 +5,8 @@ In a nutshell [the WebSocket API](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) is low level, Mercure is a high level. Mercure provides convenient built-in features such as authorization, re-connection, state reconciliation and a presence API ; while with WebSockets, you need to implement them yourself. -Also WebSockets [are not designed to leverage HTTP/2+](https://www.infoq.com/articles/websocket-and-http2-coexist) and are known to be [hard to secure](https://gravitational.com/blog/kubernetes-websocket-upgrade-security-vulnerability/). On the other hand Mercure relies on plain HTTP connections and benefits from the performance an security improvement built-in the latest versions of this protocol. +Also, WebSockets [are not designed to leverage HTTP/2+](https://www.infoq.com/articles/websocket-and-http2-coexist) and are known to be [hard to secure](https://gravitational.com/blog/kubernetes-websocket-upgrade-security-vulnerability/). +On the other hand Mercure relies on plain HTTP connections and benefits from the performance and security improvement built-in the latest versions of this protocol. HTTP/2 connections are multiplexed and bidirectional by default (it was not the case of HTTP/1). When using Mercure over a h2 connection (recommended), your app can receive data through Server-Sent Events, and send data to the server with regular `POST` (or `PUT`/`PATCH`/`DELETE`) requests, with no overhead. @@ -34,7 +35,7 @@ In summary, use the Push API to send notifications to offline users (that will b When using HTTP/2+ ([the default for almost all users](https://caniuse.com/#feat=http2)), the maximum number of simultaneous HTTP **streams** is negotiated between the server and the client (it defaults to 100). When using HTTP 1.1, this limit is of 6. -By using template selectors and by passing several `topic` parameters, it's possible to subscribe to an unlimited of topics using a single HTTP connection. +By using template selectors and by passing several `topic` parameters, it's possible to subscribe to an unlimited number of topics using a single HTTP connection. ## How to Use Mercure with GraphQL? diff --git a/event.go b/event.go index cbcadfc8..1140c6db 100644 --- a/event.go +++ b/event.go @@ -26,14 +26,14 @@ func (e *Event) String() string { var b strings.Builder if e.Type != "" { - fmt.Fprintf(&b, "event: %s\n", e.Type) + _, _ = fmt.Fprintf(&b, "event: %s\n", e.Type) } if e.Retry != 0 { - fmt.Fprintf(&b, "retry: %d\n", e.Retry) + _, _ = fmt.Fprintf(&b, "retry: %d\n", e.Retry) } r := strings.NewReplacer("\r\n", "\ndata: ", "\r", "\ndata: ", "\n", "\ndata: ") - fmt.Fprintf(&b, "id: %s\ndata: %s\n\n", e.ID, r.Replace(e.Data)) + _, _ = fmt.Fprintf(&b, "id: %s\ndata: %s\n\n", e.ID, r.Replace(e.Data)) return b.String() } diff --git a/hub.go b/hub.go index b35e6d5a..18ee3a59 100644 --- a/hub.go +++ b/hub.go @@ -1,5 +1,5 @@ -// Package mercure helps implementing the Mercure protocol (https://mercure.rocks) in Go projects. -// It provides an implementation of a Mercure hub as a HTTP handler. +// Package mercure helps implement the Mercure protocol (https://mercure.rocks) in Go projects. +// It provides an implementation of a Mercure hub as an HTTP handler. package mercure import ( diff --git a/jwt_keyfunc_test.go b/jwt_keyfunc_test.go index 044d43eb..02493e86 100644 --- a/jwt_keyfunc_test.go +++ b/jwt_keyfunc_test.go @@ -7,7 +7,7 @@ import ( ) func TestCreateJWTKeyfunc(t *testing.T) { - f, err := createJWTKeyfunc(([]byte{}), "invalid") + f, err := createJWTKeyfunc([]byte{}, "invalid") require.Error(t, err) require.Nil(t, f) } diff --git a/metrics.go b/metrics.go index e73bc9d7..8f4dc97e 100644 --- a/metrics.go +++ b/metrics.go @@ -35,7 +35,7 @@ type PrometheusMetrics struct { } // NewPrometheusMetrics creates a Prometheus metrics collector. -// This method must be called only one time or it will panic. +// This method must be called only one time, or it will panic. func NewPrometheusMetrics(registry prometheus.Registerer) *PrometheusMetrics { if registry == nil { registry = prometheus.NewRegistry() diff --git a/publish.go b/publish.go index b5efcc6d..11f2c150 100644 --- a/publish.go +++ b/publish.go @@ -75,7 +75,12 @@ func (h *Hub) PublishHandler(w http.ResponseWriter, r *http.Request) { panic(err) } - io.WriteString(w, u.ID) + if _, err := io.WriteString(w, u.ID); err != nil { + if c := h.logger.Check(zap.WarnLevel, "Failed to write publish response"); c != nil { + c.Write(zap.Object("update", u), zap.Error(err), zap.String("remote_addr", r.RemoteAddr)) + } + } + if c := h.logger.Check(zap.DebugLevel, "Update published"); c != nil { c.Write(zap.Object("update", u), zap.String("remote_addr", r.RemoteAddr)) } diff --git a/spec/mercure.md b/spec/mercure.md index 0b1b3ca9..3c026b59 100644 --- a/spec/mercure.md +++ b/spec/mercure.md @@ -51,11 +51,11 @@ interpreted as described in [@!RFC2119]. private, consequently, it must be dispatched only to subscribers allowed to receive it. * Topic selector: An expression matching one or several topics. * Publisher: An owner of a topic. Notifies the hub when the topic feed has been updated. As in - almost all pubsub systems, the publisher is unaware of the subscribers, if any. Other pubsub - systems might call the publisher the "source". Typically a site or a web API, but can also be + almost all pub-sub systems, the publisher is unaware of the subscribers, if any. Other pub-sub + systems might call the publisher the "source". Typically, a site or a web API, but can also be a web browser. * Subscriber: A client application that subscribes to real-time updates of topics using topic - selectors. Typically a web or a mobile application, but can also be a server. + selectors. Typically, a web or a mobile application, but can also be a server. * Subscription: A topic selector used by a subscriber to receive updates. A single subscriber can have several subscriptions, when it provides several topic selectors. * Hub: A server that handles subscription requests and distributes the content to subscribers when @@ -73,7 +73,7 @@ The URL of the hub **MUST** be the "well-known" [@!RFC5785] fixed path `/.well-k If the publisher is a server, it **SHOULD** advertise the URL of one or more hubs to the subscriber, allowing it to receive live updates when topics are updated. If more than one hub URL is specified, -the publisher **MUST** notifies each hub, so the subscriber **MAY** subscribe to one or more of +the publisher **MUST** notify each hub, so the subscriber **MAY** subscribe to one or more of them. Note: Publishers may wish to advertise and publish to more than one hub for fault tolerance and @@ -201,7 +201,7 @@ To determine if a string matches a selector, the following steps must be followe characteristic allows to compare a URI Template with another one. 3. If the topic selector is a valid URI Template, and that the string matches this URI Template, the string matches the selector. -4. Otherwise the string does not match the selector. +4. Otherwise, the string does not match the selector. # Subscription @@ -295,7 +295,7 @@ The request **MUST** be encoded using the `application/x-www-form-urlencoded` fo but it **CAN** contain any value including an empty string. * `id` (optional): the topic's revision identifier: it will be used as the SSE's `id` property. The provided ID **MUST NOT** start with the `#` character. The provided ID **SHOULD** be a valid - IRI. If omitted, the hub **MUST** generate a valid IRI [@!RFC3987]. An UUID [@RFC4122] or a + IRI. If omitted, the hub **MUST** generate a valid IRI [@!RFC3987]. A UUID [@RFC4122] or a [DID](https://www.w3.org/TR/did-core/) **MAY** be used. Alternatively the hub **MAY** generate a relative URI composed of a fragment (starting with `#`). This is convenient to return an offset or a sequence that is unique for this hub. Even if provided, the hub **MAY** ignore the ID @@ -415,7 +415,7 @@ array of topic selectors. See (#topic-selectors). If `mercure.publish` is not defined, or contains an empty array, then the publisher **MUST NOT** be authorized to dispatch any update. -Otherwise, the hub **MUST** check that every topics of the update to dispatch matches at least one +Otherwise, the hub **MUST** check that every topic of the update to dispatch matches at least one of the topic selectors contained in `mercure.publish`. If the publisher is not authorized for all the topics of an update, the hub **MUST NOT** dispatch @@ -488,8 +488,8 @@ The protocol allows to reconciliate states after a reconnection. It can also be [Event store](https://en.wikipedia.org/wiki/Event_store). To allow re-establishment in case of connection lost, events dispatched by the hub **MUST** include -an `id` property. The value contained in this `id` property **SHOULD** be an IRI [@!RFC3987]. An -UUID [@RFC4122] or a [DID](https://www.w3.org/TR/did-core/) **MAY** be used. +an `id` property. The value contained in this `id` property **SHOULD** be an IRI [@!RFC3987]. +A UUID [@RFC4122] or a [DID](https://www.w3.org/TR/did-core/) **MAY** be used. According to the server-sent events specification, in case of connection lost the subscriber will try to automatically re-connect. During the @@ -529,7 +529,7 @@ hub stores only a limited number of events in its history). In some cases (for i partial updates in the JSON Patch [@RFC6902] format, or when using the hub as an event store), updates lost can cause data lost. -To detect if a data lost ocurred, the subscriber **CAN** compare the value of the `Last-Event-ID` +To detect if a data lost occurred, the subscriber **CAN** compare the value of the `Last-Event-ID` response HTTP header with the last event ID it requested. In case of data lost, the subscriber **SHOULD** re-fetch the original topic. @@ -554,14 +554,14 @@ subscription is created or terminated. The topic of these updates **MUST** be an expansion of `/.well-known/mercure/subscriptions/{topic}/{subscriber}`. `{topic}` is the topic selector used for -this subscription and `{subscriber}` is an unique identifier for the subscriber. +this subscription and `{subscriber}` is a unique identifier for the subscriber. Note: Because it is recommended to use URI Templates and IRIs for the `{topic}` and `{subscriber}` variables, values will usually contain the `:`, `/`, `{` and `}` characters. Per [@!RFC6570], these characters are reserved. They **MUST** be percent encoded during the expansion process. If a subscriber has several subscriptions, it **SHOULD** be identified by the same -identifier. `{subscriber}` **SHOULD** be an IRI [@!RFC3987]. An UUID [@RFC4122] or a +identifier. `{subscriber}` **SHOULD** be an IRI [@!RFC3987]. A UUID [@RFC4122] or a [DID](https://www.w3.org/TR/did-core/) **MAY** be used. The content of the update **MUST** be a JSON-LD [@!W3C.REC-json-ld-20140116] document containing at @@ -775,7 +775,7 @@ The JSON-LD context available at `https://mercure.rocks` is the following: # Encryption -Using HTTPS does not prevent the hub from accessing the update's content. Depending of the intended +Using HTTPS does not prevent the hub from accessing the update's content. Depending on the intended privacy of information contained in the update, it **MAY** be necessary to prevent eavesdropping by the hub. @@ -1320,7 +1320,7 @@ Other implementations can be found on GitHub: