yrby-client 0.4.3 → 0.6.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 +250 -36
- package/dist/actioncable_provider.d.ts +26 -1
- package/dist/actioncable_provider.d.ts.map +1 -1
- package/dist/actioncable_provider.js +295 -150
- package/dist/actioncable_provider.js.map +1 -1
- package/dist/cjs/actioncable_provider.d.ts +26 -1
- package/dist/cjs/actioncable_provider.js +296 -151
- package/dist/cjs/document_element.d.ts +24 -0
- package/dist/cjs/document_element.js +260 -0
- package/dist/cjs/document_session.d.ts +62 -0
- package/dist/cjs/document_session.js +335 -0
- package/dist/cjs/index.d.ts +2 -0
- package/dist/cjs/index.js +4 -1
- package/dist/cjs/reliable_sync.d.ts +18 -31
- package/dist/cjs/reliable_sync.js +128 -98
- package/dist/cjs/turbo_adapter.d.ts +9 -0
- package/dist/cjs/turbo_adapter.js +81 -0
- package/dist/cjs/y_protocol_session.d.ts +7 -14
- package/dist/cjs/y_protocol_session.js +97 -92
- package/dist/document_element.d.ts +25 -0
- package/dist/document_element.d.ts.map +1 -0
- package/dist/document_element.js +225 -0
- package/dist/document_element.js.map +1 -0
- package/dist/document_session.d.ts +63 -0
- package/dist/document_session.d.ts.map +1 -0
- package/dist/document_session.js +296 -0
- package/dist/document_session.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/reliable_sync.d.ts +18 -31
- package/dist/reliable_sync.d.ts.map +1 -1
- package/dist/reliable_sync.js +128 -98
- package/dist/reliable_sync.js.map +1 -1
- package/dist/turbo_adapter.d.ts +10 -0
- package/dist/turbo_adapter.d.ts.map +1 -0
- package/dist/turbo_adapter.js +78 -0
- package/dist/turbo_adapter.js.map +1 -0
- package/dist/y_protocol_session.d.ts +7 -14
- package/dist/y_protocol_session.d.ts.map +1 -1
- package/dist/y_protocol_session.js +97 -92
- package/dist/y_protocol_session.js.map +1 -1
- package/package.json +29 -6
|
@@ -40,10 +40,10 @@ exports.YProtocolSession = exports.MessageType = void 0;
|
|
|
40
40
|
// Awareness); it works in raw Uint8Array frames and leaves the transport to the
|
|
41
41
|
// caller: base64, the { update, id } / { ack } envelope, and a socket.
|
|
42
42
|
//
|
|
43
|
-
// Call
|
|
44
|
-
//
|
|
45
|
-
// returns a reply to send, or null).
|
|
46
|
-
//
|
|
43
|
+
// Call resume() when the transport connects, pause() when it drops,
|
|
44
|
+
// acknowledge(id) on an { ack } envelope, and receive(frame) for an inbound
|
|
45
|
+
// frame (it returns a reply to send, or null). The session sends local doc and
|
|
46
|
+
// awareness edits from their "update" events.
|
|
47
47
|
const yjs_1 = require("yjs");
|
|
48
48
|
const encoding = __importStar(require("lib0/encoding"));
|
|
49
49
|
const decoding = __importStar(require("lib0/decoding"));
|
|
@@ -58,12 +58,12 @@ class YProtocolSession {
|
|
|
58
58
|
awareness;
|
|
59
59
|
#send;
|
|
60
60
|
#onError;
|
|
61
|
-
#
|
|
61
|
+
#state = { phase: "unsynced", cycle: {} };
|
|
62
62
|
#delivery;
|
|
63
63
|
#onDocUpdate;
|
|
64
64
|
#onAwarenessUpdate;
|
|
65
65
|
constructor(doc, opts) {
|
|
66
|
-
const { send, awareness = null, resendInterval, onError, setInterval:
|
|
66
|
+
const { send, awareness = null, resendInterval, onError, setInterval: setTimer, clearInterval: clearTimer } = opts ?? {};
|
|
67
67
|
if (!doc)
|
|
68
68
|
throw new TypeError("YProtocolSession requires a Y.Doc");
|
|
69
69
|
if (typeof send !== "function")
|
|
@@ -76,8 +76,8 @@ class YProtocolSession {
|
|
|
76
76
|
merge: yjs_1.mergeUpdates,
|
|
77
77
|
send: (update, id) => this.#send(this.#frameUpdate(update), id),
|
|
78
78
|
resendInterval,
|
|
79
|
-
setInterval:
|
|
80
|
-
clearInterval:
|
|
79
|
+
setInterval: setTimer,
|
|
80
|
+
clearInterval: clearTimer,
|
|
81
81
|
});
|
|
82
82
|
this.#onDocUpdate = (update, origin) => {
|
|
83
83
|
if (origin === this)
|
|
@@ -88,10 +88,10 @@ class YProtocolSession {
|
|
|
88
88
|
if (this.awareness) {
|
|
89
89
|
this.#onAwarenessUpdate = ({ added, updated, removed }, origin) => {
|
|
90
90
|
// Only broadcast our own presence changes. Updates applied from a peer,
|
|
91
|
-
// and our own remote-cleanup in
|
|
91
|
+
// and our own remote-cleanup in pause(), carry origin === this;
|
|
92
92
|
// re-sending those would echo presence and broadcast tombstones for
|
|
93
93
|
// other clients' cursors.
|
|
94
|
-
if (origin === this)
|
|
94
|
+
if (origin === this || this.#state.phase === "destroyed")
|
|
95
95
|
return;
|
|
96
96
|
const changed = added.concat(updated, removed);
|
|
97
97
|
this.#send(this.#frameAwareness(changed), undefined); // fire-and-forget
|
|
@@ -101,26 +101,35 @@ class YProtocolSession {
|
|
|
101
101
|
}
|
|
102
102
|
/** True once we've received the server's SyncStep2 (the document is caught up). */
|
|
103
103
|
get synced() {
|
|
104
|
-
return this.#synced;
|
|
104
|
+
return this.#state.phase === "synced";
|
|
105
105
|
}
|
|
106
106
|
/** True while there are unacknowledged local document updates in flight. */
|
|
107
107
|
get hasPending() {
|
|
108
108
|
return this.#delivery.hasPending;
|
|
109
109
|
}
|
|
110
|
-
/**
|
|
111
|
-
|
|
110
|
+
/** Call when the transport is up. Sends the opening handshake, re-announces presence, and replays the unacked tail. */
|
|
111
|
+
resume() {
|
|
112
|
+
if (this.#state.phase === "destroyed")
|
|
113
|
+
return;
|
|
114
|
+
const cycle = {};
|
|
115
|
+
this.#state = { phase: "unsynced", cycle };
|
|
112
116
|
this.#send(this.#frameSyncStep1(), undefined);
|
|
117
|
+
if (!this.#current(cycle))
|
|
118
|
+
return;
|
|
113
119
|
if (this.awareness && this.awareness.getLocalState() !== null) {
|
|
114
120
|
this.#send(this.#frameAwareness([this.doc.clientID]), undefined);
|
|
115
121
|
}
|
|
116
|
-
this.#
|
|
122
|
+
if (this.#current(cycle))
|
|
123
|
+
this.#delivery.resume();
|
|
117
124
|
}
|
|
118
|
-
/**
|
|
119
|
-
|
|
120
|
-
this.#
|
|
121
|
-
|
|
125
|
+
/** Call when the transport is down. Keeps the queue, stops retransmits, and clears peers' presence. */
|
|
126
|
+
pause() {
|
|
127
|
+
if (this.#state.phase === "destroyed")
|
|
128
|
+
return;
|
|
129
|
+
this.#state = { phase: "unsynced", cycle: {} };
|
|
130
|
+
this.#delivery.pause();
|
|
122
131
|
if (this.awareness) {
|
|
123
|
-
const remote = [...this.awareness.getStates().keys()].filter(
|
|
132
|
+
const remote = [...this.awareness.getStates().keys()].filter(c => c !== this.doc.clientID);
|
|
124
133
|
if (remote.length)
|
|
125
134
|
(0, awareness_1.removeAwarenessStates)(this.awareness, remote, this);
|
|
126
135
|
}
|
|
@@ -132,23 +141,13 @@ class YProtocolSession {
|
|
|
132
141
|
* waiting for the awareness timeout. A no-op when there's no local state.
|
|
133
142
|
*/
|
|
134
143
|
removeLocalAwareness() {
|
|
135
|
-
if (this.awareness && this.awareness.getLocalState() !== null) {
|
|
144
|
+
if (this.#state.phase !== "destroyed" && this.awareness && this.awareness.getLocalState() !== null) {
|
|
136
145
|
this.awareness.setLocalState(null); // fires "update" -> sends the removal frame
|
|
137
146
|
}
|
|
138
147
|
}
|
|
139
|
-
/**
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
* unhealable causal gap after repeated resyncs). The queue is pruned either
|
|
143
|
-
* way — retransmitting an unhealable update would loop forever — but a
|
|
144
|
-
* dropped settle is surfaced via onError ("ack-dropped") so the app can warn
|
|
145
|
-
* or reload instead of silently reporting synced over lost data.
|
|
146
|
-
*/
|
|
147
|
-
ack(id, dropped = false) {
|
|
148
|
-
this.#delivery.onAck(id);
|
|
149
|
-
if (dropped) {
|
|
150
|
-
this.#onError(new Error(`server dropped update ${id} as an unhealable causal gap; it was settled but NOT recorded`), "ack-dropped");
|
|
151
|
-
}
|
|
148
|
+
/** A reliable-delivery `{ ack: id }` envelope arrived. */
|
|
149
|
+
acknowledge(id) {
|
|
150
|
+
this.#delivery.acknowledge(id);
|
|
152
151
|
}
|
|
153
152
|
/**
|
|
154
153
|
* Apply an update without treating it as a local edit, so it isn't queued for
|
|
@@ -160,11 +159,12 @@ class YProtocolSession {
|
|
|
160
159
|
* keystroke becomes an outbound frame), so a bare `Y.applyUpdate(doc, update)`
|
|
161
160
|
* would look like a local edit and get echoed back on the next connect. Going
|
|
162
161
|
* through here applies under the session's own origin, which the outbound
|
|
163
|
-
* filter skips. Safe to call before `
|
|
162
|
+
* filter skips. Safe to call before `resume()`: the state folds into the
|
|
164
163
|
* SyncStep1 handshake instead of being re-sent.
|
|
165
164
|
*/
|
|
166
165
|
applyRemoteUpdate(update) {
|
|
167
|
-
|
|
166
|
+
if (this.#state.phase !== "destroyed")
|
|
167
|
+
(0, yjs_1.applyUpdate)(this.doc, update, this);
|
|
168
168
|
}
|
|
169
169
|
/**
|
|
170
170
|
* Decode and apply one incoming binary protocol frame (document sync or
|
|
@@ -172,21 +172,27 @@ class YProtocolSession {
|
|
|
172
172
|
* SyncStep1), or null if there's nothing to send.
|
|
173
173
|
*/
|
|
174
174
|
receive(frame) {
|
|
175
|
+
if (this.#state.phase === "destroyed")
|
|
176
|
+
return null;
|
|
177
|
+
const { cycle } = this.#state;
|
|
175
178
|
// A malformed/truncated frame must never take down the transport callback:
|
|
176
179
|
// decode + apply defensively, drop the frame on error, keep the session live.
|
|
177
180
|
try {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
return null;
|
|
181
|
+
if (!validateFrame(frame))
|
|
182
|
+
return null; // a y-protocols type yrby doesn't use
|
|
181
183
|
const decoder = decoding.createDecoder(frame);
|
|
182
184
|
const encoder = encoding.createEncoder();
|
|
183
185
|
const type = decoding.readVarUint(decoder);
|
|
184
186
|
switch (type) {
|
|
185
187
|
case exports.MessageType.Sync: {
|
|
186
188
|
encoding.writeVarUint(encoder, exports.MessageType.Sync);
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
this.#
|
|
189
|
+
// y-protocols catches a failing update itself and reports it through this handler.
|
|
190
|
+
const report = (error) => { if (this.#current(cycle))
|
|
191
|
+
this.#onError(error, "receive"); };
|
|
192
|
+
const syncType = (0, sync_1.readSyncMessage)(decoder, encoder, this.doc, this, report);
|
|
193
|
+
if (syncType === sync_1.messageYjsSyncStep2 && this.#current(cycle)) {
|
|
194
|
+
this.#state = { phase: "synced", cycle };
|
|
195
|
+
}
|
|
190
196
|
break;
|
|
191
197
|
}
|
|
192
198
|
case exports.MessageType.Awareness:
|
|
@@ -194,22 +200,29 @@ class YProtocolSession {
|
|
|
194
200
|
(0, awareness_1.applyAwarenessUpdate)(this.awareness, decoding.readVarUint8Array(decoder), this);
|
|
195
201
|
break;
|
|
196
202
|
default:
|
|
197
|
-
return null; //
|
|
203
|
+
return null; // unreachable, since validateFrame accepts only the two types above
|
|
198
204
|
}
|
|
199
|
-
return encoding.length(encoder) > 1 ? encoding.toUint8Array(encoder) : null;
|
|
205
|
+
return this.#current(cycle) && encoding.length(encoder) > 1 ? encoding.toUint8Array(encoder) : null;
|
|
200
206
|
}
|
|
201
207
|
catch (error) {
|
|
202
|
-
this.#
|
|
208
|
+
if (this.#current(cycle))
|
|
209
|
+
this.#onError(error, "receive");
|
|
203
210
|
return null;
|
|
204
211
|
}
|
|
205
212
|
}
|
|
206
213
|
/** Detach doc/awareness listeners and stop retransmits. */
|
|
207
214
|
destroy() {
|
|
215
|
+
if (this.#state.phase === "destroyed")
|
|
216
|
+
return;
|
|
217
|
+
this.#state = { phase: "destroyed" };
|
|
208
218
|
this.doc.off("update", this.#onDocUpdate);
|
|
209
219
|
if (this.awareness && this.#onAwarenessUpdate)
|
|
210
220
|
this.awareness.off("update", this.#onAwarenessUpdate);
|
|
211
221
|
this.#delivery.destroy();
|
|
212
222
|
}
|
|
223
|
+
#current(cycle) {
|
|
224
|
+
return this.#state.phase !== "destroyed" && this.#state.cycle === cycle;
|
|
225
|
+
}
|
|
213
226
|
#frameSyncStep1() {
|
|
214
227
|
const e = encoding.createEncoder();
|
|
215
228
|
encoding.writeVarUint(e, exports.MessageType.Sync);
|
|
@@ -228,53 +241,45 @@ class YProtocolSession {
|
|
|
228
241
|
encoding.writeVarUint8Array(e, (0, awareness_1.encodeAwarenessUpdate)(this.awareness, clients));
|
|
229
242
|
return encoding.toUint8Array(e);
|
|
230
243
|
}
|
|
231
|
-
#validateFrame(frame) {
|
|
232
|
-
const decoder = decoding.createDecoder(frame);
|
|
233
|
-
const type = decoding.readVarUint(decoder);
|
|
234
|
-
switch (type) {
|
|
235
|
-
case exports.MessageType.Sync: {
|
|
236
|
-
const scratchDoc = new yjs_1.Doc();
|
|
237
|
-
try {
|
|
238
|
-
const scratchEncoder = encoding.createEncoder();
|
|
239
|
-
encoding.writeVarUint(scratchEncoder, exports.MessageType.Sync);
|
|
240
|
-
(0, sync_1.readSyncMessage)(decoder, scratchEncoder, scratchDoc, this);
|
|
241
|
-
}
|
|
242
|
-
finally {
|
|
243
|
-
scratchDoc.destroy();
|
|
244
|
-
}
|
|
245
|
-
break;
|
|
246
|
-
}
|
|
247
|
-
case exports.MessageType.Awareness:
|
|
248
|
-
// Validate the payload's CONTENTS, not just the envelope.
|
|
249
|
-
// applyAwarenessUpdate mutates state entry by entry and only notifies
|
|
250
|
-
// listeners at the end — a bad entry mid-payload would leave earlier
|
|
251
|
-
// entries applied with no event fired. Dry-running every entry here
|
|
252
|
-
// makes the real apply infallible (and catches trailing garbage
|
|
253
|
-
// inside the blob).
|
|
254
|
-
{
|
|
255
|
-
const payload = decoding.readVarUint8Array(decoder);
|
|
256
|
-
const inner = decoding.createDecoder(payload);
|
|
257
|
-
const count = decoding.readVarUint(inner);
|
|
258
|
-
for (let i = 0; i < count; i++) {
|
|
259
|
-
decoding.readVarUint(inner); // clientID
|
|
260
|
-
decoding.readVarUint(inner); // clock
|
|
261
|
-
JSON.parse(decoding.readVarString(inner)); // state (null on removal)
|
|
262
|
-
}
|
|
263
|
-
if (decoding.hasContent(inner)) {
|
|
264
|
-
throw new Error("awareness payload has trailing bytes");
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
break;
|
|
268
|
-
default:
|
|
269
|
-
return null; // a y-protocols type yrby doesn't speak: ignore
|
|
270
|
-
}
|
|
271
|
-
// This protocol is one message per frame. Anything left after a complete
|
|
272
|
-
// message is malformed (trailing garbage, or low-level packed messages whose
|
|
273
|
-
// tail we'd silently drop), so reject it before mutating local state.
|
|
274
|
-
if (decoding.hasContent(decoder)) {
|
|
275
|
-
throw new Error("frame has trailing bytes after a complete message");
|
|
276
|
-
}
|
|
277
|
-
return type;
|
|
278
|
-
}
|
|
279
244
|
}
|
|
280
245
|
exports.YProtocolSession = YProtocolSession;
|
|
246
|
+
// Checks a frame's structure before anything is applied, so a truncated or
|
|
247
|
+
// padded frame changes nothing. Returns false for a frame type yrby ignores
|
|
248
|
+
// (auth, query-awareness) and throws for a malformed one. Yjs checks the
|
|
249
|
+
// contents of an update itself when applying it.
|
|
250
|
+
function validateFrame(frame) {
|
|
251
|
+
const decoder = decoding.createDecoder(frame);
|
|
252
|
+
const type = decoding.readVarUint(decoder);
|
|
253
|
+
if (type === exports.MessageType.Sync) {
|
|
254
|
+
// Every sync message is a subtype followed by one length-prefixed payload.
|
|
255
|
+
decoding.readVarUint(decoder);
|
|
256
|
+
decoding.readVarUint8Array(decoder);
|
|
257
|
+
}
|
|
258
|
+
else if (type === exports.MessageType.Awareness) {
|
|
259
|
+
validateAwareness(decoding.readVarUint8Array(decoder));
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
// This protocol is one message per frame. Anything left after a complete
|
|
265
|
+
// message is malformed (trailing garbage, or low-level packed messages whose
|
|
266
|
+
// tail we'd silently drop).
|
|
267
|
+
if (decoding.hasContent(decoder))
|
|
268
|
+
throw new Error("frame has trailing bytes after a complete message");
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
// applyAwarenessUpdate mutates state entry by entry and notifies listeners
|
|
272
|
+
// only at the end, so a bad entry mid-payload would leave earlier entries
|
|
273
|
+
// applied with no event fired. Reading every entry first makes the real apply
|
|
274
|
+
// infallible and catches trailing bytes inside the payload.
|
|
275
|
+
function validateAwareness(payload) {
|
|
276
|
+
const decoder = decoding.createDecoder(payload);
|
|
277
|
+
const count = decoding.readVarUint(decoder);
|
|
278
|
+
for (let i = 0; i < count; i++) {
|
|
279
|
+
decoding.readVarUint(decoder); // clientID
|
|
280
|
+
decoding.readVarUint(decoder); // clock
|
|
281
|
+
JSON.parse(decoding.readVarString(decoder)); // state (null on removal)
|
|
282
|
+
}
|
|
283
|
+
if (decoding.hasContent(decoder))
|
|
284
|
+
throw new Error("awareness payload has trailing bytes");
|
|
285
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type CableConsumer } from "./actioncable_provider.js";
|
|
2
|
+
import { type DocumentSession } from "./document_session.js";
|
|
3
|
+
declare const Base: typeof HTMLElement;
|
|
4
|
+
export declare class YrbyDocumentElement extends Base {
|
|
5
|
+
#private;
|
|
6
|
+
/** Set before adding elements to use another consumer, such as AnyCable's. */
|
|
7
|
+
static consumer: CableConsumer | Promise<CableConsumer> | undefined;
|
|
8
|
+
static observedAttributes: string[];
|
|
9
|
+
get session(): DocumentSession | undefined;
|
|
10
|
+
get doc(): DocumentSession["doc"] | undefined;
|
|
11
|
+
get provider(): DocumentSession["provider"] | undefined;
|
|
12
|
+
/** Resolves after the current attempt's first sync. If the attempt is abandoned, its promise never resolves. */
|
|
13
|
+
get whenSynced(): Promise<void>;
|
|
14
|
+
connectedCallback(): void;
|
|
15
|
+
disconnectedCallback(): void;
|
|
16
|
+
attributeChangedCallback(_name: string, oldValue: string | null, newValue: string | null): void;
|
|
17
|
+
/** @internal Called by the Turbo adapter when the page is live. A new render also retries a stalled document. */
|
|
18
|
+
activate(): void;
|
|
19
|
+
/** @internal Called by the Turbo adapter when the page is cached or previewed. */
|
|
20
|
+
deactivate(): void;
|
|
21
|
+
/** Releases the editor lease. The session keeps any unsaved work. */
|
|
22
|
+
destroy(): void;
|
|
23
|
+
}
|
|
24
|
+
export {};
|
|
25
|
+
//# sourceMappingURL=document_element.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"document_element.d.ts","sourceRoot":"","sources":["../src/document_element.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,KAAK,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EAKL,KAAK,eAAe,EACrB,MAAM,uBAAuB,CAAC;AAI/B,QAAA,MAAM,IAAI,EAAoE,OAAO,WAAW,CAAC;AAkDjG,qBAAa,mBAAoB,SAAQ,IAAI;;IAC3C,8EAA8E;IAC9E,MAAM,CAAC,QAAQ,EAAE,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC,GAAG,SAAS,CAAC;IAGpE,MAAM,CAAC,kBAAkB,WAAgC;IAazD,IAAI,OAAO,IAAI,eAAe,GAAG,SAAS,CAIzC;IACD,IAAI,GAAG,IAAI,eAAe,CAAC,KAAK,CAAC,GAAG,SAAS,CAA8B;IAC3E,IAAI,QAAQ,IAAI,eAAe,CAAC,UAAU,CAAC,GAAG,SAAS,CAAmC;IAC1F,gHAAgH;IAChH,IAAI,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAAoC;IAEnE,iBAAiB,IAAI,IAAI;IAQzB,oBAAoB,IAAI,IAAI;IAC5B,wBAAwB,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI;IAO/F,iHAAiH;IACjH,QAAQ,IAAI,IAAI;IAKhB,kFAAkF;IAClF,UAAU,IAAI,IAAI;IAIlB,qEAAqE;IACrE,OAAO,IAAI,IAAI;CA6HhB"}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
var _a;
|
|
2
|
+
import { DocumentSessionStore, documentKey, } from "./document_session.js";
|
|
3
|
+
import { registerDocumentMount } from "./turbo_adapter.js";
|
|
4
|
+
// Lets tests and SSR import this module where HTMLElement is undefined.
|
|
5
|
+
const Base = (typeof HTMLElement === "undefined" ? class {
|
|
6
|
+
} : HTMLElement);
|
|
7
|
+
// Holds the application's own inert value while the element forces inert on.
|
|
8
|
+
const INERT_ATTRIBUTE = "data-yrby-inert";
|
|
9
|
+
let sharedConsumer;
|
|
10
|
+
// Loads once per page. A failed import is cleared so a later attempt can retry it.
|
|
11
|
+
function defaultConsumer() {
|
|
12
|
+
sharedConsumer ??= import("@rails/actioncable")
|
|
13
|
+
.then(actioncable => actioncable.createConsumer())
|
|
14
|
+
.catch(error => { sharedConsumer = undefined; throw error; });
|
|
15
|
+
return sharedConsumer;
|
|
16
|
+
}
|
|
17
|
+
function deferred() {
|
|
18
|
+
let resolve;
|
|
19
|
+
const promise = new Promise(r => { resolve = r; });
|
|
20
|
+
return { promise, resolve };
|
|
21
|
+
}
|
|
22
|
+
// Returns the yrby:error detail for a session that ended a lease. A block gets
|
|
23
|
+
// a detail, and a discard gets undefined.
|
|
24
|
+
function blockReport(session) {
|
|
25
|
+
return session.state === "blocked" ? { error: session.error, session } : undefined;
|
|
26
|
+
}
|
|
27
|
+
// The element binds when it is in the page, the Turbo adapter reports the page
|
|
28
|
+
// as live, and it has a descriptor. It abandons an attempt synchronously,
|
|
29
|
+
// because Turbo copies the page as soon as before-cache fires, and after a
|
|
30
|
+
// retarget the editor has to stop writing to the old document before anything
|
|
31
|
+
// else runs. Starting and advancing an attempt and handling every async result
|
|
32
|
+
// all go through #settle, which runs after the current call stack and compares
|
|
33
|
+
// what should be bound with what is.
|
|
34
|
+
export class YrbyDocumentElement extends Base {
|
|
35
|
+
/** Set before adding elements to use another consumer, such as AnyCable's. */
|
|
36
|
+
static consumer;
|
|
37
|
+
// refresh is read when the session is acquired and isn't part of the
|
|
38
|
+
// document's identity, so changing it doesn't rebind the editor.
|
|
39
|
+
static observedAttributes = ["grant", "name", "channel"];
|
|
40
|
+
#live = false; // false while Turbo is showing a cached copy of the page
|
|
41
|
+
#attempt;
|
|
42
|
+
// Key of the document whose session blocked or failed. The element won't retry
|
|
43
|
+
// it until the page renders again, the attributes change, or the element is
|
|
44
|
+
// re-inserted.
|
|
45
|
+
#stalledKey;
|
|
46
|
+
#unregister;
|
|
47
|
+
#settleQueued = false;
|
|
48
|
+
// Resolves when the current attempt first syncs. Abandoning the attempt replaces it.
|
|
49
|
+
#firstSync = deferred();
|
|
50
|
+
get session() {
|
|
51
|
+
// Treat a lease its session aborted as gone, even before settle runs.
|
|
52
|
+
const lease = this.#attempt?.lease;
|
|
53
|
+
return lease && !lease.signal.aborted ? lease.session : undefined;
|
|
54
|
+
}
|
|
55
|
+
get doc() { return this.session?.doc; }
|
|
56
|
+
get provider() { return this.session?.provider; }
|
|
57
|
+
/** Resolves after the current attempt's first sync. If the attempt is abandoned, its promise never resolves. */
|
|
58
|
+
get whenSynced() { return this.#firstSync.promise; }
|
|
59
|
+
connectedCallback() {
|
|
60
|
+
this.#stalledKey = undefined;
|
|
61
|
+
// A same-turn move keeps its attempt, so don't make a live editor inert.
|
|
62
|
+
if (!this.#attempt)
|
|
63
|
+
this.#holdInert();
|
|
64
|
+
this.#unregister ??= registerDocumentMount(this);
|
|
65
|
+
this.#requestSettle();
|
|
66
|
+
}
|
|
67
|
+
// Same-turn moves keep their binding, because settle checks isConnected afterwards.
|
|
68
|
+
disconnectedCallback() { this.#requestSettle(); }
|
|
69
|
+
attributeChangedCallback(_name, oldValue, newValue) {
|
|
70
|
+
if (oldValue === newValue)
|
|
71
|
+
return;
|
|
72
|
+
this.#stalledKey = undefined;
|
|
73
|
+
this.#abandon();
|
|
74
|
+
this.#requestSettle();
|
|
75
|
+
}
|
|
76
|
+
/** @internal Called by the Turbo adapter when the page is live. A new render also retries a stalled document. */
|
|
77
|
+
activate() {
|
|
78
|
+
this.#live = true;
|
|
79
|
+
this.#stalledKey = undefined;
|
|
80
|
+
this.#requestSettle();
|
|
81
|
+
}
|
|
82
|
+
/** @internal Called by the Turbo adapter when the page is cached or previewed. */
|
|
83
|
+
deactivate() {
|
|
84
|
+
this.#live = false;
|
|
85
|
+
this.#abandon();
|
|
86
|
+
}
|
|
87
|
+
/** Releases the editor lease. The session keeps any unsaved work. */
|
|
88
|
+
destroy() {
|
|
89
|
+
// Reset the adapter's last report. The next connection registers again and gets a new one.
|
|
90
|
+
this.#live = false;
|
|
91
|
+
// Clear it before the call, since the call can re-enter through a replacement registration.
|
|
92
|
+
const unregister = this.#unregister;
|
|
93
|
+
this.#unregister = undefined;
|
|
94
|
+
unregister?.();
|
|
95
|
+
this.#abandon();
|
|
96
|
+
}
|
|
97
|
+
#requestSettle() {
|
|
98
|
+
if (this.#settleQueued)
|
|
99
|
+
return;
|
|
100
|
+
this.#settleQueued = true;
|
|
101
|
+
queueMicrotask(() => this.#settle());
|
|
102
|
+
}
|
|
103
|
+
#settle() {
|
|
104
|
+
this.#settleQueued = false;
|
|
105
|
+
if (!this.isConnected) {
|
|
106
|
+
this.destroy();
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const descriptor = this.#descriptor();
|
|
110
|
+
// key is undefined when nothing should be bound, because the page is cached
|
|
111
|
+
// or the attributes don't name a document yet.
|
|
112
|
+
const key = this.#live && descriptor.grant && descriptor.name ? documentKey(descriptor) : undefined;
|
|
113
|
+
const attempt = this.#attempt;
|
|
114
|
+
if (attempt && attempt.key !== key) {
|
|
115
|
+
// Should not happen, since every change to these facts already abandons the attempt.
|
|
116
|
+
this.#abandon();
|
|
117
|
+
this.#requestSettle();
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (key === undefined || key === this.#stalledKey)
|
|
121
|
+
return;
|
|
122
|
+
// Take the next step. An attempt starts, acquires a lease once the consumer
|
|
123
|
+
// loads, and announces once synced. If the attempt has ended, stall.
|
|
124
|
+
if (!attempt)
|
|
125
|
+
this.#start(key, descriptor);
|
|
126
|
+
else if (attempt.ended)
|
|
127
|
+
this.#stall(attempt.ended.detail);
|
|
128
|
+
else if (attempt.consumer && !attempt.lease)
|
|
129
|
+
this.#acquire(attempt, attempt.consumer);
|
|
130
|
+
else if (attempt.synced && !attempt.announced)
|
|
131
|
+
this.#announce(attempt);
|
|
132
|
+
}
|
|
133
|
+
#start(key, descriptor) {
|
|
134
|
+
const attempt = { key, descriptor };
|
|
135
|
+
this.#attempt = attempt;
|
|
136
|
+
Promise.resolve(_a.consumer ?? defaultConsumer()).then(consumer => { attempt.consumer = consumer; }, error => { attempt.ended = { detail: { error } }; }).then(() => this.#requestSettle());
|
|
137
|
+
}
|
|
138
|
+
#acquire(attempt, consumer) {
|
|
139
|
+
let lease;
|
|
140
|
+
try {
|
|
141
|
+
lease = DocumentSessionStore.for(consumer).acquire(attempt.descriptor);
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
this.#stall({ error });
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
attempt.lease = lease;
|
|
148
|
+
const { session } = lease;
|
|
149
|
+
// A blocked session keeps new leases for retry(), and its first sync may
|
|
150
|
+
// be long past, so an editor must not bind to it.
|
|
151
|
+
const blocked = blockReport(session);
|
|
152
|
+
if (blocked) {
|
|
153
|
+
attempt.ended = { detail: blocked };
|
|
154
|
+
this.#requestSettle();
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
// The lease aborts when its session blocks or is discarded. Read the
|
|
158
|
+
// reason now, before anything retries the session.
|
|
159
|
+
lease.signal.addEventListener("abort", () => {
|
|
160
|
+
attempt.ended ??= { detail: blockReport(session) };
|
|
161
|
+
this.#requestSettle();
|
|
162
|
+
}, { once: true });
|
|
163
|
+
void session.whenSynced.then(() => {
|
|
164
|
+
attempt.synced = true;
|
|
165
|
+
this.#requestSettle();
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
#announce(attempt) {
|
|
169
|
+
attempt.announced = true;
|
|
170
|
+
const lease = attempt.lease;
|
|
171
|
+
const { session } = lease;
|
|
172
|
+
this.#restoreInert();
|
|
173
|
+
this.#firstSync.resolve();
|
|
174
|
+
this.dispatchEvent(new CustomEvent("yrby:synced", {
|
|
175
|
+
bubbles: true,
|
|
176
|
+
detail: { session, doc: session.doc, provider: session.provider, lease, signal: lease.signal },
|
|
177
|
+
}));
|
|
178
|
+
}
|
|
179
|
+
#stall(detail) {
|
|
180
|
+
this.#stalledKey = this.#attempt?.key;
|
|
181
|
+
this.#abandon();
|
|
182
|
+
if (detail)
|
|
183
|
+
this.dispatchEvent(new CustomEvent("yrby:error", { bubbles: true, detail }));
|
|
184
|
+
}
|
|
185
|
+
// Ends the current attempt. Releasing the lease runs editor cleanup, which
|
|
186
|
+
// may change attributes or move the element, and those changes only request
|
|
187
|
+
// a settle.
|
|
188
|
+
#abandon() {
|
|
189
|
+
const attempt = this.#attempt;
|
|
190
|
+
if (!attempt)
|
|
191
|
+
return;
|
|
192
|
+
this.#attempt = undefined;
|
|
193
|
+
this.#firstSync = deferred();
|
|
194
|
+
this.#holdInert();
|
|
195
|
+
attempt.lease?.release();
|
|
196
|
+
}
|
|
197
|
+
#descriptor() {
|
|
198
|
+
return {
|
|
199
|
+
channel: this.getAttribute("channel") || undefined,
|
|
200
|
+
grant: this.getAttribute("grant") || "",
|
|
201
|
+
name: this.getAttribute("name") || "",
|
|
202
|
+
refresh: this.getAttribute("refresh") || undefined,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
// Stay inert until synced so nobody types into a document that isn't live yet.
|
|
206
|
+
// The application's own inert value goes in an attribute, which survives a
|
|
207
|
+
// Turbo cache clone, and is restored when the element is ready.
|
|
208
|
+
#holdInert() {
|
|
209
|
+
if (!this.hasAttribute(INERT_ATTRIBUTE))
|
|
210
|
+
this.setAttribute(INERT_ATTRIBUTE, String(this.inert));
|
|
211
|
+
this.inert = true;
|
|
212
|
+
}
|
|
213
|
+
#restoreInert() {
|
|
214
|
+
const saved = this.getAttribute(INERT_ATTRIBUTE);
|
|
215
|
+
if (saved === null)
|
|
216
|
+
return;
|
|
217
|
+
this.inert = saved === "true";
|
|
218
|
+
this.removeAttribute(INERT_ATTRIBUTE);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
_a = YrbyDocumentElement;
|
|
222
|
+
if (typeof customElements !== "undefined" && !customElements.get("yrby-document")) {
|
|
223
|
+
customElements.define("yrby-document", YrbyDocumentElement);
|
|
224
|
+
}
|
|
225
|
+
//# sourceMappingURL=document_element.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"document_element.js","sourceRoot":"","sources":["../src/document_element.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import * as Y from "yjs";
|
|
2
|
+
import { ActionCableProvider, type CableConsumer } from "./actioncable_provider.js";
|
|
3
|
+
export interface DocumentDescriptor {
|
|
4
|
+
channel?: string;
|
|
5
|
+
grant: string;
|
|
6
|
+
name: string;
|
|
7
|
+
/** A same-origin URL that returns `{ "grant": "..." }` for this document. The session fetches it at most once per rejection. */
|
|
8
|
+
refresh?: string;
|
|
9
|
+
}
|
|
10
|
+
export type ResolvedDescriptor = Readonly<{
|
|
11
|
+
channel: string;
|
|
12
|
+
grant: string;
|
|
13
|
+
name: string;
|
|
14
|
+
refresh?: string;
|
|
15
|
+
}>;
|
|
16
|
+
export type DocumentSessionState = "open" | "blocked" | "closed";
|
|
17
|
+
/** The identity of the document a descriptor names. Matching keys share a session. */
|
|
18
|
+
export declare function documentKey(descriptor: DocumentDescriptor): string;
|
|
19
|
+
declare const attachLease: unique symbol;
|
|
20
|
+
declare const notifyStoreChange: unique symbol;
|
|
21
|
+
/** Holds one consumer's sessions and emits "change" with the session in `detail`. */
|
|
22
|
+
export declare class DocumentSessionStore extends EventTarget {
|
|
23
|
+
#private;
|
|
24
|
+
readonly consumer: CableConsumer;
|
|
25
|
+
static for(consumer: CableConsumer): DocumentSessionStore;
|
|
26
|
+
private constructor();
|
|
27
|
+
get sessions(): readonly DocumentSession[];
|
|
28
|
+
/** Acquire a lease on this document session, creating it on first use. */
|
|
29
|
+
acquire(input: DocumentDescriptor): DocumentLease;
|
|
30
|
+
[notifyStoreChange](session: DocumentSession): void;
|
|
31
|
+
}
|
|
32
|
+
/** A caller's hold on a session. Release it when you're done with the session. */
|
|
33
|
+
export declare class DocumentLease {
|
|
34
|
+
#private;
|
|
35
|
+
readonly session: DocumentSession;
|
|
36
|
+
constructor(session: DocumentSession, onRelease: () => void);
|
|
37
|
+
/** Aborts when the lease ends, including when the session blocks or is discarded. */
|
|
38
|
+
get signal(): AbortSignal;
|
|
39
|
+
setPresence(state: Record<string, unknown> | null): void;
|
|
40
|
+
/** Runs editor cleanup synchronously, before the final check for pending work. */
|
|
41
|
+
release(): void;
|
|
42
|
+
}
|
|
43
|
+
export declare class DocumentSession {
|
|
44
|
+
#private;
|
|
45
|
+
readonly store: DocumentSessionStore;
|
|
46
|
+
readonly descriptor: ResolvedDescriptor;
|
|
47
|
+
private readonly remove;
|
|
48
|
+
readonly doc: Y.Doc;
|
|
49
|
+
readonly provider: ActionCableProvider;
|
|
50
|
+
/** Create and hold sessions through DocumentSessionStore.acquire. */
|
|
51
|
+
constructor(store: DocumentSessionStore, descriptor: ResolvedDescriptor, remove: () => void);
|
|
52
|
+
get error(): unknown;
|
|
53
|
+
get hasPending(): boolean;
|
|
54
|
+
get whenSynced(): Promise<void>;
|
|
55
|
+
get state(): DocumentSessionState;
|
|
56
|
+
[attachLease](): DocumentLease;
|
|
57
|
+
/** Reconnects with this session's current grant, which is the original one or the last one a refresh returned. */
|
|
58
|
+
retry(): void;
|
|
59
|
+
/** Closes the session and drops pending work. The application calls this explicitly, because an ordinary detach keeps pending work. */
|
|
60
|
+
discard(): void;
|
|
61
|
+
}
|
|
62
|
+
export {};
|
|
63
|
+
//# sourceMappingURL=document_session.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"document_session.d.ts","sourceRoot":"","sources":["../src/document_session.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,CAAC,MAAM,KAAK,CAAC;AAEzB,OAAO,EAAE,mBAAmB,EAAE,KAAK,aAAa,EAAuB,MAAM,2BAA2B,CAAC;AAEzG,MAAM,WAAW,kBAAkB;IACjC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,gIAAgI;IAChI,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AACD,MAAM,MAAM,kBAAkB,GAAG,QAAQ,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAG9G,MAAM,MAAM,oBAAoB,GAAG,MAAM,GAAG,SAAS,GAAG,QAAQ,CAAC;AA8BjE,sFAAsF;AACtF,wBAAgB,WAAW,CAAC,UAAU,EAAE,kBAAkB,GAAG,MAAM,CAElE;AAKD,QAAA,MAAM,WAAW,eAAwB,CAAC;AAE1C,QAAA,MAAM,iBAAiB,eAA8B,CAAC;AAEtD,qFAAqF;AACrF,qBAAa,oBAAqB,SAAQ,WAAW;;IAO/B,QAAQ,CAAC,QAAQ,EAAE,aAAa;IANpD,MAAM,CAAC,GAAG,CAAC,QAAQ,EAAE,aAAa,GAAG,oBAAoB;IAMzD,OAAO;IAIP,IAAI,QAAQ,IAAI,SAAS,eAAe,EAAE,CAAyC;IAEnF,0EAA0E;IAC1E,OAAO,CAAC,KAAK,EAAE,kBAAkB,GAAG,aAAa;IAkBjD,CAAC,iBAAiB,CAAC,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI;CAGpD;AAED,kFAAkF;AAClF,qBAAa,aAAa;;IAGZ,QAAQ,CAAC,OAAO,EAAE,eAAe;gBAAxB,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,IAAI;IAGpE,qFAAqF;IACrF,IAAI,MAAM,IAAI,WAAW,CAAoC;IAC7D,WAAW,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GAAG,IAAI;IAGxD,kFAAkF;IAClF,OAAO,IAAI,IAAI;CAKhB;AAOD,qBAAa,eAAe;;IAcxB,QAAQ,CAAC,KAAK,EAAE,oBAAoB;IACpC,QAAQ,CAAC,UAAU,EAAE,kBAAkB;IACvC,OAAO,CAAC,QAAQ,CAAC,MAAM;IAfzB,QAAQ,CAAC,GAAG,QAAe;IAC3B,QAAQ,CAAC,QAAQ,EAAE,mBAAmB,CAAC;IAUvC,qEAAqE;gBAE1D,KAAK,EAAE,oBAAoB,EAC3B,UAAU,EAAE,kBAAkB,EACtB,MAAM,EAAE,MAAM,IAAI;IAwBrC,IAAI,KAAK,IAAI,OAAO,CAAwB;IAC5C,IAAI,UAAU,IAAI,OAAO,CAAqC;IAC9D,IAAI,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAAqC;IACpE,IAAI,KAAK,IAAI,oBAAoB,CAAgD;IAEjF,CAAC,WAAW,CAAC,IAAI,aAAa;IAa9B,kHAAkH;IAClH,KAAK,IAAI,IAAI;IAGb,uIAAuI;IACvI,OAAO,IAAI,IAAI;CA+EhB"}
|