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
|
@@ -12,10 +12,11 @@ exports.ActionCableProvider = void 0;
|
|
|
12
12
|
// the server's persistence/ack path.
|
|
13
13
|
//
|
|
14
14
|
// The constructor does not auto-connect: wire up your editor binding first, then
|
|
15
|
-
// call `connect()`. Watch the connection with `onStatusChange(({ status }) => ...)`
|
|
16
|
-
// or the `status`
|
|
17
|
-
// `
|
|
18
|
-
//
|
|
15
|
+
// call `connect()`. Watch the connection with `onStatusChange(({ status, pending }) => ...)`
|
|
16
|
+
// or the `status` and `hasPending` getters. Editors that must not bind before the first sync
|
|
17
|
+
// can `await provider.whenSynced` after connect(). On `disconnect()`/`destroy()`,
|
|
18
|
+
// and on browser `pagehide`, the provider broadcasts a presence removal so peers
|
|
19
|
+
// drop our cursor right away instead of waiting for the awareness timeout.
|
|
19
20
|
const y_protocol_session_js_1 = require("./y_protocol_session.js");
|
|
20
21
|
const base64_js_1 = require("./base64.js");
|
|
21
22
|
const awareness_1 = require("y-protocols/awareness");
|
|
@@ -26,27 +27,53 @@ class ActionCableProvider {
|
|
|
26
27
|
channelParams;
|
|
27
28
|
awareness;
|
|
28
29
|
session;
|
|
29
|
-
#
|
|
30
|
+
#state = { phase: "disconnected" };
|
|
30
31
|
#onError;
|
|
31
|
-
|
|
32
|
-
#
|
|
32
|
+
// The last event sent to listeners. A refresh notifies them only when a field differs.
|
|
33
|
+
#last = { status: "disconnected", pending: false };
|
|
33
34
|
#statusListeners = new Set();
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
#
|
|
35
|
+
#resolveSynced;
|
|
36
|
+
#onDocUpdate = () => this.#refreshStatus(); // a local edit may have made the queue non-empty
|
|
37
|
+
#page = null;
|
|
38
|
+
/**
|
|
39
|
+
* Resolves once the document has first caught up with the server. Most
|
|
40
|
+
* editor bindings seed an empty document when they mount, so binding
|
|
41
|
+
* before the server's state arrives makes each client insert its own
|
|
42
|
+
* top-level node. Create the editor after this resolves:
|
|
43
|
+
*
|
|
44
|
+
* provider.connect();
|
|
45
|
+
* await provider.whenSynced;
|
|
46
|
+
* // now hand the doc to the editor binding
|
|
47
|
+
*
|
|
48
|
+
* It resolves on the first catch-up and remains resolved across later
|
|
49
|
+
* reconnects, even while `synced` is false during a re-handshake. Use
|
|
50
|
+
* `onStatusChange` to track the live connection. If the provider is
|
|
51
|
+
* destroyed before the first sync, it never resolves.
|
|
52
|
+
*/
|
|
53
|
+
whenSynced = new Promise((resolve) => { this.#resolveSynced = resolve; });
|
|
37
54
|
constructor(doc, consumer, channelName, channelParams = {}, opts = {}) {
|
|
38
55
|
this.doc = doc;
|
|
39
56
|
this.consumer = consumer;
|
|
40
57
|
this.channelName = channelName;
|
|
41
58
|
this.channelParams = channelParams;
|
|
42
|
-
|
|
43
|
-
this.#onError =
|
|
59
|
+
const onError = opts.onError ?? ((error, context) => console.warn(`[yrby] ${context}:`, error));
|
|
60
|
+
this.#onError = (error, context) => {
|
|
61
|
+
try {
|
|
62
|
+
onError(error, context);
|
|
63
|
+
}
|
|
64
|
+
catch (callbackError) {
|
|
65
|
+
console.warn("[yrby] onError callback failed:", callbackError, "while reporting:", error);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
this.awareness = new ProviderAwareness(doc, this.#onError);
|
|
44
69
|
this.session = new y_protocol_session_js_1.YProtocolSession(doc, {
|
|
45
70
|
awareness: this.awareness,
|
|
46
71
|
resendInterval: opts.resendInterval,
|
|
47
72
|
onError: this.#onError,
|
|
48
73
|
send: (frame, id) => this.#send(frame, id),
|
|
49
74
|
});
|
|
75
|
+
// Registered after the session's listener, so the queue already has the edit.
|
|
76
|
+
this.doc.on("update", this.#onDocUpdate);
|
|
50
77
|
}
|
|
51
78
|
/** True once the document has caught up with the server (received a SyncStep2). */
|
|
52
79
|
get synced() {
|
|
@@ -73,7 +100,7 @@ class ActionCableProvider {
|
|
|
73
100
|
}
|
|
74
101
|
/** Current connection status. See {@link ProviderStatus}. */
|
|
75
102
|
get status() {
|
|
76
|
-
return this.#
|
|
103
|
+
return this.#computeStatus();
|
|
77
104
|
}
|
|
78
105
|
/** Subscribe to status changes. Returns an unsubscribe function. */
|
|
79
106
|
onStatusChange(listener) {
|
|
@@ -81,171 +108,289 @@ class ActionCableProvider {
|
|
|
81
108
|
return () => this.#statusListeners.delete(listener);
|
|
82
109
|
}
|
|
83
110
|
connect() {
|
|
84
|
-
if (this.#
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
});
|
|
137
|
-
this.#installUnloadHandler();
|
|
138
|
-
this.#refreshStatus(); // -> "connecting"
|
|
139
|
-
}
|
|
140
|
-
disconnect() {
|
|
141
|
-
if (!this.#subscription)
|
|
142
|
-
return;
|
|
143
|
-
const sub = this.#subscription;
|
|
144
|
-
// Tell peers we're gone while the transport is still live, then pause and
|
|
145
|
-
// detach. Defer the unsubscribe one microtask so the removal frame flushes
|
|
146
|
-
// before the channel tears down.
|
|
147
|
-
this.session.removeLocalAwareness();
|
|
148
|
-
this.session.onDisconnect();
|
|
149
|
-
this.#connected = false;
|
|
150
|
-
this.#subscription = null;
|
|
151
|
-
this.#removeUnloadHandler();
|
|
152
|
-
// Universal teardown: both @rails/actioncable and @anycable/web subscriptions
|
|
153
|
-
// expose unsubscribe() (Rails' just calls consumer.subscriptions.remove(this)
|
|
154
|
-
// internally). @anycable has NO consumer.subscriptions.remove, so calling that
|
|
155
|
-
// would throw there.
|
|
156
|
-
queueMicrotask(() => sub.unsubscribe?.());
|
|
157
|
-
this.#refreshStatus(); // -> "disconnected"
|
|
158
|
-
}
|
|
159
|
-
destroy() {
|
|
111
|
+
if (this.#destroying())
|
|
112
|
+
throw new Error("provider is destroyed");
|
|
113
|
+
if (this.#state.phase !== "disconnected")
|
|
114
|
+
return;
|
|
115
|
+
const attempt = {};
|
|
116
|
+
this.#state = { phase: "subscribing", attempt };
|
|
117
|
+
// A consumer may invoke callbacks inside create(), before the subscription
|
|
118
|
+
// is installed, so on() defers those by one microtask.
|
|
119
|
+
const on = (callback) => (...args) => {
|
|
120
|
+
const run = () => { if (this.#active(attempt))
|
|
121
|
+
callback(...args); };
|
|
122
|
+
if (this.#state.phase === "subscribing")
|
|
123
|
+
queueMicrotask(run);
|
|
124
|
+
else
|
|
125
|
+
run();
|
|
126
|
+
};
|
|
127
|
+
let subscription;
|
|
128
|
+
try {
|
|
129
|
+
subscription = this.consumer.subscriptions.create({ channel: this.channelName, ...this.channelParams }, {
|
|
130
|
+
received: on((message) => this.#receive(message, attempt)),
|
|
131
|
+
connected: on(() => this.#connected()),
|
|
132
|
+
disconnected: on(() => this.#lost()),
|
|
133
|
+
rejected: on(() => this.#stop("reject")),
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
if (!this.#subscribing(attempt))
|
|
138
|
+
return;
|
|
139
|
+
this.#state = { phase: "disconnected" };
|
|
140
|
+
this.#refreshStatus();
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
// disconnect() or destroy() ran inside create().
|
|
144
|
+
if (!this.#subscribing(attempt)) {
|
|
145
|
+
this.#unsubscribe(subscription);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
this.#state = { phase: "connecting", attempt, subscription };
|
|
149
|
+
this.#watchPage();
|
|
150
|
+
this.#refreshStatus();
|
|
151
|
+
}
|
|
152
|
+
disconnect() { this.#stop("disconnect"); }
|
|
153
|
+
/**
|
|
154
|
+
* Resubscribes with updated channel params, such as a renewed grant. This
|
|
155
|
+
* replaces only the cable subscription and keeps the doc, the delivery
|
|
156
|
+
* queue, awareness, and this provider's ack route. Does nothing after
|
|
157
|
+
* destroy().
|
|
158
|
+
*/
|
|
159
|
+
renew(params) {
|
|
160
|
+
if (this.#destroying())
|
|
161
|
+
return;
|
|
162
|
+
Object.assign(this.channelParams, params);
|
|
160
163
|
this.disconnect();
|
|
161
|
-
this.
|
|
162
|
-
this.awareness.destroy(); // stops its reaper timer
|
|
163
|
-
this.#statusListeners.clear();
|
|
164
|
+
this.connect();
|
|
164
165
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
return "connecting";
|
|
170
|
-
return this.session.synced ? "synced" : "connected";
|
|
166
|
+
destroy() { this.#stop("destroy"); }
|
|
167
|
+
#destroying() {
|
|
168
|
+
const state = this.#state;
|
|
169
|
+
return state.phase === "destroyed" || (state.phase === "stopping" && state.reason === "destroy");
|
|
171
170
|
}
|
|
172
|
-
#
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
}
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
171
|
+
#subscribing(attempt) {
|
|
172
|
+
return this.#state.phase === "subscribing" && this.#state.attempt === attempt;
|
|
173
|
+
}
|
|
174
|
+
// Cable callbacks run only for a connecting or connected subscription.
|
|
175
|
+
#active(attempt) {
|
|
176
|
+
const state = this.#state;
|
|
177
|
+
return (state.phase === "connecting" || state.phase === "connected") && state.attempt === attempt;
|
|
178
|
+
}
|
|
179
|
+
// The cable callbacks below run only for the active attempt, which on() in
|
|
180
|
+
// connect() checks.
|
|
181
|
+
#connected() {
|
|
182
|
+
const state = this.#state;
|
|
183
|
+
if (state.phase !== "connecting")
|
|
184
|
+
return;
|
|
185
|
+
this.#state = { ...state, phase: "connected" };
|
|
186
|
+
this.session.resume();
|
|
187
|
+
this.#refreshStatus();
|
|
188
|
+
}
|
|
189
|
+
#lost() {
|
|
190
|
+
const state = this.#state;
|
|
191
|
+
if (state.phase !== "connecting" && state.phase !== "connected")
|
|
192
|
+
return;
|
|
193
|
+
this.#state = { ...state, phase: "connecting" };
|
|
194
|
+
this.session.pause();
|
|
195
|
+
this.#refreshStatus();
|
|
196
|
+
}
|
|
197
|
+
#stop(reason) {
|
|
198
|
+
const state = this.#state;
|
|
199
|
+
if (state.phase === "destroyed")
|
|
200
|
+
return;
|
|
201
|
+
if (state.phase === "stopping") {
|
|
202
|
+
if (reason === "destroy")
|
|
203
|
+
state.reason = reason;
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (state.phase === "disconnected" && reason === "disconnect")
|
|
207
|
+
return;
|
|
208
|
+
let finalReason = reason;
|
|
209
|
+
if ("subscription" in state) {
|
|
210
|
+
const stopping = { ...state, phase: "stopping", reason };
|
|
211
|
+
this.#state = stopping;
|
|
212
|
+
this.#unwatchPage();
|
|
213
|
+
// From here the provider ignores the old subscription's callbacks, but the
|
|
214
|
+
// subscription can still send our presence removal before its deferred
|
|
215
|
+
// unsubscribe.
|
|
191
216
|
this.session.removeLocalAwareness();
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
if (
|
|
217
|
+
this.session.pause();
|
|
218
|
+
this.#unsubscribe(state.subscription);
|
|
219
|
+
if (this.#state !== stopping)
|
|
195
220
|
return;
|
|
196
|
-
|
|
197
|
-
|
|
221
|
+
finalReason = stopping.reason; // a destroy() during those calls upgrades it
|
|
222
|
+
}
|
|
223
|
+
const destroyed = finalReason === "destroy";
|
|
224
|
+
this.#state = { phase: destroyed ? "destroyed" : "disconnected" };
|
|
225
|
+
if (destroyed)
|
|
226
|
+
this.#destroyOwned();
|
|
227
|
+
else if (finalReason === "reject")
|
|
228
|
+
this.#onError(new Error("subscription rejected by the server"), "rejected");
|
|
229
|
+
this.#refreshStatus();
|
|
230
|
+
if (destroyed)
|
|
231
|
+
this.#statusListeners.clear();
|
|
232
|
+
}
|
|
233
|
+
#destroyOwned() {
|
|
234
|
+
this.session.destroy();
|
|
235
|
+
this.awareness.destroy();
|
|
236
|
+
this.doc.off("update", this.#onDocUpdate);
|
|
237
|
+
}
|
|
238
|
+
#unsubscribe(subscription) {
|
|
239
|
+
queueMicrotask(() => {
|
|
240
|
+
try {
|
|
241
|
+
subscription.unsubscribe?.();
|
|
198
242
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
243
|
+
catch (error) {
|
|
244
|
+
this.#onError(error, "unsubscribe");
|
|
245
|
+
}
|
|
246
|
+
});
|
|
203
247
|
}
|
|
204
|
-
#
|
|
205
|
-
if (
|
|
248
|
+
#receive(message, attempt) {
|
|
249
|
+
if (message && message.ack !== undefined) {
|
|
250
|
+
this.session.acknowledge(message.ack);
|
|
251
|
+
this.#refreshStatus();
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
const awarenessPayload = message && message.awareness;
|
|
255
|
+
const payload = message && (awarenessPayload ?? message.update);
|
|
256
|
+
if (typeof payload !== "string")
|
|
257
|
+
return;
|
|
258
|
+
let frame;
|
|
259
|
+
try {
|
|
260
|
+
frame = (0, base64_js_1.fromBase64)(payload);
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
this.#onError(error, "received");
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (awarenessPayload !== undefined && frame[0] !== y_protocol_session_js_1.MessageType.Awareness) {
|
|
267
|
+
this.#onError(new Error("awareness envelope carried a non-awareness frame"), "received");
|
|
206
268
|
return;
|
|
207
|
-
if (this.#onUnload) {
|
|
208
|
-
window.removeEventListener("pagehide", this.#onUnload);
|
|
209
|
-
this.#onUnload = null;
|
|
210
269
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
270
|
+
const reply = this.session.receive(frame);
|
|
271
|
+
// Applying the frame runs doc observers, which may have replaced the connection.
|
|
272
|
+
if (reply && this.#active(attempt))
|
|
273
|
+
this.#send(reply, undefined);
|
|
274
|
+
this.#refreshStatus();
|
|
275
|
+
}
|
|
276
|
+
#computeStatus() {
|
|
277
|
+
switch (this.#state.phase) {
|
|
278
|
+
case "subscribing":
|
|
279
|
+
case "connecting": return "connecting";
|
|
280
|
+
case "connected": return this.session.synced ? "synced" : "connected";
|
|
281
|
+
default: return "disconnected";
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
#refreshStatus() {
|
|
285
|
+
const status = this.#computeStatus();
|
|
286
|
+
const pending = this.hasPending;
|
|
287
|
+
if (status === this.#last.status && pending === this.#last.pending)
|
|
288
|
+
return;
|
|
289
|
+
const event = this.#last = { status, pending };
|
|
290
|
+
if (status === "synced")
|
|
291
|
+
this.#resolveSynced();
|
|
292
|
+
// A throwing listener is an application bug. Report it and continue so one
|
|
293
|
+
// bad listener can't stop the others or break the cable callback that
|
|
294
|
+
// triggered this refresh.
|
|
295
|
+
for (const listener of this.#statusListeners) {
|
|
296
|
+
if (this.#last !== event)
|
|
297
|
+
break; // a listener caused a newer transition
|
|
298
|
+
try {
|
|
299
|
+
listener({ status, pending });
|
|
300
|
+
}
|
|
301
|
+
catch (error) {
|
|
302
|
+
this.#onError(error, "listener");
|
|
303
|
+
}
|
|
214
304
|
}
|
|
215
305
|
}
|
|
306
|
+
// Handles presence across the page lifecycle. `pagehide` removes our cursor
|
|
307
|
+
// while the socket is still live, so peers drop it without waiting for the
|
|
308
|
+
// awareness timeout. A `pageshow` with `persisted` means the user came back
|
|
309
|
+
// through the bfcache, so we restore the cursor. Editor bindings set
|
|
310
|
+
// awareness only once at setup, so without this the user would return with
|
|
311
|
+
// no cursor.
|
|
312
|
+
#watchPage() {
|
|
313
|
+
if (typeof window === "undefined" || this.#page)
|
|
314
|
+
return;
|
|
315
|
+
let stashed = null;
|
|
316
|
+
const page = this.#page = {
|
|
317
|
+
hide: () => {
|
|
318
|
+
if (this.#page !== page)
|
|
319
|
+
return;
|
|
320
|
+
stashed = this.awareness.getLocalState();
|
|
321
|
+
this.session.removeLocalAwareness();
|
|
322
|
+
},
|
|
323
|
+
show: (event) => {
|
|
324
|
+
if (this.#page !== page || !event.persisted || !stashed)
|
|
325
|
+
return;
|
|
326
|
+
if (this.awareness.getLocalState() === null)
|
|
327
|
+
this.awareness.setLocalState(stashed);
|
|
328
|
+
stashed = null;
|
|
329
|
+
},
|
|
330
|
+
};
|
|
331
|
+
window.addEventListener("pagehide", this.#page.hide);
|
|
332
|
+
window.addEventListener("pageshow", this.#page.show);
|
|
333
|
+
}
|
|
334
|
+
#unwatchPage() {
|
|
335
|
+
if (!this.#page || typeof window === "undefined")
|
|
336
|
+
return;
|
|
337
|
+
const page = this.#page;
|
|
338
|
+
this.#page = null;
|
|
339
|
+
window.removeEventListener("pagehide", page.hide);
|
|
340
|
+
window.removeEventListener("pageshow", page.show);
|
|
341
|
+
}
|
|
216
342
|
// Send one raw protocol frame over the cable. Awareness frames are whispered
|
|
217
343
|
// when AnyCable exposes `subscription.whisper`; otherwise they fall back to a
|
|
218
344
|
// normal send. `id` (reliable doc updates) is tagged onto the envelope so the
|
|
219
345
|
// server can ack. A no-op while disconnected: reliable frames stay queued in
|
|
220
346
|
// the session and flush on the next connect().
|
|
221
347
|
#send(frame, id) {
|
|
222
|
-
const
|
|
223
|
-
if (!
|
|
348
|
+
const state = this.#state;
|
|
349
|
+
if (!("subscription" in state))
|
|
224
350
|
return;
|
|
225
|
-
const update = (0, base64_js_1.toBase64)(frame);
|
|
226
351
|
const isAwareness = frame[0] === y_protocol_session_js_1.MessageType.Awareness;
|
|
352
|
+
if (state.phase === "stopping" && !isAwareness)
|
|
353
|
+
return;
|
|
354
|
+
const { subscription } = state;
|
|
355
|
+
const update = (0, base64_js_1.toBase64)(frame);
|
|
227
356
|
// Route transport failures (sync throws, or @anycable/web's rejected
|
|
228
357
|
// promises) to onError instead of letting them escape into update
|
|
229
358
|
// handlers. A failed send is recoverable: reliable frames stay queued
|
|
230
|
-
// until acked, and awareness is best-effort anyway.
|
|
359
|
+
// until acked, and awareness is best-effort anyway. Failures from a
|
|
360
|
+
// subscription that has since been replaced go unreported.
|
|
361
|
+
const report = (error) => {
|
|
362
|
+
const current = this.#state;
|
|
363
|
+
if ("subscription" in current && current.subscription === subscription)
|
|
364
|
+
this.#onError(error, "send");
|
|
365
|
+
};
|
|
231
366
|
try {
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
this.#observe(sub.send(payload));
|
|
367
|
+
const result = isAwareness && typeof subscription.whisper === "function"
|
|
368
|
+
? subscription.whisper({ awareness: update })
|
|
369
|
+
: subscription.send(id === undefined ? { update } : { update, id });
|
|
370
|
+
if (result instanceof Promise)
|
|
371
|
+
result.catch(report);
|
|
238
372
|
}
|
|
239
373
|
catch (error) {
|
|
240
|
-
|
|
374
|
+
report(error);
|
|
241
375
|
}
|
|
242
376
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
377
|
+
}
|
|
378
|
+
exports.ActionCableProvider = ActionCableProvider;
|
|
379
|
+
// Awareness runs application listeners during presence removal and destruction.
|
|
380
|
+
// A failing listener must not interrupt either one or leave the awareness timer
|
|
381
|
+
// running.
|
|
382
|
+
class ProviderAwareness extends awareness_1.Awareness {
|
|
383
|
+
onError;
|
|
384
|
+
constructor(doc, onError) {
|
|
385
|
+
super(doc);
|
|
386
|
+
this.onError = onError;
|
|
387
|
+
}
|
|
388
|
+
emit(...args) {
|
|
389
|
+
try {
|
|
390
|
+
super.emit(...args);
|
|
391
|
+
}
|
|
392
|
+
catch (error) {
|
|
393
|
+
this.onError(error, `awareness:${args[0]}`);
|
|
248
394
|
}
|
|
249
395
|
}
|
|
250
396
|
}
|
|
251
|
-
exports.ActionCableProvider = ActionCableProvider;
|
|
@@ -0,0 +1,24 @@
|
|
|
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 {};
|