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 +126 -0
- package/dist/actioncable_provider.d.ts +40 -0
- package/dist/actioncable_provider.d.ts.map +1 -0
- package/dist/actioncable_provider.js +102 -0
- package/dist/actioncable_provider.js.map +1 -0
- package/dist/base64.d.ts +3 -0
- package/dist/base64.d.ts.map +1 -0
- package/dist/base64.js +6 -0
- package/dist/base64.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/reliable_sync.d.ts +78 -0
- package/dist/reliable_sync.d.ts.map +1 -0
- package/dist/reliable_sync.js +130 -0
- package/dist/reliable_sync.js.map +1 -0
- package/dist/sync_engine.d.ts +73 -0
- package/dist/sync_engine.d.ts.map +1 -0
- package/dist/sync_engine.js +155 -0
- package/dist/sync_engine.js.map +1 -0
- package/package.json +73 -0
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":"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"}
|
package/dist/base64.d.ts
ADDED
|
@@ -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"}
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|