@abraca/dabra 1.9.1 → 2.0.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abraca/dabra",
3
- "version": "1.9.1",
3
+ "version": "2.0.1",
4
4
  "description": "abracadabra provider",
5
5
  "keywords": [
6
6
  "abracadabra",
@@ -11,6 +11,7 @@ import { MessageSender } from "./MessageSender.ts";
11
11
  import { AuthenticationMessage } from "./OutgoingMessages/AuthenticationMessage.ts";
12
12
  import { AwarenessMessage } from "./OutgoingMessages/AwarenessMessage.ts";
13
13
  import { StatelessMessage } from "./OutgoingMessages/StatelessMessage.ts";
14
+ import { RpcClient } from "./RpcClient.ts";
14
15
  import { SyncStepOneMessage } from "./OutgoingMessages/SyncStepOneMessage.ts";
15
16
  import { UpdateMessage } from "./OutgoingMessages/UpdateMessage.ts";
16
17
  import type {
@@ -157,6 +158,19 @@ export class AbracadabraBaseProvider extends EventEmitter {
157
158
  forceSync: null,
158
159
  };
159
160
 
161
+ /**
162
+ * Lazily-constructed RPC v1 client. See `RpcClient`/`docs/rpc-v1.md`.
163
+ * Bound to this provider's stateless transport, so calls and handlers
164
+ * ride the doc this provider is subscribed to.
165
+ */
166
+ private _rpc: RpcClient | undefined;
167
+ get rpc(): RpcClient {
168
+ if (!this._rpc) {
169
+ this._rpc = new RpcClient(this);
170
+ }
171
+ return this._rpc;
172
+ }
173
+
160
174
  constructor(configuration: AbracadabraBaseProviderConfiguration) {
161
175
  super();
162
176
  this.setConfiguration(configuration);
@@ -167,6 +181,15 @@ export class AbracadabraBaseProvider extends EventEmitter {
167
181
  ? configuration.awareness
168
182
  : new Awareness(this.document);
169
183
 
184
+ // y-protocols' setLocalStateField silently no-ops when getLocalState() is null.
185
+ // Seeding with {} once ensures the *first* setLocalStateField call from any
186
+ // consumer (TipTap CollaborationCaret, DocumentManager, app code) actually
187
+ // publishes — instead of vanishing without a trace. Idempotent for callers
188
+ // that pass their own awareness instance with state already set.
189
+ if (this.awareness && this.awareness.getLocalState() === null) {
190
+ this.awareness.setLocalState({});
191
+ }
192
+
170
193
  this.on("open", this.configuration.onOpen);
171
194
  this.on("message", this.configuration.onMessage);
172
195
  this.on("outgoingMessage", this.configuration.onOutgoingMessage);
@@ -264,7 +287,12 @@ export class AbracadabraBaseProvider extends EventEmitter {
264
287
  }
265
288
 
266
289
  private resetUnsyncedChanges() {
267
- this.unsyncedChanges = 1;
290
+ // The unsynced counter tracks pending UpdateMessage acks (decremented by
291
+ // SyncStatusMessage). The handshake itself is a connection-level concern,
292
+ // not a doc update — reflect that by resetting to 0. The synced flag and
293
+ // status: 'connecting' already surface in-flight sync to the UI; bumping
294
+ // this counter to 1 would leave it stuck (SyncStep2 doesn't decrement).
295
+ this.unsyncedChanges = 0;
268
296
  this.emit("unsyncedChanges", { number: this.unsyncedChanges });
269
297
  }
270
298
 
@@ -367,7 +395,18 @@ export class AbracadabraBaseProvider extends EventEmitter {
367
395
  this.isSynced = state;
368
396
 
369
397
  if (state) {
370
- this.emit("synced", { state });
398
+ // Defer the emit one microtask. Y.applyUpdate dispatches Y.Map /
399
+ // Y.Array observer notifications via microtasks; emitting `synced`
400
+ // synchronously would let listeners read `getArray("messages")` (or
401
+ // any sub-type populated by the just-applied state) and see length
402
+ // 0 even though the wire delivered the full state. Deferring one
403
+ // microtask lets the observers run first so everyone reading after
404
+ // the synced event sees the populated state.
405
+ queueMicrotask(() => {
406
+ // Re-check in case `synced` flipped back to false between the
407
+ // mutation and this microtask (e.g. a reconnect aborted sync).
408
+ if (this.isSynced) this.emit("synced", { state });
409
+ });
371
410
  }
372
411
  }
373
412
 
@@ -481,6 +520,16 @@ export class AbracadabraBaseProvider extends EventEmitter {
481
520
  destroy() {
482
521
  this.emit("destroy");
483
522
 
523
+ if (this._rpc) {
524
+ // Keep the (now-destroyed) RpcClient bound to `_rpc` rather than
525
+ // setting it back to `undefined`. The lazy getter would otherwise
526
+ // construct a fresh client wired against a provider whose listeners
527
+ // are gone, leaving any subsequent `rpc.call(...)` to hang forever.
528
+ // The client's own `destroyed` flag now turns `call()` into an
529
+ // immediate `CANCELLED` rejection — the right terminal outcome.
530
+ this._rpc.destroy();
531
+ }
532
+
484
533
  if (this.intervals.forceSync) {
485
534
  clearInterval(this.intervals.forceSync);
486
535
  }
@@ -562,6 +611,25 @@ export class AbracadabraBaseProvider extends EventEmitter {
562
611
  permissionDeniedHandler(reason: string) {
563
612
  this.emit("authenticationFailed", { reason });
564
613
  this.isAuthenticated = false;
614
+
615
+ // Stop the reconnect loop on a permanent denial. Without this the WS
616
+ // closes after every server-side rejection, AbracadabraWS schedules a
617
+ // retry, the new connection sends the same Subscribe → server denies
618
+ // again, ad infinitum (CPU/log/network spam, page looks "broken" to
619
+ // the user). The dominant cases:
620
+ // * "document not found" — entry-doc-id pinned to something that
621
+ // doesn't exist on this server
622
+ // * "permission denied" — operator never granted the user access
623
+ // Both are config issues; retrying won't fix them. We only stop the
624
+ // socket when this provider OWNS it (`manageSocket`); shared sockets
625
+ // (e.g. multiplexed child providers) leave the parent's lifecycle
626
+ // alone and just give up on this doc.
627
+ if (this.manageSocket) {
628
+ try {
629
+ this.configuration.websocketProvider.disconnect();
630
+ }
631
+ catch { /* WS may already be torn down — best effort */ }
632
+ }
565
633
  }
566
634
 
567
635
  authenticatedHandler(scope: string) {