Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 22 additions & 4 deletions Caddyfile
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,26 @@ handle / {
}

mercure {
publisher_jwt key
subscriber_jwt key
anonymous true
ui true
anonymous

# Mercure 1.0 modern mode: access tokens are RFC 9068 JWTs bound to an issuer
# and to this hub's resource identifier. An identifier ending in
# /.well-known/mercure doubles as the base URL relative topics resolve
# against, which is what our rel="self" link values are.
resource_identifier https://localhost/.well-known/mercure
issuer https://localhost {
publisher {
jwt key HS256
}
subscriber {
jwt key HS256
}
}

# INSECURE: dev only. Serves the hub's debugger UI at
# /.well-known/mercure/debug/. This is the 1.0 name of the former "ui"
# directive. Not "playground": that one also forces cors_origins and
# publish_origins to "*" and drops the cookie name prefix, none of which this
# same-origin test server needs.
debugger
}
8 changes: 5 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
FROM caddy:2.8-builder AS builder
FROM caddy:2.11.4-builder AS builder

RUN xcaddy build --with github.com/dunglas/mercure/caddy --with github.com/dunglas/vulcain/caddy
RUN xcaddy build \
--with github.com/dunglas/mercure/caddy@v1.0.0-alpha.3 \
--with github.com/dunglas/vulcain/caddy@v1.4.3

FROM caddy:2.8 AS app_server
FROM caddy:2.11.4 AS app_server

COPY --from=builder /usr/bin/caddy /usr/bin/caddy
4 changes: 4 additions & 0 deletions packages/mercure/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ Available options:

This can be used in conjunction with [@api-platform/ld](/linked-data) as the `fetchFn`.

### Resuming after a disconnection

The id of the last update received is kept and sent back when a subscription is rebuilt, as both the `last_event_id` query parameter and the `Last-Event-Id` request header. The query parameter is what makes this work with a native `EventSource`, which cannot set headers.

### Examples

See [our Tanstack query example](https://github.com/api-platform/esa/blob/main/tests-server/mercure.html) or the source code of our [home page](https://github.com/api-platform/esa/blob/main/api/public/index.js).
11 changes: 10 additions & 1 deletion packages/mercure/mercure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,20 @@ function listen<T>(mercureUrl: string, options: Options<T> = {}) {

const url = new URL(mercureUrl)
topics.forEach((_, topic) => {
url.searchParams.append('topic', topic)
url.searchParams.append('match', topic)
})

const headers: {[key: string]: string} = options.headers || {}
if (lastEventId) {
// Every call here opens a fresh connection, so the cursor has to travel
// with the request. A native EventSource cannot set headers, hence the
// query parameter: the hub takes the union of the query and body
// components, and last_event_id is single-valued. The header is sent too,
// for EventSource implementations that support it and for the automatic
// reconnections they perform on their own.
url.searchParams.append('last_event_id', lastEventId)
// The request header keeps its name in 1.0; only the hub's response header
// was renamed to Mercure-Last-Event-ID.
headers['Last-Event-Id'] = lastEventId
}

Expand Down
2 changes: 1 addition & 1 deletion tests-server/github.html
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@
// You can pass formData as a fetch body directly:
fetch(form.action, { method: form.method, body: body.toString(), headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtZXJjdXJlIjp7InN1YnNjcmliZSI6WyIqIl0sInB1Ymxpc2giOlsiKiJdfX0.NXhzhXJ8VTxiRRW3pAB4EgP7s_guZeibwzAGw3wZ_KY'
'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6ImF0K2p3dCJ9.eyJpc3MiOiJodHRwczovL2xvY2FsaG9zdCIsImF1ZCI6Imh0dHBzOi8vbG9jYWxob3N0Ly53ZWxsLWtub3duL21lcmN1cmUiLCJzdWIiOiJlc2EtdGVzdC1zZXJ2ZXIiLCJjbGllbnRfaWQiOiJlc2EtdGVzdC1zZXJ2ZXIiLCJpYXQiOjE3ODg0MjU1MDUsImV4cCI6NDEwMjQ0NDgwMCwiYXV0aG9yaXphdGlvbl9kZXRhaWxzIjpbeyJ0eXBlIjoiaHR0cHM6Ly9tZXJjdXJlLnJvY2tzL2F1dGhvcml6YXRpb24tZGV0YWlsIiwiYWN0aW9ucyI6WyJwdWJsaXNoIl0sInRvcGljcyI6W3sibWF0Y2giOiIqIn1dfV19.825udJ6bE3p1HpZM_1QZ83DVpOTtuvGE_cQvJby7d0k'
}
});
}
Expand Down
2 changes: 1 addition & 1 deletion tests-server/mercure.html
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
// You can pass formData as a fetch body directly:
fetch(form.action, { method: form.method, body: body.toString(), headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtZXJjdXJlIjp7InN1YnNjcmliZSI6WyIqIl0sInB1Ymxpc2giOlsiKiJdfX0.NXhzhXJ8VTxiRRW3pAB4EgP7s_guZeibwzAGw3wZ_KY'
'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6ImF0K2p3dCJ9.eyJpc3MiOiJodHRwczovL2xvY2FsaG9zdCIsImF1ZCI6Imh0dHBzOi8vbG9jYWxob3N0Ly53ZWxsLWtub3duL21lcmN1cmUiLCJzdWIiOiJlc2EtdGVzdC1zZXJ2ZXIiLCJjbGllbnRfaWQiOiJlc2EtdGVzdC1zZXJ2ZXIiLCJpYXQiOjE3ODg0MjU1MDUsImV4cCI6NDEwMjQ0NDgwMCwiYXV0aG9yaXphdGlvbl9kZXRhaWxzIjpbeyJ0eXBlIjoiaHR0cHM6Ly9tZXJjdXJlLnJvY2tzL2F1dGhvcml6YXRpb24tZGV0YWlsIiwiYWN0aW9ucyI6WyJwdWJsaXNoIl0sInRvcGljcyI6W3sibWF0Y2giOiIqIn1dfV19.825udJ6bE3p1HpZM_1QZ83DVpOTtuvGE_cQvJby7d0k'
}
});
}
Expand Down
6 changes: 3 additions & 3 deletions tests/mercure.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@ test('mercure', async ({ page }) => {
num++
}

if (request.url().startsWith('https://localhost/.well-known/mercure?topic=%2Fauthors%2F1')) {
if (request.url().startsWith('https://localhost/.well-known/mercure?match=%2Fauthors%2F1')) {
requestedMercure = true
}

if (request.url().startsWith('https://localhost/.well-known/mercure?topic=%2Fauthors%2F1&topic=%2Fauthors%2F2')) {
if (request.url().startsWith('https://localhost/.well-known/mercure?match=%2Fauthors%2F1&match=%2Fauthors%2F2')) {
subscribedToBoth = true
}

if (request.url().startsWith('https://localhost/.well-known/mercure?topic=%2Fauthors%2F2')) {
if (request.url().startsWith('https://localhost/.well-known/mercure?match=%2Fauthors%2F2')) {
unsubscribedAuthor1 = true
}
})
Expand Down
Loading