yrby-client 0.5.0 → 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.
Files changed (44) hide show
  1. package/README.md +236 -36
  2. package/dist/actioncable_provider.d.ts +18 -11
  3. package/dist/actioncable_provider.d.ts.map +1 -1
  4. package/dist/actioncable_provider.js +286 -178
  5. package/dist/actioncable_provider.js.map +1 -1
  6. package/dist/cjs/actioncable_provider.d.ts +18 -11
  7. package/dist/cjs/actioncable_provider.js +287 -179
  8. package/dist/cjs/document_element.d.ts +24 -0
  9. package/dist/cjs/document_element.js +260 -0
  10. package/dist/cjs/document_session.d.ts +62 -0
  11. package/dist/cjs/document_session.js +335 -0
  12. package/dist/cjs/index.d.ts +2 -0
  13. package/dist/cjs/index.js +4 -1
  14. package/dist/cjs/reliable_sync.d.ts +18 -31
  15. package/dist/cjs/reliable_sync.js +128 -98
  16. package/dist/cjs/turbo_adapter.d.ts +9 -0
  17. package/dist/cjs/turbo_adapter.js +81 -0
  18. package/dist/cjs/y_protocol_session.d.ts +6 -6
  19. package/dist/cjs/y_protocol_session.js +96 -81
  20. package/dist/document_element.d.ts +25 -0
  21. package/dist/document_element.d.ts.map +1 -0
  22. package/dist/document_element.js +225 -0
  23. package/dist/document_element.js.map +1 -0
  24. package/dist/document_session.d.ts +63 -0
  25. package/dist/document_session.d.ts.map +1 -0
  26. package/dist/document_session.js +296 -0
  27. package/dist/document_session.js.map +1 -0
  28. package/dist/index.d.ts +2 -0
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +2 -0
  31. package/dist/index.js.map +1 -1
  32. package/dist/reliable_sync.d.ts +18 -31
  33. package/dist/reliable_sync.d.ts.map +1 -1
  34. package/dist/reliable_sync.js +128 -98
  35. package/dist/reliable_sync.js.map +1 -1
  36. package/dist/turbo_adapter.d.ts +10 -0
  37. package/dist/turbo_adapter.d.ts.map +1 -0
  38. package/dist/turbo_adapter.js +78 -0
  39. package/dist/turbo_adapter.js.map +1 -0
  40. package/dist/y_protocol_session.d.ts +6 -6
  41. package/dist/y_protocol_session.d.ts.map +1 -1
  42. package/dist/y_protocol_session.js +96 -81
  43. package/dist/y_protocol_session.js.map +1 -1
  44. package/package.json +29 -6
