@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/abracadabra-provider.cjs +33 -7
- package/dist/abracadabra-provider.cjs.map +1 -1
- package/dist/abracadabra-provider.esm.js +33 -7
- package/dist/abracadabra-provider.esm.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/package.json +1 -1
- package/src/AbracadabraProvider.ts +32 -3
- package/src/HocuspocusProvider.ts +9 -0
- package/src/HocuspocusProviderWebsocket.ts +9 -1
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
|
@@ -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
|
-
|
|
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
|
-
},
|
|
517
|
+
}, delay);
|
|
510
518
|
}
|
|
511
519
|
}
|
|
512
520
|
|