y-reticulum 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,106 @@
1
+ # Reticulum connector for [Yjs](https://github.com/yjs/yjs)
2
+
3
+ Propagates document updates over [Reticulum](https://reticulum.network/) mesh network.
4
+
5
+ * Public key encryption and authorization using [Reticulum Identities](https://reticulum.network/manual/zen.html#identity-and-nomadism)
6
+ * Flexible network topology and multiple interfaces ranging from TCP to LoRa and HF radio links
7
+ * Very little setup needed with Reticulum announce and discovery mechanisms
8
+ * Sync and awareness traffic rides a reliable, in-order, windowed Link Channel
9
+ (retransmitted on lossy hops) for performant CRDT synchronization
10
+ * Larger CRDT updates are automatically transported as bz2 compressed Resources
11
+
12
+ Built on [reticulum-js](https://reticulum.js.org/) with aim to support browsers, Node.js, and Deno. For browsers, please read the [browser connectivity](https://reticulum.js.org/documents/Browser_Connectivity.html) notes.
13
+
14
+ ## Status
15
+
16
+ Just getting started
17
+
18
+ ## Install
19
+
20
+ ```sh
21
+ npm i y-reticulum
22
+ ```
23
+
24
+ ## Usage
25
+
26
+ Clients connected to the same room name share document updates. In addition to
27
+ a `Y.Doc`, you pass a configured [@reticulum/core](https://reticulum.js.org/)
28
+ instance — the provider does not open network interfaces itself.
29
+
30
+ ```js
31
+ import * as Y from "yjs"
32
+ import { Identity, Reticulum } from "@reticulum/core"
33
+ import { TCPClientInterface } from "@reticulum/node"
34
+ import { ReticulumProvider } from "y-reticulum"
35
+
36
+ // 1. Connect to the Reticulum mesh. Prefer the local shared instance (e.g. a
37
+ // running `rnsd`); fall back to a direct TCP interface when there is none.
38
+ const rns = new Reticulum()
39
+ const shared = await rns.connectToSharedInstance()
40
+ if (!shared) {
41
+ const tcp = new TCPClientInterface({ host: "127.0.0.1", port: 42424 })
42
+ await tcp.connect()
43
+ rns.addInterface(tcp, true)
44
+ }
45
+
46
+ // 2. An identity for this peer (persist it between runs in real apps so your
47
+ // Reticulum address stays stable).
48
+ const identity = await Identity.generate()
49
+
50
+ // 3. Create the Yjs document and the provider.
51
+ const ydoc = new Y.Doc()
52
+ const provider = new ReticulumProvider("your-room-name", ydoc, {
53
+ reticulum: rns,
54
+ identity,
55
+ })
56
+
57
+ provider.on("status", ({ connected }) => console.log("connected:", connected))
58
+ provider.on("synced", ({ synced }) => console.log("synced:", synced))
59
+ provider.on("peers", ({ added, removed }) =>
60
+ console.log("peers added:", added, "removed:", removed),
61
+ )
62
+
63
+ await provider.connect()
64
+
65
+ const yarray = ydoc.getArray("array")
66
+ ```
67
+
68
+ ## API
69
+
70
+ ```js
71
+ new ReticulumProvider(roomName, ydoc[, opts])
72
+ ```
73
+
74
+ `opts` accepts the following (all optional except `reticulum`):
75
+
76
+ ```js
77
+ {
78
+ // A configured Reticulum instance with at least one (default) interface
79
+ // attached. Required — the provider does not open interfaces itself.
80
+ reticulum,
81
+ // Identity for this peer's room destination. Generated (non-persistent) if
82
+ // omitted; supply your own to keep a stable address across restarts.
83
+ identity,
84
+ // Reuse an existing Awareness instance - see https://github.com/yjs/y-protocols
85
+ awareness: new awarenessProtocol.Awareness(ydoc),
86
+ // Upper bound on simultaneous peer Links. Mirrors y-webrtc's `maxConns`.
87
+ maxConns: 20,
88
+ // Cadence (ms) at which the room destination is re-announced for discovery.
89
+ // Delegated to @reticulum/core's Destination.startAnnouncing, which clamps
90
+ // to the 60s floor from the Reticulum spec (sub-minute intervals trigger
91
+ // ingress rate limiting).
92
+ announceIntervalMs: 60_000,
93
+ }
94
+ ```
95
+
96
+ The provider extends `ObservableV2` and emits:
97
+
98
+ | Event | Payload | When |
99
+ | --- | --- | --- |
100
+ | `status` | `{ connected: boolean }` | the provider (dis)connects from the mesh |
101
+ | `synced` | `{ synced: boolean }` | sync state with the peer mesh changes |
102
+ | `peers` | `{ added: string[], removed: string[] }` | peers are discovered or drop off |
103
+
104
+ ## License
105
+
106
+ Licensed under the [EUPL 1.2](https://interoperable-europe.ec.europa.eu/collection/eupl/eupl-text-eupl-12).
package/SPEC.md ADDED
@@ -0,0 +1,229 @@
1
+ # y-reticulum — Reticulum provider for Yjs
2
+
3
+ A Yjs provider that synchronizes documents over the [Reticulum Network System
4
+ (RNS)](https://reticulum.network/) mesh. The goal is to reach feature parity
5
+ with the [y-webrtc](https://github.com/yjs/y-webrtc) provider, substituting
6
+ Reticulum's transport and discovery primitives for WebRTC + signaling servers.
7
+
8
+ ## Goals
9
+
10
+ - Synchronize `Y.Doc` state and `Awareness` between peers over Reticulum.
11
+ - Work anywhere `reticulum-js` runs (Node.js, Deno, browsers).
12
+ - Provide an API and event surface familiar to anyone who has used
13
+ `WebrtcProvider` (`status`, `synced`, `peers`).
14
+ - Be roughly on the same feature level as y-webrtc.
15
+
16
+ ## Non-goals (for now)
17
+
18
+ - Acting as a Reticulum *transport* (routing) node. We are a leaf node, same as
19
+ the rest of `reticulum-js`.
20
+ - E2E encryption of the room beyond what Reticulum's Links already provide.
21
+ - A `BroadcastChannel` same-origin/tab shortcut. Reticulum is the single
22
+ transport.
23
+
24
+ ## Architecture
25
+
26
+ ### How y-webrtc works (reference)
27
+
28
+ - A `WebrtcProvider` wraps a `Y.Doc` and opens a `Room` (one per room name).
29
+ - Peer discovery happens through one or more **signaling servers** (WebSocket):
30
+ clients `announce` their `peerId` and relay WebRTC `offer`/`answer`/`signal`
31
+ messages through the server.
32
+ - Each pair of peers opens a **WebRTC data channel** (`simple-peer`).
33
+ - Doc/awareness updates are encoded with `lib0` and framed with a 1-byte
34
+ message-type tag, then sent over every peer channel.
35
+ - An optional `BroadcastChannel` path shortcuts same-origin tabs.
36
+
37
+ The message wire protocol (reused verbatim here):
38
+
39
+ | Tag | Meaning |
40
+ |---|---|
41
+ | `0` | sync (carries `messageYjsSyncStep1` / `Step2` / `update`) |
42
+ | `1` | awareness update |
43
+ | `3` | query awareness |
44
+ | `4` | broadcastchannel peer-id add/remove — **not needed** (no BC) |
45
+
46
+ ### Reticulum primitives we build on
47
+
48
+ - **`Destination`** (IN/OUT, SINGLE/GROUP/PLAIN) + **`Announce`** for
49
+ authenticated, signed peer discovery. Each peer's announce carries its public
50
+ identity, which others recall to open Links.
51
+ - **`Link`** — an ephemeral, encrypted channel between two destinations,
52
+ established via a `LINKREQUEST`/`LRPROOF` handshake. The base for everything
53
+ below.
54
+ - **`Channel`** — a reliable, in-order, windowed typed-message layer over a
55
+ Link (`link.getChannel()`). Adds automatic retries (retransmit on a missing
56
+ proof), send-window flow control, and dedup — so a sync update or awareness
57
+ change dropped on a lossy hop is retransmitted rather than lost. Carries the
58
+ bulk of Yjs traffic.
59
+ - **`Resource`** — chunked, hash-verified large-payload transport over a Link,
60
+ with automatic bz2 compression. Used for oversized sync payloads (initial
61
+ state, big `syncStep2`) that exceed the channel MDU.
62
+ - **`request()`/`response()`** RPC built on Links — *not* used directly; we run
63
+ our own framing so the message-type tag matches y-webrtc's semantics.
64
+
65
+ ### Concept mapping
66
+
67
+ | y-webrtc | y-reticulum |
68
+ |---|---|
69
+ | Signaling server `announce`/`publish` | Reticulum `Announce` to a deterministic destination |
70
+ | WebRTC data channel | `Link` + `Channel` |
71
+ | Raw peer `.send(bytes)` | reliable `Channel` message (small) — `ContextType.CHANNEL` DATA packets with retries + send window |
72
+ | Large peer payloads (none) | `Resource` (bz2-compressed) |
73
+ | BroadcastChannel (same-origin) | not applicable |
74
+ | `WebrtcProvider` (`ObservableV2`: `status`/`synced`/`peers`) | `ReticulumProvider` with the same events |
75
+ | Message tags 0/1/3 | reused verbatim |
76
+
77
+ ### Discovery model (chosen: deterministic destination per room)
78
+
79
+ Each peer creates a **`SINGLE` IN destination** whose full app name is derived
80
+ from the room name, e.g.:
81
+
82
+ ```
83
+ y-reticulum.sync.<hex(hash(roomName))>
84
+ ```
85
+
86
+ The peer announces this destination. Other peers running the same room name
87
+ learn the announcer's identity from the announce and open a `Link` to it.
88
+ Because every peer both announces and listens, the topology is a full mesh of
89
+ pairwise Links (bounded by `maxConns`, same as y-webrtc).
90
+
91
+ > A pure group-destination broadcast (one destination, no Links) was considered
92
+ > and rejected: it loses Link-level reliability/encryption/ordering and makes
93
+ > `peers`/`maxConns` semantics awkward. We keep the "announce → connect to peers
94
+ > individually" model that mirrors y-webrtc.
95
+
96
+ ### Room identity and the destination hash
97
+
98
+ - The room name is hashed to form the destination aspect so that two peers that
99
+ type the same room name arrive at the same destination namespace without
100
+ leaking the cleartext room name in announce app data.
101
+ - Each peer generates (and persists, when a storage adapter is available) its
102
+ own `Identity`. The announce carries a small `app_data` blob identifying this
103
+ peer (peer id + provider version) for diagnostics.
104
+
105
+ ### Wire protocol on a Link
106
+
107
+ Identical framing to y-webrtc, minus tag `4`:
108
+
109
+ ```
110
+ <1-byte tag><payload encoded with lib0>
111
+ ```
112
+
113
+ - tag `0` sync → `syncProtocol.readSyncMessage` / `writeSyncStep1/2` / `writeUpdate`
114
+ - tag `1` awareness → `awarenessProtocol.encodeAwarenessUpdate` / `applyAwarenessUpdate`
115
+ - tag `3` query awareness → reply with tag `1`
116
+
117
+ Small messages travel as reliable `Channel` messages (a `MessageBase` whose
118
+ body is the raw y-webrtc frame), giving automatic retries, in-order delivery,
119
+ and send-window flow control over the Link. Messages exceeding the channel MDU
120
+ (link MDU minus the 6-byte channel envelope) cannot fit in a single channel
121
+ message and are transported via a `Resource` (bz2-compressed), reassembled on
122
+ the receiver before being handed to the same `readMessage` path.
123
+
124
+ ## Public API (target)
125
+
126
+ ```js
127
+ import * as Y from "yjs";
128
+ import { ReticulumProvider } from "y-reticulum";
129
+
130
+ const doc = new Y.Doc();
131
+ const provider = new ReticulumProvider("my-room", doc, {
132
+ identity, // optional; generated/persisted if omitted
133
+ reticulum, // optional pre-configured Reticulum instance
134
+ awareness, // optional; created if omitted
135
+ maxConns, // optional; default 20-ish like y-webrtc
136
+ announceInterval,// optional
137
+ });
138
+
139
+ provider.on("status", ({ connected }) => { /* ... */ });
140
+ provider.on("synced", ({ synced }) => { /* ... */ });
141
+ provider.on("peers", ({ added, removed }) => { /* ... */ });
142
+ ```
143
+
144
+ Methods mirror y-webrtc: `connect()`, `disconnect()`, `destroy()`.
145
+
146
+ ## Project layout
147
+
148
+ ```
149
+ src/
150
+ index.js # public exports
151
+ provider.js # ReticulumProvider
152
+ room.js # Room abstraction (announces, tracks peer Links)
153
+ peer-conn.js # one peer-to-peer Link wrapper
154
+ messages.js # message tags + readMessage/broadcast helpers
155
+ destination.js # room-name → deterministic destination name helpers
156
+ test/
157
+ *.smoke.js # smoketests per layer
158
+ examples/ # demo clients (later)
159
+ ```
160
+
161
+ ## Type safety
162
+
163
+ All source is plain JavaScript (`.js`) with **JSDoc type annotations verified by
164
+ the TypeScript checker** — no hand-written `.ts` source files. This matches the
165
+ conventions of both `y-webrtc` and `reticulum-js`.
166
+
167
+ - `tsconfig.json` enables `allowJs: true` and `checkJs: true` (plus
168
+ `declaration` / `emitDeclarationOnly: true` so a `.d.ts` bundle is produced).
169
+ - Every function, method, and constructor gets `@param {Type} name` /
170
+ `@returns {Type}` annotations; module-level `@typedef`s describe option objects
171
+ and event payloads; `@import` (or `import` in `@type`) references types from
172
+ `yjs`, `y-protocols`, and `@reticulum/core`.
173
+ - `npm run types` (`tsc`) **must pass after every change** — this is enforced by
174
+ `AGENTS.md`. Treat type errors as build failures, not warnings.
175
+ - `lib0` types (`encoding.Encoder`, `decoding.Decoder`, `observable.ObservableV2`,
176
+ etc.) are referenced the same way `y-webrtc` references them. Note `lib0` is a
177
+ transitive dependency of `y-protocols`; if it is not resolvable it must be added
178
+ as a direct dependency (ask first, per `AGENTS.md`).
179
+ - Emitted `.d.ts` files are excluded from version control (already in
180
+ `.gitignore`).
181
+
182
+ ## Implementation phases
183
+
184
+ ### Phase 0 — Scaffolding
185
+ - Add `tsconfig.json` (`allowJs` + `checkJs` + `declaration` +
186
+ `emitDeclarationOnly`) so `npm run types` passes against `src/`.
187
+ - Empty, fully JSDoc-annotated `src/index.js` re-exporting the (upcoming)
188
+ provider.
189
+ - Confirm `npm run types` and `npm run format` are green.
190
+
191
+ ### Phase 1 — Transport smoketest (foundation)
192
+ - A smoketest that spins up two in-process `Reticulum` instances (loopback
193
+ interface), derives the same room destination name on each, announces, and
194
+ establishes a `Link`, then exchanges raw bytes both ways.
195
+ - Validates discovery + Link transport before sync semantics land.
196
+
197
+ ### Phase 2 — Provider skeleton
198
+ - `ReticulumProvider` constructor wiring (`Y.Doc`, `Awareness`, identity,
199
+ Reticulum connect), `connect()`/`disconnect()`, `status` events.
200
+ - `Room` that announces and listens for announces/links; `peers` emission.
201
+ - No Yjs sync yet — just connection lifecycle.
202
+
203
+ ### Phase 3 — Sync protocol layer
204
+ - `readMessage` / broadcast over peer Links.
205
+ - Doc `update` handler → broadcast sync `update`.
206
+ - Awareness update/query handlers.
207
+ - `synced` tracking across peers (mirror `checkIsSynced`).
208
+ - Smoketest: two providers, one edits, the other observes the change.
209
+
210
+ ### Phase 4 — Resource-backed large transfers
211
+ - Route oversized payloads through `Resource`; reassemble and feed into the
212
+ same `readMessage` path.
213
+ - Smoketest: large initial-doc sync.
214
+
215
+ ### Phase 5 — Hardening & parity
216
+ - `maxConns` enforcement, reconnection, keepalive/timeout behavior, clean
217
+ teardown (`destroy`), graceful announce removal on disconnect.
218
+ - Parity checklist against y-webrtc features.
219
+
220
+ ## Open questions
221
+
222
+ - Default announce cadence: now delegated to `@reticulum/core`'s
223
+ `Destination.startAnnouncing`, which enforces the §9.7 60 s floor; y-reticulum
224
+ defaults to that floor and forwards any user override (clamped). Open:
225
+ whether to send a path request up front to accelerate first-peer discovery
226
+ on a fresh mesh.
227
+ - Whether/how to expose the configured Reticulum interfaces (auto vs. explicit
228
+ TCP/WebSocket) or always prefer `connectToSharedInstance()` with a fallback.
229
+ - `maxConns` semantics: do we cap total Links, or per-room Links?
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "y-reticulum",
3
+ "version": "0.1.0",
4
+ "description": "Reticulum provider for Yjs",
5
+ "keywords": [
6
+ "reticulum",
7
+ "rns",
8
+ "yjs"
9
+ ],
10
+ "homepage": "https://github.com/bergie/y-reticulum#readme",
11
+ "bugs": {
12
+ "url": "https://github.com/bergie/y-reticulum/issues"
13
+ },
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git://github.com/bergie/y-reticulum.git"
17
+ },
18
+ "license": "EUPL-1.2",
19
+ "author": "Henri Bergius <henri.bergius@iki.fi>",
20
+ "type": "module",
21
+ "main": "src/index.js",
22
+ "scripts": {
23
+ "lint": "npx @biomejs/biome check --use-editorconfig=true src/",
24
+ "format": "npx @biomejs/biome check --use-editorconfig=true --write src/ test/",
25
+ "types": "npx tsc",
26
+ "test": "node --test --test-force-exit --test-timeout 5000 test/*.js test/**/*.js",
27
+ "test:deno": "deno test --no-check --allow-net --allow-env --allow-read=node_modules test/*.js",
28
+ "test:bun": "bun test"
29
+ },
30
+ "dependencies": {
31
+ "@digitaldefiance/bzip2-wasm": "^1.1.1",
32
+ "@reticulum/core": "^0.5.0",
33
+ "@reticulum/node": "^0.5.0",
34
+ "lib0": "^0.2.117",
35
+ "y-protocols": "^1.0.7"
36
+ },
37
+ "devDependencies": {
38
+ "@types/node": "^26.1.2",
39
+ "typescript": "^6.0.3",
40
+ "yjs": "^13.6.31"
41
+ }
42
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * @file compression.js
3
+ * @description Shared bzip2 provider for compressing Reticulum Resources.
4
+ *
5
+ * `@digitaldefiance/bzip2-wasm` is a hard dependency of y-reticulum, so both
6
+ * peers can always compress/decompress large sync payloads (an initial doc
7
+ * state or a big update). The WASM module needs a one-time async `init()`; this
8
+ * module exposes a shared, lazily-initialized instance. If init ever fails we
9
+ * resolve to `null` and sync transparently falls back to uncompressed Resources.
10
+ */
11
+ import BZip2 from "@digitaldefiance/bzip2-wasm";
12
+
13
+ /** @type {Promise<import("@digitaldefiance/bzip2-wasm").default | null> | null} */
14
+ let initPromise = null;
15
+
16
+ /**
17
+ * Returns a shared, initialized BZip2 instance, or `null` if the WASM module
18
+ * failed to load. Safe to call repeatedly — initialization runs only once.
19
+ *
20
+ * @returns {Promise<import("@digitaldefiance/bzip2-wasm").default | null>}
21
+ */
22
+ export function getCompressionProvider() {
23
+ if (!initPromise) {
24
+ initPromise = (async () => {
25
+ try {
26
+ const bz2 = new BZip2();
27
+ await bz2.init();
28
+ return bz2;
29
+ } catch {
30
+ // WASM unavailable / failed to load — Resources will go uncompressed.
31
+ return null;
32
+ }
33
+ })();
34
+ }
35
+ return initPromise;
36
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * @file destination.js
3
+ * @description Helpers mapping a Yjs room name to a Reticulum destination.
4
+ *
5
+ * Two peers that pass the same room name must arrive at the same Reticulum
6
+ * "aspect" so they can discover each other via the Announce mechanism. We hash
7
+ * the room name into the aspect so the cleartext name is not leaked on the wire,
8
+ * and so the resulting 10-byte `nameHash` doubles as the room-membership filter
9
+ * when comparing inbound announces (see SPEC.md → Discovery model).
10
+ */
11
+
12
+ /**
13
+ * App-name prefix shared by every y-reticulum sync destination. The trailing
14
+ * segment is a hex digest of the room name (see {@link roomDestinationName}).
15
+ */
16
+ export const DESTINATION_APP_PREFIX = "y-reticulum.sync";
17
+
18
+ /**
19
+ * Derives the deterministic Reticulum destination app-name for a Yjs room.
20
+ *
21
+ * The room name is hashed (first 8 bytes of its SHA-256, rendered as 16 hex
22
+ * chars) so the on-wire aspect does not leak the cleartext room name. Two peers
23
+ * that pass the same `roomName` arrive at the same app-name — and therefore the
24
+ * same 10-byte `nameHash` — which is exactly what room peer-discovery filters on
25
+ * when comparing inbound announces.
26
+ *
27
+ * @param {string} roomName
28
+ * @returns {Promise<string>} app-name like `y-reticulum.sync.<16 hex chars>`
29
+ */
30
+ export async function roomDestinationName(roomName) {
31
+ const digest = await crypto.subtle.digest(
32
+ "SHA-256",
33
+ new TextEncoder().encode(roomName),
34
+ );
35
+ const bytes = new Uint8Array(digest);
36
+ let hex = "";
37
+ for (let i = 0; i < 8; i++) {
38
+ hex += bytes[i].toString(16).padStart(2, "0");
39
+ }
40
+ return `${DESTINATION_APP_PREFIX}.${hex}`;
41
+ }
package/src/index.js ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * @file index.js
3
+ * @description Public entry point for the `y-reticulum` package.
4
+ */
5
+
6
+ export { getCompressionProvider } from "./compression.js";
7
+ export { roomDestinationName } from "./destination.js";
8
+ export { messageAwareness, messageSync, readMessage } from "./messages.js";
9
+ export { PeerConn } from "./peer-conn.js";
10
+ export { ReticulumProvider } from "./provider.js";
11
+ export { Room } from "./room.js";
@@ -0,0 +1,90 @@
1
+ /**
2
+ * @file messages.js
3
+ * @description The Yjs sync wire protocol used over a peer Link.
4
+ *
5
+ * This is y-webrtc's framing, minus its BroadcastChannel peer-id message
6
+ * (tag 4) which has no Reticulum equivalent. Each message is a 1-byte tag
7
+ * followed by a lib0-encoded body:
8
+ *
9
+ * 0 sync — carries syncStep1 / syncStep2 / update (y-protocols/sync)
10
+ * 1 awareness — an awareness update (y-protocols/awareness)
11
+ * 3 queryAwareness — request the peer's full awareness state
12
+ *
13
+ * Bytes flow through {@link PeerConn}; this module only knows how to decode
14
+ * them and apply them to a Doc / Awareness.
15
+ */
16
+ import * as decoding from "lib0/decoding";
17
+ import * as encoding from "lib0/encoding";
18
+ import * as awarenessProtocol from "y-protocols/awareness";
19
+ import * as syncProtocol from "y-protocols/sync";
20
+
21
+ /** @type {0} */
22
+ export const messageSync = 0;
23
+ /** @type {1} */
24
+ export const messageAwareness = 1;
25
+ /** @type {3} */
26
+ export const messageQueryAwareness = 3;
27
+
28
+ /**
29
+ * Decodes one inbound framed message, applying it to the doc / awareness, and
30
+ * returns the bytes of a reply to send back to the same peer (or `null`).
31
+ *
32
+ * Mirrors y-webrtc's `readMessage`: a `syncStep1` requests our state and so
33
+ * produces a `syncStep2` reply; a `syncStep2` delivers the peer's state and
34
+ * marks the room synced (once, via `onSynced`); `queryAwareness` produces an
35
+ * awareness reply.
36
+ *
37
+ * @param {import("yjs").Doc} doc
38
+ * @param {awarenessProtocol.Awareness} awareness
39
+ * @param {Uint8Array} buf
40
+ * @param {any} origin - transactionOrigin for any updates this applies.
41
+ * @param {boolean} roomSynced - whether the room is already synced (gates the
42
+ * one-shot `onSynced` callback, matching y-webrtc).
43
+ * @param {() => void} onSynced - invoked once when a syncStep2 first arrives.
44
+ * @returns {Uint8Array | null} reply bytes, or `null` when no reply is needed.
45
+ */
46
+ export function readMessage(doc, awareness, buf, origin, roomSynced, onSynced) {
47
+ const decoder = decoding.createDecoder(buf);
48
+ const encoder = encoding.createEncoder();
49
+ const messageType = decoding.readVarUint(decoder);
50
+ let sendReply = false;
51
+ switch (messageType) {
52
+ case messageSync: {
53
+ encoding.writeVarUint(encoder, messageSync);
54
+ const syncMessageType = syncProtocol.readSyncMessage(
55
+ decoder,
56
+ encoder,
57
+ doc,
58
+ origin,
59
+ );
60
+ if (syncMessageType === syncProtocol.messageYjsSyncStep2 && !roomSynced) {
61
+ onSynced();
62
+ }
63
+ if (syncMessageType === syncProtocol.messageYjsSyncStep1) {
64
+ sendReply = true;
65
+ }
66
+ break;
67
+ }
68
+ case messageQueryAwareness:
69
+ encoding.writeVarUint(encoder, messageAwareness);
70
+ encoding.writeVarUint8Array(
71
+ encoder,
72
+ awarenessProtocol.encodeAwarenessUpdate(
73
+ awareness,
74
+ Array.from(awareness.getStates().keys()),
75
+ ),
76
+ );
77
+ sendReply = true;
78
+ break;
79
+ case messageAwareness:
80
+ awarenessProtocol.applyAwarenessUpdate(
81
+ awareness,
82
+ decoding.readVarUint8Array(decoder),
83
+ origin,
84
+ );
85
+ break;
86
+ default:
87
+ return null;
88
+ }
89
+ return sendReply ? encoding.toUint8Array(encoder) : null;
90
+ }