yrb-lite-client 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,126 @@
1
+ # yrb-lite-client
2
+
3
+ The **client core** for the [`yrb-lite`](https://github.com/jpcamara/yrb-lite)
4
+ y-websocket protocol — everything a Yjs provider needs *except the transport*.
5
+ Bring your own socket (ActionCable, AnyCable, raw WebSocket); this owns the
6
+ protocol.
7
+
8
+ Three layers, use whichever you need:
9
+
10
+ - **`ActionCableProvider`** — a ready-made Yjs provider for ActionCable /
11
+ AnyCable. Pass a `Y.Doc`, a cable consumer, and a channel; it wires the
12
+ subscription and you're collaborating. Awareness/presence automatically rides
13
+ AnyCable's `whisper` when the consumer supports it (client-to-client, no server
14
+ round-trip), and falls back to a normal server-relayed send on plain
15
+ ActionCable — nothing to configure. Document updates always go through the
16
+ server (recorded/acked).
17
+ - **`SyncEngine`** — the transport-agnostic core. Binds to a `Y.Doc` (+ optional
18
+ `Awareness`) and owns the y-protocols **message encode/decode**, the
19
+ **sync-step handshake** (SyncStep1 / SyncStep2 / Update), **awareness**, and
20
+ reliable delivery. Speaks raw `Uint8Array` frames; you wire any socket.
21
+ - **`ReliableSync`** — the zero-dependency reliable-delivery state machine on its
22
+ own: ack-tracked queue, **sync-since-last-ack** (the unacked tail merged into
23
+ one causally-complete delta), cumulative acks, retransmit + "server doesn't
24
+ support acks" fallback, and reconnect replay. Compose it yourself if you
25
+ already have your own framing.
26
+
27
+ ## Install
28
+
29
+ ```bash
30
+ npm install yrb-lite-client
31
+ ```
32
+
33
+ `ActionCableProvider` and `SyncEngine` need `yjs` and `y-protocols` (peers — your
34
+ app already has them), plus an ActionCable/AnyCable consumer. `ReliableSync` has
35
+ **no dependencies**; import it on its own via `yrb-lite-client/reliable` if
36
+ that's all you want.
37
+
38
+ Written in **TypeScript** and ships bundled type declarations, so TS projects get
39
+ full types (typed options, methods, and errors) with no `@types` package — and
40
+ plain-JS projects use the same compiled ESM with nothing extra to install.
41
+
42
+ ## ActionCableProvider (the easy path)
43
+
44
+ ```js
45
+ import { ActionCableProvider } from "yrb-lite-client";
46
+ import * as Y from "yjs";
47
+ import { createConsumer } from "@anycable/web"; // or @rails/actioncable
48
+
49
+ const doc = new Y.Doc();
50
+ const consumer = createConsumer();
51
+ const provider = new ActionCableProvider(doc, consumer, "DocumentChannel", { id: docId });
52
+
53
+ provider.connect(); // does not auto-connect — wire your editor binding first
54
+ // provider.awareness -> the Awareness instance (a fresh one unless you pass opts.awareness)
55
+ // provider.synced -> caught up with the server
56
+ // provider.hasPending -> unacked local edits in flight
57
+ // provider.destroy() -> tear down
58
+ ```
59
+
60
+ On the server, include `YrbLite::ActionCable::Sync` in a channel named
61
+ `DocumentChannel` (the [`yrb-lite-actioncable`](https://rubygems.org/gems/yrb-lite-actioncable)
62
+ gem), which enables AnyCable whispering on the stream automatically. Need a
63
+ different transport or framing? Drop down to `SyncEngine` and supply your own
64
+ `send`.
65
+
66
+ ## SyncEngine
67
+
68
+ ```js
69
+ import { SyncEngine, toBase64, fromBase64 } from "yrb-lite-client";
70
+ import * as Y from "yjs";
71
+ import { Awareness } from "y-protocols/awareness";
72
+
73
+ const doc = new Y.Doc();
74
+ const awareness = new Awareness(doc);
75
+
76
+ const engine = new SyncEngine(doc, {
77
+ awareness,
78
+ // transmit one raw frame; `id` is set for reliable doc updates -> tag your envelope
79
+ send: (frame, id) => {
80
+ const payload = { update: toBase64(frame) };
81
+ if (id !== undefined) payload.id = id;
82
+ subscription.send(payload);
83
+ },
84
+ });
85
+
86
+ // wire your transport's callbacks:
87
+ subscription.connected = () => engine.onConnect(); // handshake + replay
88
+ subscription.disconnected = () => engine.onDisconnect(); // pause + clear presence
89
+ subscription.received = (msg) => {
90
+ if (msg.ack !== undefined) return engine.ack(msg.ack); // reliable ack envelope
91
+ const reply = engine.receive(fromBase64(msg.update || msg.m)); // decode + apply
92
+ if (reply) subscription.send({ update: toBase64(reply) }); // e.g. answer a SyncStep1
93
+ };
94
+ // engine.synced -> caught up; engine.hasPending -> unacked edits in flight
95
+ // engine.destroy() -> detach listeners + stop retransmits
96
+ ```
97
+
98
+ Local document edits and awareness changes are picked up automatically from the
99
+ doc's / awareness's `update` events — you never call anything for outbound edits.
100
+
101
+ ## ReliableSync (standalone)
102
+
103
+ ```js
104
+ import { ReliableSync } from "yrb-lite-client/reliable"; // zero-dep
105
+ import * as Y from "yjs";
106
+
107
+ const rs = new ReliableSync({
108
+ send: (update, id) => { /* frame + transmit */ },
109
+ merge: Y.mergeUpdates,
110
+ });
111
+
112
+ rs.enqueue(update); // a local document update
113
+ rs.onAck(id); // an { ack: id } arrived
114
+ rs.onConnect(); // (re)connected — replay the tail, resume retransmits
115
+ rs.onDisconnect(); // dropped — keep the queue, pause
116
+ ```
117
+
118
+ ## How it fits
119
+
120
+ The server counterpart — ack *generation*, gap detection, record-before-distribute
121
+ — is the `yrb-lite-actioncable` gem's `YrbLite::ActionCable::Sync`. This package
122
+ is the client half of the same protocol.
123
+
124
+ ## License
125
+
126
+ MIT
@@ -0,0 +1,40 @@
1
+ import { SyncEngine, type SyncEngineOptions } from "./sync_engine.js";
2
+ import { Awareness } from "y-protocols/awareness";
3
+ import type { Doc } from "yjs";
4
+ /** The minimal slice of an ActionCable/AnyCable subscription this provider uses. */
5
+ export interface CableSubscription {
6
+ send(data: unknown): unknown;
7
+ /** AnyCable client-to-client broadcast; absent on plain ActionCable. */
8
+ whisper?(data: unknown): unknown;
9
+ unsubscribe?(): void;
10
+ }
11
+ /** The minimal slice of an ActionCable/AnyCable consumer this provider uses. */
12
+ export interface CableConsumer {
13
+ subscriptions: {
14
+ create(params: object, mixin: object): CableSubscription;
15
+ remove(subscription: CableSubscription): void;
16
+ };
17
+ }
18
+ export interface ActionCableProviderOptions extends Pick<SyncEngineOptions, "reliable" | "resendInterval" | "maxUnconfirmedResends" | "onFallback"> {
19
+ /** Awareness/presence instance. Defaults to a fresh `new Awareness(doc)`. */
20
+ awareness?: Awareness | null;
21
+ }
22
+ export declare class ActionCableProvider {
23
+ readonly doc: Doc;
24
+ readonly consumer: CableConsumer;
25
+ readonly channelName: string;
26
+ readonly channelParams: object;
27
+ readonly awareness: Awareness;
28
+ readonly engine: SyncEngine;
29
+ private subscription;
30
+ constructor(doc: Doc, consumer: CableConsumer, channelName: string, channelParams?: object, opts?: ActionCableProviderOptions);
31
+ /** True once the document has caught up with the server (received a SyncStep2). */
32
+ get synced(): boolean;
33
+ /** True while there are unacknowledged local document updates in flight. */
34
+ get hasPending(): boolean;
35
+ connect(): void;
36
+ disconnect(): void;
37
+ destroy(): void;
38
+ private _send;
39
+ }
40
+ //# sourceMappingURL=actioncable_provider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"actioncable_provider.d.ts","sourceRoot":"","sources":["../src/actioncable_provider.ts"],"names":[],"mappings":"AAeA,OAAO,EAAE,UAAU,EAAe,KAAK,iBAAiB,EAAoB,MAAM,kBAAkB,CAAC;AAErG,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAClD,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAE/B,oFAAoF;AACpF,MAAM,WAAW,iBAAiB;IAChC,IAAI,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC;IAC7B,wEAAwE;IACxE,OAAO,CAAC,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC;IACjC,WAAW,CAAC,IAAI,IAAI,CAAC;CACtB;AAED,gFAAgF;AAChF,MAAM,WAAW,aAAa;IAC5B,aAAa,EAAE;QACb,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,iBAAiB,CAAC;QACzD,MAAM,CAAC,YAAY,EAAE,iBAAiB,GAAG,IAAI,CAAC;KAC/C,CAAC;CACH;AAED,MAAM,WAAW,0BACf,SAAQ,IAAI,CAAC,iBAAiB,EAAE,UAAU,GAAG,gBAAgB,GAAG,uBAAuB,GAAG,YAAY,CAAC;IACvG,6EAA6E;IAC7E,SAAS,CAAC,EAAE,SAAS,GAAG,IAAI,CAAC;CAC9B;AAQD,qBAAa,mBAAmB;IAC9B,QAAQ,CAAC,GAAG,EAAE,GAAG,CAAC;IAClB,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC;IACjC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAC5B,OAAO,CAAC,YAAY,CAAkC;gBAGpD,GAAG,EAAE,GAAG,EACR,QAAQ,EAAE,aAAa,EACvB,WAAW,EAAE,MAAM,EACnB,aAAa,GAAE,MAAW,EAC1B,IAAI,GAAE,0BAA+B;IAkBvC,mFAAmF;IACnF,IAAI,MAAM,IAAI,OAAO,CAEpB;IAED,4EAA4E;IAC5E,IAAI,UAAU,IAAI,OAAO,CAExB;IAED,OAAO,IAAI,IAAI;IA2Bf,UAAU,IAAI,IAAI;IAOlB,OAAO,IAAI,IAAI;IAUf,OAAO,CAAC,KAAK;CAYd"}
@@ -0,0 +1,102 @@
1
+ // A ready-made Yjs provider for the yrb-lite y-websocket protocol over
2
+ // ActionCable / AnyCable. It owns the cable subscription and translates between
3
+ // the cable's JSON envelope (`{ update, id }` / `{ ack }`, base64) and raw
4
+ // protocol frames; everything else (sync steps, encode/decode, awareness,
5
+ // reliable delivery) lives in SyncEngine. So this is just the transport glue.
6
+ //
7
+ // Awareness/presence frames are sent via AnyCable's `whisper` when the
8
+ // subscription supports it (client-to-client, no server round-trip); on plain
9
+ // ActionCable (no `whisper`) they fall back to a normal `send` and the server
10
+ // relays them. Document updates always go through `send` (they must be
11
+ // recorded/acked).
12
+ //
13
+ // The constructor does NOT auto-connect: wire your editor binding first, then
14
+ // call `connect()`. Same `(doc, consumer, channelName, channelParams, opts)`
15
+ // shape as a typical y-rb/actioncable provider.
16
+ import { SyncEngine, MessageType } from "./sync_engine.js";
17
+ import { toBase64, fromBase64 } from "./base64.js";
18
+ import { Awareness } from "y-protocols/awareness";
19
+ export class ActionCableProvider {
20
+ constructor(doc, consumer, channelName, channelParams = {}, opts = {}) {
21
+ this.subscription = null;
22
+ this.doc = doc;
23
+ this.consumer = consumer;
24
+ this.channelName = channelName;
25
+ this.channelParams = channelParams;
26
+ this.awareness = opts.awareness ?? new Awareness(doc);
27
+ this.engine = new SyncEngine(doc, {
28
+ awareness: this.awareness,
29
+ reliable: opts.reliable,
30
+ resendInterval: opts.resendInterval,
31
+ maxUnconfirmedResends: opts.maxUnconfirmedResends,
32
+ onFallback: opts.onFallback,
33
+ send: (frame, id, sendOpts) => this._send(frame, id, sendOpts),
34
+ });
35
+ }
36
+ /** True once the document has caught up with the server (received a SyncStep2). */
37
+ get synced() {
38
+ return this.engine.synced;
39
+ }
40
+ /** True while there are unacknowledged local document updates in flight. */
41
+ get hasPending() {
42
+ return this.engine.hasPending;
43
+ }
44
+ connect() {
45
+ if (this.subscription)
46
+ return;
47
+ const provider = this;
48
+ this.subscription = this.consumer.subscriptions.create({ channel: this.channelName, ...this.channelParams }, {
49
+ received(message) {
50
+ // Reliable-delivery ack: confirm + prune the local queue.
51
+ if (message && message.ack !== undefined) {
52
+ provider.engine.ack(message.ack);
53
+ return;
54
+ }
55
+ const payload = message && (message.m ?? message.update);
56
+ if (typeof payload !== "string")
57
+ return;
58
+ const reply = provider.engine.receive(fromBase64(payload));
59
+ if (reply)
60
+ provider._send(reply, undefined); // e.g. SyncStep2 answering a SyncStep1
61
+ },
62
+ connected() {
63
+ provider.engine.onConnect(); // handshake + replay the unacked tail
64
+ },
65
+ disconnected() {
66
+ provider.engine.onDisconnect(); // pause retransmits, clear remote presence
67
+ },
68
+ });
69
+ }
70
+ disconnect() {
71
+ if (!this.subscription)
72
+ return;
73
+ this.engine.onDisconnect();
74
+ this.consumer.subscriptions.remove(this.subscription);
75
+ this.subscription = null;
76
+ }
77
+ destroy() {
78
+ this.disconnect();
79
+ this.engine.destroy();
80
+ }
81
+ // Send one raw protocol frame over the cable. Awareness frames are whispered
82
+ // when the subscription supports it (AnyCable), else sent normally; document
83
+ // frames always go through `send`. `id` (reliable doc updates) is tagged onto
84
+ // the envelope so the server can ack. A no-op while disconnected: reliable
85
+ // frames stay queued in the engine and flush on the next connect().
86
+ _send(frame, id, opts) {
87
+ const sub = this.subscription;
88
+ if (!sub)
89
+ return;
90
+ const update = toBase64(frame);
91
+ const payload = id === undefined ? { update } : { update, id };
92
+ const isAwareness = opts?.awareness ?? frame[0] === MessageType.Awareness;
93
+ // Awareness rides AnyCable's whisper automatically when the subscription
94
+ // supports it (client-to-client, no server round-trip); otherwise a normal
95
+ // send the server relays. Document updates always send (recorded/acked).
96
+ if (isAwareness && typeof sub.whisper === "function")
97
+ sub.whisper(payload);
98
+ else
99
+ sub.send(payload);
100
+ }
101
+ }
102
+ //# sourceMappingURL=actioncable_provider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"actioncable_provider.js","sourceRoot":"","sources":["../src/actioncable_provider.ts"],"names":[],"mappings":"AAAA,uEAAuE;AACvE,gFAAgF;AAChF,2EAA2E;AAC3E,0EAA0E;AAC1E,8EAA8E;AAC9E,EAAE;AACF,uEAAuE;AACvE,8EAA8E;AAC9E,8EAA8E;AAC9E,uEAAuE;AACvE,mBAAmB;AACnB,EAAE;AACF,8EAA8E;AAC9E,6EAA6E;AAC7E,gDAAgD;AAChD,OAAO,EAAE,UAAU,EAAE,WAAW,EAA4C,MAAM,kBAAkB,CAAC;AACrG,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACnD,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AA+BlD,MAAM,OAAO,mBAAmB;IAS9B,YACE,GAAQ,EACR,QAAuB,EACvB,WAAmB,EACnB,gBAAwB,EAAE,EAC1B,OAAmC,EAAE;QAP/B,iBAAY,GAA6B,IAAI,CAAC;QASpD,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC;QACf,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;QAC/B,IAAI,CAAC,aAAa,GAAG,aAAa,CAAC;QACnC,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,SAAS,IAAI,IAAI,SAAS,CAAC,GAAG,CAAC,CAAC;QAEtD,IAAI,CAAC,MAAM,GAAG,IAAI,UAAU,CAAC,GAAG,EAAE;YAChC,SAAS,EAAE,IAAI,CAAC,SAAS;YACzB,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,cAAc,EAAE,IAAI,CAAC,cAAc;YACnC,qBAAqB,EAAE,IAAI,CAAC,qBAAqB;YACjD,UAAU,EAAE,IAAI,CAAC,UAAU;YAC3B,IAAI,EAAE,CAAC,KAAK,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,EAAE,EAAE,QAAQ,CAAC;SAC/D,CAAC,CAAC;IACL,CAAC;IAED,mFAAmF;IACnF,IAAI,MAAM;QACR,OAAO,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC;IAC5B,CAAC;IAED,4EAA4E;IAC5E,IAAI,UAAU;QACZ,OAAO,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC;IAChC,CAAC;IAED,OAAO;QACL,IAAI,IAAI,CAAC,YAAY;YAAE,OAAO;QAC9B,MAAM,QAAQ,GAAG,IAAI,CAAC;QACtB,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC,QAAQ,CAAC,aAAa,CAAC,MAAM,CACpD,EAAE,OAAO,EAAE,IAAI,CAAC,WAAW,EAAE,GAAG,IAAI,CAAC,aAAa,EAAE,EACpD;YACE,QAAQ,CAAC,OAAqB;gBAC5B,0DAA0D;gBAC1D,IAAI,OAAO,IAAI,OAAO,CAAC,GAAG,KAAK,SAAS,EAAE,CAAC;oBACzC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;oBACjC,OAAO;gBACT,CAAC;gBACD,MAAM,OAAO,GAAG,OAAO,IAAI,CAAC,OAAO,CAAC,CAAC,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;gBACzD,IAAI,OAAO,OAAO,KAAK,QAAQ;oBAAE,OAAO;gBACxC,MAAM,KAAK,GAAG,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC;gBAC3D,IAAI,KAAK;oBAAE,QAAQ,CAAC,KAAK,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC,CAAC,uCAAuC;YACtF,CAAC;YACD,SAAS;gBACP,QAAQ,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC,CAAC,sCAAsC;YACrE,CAAC;YACD,YAAY;gBACV,QAAQ,CAAC,MAAM,CAAC,YAAY,EAAE,CAAC,CAAC,2CAA2C;YAC7E,CAAC;SACF,CACF,CAAC;IACJ,CAAC;IAED,UAAU;QACR,IAAI,CAAC,IAAI,CAAC,YAAY;YAAE,OAAO;QAC/B,IAAI,CAAC,MAAM,CAAC,YAAY,EAAE,CAAC;QAC3B,IAAI,CAAC,QAAQ,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;QACtD,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC;IAC3B,CAAC;IAED,OAAO;QACL,IAAI,CAAC,UAAU,EAAE,CAAC;QAClB,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;IACxB,CAAC;IAED,6EAA6E;IAC7E,6EAA6E;IAC7E,8EAA8E;IAC9E,2EAA2E;IAC3E,oEAAoE;IAC5D,KAAK,CAAC,KAAiB,EAAE,EAAsB,EAAE,IAAkB;QACzE,MAAM,GAAG,GAAG,IAAI,CAAC,YAAY,CAAC;QAC9B,IAAI,CAAC,GAAG;YAAE,OAAO;QACjB,MAAM,MAAM,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;QAC/B,MAAM,OAAO,GAAG,EAAE,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC;QAC/D,MAAM,WAAW,GAAG,IAAI,EAAE,SAAS,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,WAAW,CAAC,SAAS,CAAC;QAC1E,yEAAyE;QACzE,2EAA2E;QAC3E,yEAAyE;QACzE,IAAI,WAAW,IAAI,OAAO,GAAG,CAAC,OAAO,KAAK,UAAU;YAAE,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;;YACtE,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACzB,CAAC;CACF"}
@@ -0,0 +1,3 @@
1
+ export declare const toBase64: (bytes: Uint8Array) => string;
2
+ export declare const fromBase64: (str: string) => Uint8Array;
3
+ //# sourceMappingURL=base64.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"base64.d.ts","sourceRoot":"","sources":["../src/base64.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,QAAQ,GAAI,OAAO,UAAU,KAAG,MACoB,CAAC;AAElE,eAAO,MAAM,UAAU,GAAI,KAAK,MAAM,KAAG,UAAgE,CAAC"}
package/dist/base64.js ADDED
@@ -0,0 +1,6 @@
1
+ // Convenience codecs for transports that carry binary frames as base64 strings
2
+ // (e.g. ActionCable's JSON envelope). Optional -- a binary WebSocket transport
3
+ // sends the raw frames directly and never needs these.
4
+ export const toBase64 = (bytes) => btoa(Array.from(bytes, (b) => String.fromCharCode(b)).join(""));
5
+ export const fromBase64 = (str) => Uint8Array.from(atob(str), (c) => c.charCodeAt(0));
6
+ //# sourceMappingURL=base64.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"base64.js","sourceRoot":"","sources":["../src/base64.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,+EAA+E;AAC/E,uDAAuD;AAEvD,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,KAAiB,EAAU,EAAE,CACpD,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC;AAElE,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,GAAW,EAAc,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC"}
@@ -0,0 +1,8 @@
1
+ export { ReliableSync } from "./reliable_sync.js";
2
+ export type { ReliableSyncOptions, TimerHandle } from "./reliable_sync.js";
3
+ export { SyncEngine, MessageType } from "./sync_engine.js";
4
+ export type { SyncEngineOptions, SendOptions } from "./sync_engine.js";
5
+ export { ActionCableProvider } from "./actioncable_provider.js";
6
+ export type { ActionCableProviderOptions, CableConsumer, CableSubscription } from "./actioncable_provider.js";
7
+ export { toBase64, fromBase64 } from "./base64.js";
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,YAAY,EAAE,mBAAmB,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAI3E,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC3D,YAAY,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAIvE,OAAO,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAChE,YAAY,EAAE,0BAA0B,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAG9G,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,11 @@
1
+ // Zero-dependency reliable-delivery core. Safe to import on its own.
2
+ export { ReliableSync } from "./reliable_sync.js";
3
+ // Batteries-included protocol client (sync steps + encode/decode + awareness).
4
+ // Requires `yjs` and `y-protocols` as peers.
5
+ export { SyncEngine, MessageType } from "./sync_engine.js";
6
+ // Ready-made ActionCable / AnyCable provider built on SyncEngine (with awareness
7
+ // whisper support). Bring your own provider instead by composing SyncEngine.
8
+ export { ActionCableProvider } from "./actioncable_provider.js";
9
+ // Optional base64 helpers for transports that carry frames as strings.
10
+ export { toBase64, fromBase64 } from "./base64.js";
11
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,qEAAqE;AACrE,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAGlD,+EAA+E;AAC/E,6CAA6C;AAC7C,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAG3D,iFAAiF;AACjF,6EAA6E;AAC7E,OAAO,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAGhE,uEAAuE;AACvE,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC"}
@@ -0,0 +1,78 @@
1
+ /** An opaque timer handle (number in browsers, Timeout in Node). */
2
+ export type TimerHandle = unknown;
3
+ export interface ReliableSyncOptions {
4
+ /**
5
+ * Transmit one update. `update` is the raw merged update bytes; `id` is the
6
+ * cumulative sequence to ack against (undefined once we've fallen back).
7
+ */
8
+ send: (update: Uint8Array, id: number | undefined) => void;
9
+ /** Merge an array of update byte-arrays into one (typically Y.mergeUpdates). */
10
+ merge: (updates: Uint8Array[]) => Uint8Array;
11
+ /** Milliseconds between retransmits of the unacked tail (default 1000). */
12
+ resendInterval?: number;
13
+ /**
14
+ * Number of resends with no ack before deciding the server doesn't support
15
+ * reliable delivery and falling back to fire-and-forget (default 8).
16
+ */
17
+ maxUnconfirmedResends?: number;
18
+ /** Called once if that fallback trips. */
19
+ onFallback?: () => void;
20
+ /** Injectable timer hooks (default to globals); handy for tests. */
21
+ setInterval?: (handler: () => void, ms: number) => TimerHandle;
22
+ clearInterval?: (handle: TimerHandle) => void;
23
+ }
24
+ interface Pending {
25
+ seq: number;
26
+ update: Uint8Array;
27
+ }
28
+ export declare class ReliableSync {
29
+ /** False after the no-ack fallback trips; updates then go fire-and-forget. */
30
+ reliable: boolean;
31
+ /** Unacked local updates, in order. */
32
+ pending: Pending[];
33
+ private _send;
34
+ private _merge;
35
+ private resendInterval;
36
+ private maxUnconfirmedResends;
37
+ private _onFallback?;
38
+ private _setInterval;
39
+ private _clearInterval;
40
+ private nextSeq;
41
+ private everAcked;
42
+ private _resendsSinceProgress;
43
+ private _connected;
44
+ private _timer;
45
+ constructor(opts: ReliableSyncOptions);
46
+ /** True while there are unacknowledged local updates. */
47
+ get hasPending(): boolean;
48
+ /**
49
+ * Record a local document update. While reliable, it's queued and the unacked
50
+ * tail is flushed; once we've fallen back, it's sent fire-and-forget.
51
+ */
52
+ enqueue(update: Uint8Array): void;
53
+ /**
54
+ * Send the whole unacked tail as one merged delta. The id is the highest seq
55
+ * in the batch, so a single { ack } cumulatively confirms everything up to it.
56
+ * No-op while disconnected (the tail is replayed on the next onConnect).
57
+ */
58
+ flush(): void;
59
+ /** Confirm delivery up to `id`: prune every queued update with seq <= id. */
60
+ onAck(id: number): void;
61
+ /** Transport (re)connected: replay the unacked tail and resume retransmits. */
62
+ onConnect(): void;
63
+ /** Transport dropped: keep the queue (for reconnect replay), pause the timer. */
64
+ onDisconnect(): void;
65
+ /**
66
+ * One retransmit tick. Exposed for deterministic testing; normally driven by
67
+ * the internal timer. If we keep resending on a live connection and never get
68
+ * an ack, the server doesn't support reliable delivery, so fall back to
69
+ * fire-and-forget (and stop tracking, since idempotent CRDT sync covers it).
70
+ */
71
+ onTick(): void;
72
+ /** Stop timers and drop references. Call when the provider is destroyed. */
73
+ destroy(): void;
74
+ private _startTimer;
75
+ private _stopTimer;
76
+ }
77
+ export {};
78
+ //# sourceMappingURL=reliable_sync.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reliable_sync.d.ts","sourceRoot":"","sources":["../src/reliable_sync.ts"],"names":[],"mappings":"AAqBA,oEAAoE;AACpE,MAAM,MAAM,WAAW,GAAG,OAAO,CAAC;AAElC,MAAM,WAAW,mBAAmB;IAClC;;;OAGG;IACH,IAAI,EAAE,CAAC,MAAM,EAAE,UAAU,EAAE,EAAE,EAAE,MAAM,GAAG,SAAS,KAAK,IAAI,CAAC;IAC3D,gFAAgF;IAChF,KAAK,EAAE,CAAC,OAAO,EAAE,UAAU,EAAE,KAAK,UAAU,CAAC;IAC7C,2EAA2E;IAC3E,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,0CAA0C;IAC1C,UAAU,CAAC,EAAE,MAAM,IAAI,CAAC;IACxB,oEAAoE;IACpE,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,IAAI,EAAE,EAAE,EAAE,MAAM,KAAK,WAAW,CAAC;IAC/D,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,CAAC;CAC/C;AAID,UAAU,OAAO;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,UAAU,CAAC;CACpB;AAED,qBAAa,YAAY;IACvB,8EAA8E;IAC9E,QAAQ,UAAQ;IAChB,uCAAuC;IACvC,OAAO,EAAE,OAAO,EAAE,CAAM;IAExB,OAAO,CAAC,KAAK,CAA8B;IAC3C,OAAO,CAAC,MAAM,CAA+B;IAC7C,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,qBAAqB,CAAS;IACtC,OAAO,CAAC,WAAW,CAAC,CAAa;IACjC,OAAO,CAAC,YAAY,CAAmD;IACvE,OAAO,CAAC,cAAc,CAAgC;IAEtD,OAAO,CAAC,OAAO,CAAK;IACpB,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,qBAAqB,CAAK;IAClC,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,MAAM,CAAsC;gBAExC,IAAI,EAAE,mBAAmB;IAerC,yDAAyD;IACzD,IAAI,UAAU,IAAI,OAAO,CAExB;IAED;;;OAGG;IACH,OAAO,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI;IASjC;;;;OAIG;IACH,KAAK,IAAI,IAAI;IAQb,6EAA6E;IAC7E,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAMvB,+EAA+E;IAC/E,SAAS,IAAI,IAAI;IAMjB,iFAAiF;IACjF,YAAY,IAAI,IAAI;IAKpB;;;;;OAKG;IACH,MAAM,IAAI,IAAI;IAYd,4EAA4E;IAC5E,OAAO,IAAI,IAAI;IAKf,OAAO,CAAC,WAAW;IAOnB,OAAO,CAAC,UAAU;CAInB"}
@@ -0,0 +1,130 @@
1
+ // Transport-agnostic reliable-delivery core for the yrb-lite y-websocket
2
+ // protocol. This owns the "nuances" a provider would otherwise re-implement:
3
+ // an ack-tracked queue of unacknowledged local updates, "sync since last ack"
4
+ // (the unacked tail is sent as one MERGED, causally-complete delta so the server
5
+ // never sees an internal gap), cumulative acks, periodic retransmit with a
6
+ // "server doesn't support acks" fallback, and reconnect replay.
7
+ //
8
+ // It does NOT touch any transport, Yjs binding, or wire encoding. You inject:
9
+ // - send(update, id): transmit one update. `update` is the raw merged update
10
+ // bytes; `id` is the cumulative sequence (or undefined,
11
+ // post-fallback). Frame + base64 + put it on your socket.
12
+ // - merge(updates): merge an array of update byte-arrays into one
13
+ // (typically Y.mergeUpdates from yjs).
14
+ // and you drive it from your provider's lifecycle:
15
+ // - enqueue(update) on every local document update (not server echoes)
16
+ // - onAck(id) when an { ack: id } frame arrives
17
+ // - onConnect()/onDisconnect() on transport (re)connect / drop
18
+ //
19
+ // Awareness/presence is intentionally out of scope -- it stays fire-and-forget
20
+ // in the provider.
21
+ const DEFAULTS = { resendInterval: 1000, maxUnconfirmedResends: 8 };
22
+ export class ReliableSync {
23
+ constructor(opts) {
24
+ /** False after the no-ack fallback trips; updates then go fire-and-forget. */
25
+ this.reliable = true;
26
+ /** Unacked local updates, in order. */
27
+ this.pending = [];
28
+ this.nextSeq = 1;
29
+ this.everAcked = false;
30
+ this._resendsSinceProgress = 0;
31
+ this._connected = false;
32
+ this._timer = undefined;
33
+ const { send, merge, resendInterval, maxUnconfirmedResends, onFallback } = opts ?? {};
34
+ if (typeof send !== "function")
35
+ throw new TypeError("ReliableSync requires a send(update, id) function");
36
+ if (typeof merge !== "function")
37
+ throw new TypeError("ReliableSync requires a merge(updates) function");
38
+ this._send = send;
39
+ this._merge = merge;
40
+ this.resendInterval = resendInterval ?? DEFAULTS.resendInterval;
41
+ this.maxUnconfirmedResends = maxUnconfirmedResends ?? DEFAULTS.maxUnconfirmedResends;
42
+ this._onFallback = onFallback;
43
+ // Injectable timer hooks make the resend loop testable; default to globals.
44
+ this._setInterval = opts.setInterval ?? ((fn, ms) => setInterval(fn, ms));
45
+ this._clearInterval = opts.clearInterval ?? ((h) => clearInterval(h));
46
+ }
47
+ /** True while there are unacknowledged local updates. */
48
+ get hasPending() {
49
+ return this.pending.length > 0;
50
+ }
51
+ /**
52
+ * Record a local document update. While reliable, it's queued and the unacked
53
+ * tail is flushed; once we've fallen back, it's sent fire-and-forget.
54
+ */
55
+ enqueue(update) {
56
+ if (!this.reliable) {
57
+ this._send(update, undefined);
58
+ return;
59
+ }
60
+ this.pending.push({ seq: this.nextSeq++, update });
61
+ this.flush();
62
+ }
63
+ /**
64
+ * Send the whole unacked tail as one merged delta. The id is the highest seq
65
+ * in the batch, so a single { ack } cumulatively confirms everything up to it.
66
+ * No-op while disconnected (the tail is replayed on the next onConnect).
67
+ */
68
+ flush() {
69
+ if (!this._connected || this.pending.length === 0)
70
+ return;
71
+ const updates = this.pending.map((p) => p.update);
72
+ const merged = updates.length === 1 ? updates[0] : this._merge(updates);
73
+ const id = this.pending[this.pending.length - 1].seq;
74
+ this._send(merged, id);
75
+ }
76
+ /** Confirm delivery up to `id`: prune every queued update with seq <= id. */
77
+ onAck(id) {
78
+ this.everAcked = true;
79
+ this._resendsSinceProgress = 0;
80
+ this.pending = this.pending.filter((p) => p.seq > id);
81
+ }
82
+ /** Transport (re)connected: replay the unacked tail and resume retransmits. */
83
+ onConnect() {
84
+ this._connected = true;
85
+ this.flush();
86
+ this._startTimer();
87
+ }
88
+ /** Transport dropped: keep the queue (for reconnect replay), pause the timer. */
89
+ onDisconnect() {
90
+ this._connected = false;
91
+ this._stopTimer();
92
+ }
93
+ /**
94
+ * One retransmit tick. Exposed for deterministic testing; normally driven by
95
+ * the internal timer. If we keep resending on a live connection and never get
96
+ * an ack, the server doesn't support reliable delivery, so fall back to
97
+ * fire-and-forget (and stop tracking, since idempotent CRDT sync covers it).
98
+ */
99
+ onTick() {
100
+ if (!this._connected || this.pending.length === 0)
101
+ return;
102
+ if (!this.everAcked && ++this._resendsSinceProgress > this.maxUnconfirmedResends) {
103
+ this.reliable = false;
104
+ this.pending = [];
105
+ this._stopTimer();
106
+ this._onFallback?.();
107
+ return;
108
+ }
109
+ this.flush();
110
+ }
111
+ /** Stop timers and drop references. Call when the provider is destroyed. */
112
+ destroy() {
113
+ this._stopTimer();
114
+ this.pending = [];
115
+ }
116
+ _startTimer() {
117
+ if (this._timer !== undefined || !this.reliable)
118
+ return;
119
+ this._timer = this._setInterval(() => this.onTick(), this.resendInterval);
120
+ const t = this._timer;
121
+ if (t && typeof t.unref === "function")
122
+ t.unref();
123
+ }
124
+ _stopTimer() {
125
+ if (this._timer !== undefined)
126
+ this._clearInterval(this._timer);
127
+ this._timer = undefined;
128
+ }
129
+ }
130
+ //# sourceMappingURL=reliable_sync.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reliable_sync.js","sourceRoot":"","sources":["../src/reliable_sync.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,73 @@
1
+ import { type Doc } from "yjs";
2
+ import { type Awareness } from "y-protocols/awareness";
3
+ import { type TimerHandle } from "./reliable_sync.js";
4
+ export declare const MessageType: {
5
+ readonly Sync: 0;
6
+ readonly Awareness: 1;
7
+ readonly Auth: 2;
8
+ readonly QueryAwareness: 3;
9
+ };
10
+ /** Hints about an outgoing frame, so the transport can route it appropriately. */
11
+ export interface SendOptions {
12
+ /**
13
+ * True for awareness/presence frames. These are ephemeral and fire-and-forget,
14
+ * so a transport that supports it (e.g. AnyCable `whisper`) can broadcast them
15
+ * client-to-client without a server round-trip. Transports without that just
16
+ * send normally.
17
+ */
18
+ awareness?: boolean;
19
+ }
20
+ export interface SyncEngineOptions {
21
+ /**
22
+ * Transmit one raw protocol frame. `id` is set only for reliable document
23
+ * updates (tag it onto your envelope so the server can ack). `opts.awareness`
24
+ * marks presence frames so the transport can whisper them where supported.
25
+ */
26
+ send: (frame: Uint8Array, id: number | undefined, opts?: SendOptions) => void;
27
+ /** Optional awareness/presence. When omitted, awareness frames are ignored. */
28
+ awareness?: Awareness | null;
29
+ /** Use ack-tracked reliable delivery (default true). */
30
+ reliable?: boolean;
31
+ /** Forwarded to ReliableSync. */
32
+ resendInterval?: number;
33
+ /** Forwarded to ReliableSync. */
34
+ maxUnconfirmedResends?: number;
35
+ /** Forwarded to ReliableSync. */
36
+ onFallback?: () => void;
37
+ /** Injectable timer hooks (forwarded to ReliableSync); handy for tests. */
38
+ setInterval?: (handler: () => void, ms: number) => TimerHandle;
39
+ clearInterval?: (handle: TimerHandle) => void;
40
+ }
41
+ export declare class SyncEngine {
42
+ readonly doc: Doc;
43
+ readonly awareness: Awareness | null;
44
+ reliable: boolean;
45
+ private _send;
46
+ private _synced;
47
+ private _delivery;
48
+ private _onDocUpdate;
49
+ private _onAwarenessUpdate?;
50
+ constructor(doc: Doc, opts: SyncEngineOptions);
51
+ /** True once we've received the server's SyncStep2 (the document is caught up). */
52
+ get synced(): boolean;
53
+ /** True while there are unacknowledged local document updates in flight. */
54
+ get hasPending(): boolean;
55
+ /** Transport connected: send the opening handshake and replay the unacked tail. */
56
+ onConnect(): void;
57
+ /** Transport dropped: pause retransmits (queue kept) and clear remote presence. */
58
+ onDisconnect(): void;
59
+ /** A reliable-delivery `{ ack: id }` envelope arrived. */
60
+ ack(id: number): void;
61
+ /**
62
+ * Decode and apply one incoming binary protocol frame (document sync, awareness,
63
+ * query, or auth). Returns a reply frame to transmit (e.g. SyncStep2 answering a
64
+ * SyncStep1, or an awareness reply to a query), or null if there's nothing to send.
65
+ */
66
+ receive(frame: Uint8Array): Uint8Array | null;
67
+ /** Detach doc/awareness listeners and stop retransmits. */
68
+ destroy(): void;
69
+ private _frameSyncStep1;
70
+ private _frameUpdate;
71
+ private _frameAwareness;
72
+ }
73
+ //# sourceMappingURL=sync_engine.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync_engine.d.ts","sourceRoot":"","sources":["../src/sync_engine.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAgB,KAAK,GAAG,EAAE,MAAM,KAAK,CAAC;AAI7C,OAAO,EAIL,KAAK,SAAS,EACf,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EAAgB,KAAK,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEpE,eAAO,MAAM,WAAW;;;;;CAAiE,CAAC;AAE1F,kFAAkF;AAClF,MAAM,WAAW,WAAW;IAC1B;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,iBAAiB;IAChC;;;;OAIG;IACH,IAAI,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,EAAE,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,IAAI,CAAC;IAC9E,+EAA+E;IAC/E,SAAS,CAAC,EAAE,SAAS,GAAG,IAAI,CAAC;IAC7B,wDAAwD;IACxD,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,iCAAiC;IACjC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,iCAAiC;IACjC,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,iCAAiC;IACjC,UAAU,CAAC,EAAE,MAAM,IAAI,CAAC;IACxB,2EAA2E;IAC3E,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,IAAI,EAAE,EAAE,EAAE,MAAM,KAAK,WAAW,CAAC;IAC/D,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,CAAC;CAC/C;AAID,qBAAa,UAAU;IACrB,QAAQ,CAAC,GAAG,EAAE,GAAG,CAAC;IAClB,QAAQ,CAAC,SAAS,EAAE,SAAS,GAAG,IAAI,CAAC;IACrC,QAAQ,EAAE,OAAO,CAAC;IAElB,OAAO,CAAC,KAAK,CAA4B;IACzC,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,SAAS,CAAe;IAChC,OAAO,CAAC,YAAY,CAAgD;IACpE,OAAO,CAAC,kBAAkB,CAAC,CAAqD;gBAEpE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,iBAAiB;IA6C7C,mFAAmF;IACnF,IAAI,MAAM,IAAI,OAAO,CAEpB;IAED,4EAA4E;IAC5E,IAAI,UAAU,IAAI,OAAO,CAExB;IAED,mFAAmF;IACnF,SAAS,IAAI,IAAI;IAQjB,mFAAmF;IACnF,YAAY,IAAI,IAAI;IASpB,0DAA0D;IAC1D,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAIrB;;;;OAIG;IACH,OAAO,CAAC,KAAK,EAAE,UAAU,GAAG,UAAU,GAAG,IAAI;IA+B7C,2DAA2D;IAC3D,OAAO,IAAI,IAAI;IAMf,OAAO,CAAC,eAAe;IAOvB,OAAO,CAAC,YAAY;IAOpB,OAAO,CAAC,eAAe;CAMxB"}
@@ -0,0 +1,155 @@
1
+ // A batteries-included client core for the yrb-lite y-websocket protocol.
2
+ //
3
+ // SyncEngine composes ReliableSync and additionally owns the parts a provider
4
+ // would otherwise re-implement: the y-protocols message framing (encode/decode),
5
+ // the sync-step handshake (SyncStep1 / SyncStep2 / Update), and awareness
6
+ // encode/apply. It binds to a Y.Doc (and optional Awareness) and speaks in raw
7
+ // Uint8Array frames -- you bring only the transport: base64 + the
8
+ // `{ update, id }` / `{ ack }` envelope and the socket.
9
+ //
10
+ // Drive it from your transport:
11
+ // onConnect() -> sends the opening handshake, replays the unacked tail
12
+ // onDisconnect() -> pauses retransmits, clears remote presence
13
+ // ack(id) -> a `{ ack: id }` envelope arrived
14
+ // const reply = receive(frame) -> a binary protocol frame arrived; send `reply` if non-null
15
+ // Local document edits and awareness changes are picked up automatically via the
16
+ // doc's / awareness's "update" events.
17
+ import { mergeUpdates } from "yjs";
18
+ import * as encoding from "lib0/encoding";
19
+ import * as decoding from "lib0/decoding";
20
+ import { readSyncMessage, writeSyncStep1, writeUpdate, messageYjsSyncStep2 } from "y-protocols/sync";
21
+ import { encodeAwarenessUpdate, applyAwarenessUpdate, removeAwarenessStates, } from "y-protocols/awareness";
22
+ import { readAuthMessage } from "y-protocols/auth";
23
+ import { ReliableSync } from "./reliable_sync.js";
24
+ export const MessageType = { Sync: 0, Awareness: 1, Auth: 2, QueryAwareness: 3 };
25
+ export class SyncEngine {
26
+ constructor(doc, opts) {
27
+ this._synced = false;
28
+ const { send, awareness = null, reliable = true, resendInterval, maxUnconfirmedResends, onFallback, setInterval: setIntervalFn, clearInterval: clearIntervalFn, } = opts ?? {};
29
+ if (!doc)
30
+ throw new TypeError("SyncEngine requires a Y.Doc");
31
+ if (typeof send !== "function")
32
+ throw new TypeError("SyncEngine requires a send(frame, id) function");
33
+ this.doc = doc;
34
+ this.awareness = awareness;
35
+ this.reliable = reliable;
36
+ this._send = send;
37
+ this._delivery = new ReliableSync({
38
+ merge: mergeUpdates,
39
+ send: (update, id) => this._send(this._frameUpdate(update), id),
40
+ resendInterval,
41
+ maxUnconfirmedResends,
42
+ onFallback,
43
+ setInterval: setIntervalFn,
44
+ clearInterval: clearIntervalFn,
45
+ });
46
+ this._onDocUpdate = (update, origin) => {
47
+ if (origin === this)
48
+ return; // applied from the server; don't echo it back
49
+ if (this.reliable && this._delivery.reliable)
50
+ this._delivery.enqueue(update);
51
+ else
52
+ this._send(this._frameUpdate(update), undefined);
53
+ };
54
+ this.doc.on("update", this._onDocUpdate);
55
+ if (this.awareness) {
56
+ this._onAwarenessUpdate = ({ added, updated, removed }) => {
57
+ const changed = added.concat(updated, removed);
58
+ this._send(this._frameAwareness(changed), undefined, { awareness: true }); // fire-and-forget
59
+ };
60
+ this.awareness.on("update", this._onAwarenessUpdate);
61
+ }
62
+ }
63
+ /** True once we've received the server's SyncStep2 (the document is caught up). */
64
+ get synced() {
65
+ return this._synced;
66
+ }
67
+ /** True while there are unacknowledged local document updates in flight. */
68
+ get hasPending() {
69
+ return this._delivery.hasPending;
70
+ }
71
+ /** Transport connected: send the opening handshake and replay the unacked tail. */
72
+ onConnect() {
73
+ this._send(this._frameSyncStep1(), undefined);
74
+ if (this.awareness && this.awareness.getLocalState() !== null) {
75
+ this._send(this._frameAwareness([this.doc.clientID]), undefined, { awareness: true });
76
+ }
77
+ if (this.reliable)
78
+ this._delivery.onConnect();
79
+ }
80
+ /** Transport dropped: pause retransmits (queue kept) and clear remote presence. */
81
+ onDisconnect() {
82
+ this._synced = false;
83
+ this._delivery.onDisconnect();
84
+ if (this.awareness) {
85
+ const remote = [...this.awareness.getStates().keys()].filter((c) => c !== this.doc.clientID);
86
+ if (remote.length)
87
+ removeAwarenessStates(this.awareness, remote, this);
88
+ }
89
+ }
90
+ /** A reliable-delivery `{ ack: id }` envelope arrived. */
91
+ ack(id) {
92
+ this._delivery.onAck(id);
93
+ }
94
+ /**
95
+ * Decode and apply one incoming binary protocol frame (document sync, awareness,
96
+ * query, or auth). Returns a reply frame to transmit (e.g. SyncStep2 answering a
97
+ * SyncStep1, or an awareness reply to a query), or null if there's nothing to send.
98
+ */
99
+ receive(frame) {
100
+ const decoder = decoding.createDecoder(frame);
101
+ const encoder = encoding.createEncoder();
102
+ const type = decoding.readVarUint(decoder);
103
+ switch (type) {
104
+ case MessageType.Sync: {
105
+ encoding.writeVarUint(encoder, MessageType.Sync);
106
+ const syncType = readSyncMessage(decoder, encoder, this.doc, this);
107
+ if (!this._synced && syncType === messageYjsSyncStep2)
108
+ this._synced = true;
109
+ break;
110
+ }
111
+ case MessageType.Awareness:
112
+ if (this.awareness)
113
+ applyAwarenessUpdate(this.awareness, decoding.readVarUint8Array(decoder), this);
114
+ return null;
115
+ case MessageType.QueryAwareness:
116
+ if (!this.awareness)
117
+ return null;
118
+ encoding.writeVarUint(encoder, MessageType.Awareness);
119
+ encoding.writeVarUint8Array(encoder, encodeAwarenessUpdate(this.awareness, [...this.awareness.getStates().keys()]));
120
+ break;
121
+ case MessageType.Auth:
122
+ readAuthMessage(decoder, this.doc, (_doc, reason) => console.warn(`[yrb-lite] auth denied: ${reason}`));
123
+ return null;
124
+ default:
125
+ return null;
126
+ }
127
+ return encoding.length(encoder) > 1 ? encoding.toUint8Array(encoder) : null;
128
+ }
129
+ /** Detach doc/awareness listeners and stop retransmits. */
130
+ destroy() {
131
+ this.doc.off("update", this._onDocUpdate);
132
+ if (this.awareness && this._onAwarenessUpdate)
133
+ this.awareness.off("update", this._onAwarenessUpdate);
134
+ this._delivery.destroy();
135
+ }
136
+ _frameSyncStep1() {
137
+ const e = encoding.createEncoder();
138
+ encoding.writeVarUint(e, MessageType.Sync);
139
+ writeSyncStep1(e, this.doc);
140
+ return encoding.toUint8Array(e);
141
+ }
142
+ _frameUpdate(update) {
143
+ const e = encoding.createEncoder();
144
+ encoding.writeVarUint(e, MessageType.Sync);
145
+ writeUpdate(e, update);
146
+ return encoding.toUint8Array(e);
147
+ }
148
+ _frameAwareness(clients) {
149
+ const e = encoding.createEncoder();
150
+ encoding.writeVarUint(e, MessageType.Awareness);
151
+ encoding.writeVarUint8Array(e, encodeAwarenessUpdate(this.awareness, clients));
152
+ return encoding.toUint8Array(e);
153
+ }
154
+ }
155
+ //# sourceMappingURL=sync_engine.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync_engine.js","sourceRoot":"","sources":["../src/sync_engine.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "yrb-lite-client",
3
+ "version": "0.1.0",
4
+ "description": "JavaScript client for the yrb-lite y-websocket protocol: a ready-made ActionCable/AnyCable provider, a transport-agnostic sync engine (sync steps, encode/decode, awareness), and a reliable-delivery core (ack-tracked queue, sync-since-last-ack, retransmit + reconnect replay). Written in TypeScript with bundled types; usable from plain JS.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "default": "./dist/index.js"
13
+ },
14
+ "./reliable": {
15
+ "types": "./dist/reliable_sync.d.ts",
16
+ "default": "./dist/reliable_sync.js"
17
+ },
18
+ "./base64": {
19
+ "types": "./dist/base64.d.ts",
20
+ "default": "./dist/base64.js"
21
+ }
22
+ },
23
+ "files": [
24
+ "dist",
25
+ "README.md"
26
+ ],
27
+ "scripts": {
28
+ "build": "tsc",
29
+ "typecheck": "tsc --noEmit",
30
+ "prepack": "npm run build",
31
+ "test": "npm run build && node --test"
32
+ },
33
+ "license": "MIT",
34
+ "author": "JP Camara <jp@jpcamara.com>",
35
+ "homepage": "https://github.com/jpcamara/yrb-lite/tree/main/packages/yrb-lite-client#readme",
36
+ "bugs": {
37
+ "url": "https://github.com/jpcamara/yrb-lite/issues"
38
+ },
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "git+https://github.com/jpcamara/yrb-lite.git",
42
+ "directory": "packages/yrb-lite-client"
43
+ },
44
+ "keywords": [
45
+ "yjs",
46
+ "crdt",
47
+ "yrb-lite",
48
+ "reliable-delivery",
49
+ "actioncable",
50
+ "y-websocket",
51
+ "typescript"
52
+ ],
53
+ "publishConfig": {
54
+ "access": "public"
55
+ },
56
+ "peerDependencies": {
57
+ "yjs": "^13.6.0",
58
+ "y-protocols": "^1.0.5"
59
+ },
60
+ "peerDependenciesMeta": {
61
+ "yjs": {
62
+ "optional": true
63
+ },
64
+ "y-protocols": {
65
+ "optional": true
66
+ }
67
+ },
68
+ "devDependencies": {
69
+ "yjs": "^13.6.0",
70
+ "y-protocols": "^1.0.5",
71
+ "typescript": "^5.6.0"
72
+ }
73
+ }