@abraca/dabra 0.1.6 → 0.2.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/dist/index.d.ts CHANGED
@@ -311,6 +311,7 @@ declare class AbracadabraProvider extends HocuspocusProvider {
311
311
  private _client;
312
312
  private offlineStore;
313
313
  private childProviders;
314
+ private pendingLoads;
314
315
  private subdocLoading;
315
316
  private abracadabraConfig;
316
317
  private readonly boundHandleYSubdocsChange;
@@ -353,6 +354,7 @@ declare class AbracadabraProvider extends HocuspocusProvider {
353
354
  * the server is document-scoped (one WebSocket ↔ one document).
354
355
  */
355
356
  loadChild(childId: string): Promise<AbracadabraProvider>;
357
+ private _doLoadChild;
356
358
  private unloadChild;
357
359
  /** Return all currently-loaded child providers. */
358
360
  get children(): Map<string, AbracadabraProvider>;
@@ -766,6 +768,7 @@ interface CompleteHocuspocusProviderConfiguration {
766
768
  forceSyncInterval: false | number;
767
769
  onAuthenticated: (data: onAuthenticatedParameters) => void;
768
770
  onAuthenticationFailed: (data: onAuthenticationFailedParameters) => void;
771
+ onRateLimited: () => void;
769
772
  onOpen: (data: onOpenParameters) => void;
770
773
  onConnect: () => void;
771
774
  onStatus: (data: onStatusParameters) => void;
@@ -807,6 +810,7 @@ declare class HocuspocusProvider extends EventEmitter {
807
810
  forwardClose: (e: onCloseParameters) => this;
808
811
  forwardDisconnect: (e: onDisconnectParameters) => this;
809
812
  forwardDestroy: () => this;
813
+ forwardRateLimited: () => this;
810
814
  setConfiguration(configuration?: Partial<HocuspocusProviderConfiguration>): void;
811
815
  get document(): Y.Doc;
812
816
  get isAttached(): boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abraca/dabra",
3
- "version": "0.1.6",
3
+ "version": "0.2.0",
4
4
  "description": "abracadabra provider",
5
5
  "keywords": [
6
6
  "abracadabra",
@@ -61,6 +61,11 @@ export interface AbracadabraProviderConfiguration
61
61
  websocketProvider?: HocuspocusProviderWebsocket;
62
62
  }
63
63
 
64
+ /** Validate that a string is a UUID acceptable by the server's DocId parser. */
65
+ function isValidDocId(id: string): boolean {
66
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id);
67
+ }
68
+
64
69
  /**
65
70
  * AbracadabraProvider extends HocuspocusProvider with:
66
71
  *
@@ -82,6 +87,7 @@ export class AbracadabraProvider extends HocuspocusProvider {
82
87
  private _client: AbracadabraClient | null;
83
88
  private offlineStore: OfflineStore | null;
84
89
  private childProviders = new Map<string, AbracadabraProvider>();
90
+ private pendingLoads = new Map<string, Promise<AbracadabraProvider>>();
85
91
  private subdocLoading: "lazy" | "eager";
86
92
 
87
93
  private abracadabraConfig: AbracadabraProviderConfiguration;
@@ -281,6 +287,7 @@ export class AbracadabraProvider extends HocuspocusProvider {
281
287
  loaded: Set<Y.Doc>;
282
288
  }) {
283
289
  for (const subdoc of added) {
290
+ if (!isValidDocId(subdoc.guid)) continue;
284
291
  this.registerSubdoc(subdoc);
285
292
  }
286
293
  for (const subdoc of removed) {
@@ -315,11 +322,33 @@ export class AbracadabraProvider extends HocuspocusProvider {
315
322
  * child document id. Each child opens its own WebSocket connection because
316
323
  * the server is document-scoped (one WebSocket ↔ one document).
317
324
  */
318
- async loadChild(childId: string): Promise<AbracadabraProvider> {
325
+ loadChild(childId: string): Promise<AbracadabraProvider> {
326
+ if (!isValidDocId(childId)) {
327
+ return Promise.reject(
328
+ new Error(
329
+ `loadChild: "${childId}" is not a valid document ID (must be a UUID). ` +
330
+ `If this node was created with an older version of the app, delete it and recreate it.`,
331
+ ),
332
+ );
333
+ }
334
+
319
335
  if (this.childProviders.has(childId)) {
320
- return this.childProviders.get(childId)!;
336
+ return Promise.resolve(this.childProviders.get(childId)!);
321
337
  }
322
338
 
339
+ // Deduplicate concurrent calls: return the same Promise so both callers
340
+ // get the exact same AbracadabraProvider instance.
341
+ if (this.pendingLoads.has(childId)) {
342
+ return this.pendingLoads.get(childId)!;
343
+ }
344
+
345
+ const load = this._doLoadChild(childId);
346
+ this.pendingLoads.set(childId, load);
347
+ load.finally(() => this.pendingLoads.delete(childId));
348
+ return load;
349
+ }
350
+
351
+ private async _doLoadChild(childId: string): Promise<AbracadabraProvider> {
323
352
  const childDoc = new Y.Doc({ guid: childId });
324
353
 
325
354
  // Notify the server that this child belongs to the parent document and
@@ -346,7 +375,7 @@ export class AbracadabraProvider extends HocuspocusProvider {
346
375
  const childProvider = new AbracadabraProvider({
347
376
  name: childId,
348
377
  document: childDoc,
349
- url: this.abracadabraConfig.url,
378
+ url: this.abracadabraConfig.url ?? this.configuration.websocketProvider?.url,
350
379
  token: this.configuration.token,
351
380
  subdocLoading: this.subdocLoading,
352
381
  disableOfflineStore: this.abracadabraConfig.disableOfflineStore,
@@ -78,6 +78,7 @@ export interface CompleteHocuspocusProviderConfiguration {
78
78
 
79
79
  onAuthenticated: (data: onAuthenticatedParameters) => void;
80
80
  onAuthenticationFailed: (data: onAuthenticationFailedParameters) => void;
81
+ onRateLimited: () => void;
81
82
  onOpen: (data: onOpenParameters) => void;
82
83
  onConnect: () => void;
83
84
  onStatus: (data: onStatusParameters) => void;
@@ -108,6 +109,7 @@ export class HocuspocusProvider extends EventEmitter {
108
109
  forceSyncInterval: false,
109
110
  onAuthenticated: () => null,
110
111
  onAuthenticationFailed: () => null,
112
+ onRateLimited: () => null,
111
113
  onOpen: () => null,
112
114
  onConnect: () => null,
113
115
  onMessage: () => null,
@@ -162,6 +164,7 @@ export class HocuspocusProvider extends EventEmitter {
162
164
 
163
165
  this.on("authenticated", this.configuration.onAuthenticated);
164
166
  this.on("authenticationFailed", this.configuration.onAuthenticationFailed);
167
+ this.on("rateLimited", this.configuration.onRateLimited);
165
168
 
166
169
  this.awareness?.on("update", () => {
167
170
  this.emit("awarenessUpdate", {
@@ -215,6 +218,8 @@ export class HocuspocusProvider extends EventEmitter {
215
218
 
216
219
  forwardDestroy = () => this.emit("destroy");
217
220
 
221
+ forwardRateLimited = () => this.emit("rateLimited");
222
+
218
223
  public setConfiguration(configuration: Partial<HocuspocusProviderConfiguration> = {}): void {
219
224
  if (!configuration.websocketProvider) {
220
225
  this.manageSocket = true;
@@ -492,6 +497,8 @@ export class HocuspocusProvider extends EventEmitter {
492
497
  this.configuration.websocketProvider.off("destroy", this.configuration.onDestroy);
493
498
  this.configuration.websocketProvider.off("destroy", this.forwardDestroy);
494
499
 
500
+ this.configuration.websocketProvider.off("rateLimited", this.forwardRateLimited);
501
+
495
502
  this.configuration.websocketProvider.detach(this);
496
503
 
497
504
  this._isAttached = false;
@@ -518,6 +525,8 @@ export class HocuspocusProvider extends EventEmitter {
518
525
  this.configuration.websocketProvider.on("destroy", this.configuration.onDestroy);
519
526
  this.configuration.websocketProvider.on("destroy", this.forwardDestroy);
520
527
 
528
+ this.configuration.websocketProvider.on("rateLimited", this.forwardRateLimited);
529
+
521
530
  this.configuration.websocketProvider.attach(this);
522
531
 
523
532
  this._isAttached = true;
@@ -500,13 +500,21 @@ export class HocuspocusProviderWebsocket extends EventEmitter {
500
500
  // Let’s update the connection status.
501
501
  this.status = WebSocketStatus.Disconnected;
502
502
  this.emit("status", { status: WebSocketStatus.Disconnected });
503
+
504
+ // Detect server-side rate-limit close (code 4429).
505
+ const isRateLimited = (event as any)?.code === 4429;
503
506
  this.emit("disconnect", { event });
507
+ if (isRateLimited) {
508
+ this.emit("rateLimited");
509
+ }
504
510
 
505
511
  // trigger connect if no retry is running and we want to have a connection
506
512
  if (!this.cancelWebsocketRetry && this.shouldConnect) {
513
+ // Apply a much longer delay for rate-limited closes to let the server window reset.
514
+ const delay = isRateLimited ? 60_000 : this.configuration.delay;
507
515
  setTimeout(() => {
508
516
  this.connect();
509
- }, this.configuration.delay);
517
+ }, delay);
510
518
  }
511
519
  }
512
520