From d1a519ded5b199a04c24ebb9a99cb01179a1f722 Mon Sep 17 00:00:00 2001 From: Melvin Carvalho Date: Sun, 17 May 2026 11:29:20 +0200 Subject: [PATCH 1/3] docs(websocket-notifications): state position + lightly spec solid-0.1 Existing page was a thin tutorial. Two gaps closed: - Position section: JSS picks solid-0.1 as the primary notifications surface. Performance is a non-negotiable design constraint; solid-0.1 is ~10x lighter than WebSocketChannel2023 on every axis (round-trips, frame size, multiplex, client code). Channel-based protocol may be added later as a compat layer, not a replacement. - Light reference spec: frame types (greeting, sub/ack/err, pub, unsub), semantics (multiplex, ACL scope, ordering, no payload, lifecycle), implementation limits, reconnect contract. Adds JS + websocat examples. Closes #17. --- docs/features/websocket-notifications.md | 157 ++++++++++++++++++++--- 1 file changed, 137 insertions(+), 20 deletions(-) diff --git a/docs/features/websocket-notifications.md b/docs/features/websocket-notifications.md index 4fb3dcd..252d5b7 100644 --- a/docs/features/websocket-notifications.md +++ b/docs/features/websocket-notifications.md @@ -1,53 +1,170 @@ --- sidebar_position: 7 title: WebSocket Notifications -description: Real-time updates via solid-0.1 protocol +description: Real-time updates via the solid-0.1 protocol, with JSS as a reference implementation --- # WebSocket Notifications -JSS supports real-time notifications for resource changes. +JSS treats WebSocket notifications as a **first-class, performance-critical** feature. A client subscribes to a resource URL on one socket; the server pushes a tiny text frame whenever that resource changes. The whole exchange is a handful of bytes per event. -## Enable notifications +## Position + +JSS ships the **`solid-0.1`** protocol as the primary notifications surface. This is a deliberate choice — performance is a non-negotiable design constraint for JSS, and `solid-0.1` is roughly **an order of magnitude** lighter than the channel-based W3C Solid Notifications Protocol on every axis that matters: + +| | `solid-0.1` | WebSocketChannel2023 | +|---|---|---| +| Setup round-trips | 1 (open WS) | 3 (discover + subscribe + open) | +| Wire format | Plain text frames | JSON-LD activities with `@context` | +| Bytes per change notification | ~30 (`pub `) | ~300 (full JSON-LD envelope) | +| Multiplex | 1 socket → N subscriptions | 1 channel per subscription | +| Latency to first message | Single-digit ms | Tens of ms (multiple roundtrips) | +| Client code | ~10 LoC | ~50 LoC + JSON-LD library | +| Debugging | `nc`, `websocat`, any TCP tool | JSON-LD-aware tooling required | + +For the kind of work JSS is built for — small Solid-native apps, real-time pod-mediated state, single-board / embedded deployments — those numbers move the design space. The same 50-line PDF reader that does live page-flip via `solid-0.1` would need a JSON-LD parser and per-resource channels under the modern spec. + +We may add `WebSocketChannel2023` later as a **compatibility layer** for SDK-driven clients that require it. We won't deprecate `solid-0.1`. + +## Enable ```bash jss start --notifications ``` -## Discover WebSocket URL +## Discover -Check the `Updates-Via` header: +Every GET response sets an `Updates-Via` header pointing at the server's notification WebSocket: ```bash curl -I http://localhost:3000/alice/public/ # Updates-Via: ws://localhost:3000/.notifications ``` -## Protocol (solid-0.1) +There's one WebSocket per server. Subscribe to as many resources as you want on the one connection. + +## Protocol: `solid-0.1` + +`solid-0.1` originated in SolidOS/mashlib; it was never formally written down. JSS is the most active server implementing it today. The wire format is captured here as a normative reference for anyone writing a client. + +### Frames + +Every frame is a single line of UTF-8 text. The first space-delimited token is the **verb**; the remainder is the **argument**. + +**Server greeting** (sent on connect): + +``` +protocol solid-0.1 +``` + +**Client → server: subscribe** -Compatible with SolidOS: +``` +sub +``` + +Subscribes the current connection to change notifications for ``. The URL must be absolute and within the server's scope. The server runs ACL Read on the resource as the connection's authenticated WebID (or `null` for anonymous); only authorized subscriptions are accepted. + +**Server → client: ack** + +``` +ack +``` + +Confirms a successful subscribe. + +**Server → client: err** ``` -Server: protocol solid-0.1 -Client: sub http://localhost:3000/alice/public/data.json -Server: ack http://localhost:3000/alice/public/data.json -Server: pub http://localhost:3000/alice/public/data.json (on change) +err ``` -## JavaScript Example +Subscribe denied. Defined `` tokens: + +- `forbidden` — ACL denied +- `not_found` — resource doesn't exist (and the policy doesn't auto-create) +- `bad_request` — URL malformed, exceeds length limit, or out of scope + +**Server → client: publish** + +``` +pub +``` + +The resource at `` has changed (PUT, POST, PATCH, DELETE, or container child add/remove). The client should refetch if it needs the new state. JSS does not include the new content in the frame — the notification is a signal, not a payload. + +**Client → server: unsubscribe** *(optional)* + +``` +unsub +``` + +Removes a previously-acknowledged subscription. Servers SHOULD support this; clients SHOULD NOT rely on it (closing the socket is the canonical "stop"). + +### Semantics + +- **Multiplex.** One connection carries many subscriptions. The server tracks a `socket → Set` map; clients track their own. There is no per-resource "channel" concept. +- **Auth scope.** ACL is checked at subscribe time. If the client's permissions later change (e.g. ACL is tightened), in-flight subscriptions MAY continue to receive notifications until the connection closes. Clients should treat published URLs as hints, not authorization grants — refetching the resource will re-check ACL. +- **Ordering.** Notifications for distinct URLs are unordered. Notifications for the same URL are delivered in the order the server applies the change. +- **De-duplication.** None at the protocol level. A rapid burst of writes against the same resource may produce one frame per write; servers MAY coalesce. +- **No payload.** `pub` carries a URL, not the new representation. This is intentional — keeps frames small, avoids invalidating client caches on partial reads, and side-steps content negotiation entirely. +- **Lifecycle.** Subscriptions live until the socket closes or the client sends `unsub`. There is no TTL. + +### Limits (JSS implementation) + +- `MAX_SUBSCRIPTIONS_PER_CONNECTION = 100` +- `MAX_URL_LENGTH = 2048` + +A subscribe that exceeds either limit is rejected with `err bad_request`. These values are policy, not protocol — other servers MAY set them differently. + +### Reconnect + +If the socket drops, the client reconnects and re-subscribes from scratch. There is no resume token. In practice this is invisible: the typical client uses exponential-backoff reconnect (50 ms → 10 s cap) and rebuilds subscriptions in a few milliseconds. + +## JavaScript example ```javascript +const url = 'http://localhost:3000/alice/public/data.json'; const ws = new WebSocket('ws://localhost:3000/.notifications'); -ws.onopen = () => { - ws.send('sub http://localhost:3000/alice/public/data.json'); -}; +ws.onopen = () => ws.send('sub ' + url); -ws.onmessage = (event) => { - if (event.data.startsWith('pub ')) { - const url = event.data.slice(4); - console.log('Resource changed:', url); - // Refetch the resource +ws.onmessage = (e) => { + if (typeof e.data !== 'string') return; + if (e.data.startsWith('pub ')) { + const changed = e.data.slice(4); + console.log('changed:', changed); + // refetch if needed + } else if (e.data.startsWith('ack ')) { + console.log('subscribed:', e.data.slice(4)); + } else if (e.data.startsWith('err ')) { + console.warn('subscribe failed:', e.data.slice(4)); } }; + +ws.onclose = () => { /* reconnect with backoff */ }; ``` + +## Shell example + +```bash +# requires websocat (https://github.com/vi/websocat) +echo "sub http://localhost:3000/alice/public/data.json" \ + | websocat -n1 ws://localhost:3000/.notifications - +``` + +## Relation to the W3C Solid Notifications Protocol + +The W3C [Solid Notifications Protocol](https://solidproject.org/TR/notifications-protocol) defines a more general "channel" abstraction — `WebSocketChannel2023`, `WebhookChannel2023`, `StreamingHTTPChannel2023`, etc. — discovered via a subscription endpoint, negotiated with JSON-LD subscription documents, and instantiated as per-subscription channels. + +JSS does not currently implement these channel types. The notifications surface here is intentionally narrower and lighter. We may add channel-protocol endpoints in future as a compatibility layer for clients that require them; the priorities remain (1) keep `solid-0.1` working, (2) keep it the fastest path for new clients. + +## Why this matters for app design + +Because the protocol is cheap, you can use the pod as a real-time state bus without thinking about cost: + +- Write a tiny JSON-LD doc; subscribe to it on every connected client; one PUT propagates to everyone. +- Treat the doc as a control plane — one byte changed, all subscribers know. +- The transport overhead per event is dominated by the URL, not the payload. + +The [PDF reader](https://github.com/solid-apps/pdf) and [Solid Chat](https://github.com/solid-chat/app) both use this pattern. The PDF reader's "flip the page from a curl command" demo is 50 lines of viewer code precisely because the protocol is small enough that 50 lines is what it takes. From f2263032b2664948faaf15916eb70a25bb042414 Mon Sep 17 00:00:00 2001 From: Melvin Carvalho Date: Sun, 17 May 2026 11:38:31 +0200 Subject: [PATCH 2/3] docs(websocket-notifications): reference solid-spec api-websockets, mark JSS additions solid-0.1 IS specced in solid/solid-spec/api-websockets.md. Earlier draft mistakenly claimed otherwise. Restructure to: - Link the spec as authoritative for sub/pub/protocol greeting - Add the Sec-WebSocket-Protocol header convention - Add container subscription (spec feature, was missing) - Reframe ack/err/unsub as JSS-specific extensions, not part of solid-0.1 - Keep the position section (legacy-first for performance) as is --- docs/features/websocket-notifications.md | 97 ++++++++++++------------ 1 file changed, 50 insertions(+), 47 deletions(-) diff --git a/docs/features/websocket-notifications.md b/docs/features/websocket-notifications.md index 252d5b7..42e7a1c 100644 --- a/docs/features/websocket-notifications.md +++ b/docs/features/websocket-notifications.md @@ -1,16 +1,18 @@ --- sidebar_position: 7 title: WebSocket Notifications -description: Real-time updates via the solid-0.1 protocol, with JSS as a reference implementation +description: Real-time updates via the solid-0.1 protocol — JSS's primary notifications surface --- # WebSocket Notifications JSS treats WebSocket notifications as a **first-class, performance-critical** feature. A client subscribes to a resource URL on one socket; the server pushes a tiny text frame whenever that resource changes. The whole exchange is a handful of bytes per event. +JSS implements the [Solid WebSockets API spec](https://github.com/solid/solid-spec/blob/master/api-websockets.md) (`solid-0.1`). + ## Position -JSS ships the **`solid-0.1`** protocol as the primary notifications surface. This is a deliberate choice — performance is a non-negotiable design constraint for JSS, and `solid-0.1` is roughly **an order of magnitude** lighter than the channel-based W3C Solid Notifications Protocol on every axis that matters: +JSS ships `solid-0.1` as the **primary** notifications surface. This is a deliberate choice — performance is a non-negotiable design constraint for JSS, and `solid-0.1` is roughly **an order of magnitude** lighter than the channel-based W3C Solid Notifications Protocol on every axis that matters: | | `solid-0.1` | WebSocketChannel2023 | |---|---|---| @@ -34,98 +36,99 @@ jss start --notifications ## Discover -Every GET response sets an `Updates-Via` header pointing at the server's notification WebSocket: +Every response sets an `Updates-Via` header pointing at the server's notification WebSocket: ```bash curl -I http://localhost:3000/alice/public/ # Updates-Via: ws://localhost:3000/.notifications ``` -There's one WebSocket per server. Subscribe to as many resources as you want on the one connection. +The spec defines this header on `OPTIONS`; JSS additionally sets it on every GET so clients don't need a separate request. + +There's **one WebSocket per server**. Subscribe to as many resources as you want on the one connection. + +## Connect -## Protocol: `solid-0.1` +Per the spec, clients SHOULD include `solid-0.1` in the `Sec-WebSocket-Protocol` header: -`solid-0.1` originated in SolidOS/mashlib; it was never formally written down. JSS is the most active server implementing it today. The wire format is captured here as a normative reference for anyone writing a client. +```javascript +const ws = new WebSocket('ws://localhost:3000/.notifications', ['solid-0.1']); +``` -### Frames +JSS sends `protocol solid-0.1` as the first frame on every connection. -Every frame is a single line of UTF-8 text. The first space-delimited token is the **verb**; the remainder is the **argument**. +## Subscribe -**Server greeting** (sent on connect): +Once connected, send `sub `: ``` -protocol solid-0.1 +sub http://localhost:3000/alice/public/data.json ``` -**Client → server: subscribe** +Subscribing to a **container** also works: changes to any child resource (POST, PUT, PATCH, DELETE) produce a `pub` for the container URI. This is the canonical pattern for "tell me when anything in this folder changes." ``` -sub +sub http://localhost:3000/alice/public/ ``` -Subscribes the current connection to change notifications for ``. The URL must be absolute and within the server's scope. The server runs ACL Read on the resource as the connection's authenticated WebID (or `null` for anonymous); only authorized subscriptions are accepted. - -**Server → client: ack** +On any change: ``` -ack +pub http://localhost:3000/alice/public/ ``` -Confirms a successful subscribe. +The `pub` frame carries the URI of the changed resource, not its new content. Clients refetch if they need the new state. This is intentional — keeps frames small, avoids invalidating partial caches, and side-steps content negotiation entirely. -**Server → client: err** +## JSS-specific extensions -``` -err -``` +The base spec defines `sub` and `pub`. JSS adds these to make subscription state observable and recoverable: + +### `ack ` + +Sent by the server after a successful subscribe. Lets clients distinguish "subscribed and listening" from "still negotiating." Clients can safely ignore it; tools that want to confirm subscriptions should wait for it. -Subscribe denied. Defined `` tokens: +### `err ` + +Sent when a subscribe is rejected. Defined `` tokens: - `forbidden` — ACL denied -- `not_found` — resource doesn't exist (and the policy doesn't auto-create) +- `not_found` — resource doesn't exist - `bad_request` — URL malformed, exceeds length limit, or out of scope -**Server → client: publish** +### `unsub ` -``` -pub -``` +Client→server: cancel a subscription without closing the socket. Closing the connection is the canonical "stop everything"; `unsub` is for clients that want fine-grained control on a long-lived socket. -The resource at `` has changed (PUT, POST, PATCH, DELETE, or container child add/remove). The client should refetch if it needs the new state. JSS does not include the new content in the frame — the notification is a signal, not a payload. +These extensions are additive — clients that ignore them still get correct `pub` events. -**Client → server: unsubscribe** *(optional)* +## Implementation limits -``` -unsub -``` +JSS enforces: -Removes a previously-acknowledged subscription. Servers SHOULD support this; clients SHOULD NOT rely on it (closing the socket is the canonical "stop"). +- `MAX_SUBSCRIPTIONS_PER_CONNECTION = 100` +- `MAX_URL_LENGTH = 2048` -### Semantics +Subscribes that exceed either are rejected with `err bad_request`. These are policy, not protocol. -- **Multiplex.** One connection carries many subscriptions. The server tracks a `socket → Set` map; clients track their own. There is no per-resource "channel" concept. -- **Auth scope.** ACL is checked at subscribe time. If the client's permissions later change (e.g. ACL is tightened), in-flight subscriptions MAY continue to receive notifications until the connection closes. Clients should treat published URLs as hints, not authorization grants — refetching the resource will re-check ACL. -- **Ordering.** Notifications for distinct URLs are unordered. Notifications for the same URL are delivered in the order the server applies the change. -- **De-duplication.** None at the protocol level. A rapid burst of writes against the same resource may produce one frame per write; servers MAY coalesce. -- **No payload.** `pub` carries a URL, not the new representation. This is intentional — keeps frames small, avoids invalidating client caches on partial reads, and side-steps content negotiation entirely. -- **Lifecycle.** Subscriptions live until the socket closes or the client sends `unsub`. There is no TTL. +## Auth -### Limits (JSS implementation) +ACL `Read` is enforced **at subscribe time** against the connection's authenticated WebID (or `null` for anonymous). Authorized resources stay subscribed for the life of the socket; if the resource's ACL is later tightened, in-flight subscriptions MAY continue receiving notifications until the socket closes. Treat published URLs as **hints**, not authorization grants — refetching the resource re-checks ACL. -- `MAX_SUBSCRIPTIONS_PER_CONNECTION = 100` -- `MAX_URL_LENGTH = 2048` +## Ordering and delivery -A subscribe that exceeds either limit is rejected with `err bad_request`. These values are policy, not protocol — other servers MAY set them differently. +- Notifications for distinct URLs are unordered. +- Notifications for the same URL are delivered in the order the server applies the change. +- No deduplication at the protocol level. Rapid bursts of writes against the same resource may produce one frame per write. Servers MAY coalesce; JSS does not. -### Reconnect +## Reconnect -If the socket drops, the client reconnects and re-subscribes from scratch. There is no resume token. In practice this is invisible: the typical client uses exponential-backoff reconnect (50 ms → 10 s cap) and rebuilds subscriptions in a few milliseconds. +If the socket drops, the client reconnects and re-subscribes from scratch. There is no resume token. Typical clients use exponential backoff (50 ms → 10 s cap); rebuilding subscriptions takes a few milliseconds. ## JavaScript example ```javascript const url = 'http://localhost:3000/alice/public/data.json'; -const ws = new WebSocket('ws://localhost:3000/.notifications'); +const ws = new WebSocket('ws://localhost:3000/.notifications', ['solid-0.1']); ws.onopen = () => ws.send('sub ' + url); From f0a443464eccf5d883efd540f79119a5fe07fc1d Mon Sep 17 00:00:00 2001 From: Melvin Carvalho Date: Sun, 17 May 2026 11:39:58 +0200 Subject: [PATCH 3/3] docs: Sec-WebSocket-Protocol header is optional in practice The header was a later spec addition. SolidOS, mashlib, and our reference clients all omit it. JSS doesn't require it. The first-frame 'protocol solid-0.1' greeting is the practical version handshake. Drop ['solid-0.1'] from both code examples. --- docs/features/websocket-notifications.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/features/websocket-notifications.md b/docs/features/websocket-notifications.md index 42e7a1c..65f06ed 100644 --- a/docs/features/websocket-notifications.md +++ b/docs/features/websocket-notifications.md @@ -49,14 +49,16 @@ There's **one WebSocket per server**. Subscribe to as many resources as you want ## Connect -Per the spec, clients SHOULD include `solid-0.1` in the `Sec-WebSocket-Protocol` header: +Open a WebSocket to the URL from `Updates-Via`: ```javascript -const ws = new WebSocket('ws://localhost:3000/.notifications', ['solid-0.1']); +const ws = new WebSocket('ws://localhost:3000/.notifications'); ``` JSS sends `protocol solid-0.1` as the first frame on every connection. +The spec also mentions a `Sec-WebSocket-Protocol: solid-0.1` header, but this was a later addition and almost no client in the wild sends it. JSS does not require it, and SolidOS / mashlib / our reference clients all omit it. Treat the header as optional; the first-frame greeting is the practical version handshake. + ## Subscribe Once connected, send `sub `: @@ -128,7 +130,7 @@ If the socket drops, the client reconnects and re-subscribes from scratch. There ```javascript const url = 'http://localhost:3000/alice/public/data.json'; -const ws = new WebSocket('ws://localhost:3000/.notifications', ['solid-0.1']); +const ws = new WebSocket('ws://localhost:3000/.notifications'); ws.onopen = () => ws.send('sub ' + url);