Skip to content

Commit f93799e

Browse files
committed
docs: add HTTP push + SSE transport profile
Co-authored-by: lody <agent@lody.ai>
1 parent 206fa93 commit f93799e

2 files changed

Lines changed: 157 additions & 0 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ loro-protocol is a small, transport-agnostic syncing protocol for collaborative
77
- Transports: WebSocket or any integrity-preserving transport (e.g., WebRTC)
88

99
See `protocol.md` for the full wire spec.
10+
See `http-push-sse-protocol.md` for an HTTP push + SSE transport profile (no wire format changes).
1011

1112
## Packages
1213

http-push-sse-protocol.md

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
# HTTP Push + SSE Transport Profile (Loro Syncing Protocol)
2+
3+
Transport profile version: 0. Extends `protocol.md` (Protocol v1) without changing the binary wire format.
4+
5+
This document describes a transport mapping that uses:
6+
7+
- **HTTP push** for client → server frames (request body carries a single protocol frame).
8+
- **SSE (Server‑Sent Events)** for server → client frames (each SSE event carries a single protocol frame).
9+
10+
The goal is to reuse all message types, fragmentation rules, and semantics from `protocol.md`, while allowing two common exchanges to be handled as simple **HTTP request/response** pairs:
11+
12+
- `JoinRequest``JoinResponseOk` / `JoinError`
13+
- `DocUpdate` / `DocUpdateFragment*``Ack`
14+
15+
## Non‑Goals
16+
17+
- Defining specific HTTP routes, parameters, or auth schemes. These are application decisions.
18+
- Providing reliable replay for SSE on reconnect (out of scope). Clients rejoin to recover.
19+
20+
## Terminology
21+
22+
- **Protocol frame**: the exact bytes produced by `encode(message)` from `loro-protocol` (and parsed by `decode(bytes)`).
23+
- **Session key**: an application-defined opaque identifier that binds one client's push requests to its SSE stream.
24+
- It can be carried via cookie, header, query parameter, etc.
25+
- The transport must ensure the same session key is used for both directions.
26+
27+
## Core Invariants (Unchanged from `protocol.md`)
28+
29+
- The binary protocol frame format is unchanged.
30+
- Message types and semantics are unchanged (`JoinRequest`, `DocUpdate`, fragments, `Ack`, `RoomError`, `Leave`, …).
31+
- **Max frame size is still 256 KiB**. Payloads that would exceed the limit MUST use fragmentation.
32+
33+
## Frame Encodings
34+
35+
### HTTP push (client → server)
36+
37+
- Request body: a single protocol frame (binary).
38+
- Recommended headers:
39+
- `Content-Type: application/octet-stream`
40+
- `Content-Length: <frame size>`
41+
- Push responses MAY return a protocol frame (binary) when the exchange is naturally request/response:
42+
- `JoinRequest``JoinResponseOk` / `JoinError`
43+
- `DocUpdate` / fragments completing a batch → `Ack`
44+
45+
For other push messages (e.g., `Leave`), the response body can be empty.
46+
47+
### SSE pull (server → client)
48+
49+
SSE is text-based, so each binary protocol frame is encoded as base64url.
50+
51+
Event format:
52+
53+
```
54+
event: msg
55+
data: <base64url(protocol-frame)>
56+
57+
```
58+
59+
Notes:
60+
61+
- **Exactly one protocol frame per SSE event**.
62+
- `data:` MAY be split across multiple lines; SSE concatenates them with `\n`. Implementations SHOULD either:
63+
- emit a single `data:` line, or
64+
- split into multiple `data:` lines and base64url‑decode after concatenation with `\n` removed.
65+
66+
Base64url:
67+
68+
- RFC 4648 "base64url" (`-` and `_` instead of `+` and `/`).
69+
- Padding (`=`) is OPTIONAL; decoders SHOULD accept both forms.
70+
71+
## Session Binding
72+
73+
Because HTTP requests are stateless and SSE is a long-lived stream, implementations MUST bind them with a session key.
74+
75+
The transport profile does not dictate how, but it MUST satisfy:
76+
77+
- A push request can be associated with exactly one logical session.
78+
- A server can route room broadcasts to all sessions that have joined that room.
79+
80+
Security note: if the session key is sensitive, prefer cookie/header transport over query strings (URLs are often logged).
81+
82+
## Request/Response Simplifications
83+
84+
### Join handshake (`JoinRequest``JoinResponse*`)
85+
86+
Recommended pattern:
87+
88+
1. Client issues a push with a `JoinRequest` frame.
89+
2. Server responds in the same HTTP response body with:
90+
- `JoinResponseOk`, or
91+
- `JoinError`.
92+
3. After `JoinResponseOk`, server MAY send backfills (`DocUpdate` or fragments) over SSE.
93+
94+
Rationale: SSE reconnections can drop in-flight frames; making join responses part of the push response avoids depending on SSE delivery guarantees.
95+
96+
### Client-originated updates (`DocUpdate*``Ack`)
97+
98+
Recommended pattern:
99+
100+
- For `DocUpdate` (single frame):
101+
- Client pushes `DocUpdate`.
102+
- Server MUST respond with `Ack` (binary) in the HTTP response body.
103+
104+
- For fragmented updates (`DocUpdateFragmentHeader` + `DocUpdateFragment`):
105+
- Client pushes the header and fragments.
106+
- Server MUST emit exactly one `Ack` per batch ID, referencing the batch ID.
107+
- It is RECOMMENDED that the `Ack` is returned as the HTTP response to the push that completes the batch
108+
(typically the final fragment).
109+
- Server MAY return an early non‑OK `Ack` when it can reject immediately (not joined, permission denied, rate limited, etc.).
110+
111+
After accepting and applying a client update, the server broadcasts it to other sessions joined to the room via SSE:
112+
113+
- Broadcast is typically `DocUpdate` with the same `batchId`, or the original fragments if fragmentation was used.
114+
- The sender does not need to receive its own update (implementation choice).
115+
116+
## Client Handling of Server Frames (SSE)
117+
118+
- Server-originated updates and backfills arrive on SSE as `event: msg` frames.
119+
- The client processes them exactly as it would process WebSocket binary frames.
120+
121+
Ack directionality:
122+
123+
- Clients SHOULD NOT send `Ack(status=0x00)` for server-originated updates (same reasoning as `protocol.md` WebSocket directionality).
124+
- Clients MAY send a non‑zero `Ack` via HTTP push if they fail to apply a server update (e.g., `invalid_update`, `fragment_timeout`).
125+
126+
## Keepalive
127+
128+
The `"ping"`/`"pong"` out-of-band keepalive in `protocol.md` is specific to WebSocket text frames.
129+
130+
For SSE:
131+
132+
- Implementations MAY send periodic SSE comments as heartbeats, e.g. `:keepalive\n\n`.
133+
- Heartbeats MUST NOT be parsed as protocol frames and MUST NOT be forwarded to rooms.
134+
135+
## Ordering and Concurrency
136+
137+
HTTP push requests can arrive concurrently, which can break assumptions about fragment ordering.
138+
139+
Recommendations:
140+
141+
- Serialize push handling per session key.
142+
- Enforce that `DocUpdateFragmentHeader` is observed before accepting fragments for that batch (or buffer until header arrives).
143+
- Use existing batch IDs as the correlation key for both fragments and `Ack`.
144+
145+
## Loss Recovery on SSE Reconnect
146+
147+
This profile assumes SSE can disconnect without replay. To recover:
148+
149+
- Clients SHOULD treat an SSE reconnect as a connection reconnect.
150+
- Clients SHOULD re-issue `JoinRequest` for each active room with their current version so the server can backfill missing updates.
151+
152+
## Compatibility
153+
154+
- Works for all CRDT magic types defined in `protocol.md` (including `%ELO` from `protocol-e2ee.md`).
155+
- `%ELO` payload semantics remain unchanged; only the transport encoding differs.
156+

0 commit comments

Comments
 (0)