@abraca/dabra 1.0.17 → 1.0.18

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
@@ -1647,6 +1647,10 @@ interface BackgroundSyncManagerOptions {
1647
1647
  syncTimeout?: number;
1648
1648
  /** Pre-cache file blobs after syncing a doc. Default: true. */
1649
1649
  prefetchFiles?: boolean;
1650
+ /** Delay (ms) between starting each doc sync to avoid server pressure. Default: 50. */
1651
+ throttleMs?: number;
1652
+ /** Max retries for failed docs within a single syncAll() run. Default: 2. */
1653
+ maxRetries?: number;
1650
1654
  }
1651
1655
  declare class BackgroundSyncManager extends EventEmitter {
1652
1656
  private readonly rootProvider;
@@ -1692,6 +1696,7 @@ declare class BackgroundSyncManager extends EventEmitter {
1692
1696
  * 3. Errored docs last
1693
1697
  */
1694
1698
  private _buildQueue;
1699
+ /** Returns true on success (or skip), false on error. */
1695
1700
  private _syncWithSemaphore;
1696
1701
  private _doSyncDoc;
1697
1702
  private _syncNonE2EDoc;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abraca/dabra",
3
- "version": "1.0.17",
3
+ "version": "1.0.18",
4
4
  "description": "abracadabra provider",
5
5
  "keywords": [
6
6
  "abracadabra",
@@ -584,11 +584,25 @@ export class AbracadabraProvider extends AbracadabraBaseProvider {
584
584
  override destroy() {
585
585
  this.document.off("subdocs", this.boundHandleYSubdocsChange);
586
586
 
587
+ // Collect child IDs before destroying — detach() may skip cleanup if
588
+ // a new provider already overwrote the slot during teardown/re-init races.
589
+ const childIds = [...this.childProviders.keys()];
590
+
587
591
  for (const provider of this.childProviders.values()) {
588
592
  provider.destroy();
589
593
  }
590
594
  this.childProviders.clear();
591
595
 
596
+ // Force-clear any stale providerMap entries for our children.
597
+ // detach() only removes if current === provider, so orphans can remain
598
+ // when a new provider attached before the old one was destroyed.
599
+ const wsProviderMap = this.configuration.websocketProvider?.configuration?.providerMap;
600
+ if (wsProviderMap) {
601
+ for (const childId of childIds) {
602
+ wsProviderMap.delete(childId);
603
+ }
604
+ }
605
+
592
606
  this.offlineStore?.destroy();
593
607
  this.offlineStore = null;
594
608
 
@@ -38,6 +38,10 @@ export interface BackgroundSyncManagerOptions {
38
38
  syncTimeout?: number;
39
39
  /** Pre-cache file blobs after syncing a doc. Default: true. */
40
40
  prefetchFiles?: boolean;
41
+ /** Delay (ms) between starting each doc sync to avoid server pressure. Default: 50. */
42
+ throttleMs?: number;
43
+ /** Max retries for failed docs within a single syncAll() run. Default: 2. */
44
+ maxRetries?: number;
41
45
  }
42
46
 
43
47
  /**
@@ -96,6 +100,8 @@ export class BackgroundSyncManager extends EventEmitter {
96
100
  concurrency: opts?.concurrency ?? 2,
97
101
  syncTimeout: opts?.syncTimeout ?? 15_000,
98
102
  prefetchFiles: opts?.prefetchFiles ?? true,
103
+ throttleMs: opts?.throttleMs ?? 50,
104
+ maxRetries: opts?.maxRetries ?? 2,
99
105
  };
100
106
 
101
107
  // Derive server origin from client URL for IDB namespacing
@@ -155,12 +161,54 @@ export class BackgroundSyncManager extends EventEmitter {
155
161
  // Build the priority queue
156
162
  const queue = this._buildQueue(entries);
157
163
 
158
- // Sync all docs respecting concurrency limit
159
- await Promise.all(
160
- queue.map((docId) =>
161
- this._syncWithSemaphore(docId, updatedAtMap.get(docId) ?? 0),
162
- ),
163
- );
164
+ // Sync all docs with throttling: stagger starts to avoid server pressure.
165
+ // We use a feeding loop instead of Promise.all(queue.map(...)) so that
166
+ // each new doc start is spaced by throttleMs.
167
+ const failed: string[] = [];
168
+ let idx = 0;
169
+ const next = async (): Promise<void> => {
170
+ while (idx < queue.length) {
171
+ if (this._destroyed) return;
172
+ const docId = queue[idx++]!;
173
+ const updatedAt = updatedAtMap.get(docId) ?? 0;
174
+ const ok = await this._syncWithSemaphore(docId, updatedAt);
175
+ if (!ok) failed.push(docId);
176
+ // Throttle between starts
177
+ if (this.opts.throttleMs > 0 && idx < queue.length) {
178
+ await new Promise((r) => setTimeout(r, this.opts.throttleMs));
179
+ }
180
+ }
181
+ };
182
+
183
+ // Launch `concurrency` parallel workers feeding from the shared index.
184
+ const workers = Array.from({ length: this.opts.concurrency }, () => next());
185
+ await Promise.all(workers);
186
+
187
+ // Retry failed docs with increasing backoff.
188
+ for (let retry = 0; retry < this.opts.maxRetries && failed.length > 0; retry++) {
189
+ if (this._destroyed) return;
190
+ const batch = failed.splice(0, failed.length);
191
+ // Backoff: 2s, 4s, 8s...
192
+ const backoff = 2000 * 2 ** retry;
193
+ await new Promise((r) => setTimeout(r, backoff));
194
+
195
+ idx = 0;
196
+ const retryQueue = batch;
197
+ const retryNext = async (): Promise<void> => {
198
+ while (idx < retryQueue.length) {
199
+ if (this._destroyed) return;
200
+ const docId = retryQueue[idx++]!;
201
+ const updatedAt = updatedAtMap.get(docId) ?? 0;
202
+ const ok = await this._syncWithSemaphore(docId, updatedAt);
203
+ if (!ok) failed.push(docId);
204
+ if (this.opts.throttleMs > 0 && idx < retryQueue.length) {
205
+ await new Promise((r) => setTimeout(r, this.opts.throttleMs));
206
+ }
207
+ }
208
+ };
209
+ const retryWorkers = Array.from({ length: this.opts.concurrency }, () => retryNext());
210
+ await Promise.all(retryWorkers);
211
+ }
164
212
  }
165
213
 
166
214
  /** Sync a single document by ID. */
@@ -273,11 +321,12 @@ export class BackgroundSyncManager extends EventEmitter {
273
321
  return items.map((i) => i.docId);
274
322
  }
275
323
 
324
+ /** Returns true on success (or skip), false on error. */
276
325
  private async _syncWithSemaphore(
277
326
  docId: string,
278
327
  updatedAt: number,
279
- ): Promise<void> {
280
- if (this._destroyed) return;
328
+ ): Promise<boolean> {
329
+ if (this._destroyed) return true;
281
330
 
282
331
  // Skip if already synced and doc hasn't changed since last sync
283
332
  const existing = this.syncStates.get(docId);
@@ -288,7 +337,7 @@ export class BackgroundSyncManager extends EventEmitter {
288
337
  existing.lastSynced >= updatedAt
289
338
  ) {
290
339
  this.emit("stateChanged", { docId, state: existing });
291
- return;
340
+ return true;
292
341
  }
293
342
 
294
343
  await this.semaphore.acquire();
@@ -297,6 +346,7 @@ export class BackgroundSyncManager extends EventEmitter {
297
346
  this.syncStates.set(docId, state);
298
347
  await this.persistence.setState(state).catch(() => null);
299
348
  this.emit("stateChanged", { docId, state });
349
+ return state.status !== "error";
300
350
  } finally {
301
351
  this.semaphore.release();
302
352
  }