@@ -12,8 +12,8 @@ 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` getter. Editors that must not bind before the first sync
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
17
  // can `await provider.whenSynced` after connect(). On `disconnect()`/`destroy()`,
18
18
  // and on browser `pagehide`, the provider broadcasts a presence removal so peers
19
19
  // drop our cursor right away instead of waiting for the awareness timeout.
@@ -27,68 +27,58 @@ class ActionCableProvider {
27
27
  channelParams;
28
28
  awareness;
29
29
  session;
30
- #subscription = null;
30
+ #state = { phase: "disconnected" };
31
31
  #onError;
32
- #connected = false;
33
- #status = "disconnected";
32
+ // The last event sent to listeners. A refresh notifies them only when a field differs.
33
+ #last = { status: "disconnected", pending: false };
34
34
  #statusListeners = new Set();
35
- #whenSynced = null;
36
- // `session.synced` resets on every transport drop (a reconnect
37
- // re-handshakes). Whether the first catch-up has ever happened is tracked
38
- // separately here, so `whenSynced` does not depend on when it is first
39
- // read.
40
- #everSynced = false;
41
- #onUnload = null;
42
- #onRestore = null;
43
- #stashedPresence = null;
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; });
44
54
  constructor(doc, consumer, channelName, channelParams = {}, opts = {}) {
45
55
  this.doc = doc;
46
56
  this.consumer = consumer;
47
57
  this.channelName = channelName;
48
58
  this.channelParams = channelParams;
49
- this.awareness = new awareness_1.Awareness(doc);
50
- this.#onError = opts.onError ?? ((error, context) => console.warn(`[yrby] ${context}:`, error));
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);
51
69
  this.session = new y_protocol_session_js_1.YProtocolSession(doc, {
52
70
  awareness: this.awareness,
53
71
  resendInterval: opts.resendInterval,
54
72
  onError: this.#onError,
55
73
  send: (frame, id) => this.#send(frame, id),
56
74
  });
75
+ // Registered after the session's listener, so the queue already has the edit.
76
+ this.doc.on("update", this.#onDocUpdate);
57
77
  }
58
78
  /** True once the document has caught up with the server (received a SyncStep2). */
59
79
  get synced() {
60
80
  return this.session.synced;
61
81
  }
62
- /**
63
- * Resolves once the document has first caught up with the server. Most
64
- * editor bindings seed an empty document when they mount, so binding
65
- * before the server's state arrives makes each client insert its own
66
- * top-level node. Create the editor after this resolves:
67
- *
68
- * provider.connect();
69
- * await provider.whenSynced;
70
- * // now hand the doc to the editor binding
71
- *
72
- * Resolves immediately if the first catch-up has already happened, even
73
- * while the transport is down (`synced` is false during a reconnect;
74
- * whether the doc has ever synced does not change). It stays resolved
75
- * across later reconnects; use `onStatusChange` to track the live
76
- * connection. If the provider is destroyed before the first sync, the
77
- * promise never settles.
78
- */
79
- get whenSynced() {
80
- this.#whenSynced ??= this.#everSynced
81
- ? Promise.resolve()
82
- : new Promise((resolve) => {
83
- const off = this.onStatusChange(({ status }) => {
84
- if (status !== "synced")
85
- return;
86
- off();
87
- resolve();
88
- });
89
- });
90
- return this.#whenSynced;
91
- }
92
82
  /** True while there are unacknowledged local document updates in flight. */
93
83
  get hasPending() {
94
84
  return this.session.hasPending;
@@ -110,7 +100,7 @@ class ActionCableProvider {
110
100
  }
111
101
  /** Current connection status. See {@link ProviderStatus}. */
112
102
  get status() {
113
- return this.#status;
103
+ return this.#computeStatus();
114
104
  }
115
105
  /** Subscribe to status changes. Returns an unsubscribe function. */
116
106
  onStatusChange(listener) {
@@ -118,171 +108,289 @@ class ActionCableProvider {
118
108
  return () => this.#statusListeners.delete(listener);
119
109
  }
120
110
  connect() {
121
- if (this.#subscription)
111
+ if (this.#destroying())
112
+ throw new Error("provider is destroyed");
113
+ if (this.#state.phase !== "disconnected")
122
114
  return;
123
- const provider = this;
124
- this.#subscription = this.consumer.subscriptions.create({ channel: this.channelName, ...this.channelParams }, {
125
- received(message) {
126
- // Reliable-delivery ack: confirm + prune the local queue.
127
- if (message && message.ack !== undefined) {
128
- provider.session.ack(message.ack);
129
- return;
130
- }
131
- const awarenessPayload = message && message.awareness;
132
- const payload = message && (awarenessPayload ?? message.update);
133
- if (typeof payload !== "string")
134
- return;
135
- // Guard base64 decode too: a malformed envelope must not throw into
136
- // the cable callback (session.receive is itself defensive).
137
- let frame;
138
- try {
139
- frame = (0, base64_js_1.fromBase64)(payload);
140
- }
141
- catch (error) {
142
- provider.#onError(error, "received");
143
- return;
144
- }
145
- if (awarenessPayload !== undefined && frame[0] !== y_protocol_session_js_1.MessageType.Awareness) {
146
- provider.#onError(new Error("awareness envelope carried a non-awareness frame"), "received");
147
- return;
148
- }
149
- const reply = provider.session.receive(frame);
150
- if (reply)
151
- provider.#send(reply, undefined); // e.g. SyncStep2 answering a SyncStep1
152
- provider.#refreshStatus(); // a SyncStep2 may have just flipped us to "synced"
153
- },
154
- connected() {
155
- provider.#connected = true;
156
- provider.session.onConnect(); // handshake + replay the unacked tail
157
- provider.#refreshStatus();
158
- },
159
- disconnected() {
160
- provider.#connected = false;
161
- provider.session.onDisconnect(); // pause retransmits, clear remote presence
162
- provider.#refreshStatus(); // subscription still set -> "connecting" (retrying)
163
- },
164
- rejected() {
165
- // The channel refused the subscription (auth, missing doc). Surface
166
- // it and tear down — otherwise the provider sits at "connecting"
167
- // forever, silently queueing edits. The app decides what's next.
168
- provider.#onError(new Error("subscription rejected by the server"), "rejected");
169
- provider.disconnect();
170
- },
171
- });
172
- this.#installUnloadHandler();
173
- this.#refreshStatus(); // -> "connecting"
174
- }
175
- disconnect() {
176
- if (!this.#subscription)
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);
177
146
  return;
178
- const sub = this.#subscription;
179
- // Tell peers we're gone while the transport is still live, then pause and
180
- // detach. Defer the unsubscribe one microtask so the removal frame flushes
181
- // before the channel tears down.
182
- this.session.removeLocalAwareness();
183
- this.session.onDisconnect();
184
- this.#connected = false;
185
- this.#subscription = null;
186
- this.#removeUnloadHandler();
187
- // Universal teardown: both @rails/actioncable and @anycable/web subscriptions
188
- // expose unsubscribe() (Rails' just calls consumer.subscriptions.remove(this)
189
- // internally). @anycable has NO consumer.subscriptions.remove, so calling that
190
- // would throw there.
191
- queueMicrotask(() => sub.unsubscribe?.());
192
- this.#refreshStatus(); // -> "disconnected"
147
+ }
148
+ this.#state = { phase: "connecting", attempt, subscription };
149
+ this.#watchPage();
150
+ this.#refreshStatus();
193
151
  }
194
- destroy() {
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);
195
163
  this.disconnect();
196
- this.session.destroy();
197
- this.awareness.destroy(); // stops its reaper timer
198
- this.#statusListeners.clear();
164
+ this.connect();
199
165
  }
200
- #computeStatus() {
201
- if (!this.#subscription)
202
- return "disconnected";
203
- if (!this.#connected)
204
- return "connecting";
205
- 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");
206
170
  }
207
- #refreshStatus() {
208
- const next = this.#computeStatus();
209
- if (next === this.#status)
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")
210
192
  return;
211
- this.#status = next;
212
- if (next === "synced")
213
- this.#everSynced = true;
214
- for (const listener of this.#statusListeners)
215
- listener({ status: next });
193
+ this.#state = { ...state, phase: "connecting" };
194
+ this.session.pause();
195
+ this.#refreshStatus();
216
196
  }
217
- // Presence teardown/restore around page lifecycle:
218
- // - `pagehide`: remove local presence while the socket is still live so peers
219
- // drop our cursor now (bfcache-safe; the awareness timeout is the backstop).
220
- // - `pageshow` with `persisted`: the user came BACK (bfcache restore), so put
221
- // their presence back — editors set awareness once at setup, so without
222
- // this they'd rejoin as a ghost with no cursor.
223
- #installUnloadHandler() {
224
- if (typeof window === "undefined" || this.#onUnload)
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;
225
204
  return;
226
- this.#onUnload = () => {
227
- this.#stashedPresence = this.awareness.getLocalState();
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.
228
216
  this.session.removeLocalAwareness();
229
- };
230
- this.#onRestore = (event) => {
231
- if (!event.persisted || !this.#stashedPresence)
217
+ this.session.pause();
218
+ this.#unsubscribe(state.subscription);
219
+ if (this.#state !== stopping)
232
220
  return;
233
- if (this.awareness.getLocalState() === null) {
234
- this.awareness.setLocalState(this.#stashedPresence);
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?.();
235
242
  }
236
- this.#stashedPresence = null;
237
- };
238
- window.addEventListener("pagehide", this.#onUnload);
239
- window.addEventListener("pageshow", this.#onRestore);
243
+ catch (error) {
244
+ this.#onError(error, "unsubscribe");
245
+ }
246
+ });
240
247
  }
241
- #removeUnloadHandler() {
242
- if (typeof window === "undefined")
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");
243
264
  return;
244
- if (this.#onUnload) {
245
- window.removeEventListener("pagehide", this.#onUnload);
246
- this.#onUnload = null;
247
265
  }
248
- if (this.#onRestore) {
249
- window.removeEventListener("pageshow", this.#onRestore);
250
- this.#onRestore = null;
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");
268
+ return;
269
+ }
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
+ }
251
304
  }
252
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
+ }
253
342
  // Send one raw protocol frame over the cable. Awareness frames are whispered
254
343
  // when AnyCable exposes `subscription.whisper`; otherwise they fall back to a
255
344
  // normal send. `id` (reliable doc updates) is tagged onto the envelope so the
256
345
  // server can ack. A no-op while disconnected: reliable frames stay queued in
257
346
  // the session and flush on the next connect().
258
347
  #send(frame, id) {
259
- const sub = this.#subscription;
260
- if (!sub)
348
+ const state = this.#state;
349
+ if (!("subscription" in state))
261
350
  return;
262
- const update = (0, base64_js_1.toBase64)(frame);
263
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);
264
356
  // Route transport failures (sync throws, or @anycable/web's rejected
265
357
  // promises) to onError instead of letting them escape into update
266
358
  // handlers. A failed send is recoverable: reliable frames stay queued
267
- // 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
+ };
268
366
  try {
269
- if (isAwareness && typeof sub.whisper === "function") {
270
- this.#observe(sub.whisper({ awareness: update }));
271
- return;
272
- }
273
- const payload = id === undefined ? { update } : { update, id };
274
- 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);
275
372
  }
276
373
  catch (error) {
277
- this.#onError(error, "send");
374
+ report(error);
278
375
  }
279
376
  }
280
- // Attach a rejection handler when a transport returns a promise, so failures
281
- // surface via onError instead of as unhandled rejections.
282
- #observe(result) {
283
- if (result instanceof Promise) {
284
- result.catch((error) => this.#onError(error, "send"));
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]}`);
285
394
  }
286
395
  }
287
396
  }
288
- 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 {};