@tanstack/db-sqlite-persistence-core 0.2.23 → 0.4.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.
Files changed (58) hide show
  1. package/README.md +258 -7
  2. package/dist/cjs/broadcast-coordinator.cjs +1689 -0
  3. package/dist/cjs/broadcast-coordinator.cjs.map +1 -0
  4. package/dist/cjs/broadcast-coordinator.d.cts +116 -0
  5. package/dist/cjs/errors.cjs +73 -0
  6. package/dist/cjs/errors.cjs.map +1 -1
  7. package/dist/cjs/errors.d.cts +44 -0
  8. package/dist/cjs/index.cjs +19 -0
  9. package/dist/cjs/index.cjs.map +1 -1
  10. package/dist/cjs/index.d.cts +2 -0
  11. package/dist/cjs/persisted.cjs +1782 -394
  12. package/dist/cjs/persisted.cjs.map +1 -1
  13. package/dist/cjs/persisted.d.cts +176 -11
  14. package/dist/cjs/remote-subset-owner.cjs +20 -0
  15. package/dist/cjs/remote-subset-owner.cjs.map +1 -0
  16. package/dist/cjs/remote-subset-owner.d.cts +7 -0
  17. package/dist/cjs/remote-subset-wire.cjs +681 -0
  18. package/dist/cjs/remote-subset-wire.cjs.map +1 -0
  19. package/dist/cjs/remote-subset-wire.d.cts +59 -0
  20. package/dist/cjs/sqlite-core-adapter.cjs +969 -364
  21. package/dist/cjs/sqlite-core-adapter.cjs.map +1 -1
  22. package/dist/cjs/sqlite-core-adapter.d.cts +45 -1
  23. package/dist/cjs/sqlite-value.cjs +31 -0
  24. package/dist/cjs/sqlite-value.cjs.map +1 -0
  25. package/dist/cjs/sqlite-value.d.cts +10 -0
  26. package/dist/esm/broadcast-coordinator.d.ts +116 -0
  27. package/dist/esm/broadcast-coordinator.js +1689 -0
  28. package/dist/esm/broadcast-coordinator.js.map +1 -0
  29. package/dist/esm/errors.d.ts +44 -0
  30. package/dist/esm/errors.js +74 -1
  31. package/dist/esm/errors.js.map +1 -1
  32. package/dist/esm/index.d.ts +2 -0
  33. package/dist/esm/index.js +21 -2
  34. package/dist/esm/index.js.map +1 -1
  35. package/dist/esm/persisted.d.ts +176 -11
  36. package/dist/esm/persisted.js +1784 -396
  37. package/dist/esm/persisted.js.map +1 -1
  38. package/dist/esm/remote-subset-owner.d.ts +7 -0
  39. package/dist/esm/remote-subset-owner.js +20 -0
  40. package/dist/esm/remote-subset-owner.js.map +1 -0
  41. package/dist/esm/remote-subset-wire.d.ts +59 -0
  42. package/dist/esm/remote-subset-wire.js +681 -0
  43. package/dist/esm/remote-subset-wire.js.map +1 -0
  44. package/dist/esm/sqlite-core-adapter.d.ts +45 -1
  45. package/dist/esm/sqlite-core-adapter.js +955 -350
  46. package/dist/esm/sqlite-core-adapter.js.map +1 -1
  47. package/dist/esm/sqlite-value.d.ts +10 -0
  48. package/dist/esm/sqlite-value.js +31 -0
  49. package/dist/esm/sqlite-value.js.map +1 -0
  50. package/package.json +12 -2
  51. package/src/broadcast-coordinator.ts +2456 -0
  52. package/src/errors.ts +120 -0
  53. package/src/index.ts +2 -0
  54. package/src/persisted.ts +2617 -442
  55. package/src/remote-subset-owner.ts +36 -0
  56. package/src/remote-subset-wire.ts +946 -0
  57. package/src/sqlite-core-adapter.ts +1297 -410
  58. package/src/sqlite-value.ts +30 -0
@@ -0,0 +1,1689 @@
1
+ import { safeRandomUUID } from "@tanstack/db";
2
+ import { DuplicateRemoteSubsetOwnerError, RetryableRemoteSubsetAcquisitionError, IndeterminateCommitError, toPersistedCollectionDurabilityError, PersistedCollectionDurabilityError } from "./errors.js";
3
+ import { toTransportedLoadSubsetOptions, toProcessLocalLoadSubsetOptions } from "./remote-subset-wire.js";
4
+ import { unloadRemoteSubsetOwner } from "./remote-subset-owner.js";
5
+ const HEARTBEAT_INTERVAL_MS = 3e3;
6
+ const LEADERSHIP_RETRY_DELAY_MS = 200;
7
+ const RPC_TIMEOUT_MS = 1e4;
8
+ const RPC_RETRY_ATTEMPTS = 2;
9
+ const RPC_RETRY_DELAY_MS = 200;
10
+ const REMOTE_SUBSET_REPLAY_RETRY_ATTEMPTS = 2;
11
+ const RPC_DEDUPE_RETENTION_MS = 6e4;
12
+ const WRITER_LOCK_BUSY_RETRY_MS = 50;
13
+ const WRITER_LOCK_MAX_RETRIES = 20;
14
+ class BroadcastCollectionCoordinator {
15
+ constructor(options) {
16
+ this.nodeId = safeRandomUUID();
17
+ this.collectionAdapters = /* @__PURE__ */ new Map();
18
+ this.remoteSubsetOwners = /* @__PURE__ */ new Map();
19
+ this.remoteSubsetIds = /* @__PURE__ */ new Map();
20
+ this.outboundRemoteSubsetAcquisitions = /* @__PURE__ */ new Map();
21
+ this.inboundRemoteSubsetAcquisitions = /* @__PURE__ */ new Map();
22
+ this.releasedRemoteSubsetAcquisitionTimes = /* @__PURE__ */ new Map();
23
+ this.collections = /* @__PURE__ */ new Map();
24
+ this.pendingRPCs = /* @__PURE__ */ new Map();
25
+ this.appliedEnvelopes = /* @__PURE__ */ new Map();
26
+ this.inFlightEnvelopes = /* @__PURE__ */ new Map();
27
+ this.appliedEnvelopePruneTimer = null;
28
+ this.rejectDisposed = null;
29
+ this.disposed = false;
30
+ this.dbName = options.dbName;
31
+ this.coordinatorName = options.coordinatorName;
32
+ this.defaultAdapter = options.adapter ?? null;
33
+ this.channel = new BroadcastChannel(`tsdb:coord:${this.dbName}`);
34
+ this.channel.onmessage = (event) => {
35
+ this.onChannelMessage(event.data);
36
+ };
37
+ this.disposedPromise = new Promise((_resolve, reject) => {
38
+ this.rejectDisposed = reject;
39
+ });
40
+ void this.disposedPromise.catch(() => void 0);
41
+ }
42
+ /** Method indirection to prevent TypeScript from narrowing `disposed` across awaits */
43
+ isDisposed() {
44
+ return this.disposed;
45
+ }
46
+ /** Method indirection to re-read leadership participation across awaits. */
47
+ participatesInLeadership(state) {
48
+ return state.participatesInLeadership;
49
+ }
50
+ requireAdapter(collectionId) {
51
+ let adapter = this.collectionAdapters.get(collectionId);
52
+ if (!adapter && this.defaultAdapter) {
53
+ adapter = this.defaultAdapter;
54
+ this.collectionAdapters.set(collectionId, adapter);
55
+ }
56
+ if (!adapter) {
57
+ throw new Error(
58
+ `${this.coordinatorName}: adapter not set for collection "${collectionId}". Call setAdapterForCollection() before using leader-side operations.`
59
+ );
60
+ }
61
+ return adapter;
62
+ }
63
+ /**
64
+ * Set or replace the persistence adapter used for leader-side RPC handling.
65
+ * Called by a host persistence factory to wire the internally-created adapter.
66
+ */
67
+ setAdapter(adapter) {
68
+ this.defaultAdapter = adapter;
69
+ }
70
+ /** Register the persistence adapter that owns one collection. */
71
+ setAdapterForCollection(collectionId, adapter) {
72
+ this.collectionAdapters.set(collectionId, adapter);
73
+ }
74
+ registerRemoteSubsetOwner(collectionId, owner) {
75
+ if (this.remoteSubsetOwners.has(collectionId)) {
76
+ throw new DuplicateRemoteSubsetOwnerError(collectionId);
77
+ }
78
+ this.remoteSubsetOwners.set(collectionId, owner);
79
+ for (const acquisition of this.outboundRemoteSubsetAcquisitions.values()) {
80
+ if (acquisition.collectionId !== collectionId) continue;
81
+ acquisition.acquiredLeaderId = null;
82
+ acquisition.forceReplay = true;
83
+ }
84
+ void this.replayRemoteSubsetAcquisitions(collectionId);
85
+ this.rebindRemoteInboundSubsetAcquisitions(collectionId, owner);
86
+ return () => {
87
+ if (this.remoteSubsetOwners.get(collectionId) !== owner) return;
88
+ this.remoteSubsetOwners.delete(collectionId);
89
+ this.releaseInboundRemoteSubsetAcquisitions(collectionId, owner);
90
+ };
91
+ }
92
+ // -----------------------------------------------------------------------
93
+ // PersistedCollectionCoordinator interface
94
+ // -----------------------------------------------------------------------
95
+ getNodeId() {
96
+ return this.nodeId;
97
+ }
98
+ subscribe(collectionId, onMessage) {
99
+ const state = this.ensureCollectionState(collectionId);
100
+ state.subscribers.add(onMessage);
101
+ return () => {
102
+ state.subscribers.delete(onMessage);
103
+ if (state.subscribers.size === 0) {
104
+ state.participatesInLeadership = false;
105
+ this.releaseCollectionIfUnused(collectionId, state);
106
+ }
107
+ };
108
+ }
109
+ publish(_collectionId, message) {
110
+ this.channel.postMessage(message);
111
+ }
112
+ isLeader(collectionId) {
113
+ return this.collections.get(collectionId)?.isLeader ?? false;
114
+ }
115
+ async ensureLeadership(collectionId) {
116
+ const state = this.ensureCollectionState(collectionId);
117
+ if (state.isLeader) return;
118
+ await this.acquireLeadership(collectionId, state);
119
+ }
120
+ async requestEnsureRemoteSubset(collectionId, options) {
121
+ const transportedOptions = toTransportedLoadSubsetOptions(options);
122
+ const localOptions = toProcessLocalLoadSubsetOptions(
123
+ options,
124
+ transportedOptions
125
+ );
126
+ let collectionIds = this.remoteSubsetIds.get(collectionId);
127
+ if (!collectionIds) {
128
+ collectionIds = /* @__PURE__ */ new WeakMap();
129
+ this.remoteSubsetIds.set(collectionId, collectionIds);
130
+ }
131
+ let acquisitionId = collectionIds.get(options);
132
+ let acquisition = acquisitionId ? this.outboundRemoteSubsetAcquisitions.get(
133
+ remoteSubsetAcquisitionKey(collectionId, acquisitionId)
134
+ ) : void 0;
135
+ if (!acquisition) {
136
+ acquisitionId = safeRandomUUID();
137
+ collectionIds.set(options, acquisitionId);
138
+ acquisition = {
139
+ collectionId,
140
+ acquisitionId,
141
+ options: transportedOptions,
142
+ localOptions,
143
+ acquiredLeaderId: null,
144
+ inFlight: null,
145
+ release: null,
146
+ releaseRequested: false,
147
+ forceReplay: false,
148
+ retryTimer: null,
149
+ retryAttempts: 0
150
+ };
151
+ this.outboundRemoteSubsetAcquisitions.set(
152
+ remoteSubsetAcquisitionKey(collectionId, acquisitionId),
153
+ acquisition
154
+ );
155
+ }
156
+ try {
157
+ await this.acquireRemoteSubset(acquisition);
158
+ } catch (error) {
159
+ if (options.signal?.aborted) {
160
+ acquisition.releaseRequested = true;
161
+ this.cancelRemoteSubsetReplayRetry(acquisition);
162
+ this.postRemoteSubsetRelease(acquisition);
163
+ const key = remoteSubsetAcquisitionKey(
164
+ collectionId,
165
+ acquisition.acquisitionId
166
+ );
167
+ if (this.outboundRemoteSubsetAcquisitions.get(key) === acquisition) {
168
+ this.outboundRemoteSubsetAcquisitions.delete(key);
169
+ collectionIds.delete(options);
170
+ }
171
+ }
172
+ throw error;
173
+ }
174
+ }
175
+ async requestReleaseRemoteSubset(collectionId, options) {
176
+ const collectionIds = this.remoteSubsetIds.get(collectionId);
177
+ const acquisitionId = collectionIds?.get(options);
178
+ if (!acquisitionId) return;
179
+ const key = remoteSubsetAcquisitionKey(collectionId, acquisitionId);
180
+ const acquisition = this.outboundRemoteSubsetAcquisitions.get(key);
181
+ if (!acquisition) return;
182
+ acquisition.releaseRequested = true;
183
+ this.cancelRemoteSubsetReplayRetry(acquisition);
184
+ if (!acquisition.release) {
185
+ acquisition.release = (async () => {
186
+ const request = {
187
+ type: `rpc:releaseRemoteSubset:req`,
188
+ rpcId: safeRandomUUID(),
189
+ acquisitionId
190
+ };
191
+ const localLeader = this.isLeader(collectionId);
192
+ let releaseReachedLeader = localLeader;
193
+ try {
194
+ const response = localLeader ? await this.handleReleaseRemoteSubset(
195
+ collectionId,
196
+ request,
197
+ this.nodeId
198
+ ) : await this.sendRPC(
199
+ collectionId,
200
+ request
201
+ );
202
+ releaseReachedLeader = true;
203
+ if (!response.ok) {
204
+ throw new Error(`releaseRemoteSubset failed: ${response.error}`);
205
+ }
206
+ } finally {
207
+ if (releaseReachedLeader && this.outboundRemoteSubsetAcquisitions.get(key) === acquisition) {
208
+ this.outboundRemoteSubsetAcquisitions.delete(key);
209
+ collectionIds.delete(options);
210
+ }
211
+ }
212
+ })();
213
+ }
214
+ try {
215
+ await acquisition.release;
216
+ } finally {
217
+ acquisition.release = null;
218
+ }
219
+ }
220
+ async acquireRemoteSubset(acquisition) {
221
+ if (acquisition.inFlight) return acquisition.inFlight;
222
+ const route = { localOwner: false };
223
+ let resolveWork;
224
+ let rejectWork;
225
+ const work = new Promise((resolve, reject) => {
226
+ resolveWork = resolve;
227
+ rejectWork = reject;
228
+ });
229
+ acquisition.inFlight = work;
230
+ const run = async () => {
231
+ const request = {
232
+ type: `rpc:ensureRemoteSubset:req`,
233
+ rpcId: safeRandomUUID(),
234
+ acquisitionId: acquisition.acquisitionId,
235
+ options: acquisition.options
236
+ };
237
+ const state = this.collections.get(acquisition.collectionId);
238
+ if (state?.participatesInLeadership && !state.isLeader && state.leaderId === null) {
239
+ await this.waitForLeadershipRoute(
240
+ acquisition.collectionId,
241
+ state,
242
+ acquisition.localOptions.signal
243
+ );
244
+ }
245
+ route.localOwner = this.isLeader(acquisition.collectionId);
246
+ let response;
247
+ try {
248
+ response = route.localOwner ? await this.handleEnsureRemoteSubset(
249
+ acquisition.collectionId,
250
+ request,
251
+ this.nodeId,
252
+ acquisition.localOptions
253
+ ) : await this.sendRPC(
254
+ acquisition.collectionId,
255
+ request
256
+ );
257
+ } catch (error) {
258
+ if (route.localOwner || error instanceof RetryableRemoteSubsetAcquisitionError) {
259
+ throw error;
260
+ }
261
+ throw new RetryableRemoteSubsetAcquisitionError(
262
+ `Remote subset transport failed`,
263
+ error
264
+ );
265
+ }
266
+ if (!response.ok) {
267
+ if (response.retryable) {
268
+ throw new RetryableRemoteSubsetAcquisitionError(response.error);
269
+ }
270
+ throw new Error(`ensureRemoteSubset failed: ${response.error}`);
271
+ }
272
+ acquisition.acquiredLeaderId = response.leaderId;
273
+ };
274
+ void run().then(resolveWork, rejectWork);
275
+ let acquired = false;
276
+ try {
277
+ await work;
278
+ acquired = true;
279
+ this.cancelRemoteSubsetReplayRetry(acquisition);
280
+ } finally {
281
+ if (acquisition.inFlight === work) acquisition.inFlight = null;
282
+ const current = this.collections.get(acquisition.collectionId);
283
+ const currentLeaderId = current?.isLeader ? this.nodeId : current?.leaderId ?? null;
284
+ const key = remoteSubsetAcquisitionKey(
285
+ acquisition.collectionId,
286
+ acquisition.acquisitionId
287
+ );
288
+ if (acquired && !acquisition.releaseRequested && this.outboundRemoteSubsetAcquisitions.get(key) === acquisition && (acquisition.forceReplay || currentLeaderId !== null && acquisition.acquiredLeaderId !== currentLeaderId)) {
289
+ acquisition.forceReplay = false;
290
+ void this.acquireRemoteSubset(acquisition).catch((error) => {
291
+ this.scheduleRemoteSubsetReplayRetry(acquisition, error);
292
+ });
293
+ }
294
+ }
295
+ }
296
+ async requestEnsurePersistedIndex(collectionId, signature, spec, scopedAdapter, localEnsureCompleted = false) {
297
+ if (this.isLeader(collectionId)) {
298
+ if (localEnsureCompleted) return;
299
+ await (scopedAdapter ?? this.requireAdapter(collectionId)).ensureIndex(
300
+ collectionId,
301
+ signature,
302
+ spec
303
+ );
304
+ return;
305
+ }
306
+ const response = await this.sendRPC(collectionId, {
307
+ type: `rpc:ensurePersistedIndex:req`,
308
+ rpcId: safeRandomUUID(),
309
+ signature,
310
+ spec
311
+ });
312
+ if (!response.ok) {
313
+ throw new Error(
314
+ `ensurePersistedIndex failed: ${response.error ?? `unknown error`}`
315
+ );
316
+ }
317
+ }
318
+ async requestApplyLocalMutations(collectionId, mutations) {
319
+ const initialRoute = this.waitForInitialRoute(collectionId);
320
+ if (initialRoute) await initialRoute;
321
+ if (this.isLeader(collectionId)) {
322
+ return this.handleApplyLocalMutations(collectionId, {
323
+ type: `rpc:applyLocalMutations:req`,
324
+ rpcId: safeRandomUUID(),
325
+ envelopeId: safeRandomUUID(),
326
+ mutations
327
+ });
328
+ }
329
+ return this.sendRPC(collectionId, {
330
+ type: `rpc:applyLocalMutations:req`,
331
+ rpcId: safeRandomUUID(),
332
+ envelopeId: safeRandomUUID(),
333
+ mutations
334
+ });
335
+ }
336
+ async requestApplyCommittedTx(collectionId, tx, scopedAdapter) {
337
+ const initialRoute = this.waitForInitialRoute(collectionId);
338
+ if (initialRoute) await initialRoute;
339
+ const request = {
340
+ type: `rpc:applyCommittedTx:req`,
341
+ rpcId: safeRandomUUID(),
342
+ envelopeId: safeRandomUUID(),
343
+ tx
344
+ };
345
+ if (this.isLeader(collectionId)) {
346
+ return this.handleApplyCommittedTx(collectionId, request, scopedAdapter);
347
+ }
348
+ return this.sendRPC(collectionId, request);
349
+ }
350
+ async pullSince(collectionId, fromRowVersion, scopedAdapter) {
351
+ if (this.isLeader(collectionId)) {
352
+ return this.handlePullSince(
353
+ collectionId,
354
+ {
355
+ type: `rpc:pullSince:req`,
356
+ rpcId: safeRandomUUID(),
357
+ fromRowVersion
358
+ },
359
+ scopedAdapter
360
+ );
361
+ }
362
+ return this.sendRPC(collectionId, {
363
+ type: `rpc:pullSince:req`,
364
+ rpcId: safeRandomUUID(),
365
+ fromRowVersion
366
+ });
367
+ }
368
+ // -----------------------------------------------------------------------
369
+ // Lifecycle
370
+ // -----------------------------------------------------------------------
371
+ dispose() {
372
+ if (this.disposed) return;
373
+ this.disposed = true;
374
+ const disposedError = new Error(`coordinator disposed`);
375
+ this.rejectDisposed?.(disposedError);
376
+ this.rejectDisposed = null;
377
+ for (const acquisition of this.outboundRemoteSubsetAcquisitions.values()) {
378
+ this.cancelRemoteSubsetReplayRetry(acquisition);
379
+ this.postRemoteSubsetRelease(acquisition);
380
+ }
381
+ this.outboundRemoteSubsetAcquisitions.clear();
382
+ this.remoteSubsetIds.clear();
383
+ for (const [collectionId, state] of this.collections) {
384
+ this.releaseLeadership(collectionId, state);
385
+ }
386
+ for (const [, pending] of this.pendingRPCs) {
387
+ clearTimeout(pending.timer);
388
+ pending.reject(disposedError);
389
+ }
390
+ this.pendingRPCs.clear();
391
+ if (this.appliedEnvelopePruneTimer !== null) {
392
+ clearTimeout(this.appliedEnvelopePruneTimer);
393
+ this.appliedEnvelopePruneTimer = null;
394
+ }
395
+ this.channel.close();
396
+ this.collections.clear();
397
+ this.collectionAdapters.clear();
398
+ for (const collectionId of this.remoteSubsetOwners.keys()) {
399
+ this.releaseInboundRemoteSubsetAcquisitions(collectionId);
400
+ }
401
+ this.remoteSubsetOwners.clear();
402
+ this.inboundRemoteSubsetAcquisitions.clear();
403
+ this.releasedRemoteSubsetAcquisitionTimes.clear();
404
+ this.appliedEnvelopes.clear();
405
+ this.inFlightEnvelopes.clear();
406
+ }
407
+ // -----------------------------------------------------------------------
408
+ // Leadership via Web Locks
409
+ // -----------------------------------------------------------------------
410
+ getOrCreateCollectionState(collectionId) {
411
+ let state = this.collections.get(collectionId);
412
+ if (!state) {
413
+ state = {
414
+ participatesInLeadership: false,
415
+ isLeader: false,
416
+ leaderId: null,
417
+ lockAbortController: null,
418
+ heartbeatTimer: null,
419
+ latestTerm: 0,
420
+ latestSeq: 0,
421
+ latestRowVersion: 0,
422
+ subscribers: /* @__PURE__ */ new Set(),
423
+ routeWaiters: /* @__PURE__ */ new Set()
424
+ };
425
+ this.collections.set(collectionId, state);
426
+ }
427
+ return state;
428
+ }
429
+ ensureCollectionState(collectionId) {
430
+ const state = this.getOrCreateCollectionState(collectionId);
431
+ if (!state.participatesInLeadership) {
432
+ state.participatesInLeadership = true;
433
+ void this.acquireLeadership(collectionId, state);
434
+ }
435
+ return state;
436
+ }
437
+ async acquireLeadership(collectionId, state) {
438
+ if (this.disposed || !state.participatesInLeadership || state.isLeader) {
439
+ return;
440
+ }
441
+ const lockName = `tsdb:leader:${this.dbName}:${collectionId}`;
442
+ const abortController = new AbortController();
443
+ state.lockAbortController?.abort();
444
+ state.lockAbortController = abortController;
445
+ try {
446
+ await navigator.locks.request(
447
+ lockName,
448
+ { signal: abortController.signal },
449
+ async () => {
450
+ if (this.isDisposed() || !this.participatesInLeadership(state) || this.collections.get(collectionId) !== state) {
451
+ return;
452
+ }
453
+ try {
454
+ const adapter = this.requireAdapter(collectionId);
455
+ if (adapter.getStreamPosition) {
456
+ const pos = await adapter.getStreamPosition(collectionId);
457
+ state.latestTerm = pos.latestTerm;
458
+ state.latestSeq = pos.latestSeq;
459
+ state.latestRowVersion = pos.latestRowVersion;
460
+ }
461
+ if (this.isDisposed() || !this.participatesInLeadership(state) || this.collections.get(collectionId) !== state || abortController.signal.aborted || state.lockAbortController !== abortController) {
462
+ return;
463
+ }
464
+ state.latestTerm++;
465
+ state.isLeader = true;
466
+ state.leaderId = this.nodeId;
467
+ this.notifyRouteWaiters(state);
468
+ this.emitHeartbeat(collectionId, state);
469
+ void this.replayRemoteSubsetAcquisitions(collectionId);
470
+ state.heartbeatTimer = setInterval(() => {
471
+ this.emitHeartbeat(collectionId, state);
472
+ }, HEARTBEAT_INTERVAL_MS);
473
+ await new Promise((resolve) => {
474
+ const onAbort = () => {
475
+ abortController.signal.removeEventListener(`abort`, onAbort);
476
+ resolve();
477
+ };
478
+ if (abortController.signal.aborted) {
479
+ resolve();
480
+ return;
481
+ }
482
+ abortController.signal.addEventListener(`abort`, onAbort);
483
+ });
484
+ } finally {
485
+ this.releaseInboundRemoteSubsetAcquisitions(collectionId);
486
+ state.isLeader = false;
487
+ state.leaderId = null;
488
+ if (state.heartbeatTimer) {
489
+ clearInterval(state.heartbeatTimer);
490
+ state.heartbeatTimer = null;
491
+ }
492
+ }
493
+ }
494
+ );
495
+ } catch (error) {
496
+ if (error instanceof DOMException && error.name === `AbortError`) {
497
+ return;
498
+ }
499
+ console.warn(`Failed to acquire leadership for ${collectionId}:`, error);
500
+ if (!this.isDisposed() && this.participatesInLeadership(state) && this.collections.get(collectionId) === state) {
501
+ await sleep(LEADERSHIP_RETRY_DELAY_MS);
502
+ }
503
+ } finally {
504
+ if (state.lockAbortController === abortController) {
505
+ state.lockAbortController = null;
506
+ }
507
+ }
508
+ if (!this.isDisposed() && this.participatesInLeadership(state) && this.collections.get(collectionId) === state && state.lockAbortController === null) {
509
+ void this.acquireLeadership(collectionId, state);
510
+ }
511
+ }
512
+ waitForInitialRoute(collectionId) {
513
+ const state = this.collections.get(collectionId);
514
+ if (state?.participatesInLeadership && !state.isLeader && state.leaderId === null) {
515
+ this.channel.postMessage({
516
+ v: 1,
517
+ dbName: this.dbName,
518
+ collectionId,
519
+ senderId: this.nodeId,
520
+ ts: Date.now(),
521
+ payload: { type: `leader:routeRequest` }
522
+ });
523
+ return this.waitForLeadershipRoute(
524
+ collectionId,
525
+ state,
526
+ void 0,
527
+ RPC_TIMEOUT_MS
528
+ );
529
+ }
530
+ return void 0;
531
+ }
532
+ waitForLeadershipRoute(collectionId, state, signal, timeoutMs) {
533
+ if (state.isLeader || state.leaderId !== null) return Promise.resolve();
534
+ return new Promise((resolve, reject) => {
535
+ let timer;
536
+ const finish = () => {
537
+ state.routeWaiters?.delete(onRouteChange);
538
+ signal?.removeEventListener(`abort`, onAbort);
539
+ if (timer !== void 0) clearTimeout(timer);
540
+ };
541
+ const onAbort = () => {
542
+ finish();
543
+ reject(
544
+ new DOMException(`Remote subset acquisition aborted`, `AbortError`)
545
+ );
546
+ };
547
+ const onRouteChange = () => {
548
+ if (this.isDisposed() || !state.participatesInLeadership) {
549
+ finish();
550
+ reject(
551
+ new Error(
552
+ `${this.coordinatorName}: leadership route closed for collection "${collectionId}"`
553
+ )
554
+ );
555
+ return;
556
+ }
557
+ if (state.isLeader || state.leaderId !== null) {
558
+ finish();
559
+ resolve();
560
+ }
561
+ };
562
+ (state.routeWaiters ??= /* @__PURE__ */ new Set()).add(onRouteChange);
563
+ if (timeoutMs !== void 0) {
564
+ timer = setTimeout(() => {
565
+ finish();
566
+ reject(
567
+ new Error(
568
+ `${this.coordinatorName}: leadership route for collection "${collectionId}" timed out after ${timeoutMs}ms`
569
+ )
570
+ );
571
+ }, timeoutMs);
572
+ }
573
+ signal?.addEventListener(`abort`, onAbort, { once: true });
574
+ if (signal?.aborted) {
575
+ onAbort();
576
+ } else {
577
+ onRouteChange();
578
+ }
579
+ });
580
+ }
581
+ notifyRouteWaiters(state) {
582
+ for (const notify of Array.from(state.routeWaiters ?? [])) notify();
583
+ }
584
+ releaseLeadership(collectionId, state) {
585
+ this.releaseInboundRemoteSubsetAcquisitions(collectionId);
586
+ if (state.lockAbortController) {
587
+ state.lockAbortController.abort();
588
+ state.lockAbortController = null;
589
+ }
590
+ if (state.heartbeatTimer) {
591
+ clearInterval(state.heartbeatTimer);
592
+ state.heartbeatTimer = null;
593
+ }
594
+ state.isLeader = false;
595
+ state.leaderId = null;
596
+ this.notifyRouteWaiters(state);
597
+ }
598
+ releaseCollectionIfUnused(collectionId, state) {
599
+ if (state.subscribers.size > 0 || this.collections.get(collectionId) !== state) {
600
+ return;
601
+ }
602
+ this.releaseLeadership(collectionId, state);
603
+ this.collections.delete(collectionId);
604
+ this.collectionAdapters.delete(collectionId);
605
+ const prefix = `${JSON.stringify([collectionId]).slice(0, -1)},`;
606
+ for (const key of this.appliedEnvelopes.keys()) {
607
+ if (key.startsWith(prefix)) this.appliedEnvelopes.delete(key);
608
+ }
609
+ this.pruneAppliedEnvelopes();
610
+ }
611
+ postRemoteSubsetRelease(acquisition) {
612
+ if (acquisition.release) return;
613
+ const request = {
614
+ type: `rpc:releaseRemoteSubset:req`,
615
+ rpcId: safeRandomUUID(),
616
+ acquisitionId: acquisition.acquisitionId
617
+ };
618
+ if (this.isLeader(acquisition.collectionId)) {
619
+ void this.handleReleaseRemoteSubset(
620
+ acquisition.collectionId,
621
+ request,
622
+ this.nodeId
623
+ ).catch(() => {
624
+ });
625
+ return;
626
+ }
627
+ this.channel.postMessage({
628
+ v: 1,
629
+ dbName: this.dbName,
630
+ collectionId: acquisition.collectionId,
631
+ senderId: this.nodeId,
632
+ ts: Date.now(),
633
+ payload: request
634
+ });
635
+ }
636
+ cancelRemoteSubsetReplayRetry(acquisition) {
637
+ if (acquisition.retryTimer !== null) {
638
+ clearTimeout(acquisition.retryTimer);
639
+ acquisition.retryTimer = null;
640
+ }
641
+ acquisition.retryAttempts = 0;
642
+ }
643
+ scheduleRemoteSubsetReplayRetry(acquisition, error) {
644
+ const key = remoteSubsetAcquisitionKey(
645
+ acquisition.collectionId,
646
+ acquisition.acquisitionId
647
+ );
648
+ if (!(error instanceof RetryableRemoteSubsetAcquisitionError) || this.isDisposed() || this.outboundRemoteSubsetAcquisitions.get(key) !== acquisition || acquisition.releaseRequested || acquisition.retryTimer !== null || acquisition.retryAttempts >= REMOTE_SUBSET_REPLAY_RETRY_ATTEMPTS) {
649
+ return;
650
+ }
651
+ acquisition.retryAttempts++;
652
+ acquisition.retryTimer = setTimeout(() => {
653
+ acquisition.retryTimer = null;
654
+ if (this.isDisposed() || this.outboundRemoteSubsetAcquisitions.get(key) !== acquisition || acquisition.releaseRequested) {
655
+ return;
656
+ }
657
+ void this.acquireRemoteSubset(acquisition).catch((retryError) => {
658
+ this.scheduleRemoteSubsetReplayRetry(acquisition, retryError);
659
+ });
660
+ }, RPC_RETRY_DELAY_MS);
661
+ }
662
+ async replayRemoteSubsetAcquisitions(collectionId) {
663
+ if (this.isDisposed()) return;
664
+ const state = this.collections.get(collectionId);
665
+ const leaderId = state?.isLeader ? this.nodeId : state?.leaderId;
666
+ if (!leaderId) return;
667
+ const replays = [];
668
+ for (const acquisition of this.outboundRemoteSubsetAcquisitions.values()) {
669
+ if (acquisition.collectionId !== collectionId || acquisition.releaseRequested || !acquisition.forceReplay && acquisition.acquiredLeaderId === leaderId) {
670
+ continue;
671
+ }
672
+ if (acquisition.inFlight) {
673
+ acquisition.forceReplay = true;
674
+ continue;
675
+ }
676
+ acquisition.forceReplay = false;
677
+ replays.push(
678
+ this.acquireRemoteSubset(acquisition).catch((error) => {
679
+ this.scheduleRemoteSubsetReplayRetry(acquisition, error);
680
+ })
681
+ );
682
+ }
683
+ await Promise.all(replays);
684
+ }
685
+ emitHeartbeat(collectionId, state) {
686
+ const envelope = {
687
+ v: 1,
688
+ dbName: this.dbName,
689
+ collectionId,
690
+ senderId: this.nodeId,
691
+ ts: Date.now(),
692
+ payload: {
693
+ type: `leader:heartbeat`,
694
+ term: state.latestTerm,
695
+ leaderId: this.nodeId,
696
+ latestSeq: state.latestSeq,
697
+ latestRowVersion: state.latestRowVersion
698
+ }
699
+ };
700
+ this.channel.postMessage(envelope);
701
+ }
702
+ // -----------------------------------------------------------------------
703
+ // BroadcastChannel message handling
704
+ // -----------------------------------------------------------------------
705
+ onChannelMessage(data) {
706
+ if (!isProtocolEnvelope(data)) return;
707
+ const envelope = data;
708
+ if (envelope.senderId === this.nodeId) return;
709
+ const payload = envelope.payload;
710
+ if (!payload || typeof payload !== `object`) return;
711
+ const type = payload.type;
712
+ if (type === `leader:routeRequest`) {
713
+ const state2 = this.collections.get(envelope.collectionId);
714
+ if (state2?.isLeader) this.emitHeartbeat(envelope.collectionId, state2);
715
+ return;
716
+ }
717
+ if (type === `leader:heartbeat`) {
718
+ const heartbeat = payload;
719
+ if (typeof heartbeat.leaderId === `string` && typeof heartbeat.term === `number` && typeof heartbeat.latestSeq === `number` && typeof heartbeat.latestRowVersion === `number`) {
720
+ const state2 = this.getOrCreateCollectionState(envelope.collectionId);
721
+ if (heartbeat.term < state2.latestTerm) return;
722
+ const changedLeader = state2.leaderId !== heartbeat.leaderId;
723
+ state2.leaderId = heartbeat.leaderId;
724
+ state2.latestTerm = Math.max(state2.latestTerm, heartbeat.term);
725
+ state2.latestSeq = Math.max(state2.latestSeq, heartbeat.latestSeq);
726
+ state2.latestRowVersion = Math.max(
727
+ state2.latestRowVersion,
728
+ heartbeat.latestRowVersion
729
+ );
730
+ this.notifyRouteWaiters(state2);
731
+ if (changedLeader) {
732
+ void this.replayRemoteSubsetAcquisitions(envelope.collectionId);
733
+ }
734
+ }
735
+ }
736
+ if (type && type.endsWith(`:res`)) {
737
+ const rpcId = payload.rpcId;
738
+ if (rpcId && this.pendingRPCs.has(rpcId)) {
739
+ const pending = this.pendingRPCs.get(rpcId);
740
+ this.pendingRPCs.delete(rpcId);
741
+ clearTimeout(pending.timer);
742
+ pending.resolve(payload);
743
+ return;
744
+ }
745
+ }
746
+ if (type && type.endsWith(`:req`)) {
747
+ if (!isRPCRequest(payload)) return;
748
+ const collectionId = envelope.collectionId;
749
+ if (this.isLeader(collectionId)) {
750
+ void this.handleRPCRequest(collectionId, payload, envelope.senderId);
751
+ }
752
+ return;
753
+ }
754
+ const state = this.collections.get(envelope.collectionId);
755
+ if (state) {
756
+ for (const subscriber of state.subscribers) {
757
+ subscriber(envelope);
758
+ }
759
+ }
760
+ }
761
+ // -----------------------------------------------------------------------
762
+ // RPC - Outbound (follower side)
763
+ // -----------------------------------------------------------------------
764
+ async sendRPC(collectionId, request) {
765
+ if (this.isDisposed()) throw new Error(`coordinator disposed`);
766
+ let lastError;
767
+ let firstTransportCause;
768
+ const mutationRequestType = isMutatingRPCRequest(request) ? request.type : void 0;
769
+ const mutationRoute = mutationRequestType ? this.captureMutationRoute(collectionId) : void 0;
770
+ for (let attempt = 0; attempt <= RPC_RETRY_ATTEMPTS; attempt++) {
771
+ if (attempt > 0) {
772
+ await Promise.race([
773
+ sleep(RPC_RETRY_DELAY_MS * attempt),
774
+ this.disposedPromise
775
+ ]);
776
+ }
777
+ if (this.isDisposed()) throw new Error(`coordinator disposed`);
778
+ if (mutationRoute && mutationRequestType && firstTransportCause !== void 0) {
779
+ this.assertMutationRouteUnchanged(
780
+ collectionId,
781
+ mutationRequestType,
782
+ mutationRoute,
783
+ firstTransportCause
784
+ );
785
+ }
786
+ if (this.isLeader(collectionId)) {
787
+ return await this.dispatchRPCRequest(
788
+ collectionId,
789
+ request,
790
+ this.nodeId
791
+ );
792
+ }
793
+ try {
794
+ return await this.sendRPCOnce(collectionId, request);
795
+ } catch (error) {
796
+ if (this.isDisposed()) throw error;
797
+ firstTransportCause ??= error;
798
+ if (mutationRoute && mutationRequestType) {
799
+ this.assertMutationRouteUnchanged(
800
+ collectionId,
801
+ mutationRequestType,
802
+ mutationRoute,
803
+ firstTransportCause
804
+ );
805
+ }
806
+ lastError = error instanceof Error ? error : new Error(String(error));
807
+ }
808
+ }
809
+ throw lastError ?? new Error(`RPC failed after retries`);
810
+ }
811
+ captureMutationRoute(collectionId) {
812
+ const state = this.collections.get(collectionId);
813
+ return {
814
+ leaderId: state?.isLeader ? this.nodeId : state?.leaderId ?? null,
815
+ term: state?.latestTerm ?? null
816
+ };
817
+ }
818
+ assertMutationRouteUnchanged(collectionId, requestType, previous, cause) {
819
+ const current = this.captureMutationRoute(collectionId);
820
+ if (previous.leaderId !== null && previous.term !== null && current.leaderId === previous.leaderId && current.term === previous.term) {
821
+ return;
822
+ }
823
+ throw new IndeterminateCommitError({
824
+ collectionId,
825
+ requestType,
826
+ previousLeaderId: previous.leaderId,
827
+ previousTerm: previous.term,
828
+ currentLeaderId: current.leaderId,
829
+ currentTerm: current.term,
830
+ cause
831
+ });
832
+ }
833
+ sendRPCOnce(collectionId, request) {
834
+ return new Promise((resolve, reject) => {
835
+ const rpcId = request.rpcId;
836
+ const timer = setTimeout(() => {
837
+ this.pendingRPCs.delete(rpcId);
838
+ reject(
839
+ new Error(`RPC ${request.type} timed out after ${RPC_TIMEOUT_MS}ms`)
840
+ );
841
+ }, RPC_TIMEOUT_MS);
842
+ this.pendingRPCs.set(rpcId, {
843
+ resolve,
844
+ reject,
845
+ timer
846
+ });
847
+ const envelope = {
848
+ v: 1,
849
+ dbName: this.dbName,
850
+ collectionId,
851
+ senderId: this.nodeId,
852
+ ts: Date.now(),
853
+ payload: request
854
+ };
855
+ this.channel.postMessage(envelope);
856
+ });
857
+ }
858
+ // -----------------------------------------------------------------------
859
+ // RPC - Inbound (leader side)
860
+ // -----------------------------------------------------------------------
861
+ async handleRPCRequest(collectionId, request, requesterId) {
862
+ let response;
863
+ try {
864
+ response = await this.dispatchRPCRequest(
865
+ collectionId,
866
+ request,
867
+ requesterId
868
+ );
869
+ } catch (error) {
870
+ response = createRPCErrorResponse(request, error);
871
+ }
872
+ if (this.isDisposed()) {
873
+ return;
874
+ }
875
+ const envelope = {
876
+ v: 1,
877
+ dbName: this.dbName,
878
+ collectionId,
879
+ senderId: this.nodeId,
880
+ ts: Date.now(),
881
+ payload: response
882
+ };
883
+ this.channel.postMessage(envelope);
884
+ }
885
+ dispatchRPCRequest(collectionId, request, requesterId) {
886
+ switch (request.type) {
887
+ case `rpc:ensureRemoteSubset:req`:
888
+ return this.handleEnsureRemoteSubset(collectionId, request, requesterId);
889
+ case `rpc:releaseRemoteSubset:req`:
890
+ return this.handleReleaseRemoteSubset(
891
+ collectionId,
892
+ request,
893
+ requesterId
894
+ );
895
+ case `rpc:ensurePersistedIndex:req`:
896
+ return this.handleEnsurePersistedIndex(collectionId, request);
897
+ case `rpc:applyLocalMutations:req`:
898
+ return this.handleApplyLocalMutations(collectionId, request);
899
+ case `rpc:applyCommittedTx:req`:
900
+ return this.handleApplyCommittedTx(collectionId, request);
901
+ case `rpc:pullSince:req`:
902
+ return this.handlePullSince(collectionId, request);
903
+ }
904
+ }
905
+ async handleEnsureRemoteSubset(collectionId, request, requesterId, localOptions) {
906
+ this.pruneReleasedRemoteSubsetAcquisitions();
907
+ const key = inboundRemoteSubsetAcquisitionKey(
908
+ collectionId,
909
+ requesterId,
910
+ request.acquisitionId
911
+ );
912
+ const existing = this.inboundRemoteSubsetAcquisitions.get(key);
913
+ const awaitingOwner = existing && `awaitingOwner` in existing ? existing : void 0;
914
+ if (existing) {
915
+ if (`owner` in existing) {
916
+ if (!existing.released) {
917
+ await existing.load;
918
+ return {
919
+ type: `rpc:ensureRemoteSubset:res`,
920
+ rpcId: request.rpcId,
921
+ ok: true,
922
+ leaderId: this.nodeId
923
+ };
924
+ }
925
+ await existing.release;
926
+ if (existing.terminalRelease) {
927
+ return {
928
+ type: `rpc:ensureRemoteSubset:res`,
929
+ rpcId: request.rpcId,
930
+ ok: true,
931
+ leaderId: this.nodeId
932
+ };
933
+ }
934
+ if (this.inboundRemoteSubsetAcquisitions.get(key) === existing) {
935
+ this.inboundRemoteSubsetAcquisitions.delete(key);
936
+ }
937
+ } else if (!(`awaitingOwner` in existing)) {
938
+ return {
939
+ type: `rpc:ensureRemoteSubset:res`,
940
+ rpcId: request.rpcId,
941
+ ok: true,
942
+ leaderId: this.nodeId
943
+ };
944
+ }
945
+ }
946
+ const owner = this.remoteSubsetOwners.get(collectionId);
947
+ if (!owner) {
948
+ throw new RetryableRemoteSubsetAcquisitionError(
949
+ `${this.coordinatorName}: no remote subset owner registered for collection "${collectionId}"`
950
+ );
951
+ }
952
+ await this.startRemoteSubsetOwnerAcquisition({
953
+ collectionId,
954
+ requesterId,
955
+ acquisitionId: request.acquisitionId,
956
+ owner,
957
+ options: awaitingOwner?.options ?? localOptions ?? request.options,
958
+ restoreOnSynchronousFailure: awaitingOwner
959
+ });
960
+ return {
961
+ type: `rpc:ensureRemoteSubset:res`,
962
+ rpcId: request.rpcId,
963
+ ok: true,
964
+ leaderId: this.nodeId
965
+ };
966
+ }
967
+ async handleReleaseRemoteSubset(collectionId, request, requesterId) {
968
+ this.pruneReleasedRemoteSubsetAcquisitions();
969
+ const key = inboundRemoteSubsetAcquisitionKey(
970
+ collectionId,
971
+ requesterId,
972
+ request.acquisitionId
973
+ );
974
+ const acquisition = this.inboundRemoteSubsetAcquisitions.get(key);
975
+ if (!acquisition) {
976
+ this.setReleasedRemoteSubsetAcquisition(key, {
977
+ collectionId,
978
+ requesterId,
979
+ acquisitionId: request.acquisitionId,
980
+ released: true
981
+ });
982
+ } else if (`owner` in acquisition) {
983
+ acquisition.terminalRelease = true;
984
+ try {
985
+ await this.releaseRemoteSubsetAcquisition(acquisition);
986
+ } finally {
987
+ if (this.inboundRemoteSubsetAcquisitions.get(key) === acquisition) {
988
+ this.setReleasedRemoteSubsetAcquisition(key, {
989
+ collectionId,
990
+ requesterId,
991
+ acquisitionId: request.acquisitionId,
992
+ released: true
993
+ });
994
+ }
995
+ }
996
+ } else if (`awaitingOwner` in acquisition) {
997
+ this.setReleasedRemoteSubsetAcquisition(key, {
998
+ collectionId,
999
+ requesterId,
1000
+ acquisitionId: request.acquisitionId,
1001
+ released: true
1002
+ });
1003
+ }
1004
+ return {
1005
+ type: `rpc:releaseRemoteSubset:res`,
1006
+ rpcId: request.rpcId,
1007
+ ok: true
1008
+ };
1009
+ }
1010
+ releaseRemoteSubsetAcquisition(acquisition) {
1011
+ if (acquisition.release) return acquisition.release;
1012
+ acquisition.released = true;
1013
+ acquisition.release = (async () => {
1014
+ try {
1015
+ await acquisition.load;
1016
+ } catch {
1017
+ }
1018
+ try {
1019
+ if (acquisition.transferred) {
1020
+ await unloadRemoteSubsetOwner(acquisition.owner, acquisition.options);
1021
+ }
1022
+ } finally {
1023
+ if (!acquisition.terminalRelease) {
1024
+ const key = inboundRemoteSubsetAcquisitionKey(
1025
+ acquisition.collectionId,
1026
+ acquisition.requesterId,
1027
+ acquisition.acquisitionId
1028
+ );
1029
+ if (this.inboundRemoteSubsetAcquisitions.get(key) === acquisition) {
1030
+ this.inboundRemoteSubsetAcquisitions.set(key, {
1031
+ collectionId: acquisition.collectionId,
1032
+ requesterId: acquisition.requesterId,
1033
+ acquisitionId: acquisition.acquisitionId,
1034
+ options: acquisition.options,
1035
+ released: true,
1036
+ awaitingOwner: true
1037
+ });
1038
+ }
1039
+ }
1040
+ }
1041
+ })();
1042
+ return acquisition.release;
1043
+ }
1044
+ releaseInboundRemoteSubsetAcquisitions(collectionId, owner) {
1045
+ for (const acquisition of this.inboundRemoteSubsetAcquisitions.values()) {
1046
+ if (!(`owner` in acquisition) || acquisition.collectionId !== collectionId || owner && acquisition.owner !== owner) {
1047
+ continue;
1048
+ }
1049
+ void this.releaseRemoteSubsetAcquisition(acquisition).catch(
1050
+ () => void 0
1051
+ );
1052
+ }
1053
+ }
1054
+ rebindRemoteInboundSubsetAcquisitions(collectionId, owner) {
1055
+ for (const acquisition of this.inboundRemoteSubsetAcquisitions.values()) {
1056
+ if (`awaitingOwner` in acquisition) {
1057
+ if (acquisition.collectionId === collectionId && acquisition.requesterId !== this.nodeId) {
1058
+ void this.bindAwaitingRemoteSubsetAcquisition(
1059
+ acquisition,
1060
+ owner
1061
+ ).catch(() => {
1062
+ });
1063
+ }
1064
+ continue;
1065
+ }
1066
+ if (!(`owner` in acquisition) || acquisition.collectionId !== collectionId || acquisition.requesterId === this.nodeId || !acquisition.released || acquisition.terminalRelease) {
1067
+ continue;
1068
+ }
1069
+ void this.rebindRemoteInboundSubsetAcquisition(acquisition, owner).catch(
1070
+ () => void 0
1071
+ );
1072
+ }
1073
+ }
1074
+ async rebindRemoteInboundSubsetAcquisition(previous, owner) {
1075
+ await previous.release;
1076
+ if (previous.terminalRelease || this.remoteSubsetOwners.get(previous.collectionId) !== owner) {
1077
+ return;
1078
+ }
1079
+ const key = inboundRemoteSubsetAcquisitionKey(
1080
+ previous.collectionId,
1081
+ previous.requesterId,
1082
+ previous.acquisitionId
1083
+ );
1084
+ const current = this.inboundRemoteSubsetAcquisitions.get(key);
1085
+ if (current && current !== previous) {
1086
+ if (`awaitingOwner` in current) {
1087
+ await this.bindAwaitingRemoteSubsetAcquisition(current, owner);
1088
+ }
1089
+ return;
1090
+ }
1091
+ const awaitingOwner = {
1092
+ collectionId: previous.collectionId,
1093
+ requesterId: previous.requesterId,
1094
+ acquisitionId: previous.acquisitionId,
1095
+ options: previous.options,
1096
+ released: true,
1097
+ awaitingOwner: true
1098
+ };
1099
+ this.inboundRemoteSubsetAcquisitions.set(key, awaitingOwner);
1100
+ await this.bindAwaitingRemoteSubsetAcquisition(awaitingOwner, owner);
1101
+ }
1102
+ async bindAwaitingRemoteSubsetAcquisition(awaitingOwner, owner) {
1103
+ if (this.remoteSubsetOwners.get(awaitingOwner.collectionId) !== owner) {
1104
+ return;
1105
+ }
1106
+ const key = inboundRemoteSubsetAcquisitionKey(
1107
+ awaitingOwner.collectionId,
1108
+ awaitingOwner.requesterId,
1109
+ awaitingOwner.acquisitionId
1110
+ );
1111
+ if (this.inboundRemoteSubsetAcquisitions.get(key) !== awaitingOwner) return;
1112
+ await this.startRemoteSubsetOwnerAcquisition({
1113
+ collectionId: awaitingOwner.collectionId,
1114
+ requesterId: awaitingOwner.requesterId,
1115
+ acquisitionId: awaitingOwner.acquisitionId,
1116
+ owner,
1117
+ options: awaitingOwner.options,
1118
+ restoreOnSynchronousFailure: awaitingOwner
1119
+ });
1120
+ }
1121
+ async startRemoteSubsetOwnerAcquisition(options) {
1122
+ const key = inboundRemoteSubsetAcquisitionKey(
1123
+ options.collectionId,
1124
+ options.requesterId,
1125
+ options.acquisitionId
1126
+ );
1127
+ const acquisition = {
1128
+ collectionId: options.collectionId,
1129
+ requesterId: options.requesterId,
1130
+ acquisitionId: options.acquisitionId,
1131
+ owner: options.owner,
1132
+ options: options.options,
1133
+ load: Promise.resolve(),
1134
+ transferred: false,
1135
+ released: false,
1136
+ terminalRelease: false,
1137
+ release: null
1138
+ };
1139
+ this.releasedRemoteSubsetAcquisitionTimes.delete(key);
1140
+ this.inboundRemoteSubsetAcquisitions.set(key, acquisition);
1141
+ let resolveLoad;
1142
+ let rejectLoad;
1143
+ acquisition.load = new Promise((resolve, reject) => {
1144
+ resolveLoad = resolve;
1145
+ rejectLoad = reject;
1146
+ });
1147
+ try {
1148
+ const load = options.owner(acquisition.options);
1149
+ acquisition.transferred = true;
1150
+ void Promise.resolve(load).then(resolveLoad, rejectLoad);
1151
+ } catch (error) {
1152
+ rejectLoad(error);
1153
+ }
1154
+ try {
1155
+ await acquisition.load;
1156
+ } catch (error) {
1157
+ if (!acquisition.transferred) {
1158
+ if (this.inboundRemoteSubsetAcquisitions.get(key) === acquisition) {
1159
+ if (options.restoreOnSynchronousFailure) {
1160
+ this.inboundRemoteSubsetAcquisitions.set(
1161
+ key,
1162
+ options.restoreOnSynchronousFailure
1163
+ );
1164
+ } else {
1165
+ this.inboundRemoteSubsetAcquisitions.delete(key);
1166
+ }
1167
+ }
1168
+ } else {
1169
+ await this.releaseRemoteSubsetAcquisition(acquisition).catch(
1170
+ () => void 0
1171
+ );
1172
+ }
1173
+ throw error;
1174
+ }
1175
+ }
1176
+ async handleEnsurePersistedIndex(collectionId, request) {
1177
+ await this.withScheduledWriterLock(
1178
+ collectionId,
1179
+ (adapter) => adapter.ensureIndex(collectionId, request.signature, request.spec)
1180
+ );
1181
+ return {
1182
+ type: `rpc:ensurePersistedIndex:res`,
1183
+ rpcId: request.rpcId,
1184
+ ok: true
1185
+ };
1186
+ }
1187
+ async handleApplyLocalMutations(collectionId, request) {
1188
+ const envelopeKey = appliedEnvelopeKey(collectionId, request.envelopeId);
1189
+ return this.runDeduplicatedEnvelope({
1190
+ envelopeKey,
1191
+ rpcId: request.rpcId,
1192
+ selectApplied: (envelope) => envelope.requestType === `rpc:applyLocalMutations:req` ? envelope.response : void 0,
1193
+ selectInFlight: (envelope) => envelope.requestType === `rpc:applyLocalMutations:req` ? envelope.response : void 0,
1194
+ conflict: (phase) => ({
1195
+ type: `rpc:applyLocalMutations:res`,
1196
+ rpcId: request.rpcId,
1197
+ ok: false,
1198
+ code: `CONFLICT`,
1199
+ error: `envelope ${request.envelopeId} ${phase}`
1200
+ }),
1201
+ apply: () => this.applyLocalMutationsOnce(collectionId, request),
1202
+ createPending: (response) => ({
1203
+ requestType: request.type,
1204
+ response
1205
+ })
1206
+ });
1207
+ }
1208
+ async applyLocalMutationsOnce(collectionId, request) {
1209
+ const state = this.collections.get(collectionId);
1210
+ if (!state || !state.isLeader) {
1211
+ return {
1212
+ type: `rpc:applyLocalMutations:res`,
1213
+ rpcId: request.rpcId,
1214
+ ok: false,
1215
+ code: `NOT_LEADER`,
1216
+ error: `not the leader for ${collectionId}`
1217
+ };
1218
+ }
1219
+ const rowMetadataMutations = [];
1220
+ for (const mutation of request.mutations) {
1221
+ if (!(`metadataChanged` in mutation) || !mutation.metadataChanged) {
1222
+ continue;
1223
+ }
1224
+ rowMetadataMutations.push(
1225
+ mutation.metadata === void 0 ? { type: `delete`, key: mutation.key } : {
1226
+ type: `set`,
1227
+ key: mutation.key,
1228
+ value: mutation.metadata
1229
+ }
1230
+ );
1231
+ }
1232
+ const pendingTx = {
1233
+ txId: safeRandomUUID(),
1234
+ mutations: request.mutations.map((m) => ({
1235
+ type: m.type,
1236
+ key: m.key,
1237
+ value: m.value,
1238
+ ...`metadataChanged` in m ? { metadata: m.metadata, metadataChanged: m.metadataChanged } : {}
1239
+ })),
1240
+ rowMetadataMutations
1241
+ };
1242
+ const tx = await this.applyDurablyAtNextStreamPosition(
1243
+ collectionId,
1244
+ state,
1245
+ pendingTx
1246
+ );
1247
+ if (tx === null) {
1248
+ return {
1249
+ type: `rpc:applyLocalMutations:res`,
1250
+ rpcId: request.rpcId,
1251
+ ok: false,
1252
+ code: `NOT_LEADER`,
1253
+ error: `not the leader for ${collectionId}`
1254
+ };
1255
+ }
1256
+ const response = {
1257
+ type: `rpc:applyLocalMutations:res`,
1258
+ rpcId: request.rpcId,
1259
+ ok: true,
1260
+ term: tx.term,
1261
+ seq: tx.seq,
1262
+ latestRowVersion: tx.rowVersion,
1263
+ acceptedMutationIds: request.mutations.map((m) => m.mutationId)
1264
+ };
1265
+ if (this.isDisposed() || this.collections.get(collectionId) !== state) {
1266
+ return response;
1267
+ }
1268
+ this.appliedEnvelopes.set(
1269
+ appliedEnvelopeKey(collectionId, request.envelopeId),
1270
+ {
1271
+ appliedAt: Date.now(),
1272
+ requestType: request.type,
1273
+ response
1274
+ }
1275
+ );
1276
+ this.pruneAppliedEnvelopes();
1277
+ const changedRows = request.mutations.filter((m) => m.type !== `delete`).map((m) => ({ key: m.key, value: m.value }));
1278
+ const deletedKeys = request.mutations.filter((m) => m.type === `delete`).map((m) => m.key);
1279
+ const txCommitted = {
1280
+ v: 1,
1281
+ dbName: this.dbName,
1282
+ collectionId,
1283
+ senderId: this.nodeId,
1284
+ ts: Date.now(),
1285
+ payload: {
1286
+ type: `tx:committed`,
1287
+ term: tx.term,
1288
+ seq: tx.seq,
1289
+ txId: tx.txId,
1290
+ latestRowVersion: tx.rowVersion,
1291
+ requiresFullReload: false,
1292
+ changedRows,
1293
+ deletedKeys,
1294
+ rowMetadataMutations
1295
+ }
1296
+ };
1297
+ this.channel.postMessage(txCommitted);
1298
+ for (const subscriber of state.subscribers) {
1299
+ subscriber(txCommitted);
1300
+ }
1301
+ return response;
1302
+ }
1303
+ async handleApplyCommittedTx(collectionId, request, scopedAdapter) {
1304
+ const envelopeKey = appliedEnvelopeKey(collectionId, request.envelopeId);
1305
+ return this.runDeduplicatedEnvelope({
1306
+ envelopeKey,
1307
+ rpcId: request.rpcId,
1308
+ selectApplied: (envelope) => envelope.requestType === `rpc:applyCommittedTx:req` ? envelope.response : void 0,
1309
+ selectInFlight: (envelope) => envelope.requestType === `rpc:applyCommittedTx:req` ? envelope.response : void 0,
1310
+ conflict: (phase) => ({
1311
+ type: `rpc:applyCommittedTx:res`,
1312
+ rpcId: request.rpcId,
1313
+ ok: false,
1314
+ code: `CONFLICT`,
1315
+ error: `envelope ${request.envelopeId} ${phase}`
1316
+ }),
1317
+ apply: () => this.applyCommittedTxOnce(collectionId, request, scopedAdapter),
1318
+ createPending: (response) => ({
1319
+ requestType: request.type,
1320
+ response
1321
+ })
1322
+ });
1323
+ }
1324
+ async runDeduplicatedEnvelope(options) {
1325
+ const appliedEnvelope = this.appliedEnvelopes.get(options.envelopeKey);
1326
+ if (appliedEnvelope) {
1327
+ const response2 = options.selectApplied(appliedEnvelope);
1328
+ return response2 ? { ...response2, rpcId: options.rpcId } : options.conflict(`already applied`);
1329
+ }
1330
+ const inFlightEnvelope = this.inFlightEnvelopes.get(options.envelopeKey);
1331
+ if (inFlightEnvelope) {
1332
+ const pending = options.selectInFlight(inFlightEnvelope);
1333
+ if (!pending) return options.conflict(`is already in flight`);
1334
+ const response2 = await pending;
1335
+ return { ...response2, rpcId: options.rpcId };
1336
+ }
1337
+ const response = options.apply();
1338
+ const pendingEnvelope = options.createPending(response);
1339
+ this.inFlightEnvelopes.set(options.envelopeKey, pendingEnvelope);
1340
+ try {
1341
+ return await response;
1342
+ } finally {
1343
+ if (this.inFlightEnvelopes.get(options.envelopeKey) === pendingEnvelope) {
1344
+ this.inFlightEnvelopes.delete(options.envelopeKey);
1345
+ }
1346
+ }
1347
+ }
1348
+ async applyCommittedTxOnce(collectionId, request, scopedAdapter) {
1349
+ const state = this.collections.get(collectionId);
1350
+ if (!state || !state.isLeader) {
1351
+ return {
1352
+ type: `rpc:applyCommittedTx:res`,
1353
+ rpcId: request.rpcId,
1354
+ ok: false,
1355
+ code: `NOT_LEADER`,
1356
+ error: `not the leader for ${collectionId}`
1357
+ };
1358
+ }
1359
+ const tx = await this.applyDurablyAtNextStreamPosition(
1360
+ collectionId,
1361
+ state,
1362
+ request.tx,
1363
+ scopedAdapter
1364
+ );
1365
+ if (tx === null) {
1366
+ return {
1367
+ type: `rpc:applyCommittedTx:res`,
1368
+ rpcId: request.rpcId,
1369
+ ok: false,
1370
+ code: `NOT_LEADER`,
1371
+ error: `not the leader for ${collectionId}`
1372
+ };
1373
+ }
1374
+ const response = {
1375
+ type: `rpc:applyCommittedTx:res`,
1376
+ rpcId: request.rpcId,
1377
+ ok: true,
1378
+ term: tx.term,
1379
+ seq: tx.seq,
1380
+ latestRowVersion: tx.rowVersion
1381
+ };
1382
+ if (this.isDisposed() || this.collections.get(collectionId) !== state) {
1383
+ return response;
1384
+ }
1385
+ this.appliedEnvelopes.set(
1386
+ appliedEnvelopeKey(collectionId, request.envelopeId),
1387
+ {
1388
+ appliedAt: Date.now(),
1389
+ requestType: request.type,
1390
+ response
1391
+ }
1392
+ );
1393
+ this.pruneAppliedEnvelopes();
1394
+ const committedBase = {
1395
+ type: `tx:committed`,
1396
+ term: tx.term,
1397
+ seq: tx.seq,
1398
+ txId: tx.txId,
1399
+ latestRowVersion: tx.rowVersion
1400
+ };
1401
+ const committedPayload = tx.truncate ? {
1402
+ ...committedBase,
1403
+ requiresFullReload: true
1404
+ } : {
1405
+ ...committedBase,
1406
+ requiresFullReload: false,
1407
+ changedRows: tx.mutations.filter((mutation) => mutation.type !== `delete`).map((mutation) => ({
1408
+ key: mutation.key,
1409
+ value: mutation.value
1410
+ })),
1411
+ deletedKeys: tx.mutations.filter((mutation) => mutation.type === `delete`).map((mutation) => mutation.key),
1412
+ rowMetadataMutations: tx.rowMetadataMutations,
1413
+ collectionMetadataMutations: tx.collectionMetadataMutations
1414
+ };
1415
+ const committed = {
1416
+ v: 1,
1417
+ dbName: this.dbName,
1418
+ collectionId,
1419
+ senderId: this.nodeId,
1420
+ ts: Date.now(),
1421
+ payload: committedPayload
1422
+ };
1423
+ this.channel.postMessage(committed);
1424
+ for (const subscriber of state.subscribers) {
1425
+ subscriber(committed);
1426
+ }
1427
+ return response;
1428
+ }
1429
+ async applyDurablyAtNextStreamPosition(collectionId, state, pendingTx, scopedAdapter) {
1430
+ const expectedTerm = state.latestTerm;
1431
+ return this.withScheduledWriterLock(
1432
+ collectionId,
1433
+ async (adapter) => {
1434
+ if (this.isDisposed() || this.collections.get(collectionId) !== state || !state.isLeader || state.leaderId !== this.nodeId || state.latestTerm !== expectedTerm) {
1435
+ return null;
1436
+ }
1437
+ const tx = {
1438
+ ...pendingTx,
1439
+ term: state.latestTerm,
1440
+ seq: state.latestSeq + 1,
1441
+ rowVersion: state.latestRowVersion + 1
1442
+ };
1443
+ try {
1444
+ await adapter.applyCommittedTx(collectionId, tx);
1445
+ } catch (error) {
1446
+ throw toPersistedCollectionDurabilityError(collectionId, error);
1447
+ }
1448
+ state.latestSeq = tx.seq;
1449
+ state.latestRowVersion = tx.rowVersion;
1450
+ return tx;
1451
+ },
1452
+ scopedAdapter
1453
+ );
1454
+ }
1455
+ async handlePullSince(collectionId, request, scopedAdapter) {
1456
+ const state = this.collections.get(collectionId);
1457
+ const adapter = scopedAdapter ?? this.requireAdapter(collectionId);
1458
+ if (!adapter.pullSince) {
1459
+ return {
1460
+ type: `rpc:pullSince:res`,
1461
+ rpcId: request.rpcId,
1462
+ ok: true,
1463
+ latestTerm: state?.latestTerm ?? 0,
1464
+ latestSeq: state?.latestSeq ?? 0,
1465
+ latestRowVersion: state?.latestRowVersion ?? 0,
1466
+ requiresFullReload: true
1467
+ };
1468
+ }
1469
+ const result = await adapter.pullSince(collectionId, request.fromRowVersion);
1470
+ if (result.requiresFullReload) {
1471
+ return {
1472
+ type: `rpc:pullSince:res`,
1473
+ rpcId: request.rpcId,
1474
+ ok: true,
1475
+ latestTerm: state?.latestTerm ?? 0,
1476
+ latestSeq: state?.latestSeq ?? 0,
1477
+ latestRowVersion: result.latestRowVersion,
1478
+ requiresFullReload: true
1479
+ };
1480
+ }
1481
+ return {
1482
+ type: `rpc:pullSince:res`,
1483
+ rpcId: request.rpcId,
1484
+ ok: true,
1485
+ latestTerm: state?.latestTerm ?? 0,
1486
+ latestSeq: state?.latestSeq ?? 0,
1487
+ latestRowVersion: result.latestRowVersion,
1488
+ requiresFullReload: false,
1489
+ changedKeys: result.changedKeys,
1490
+ deletedKeys: result.deletedKeys
1491
+ };
1492
+ }
1493
+ // -----------------------------------------------------------------------
1494
+ // DB Writer Lock
1495
+ // -----------------------------------------------------------------------
1496
+ withScheduledWriterLock(collectionId, task, scopedAdapter) {
1497
+ if (scopedAdapter) {
1498
+ return this.withWriterLock(() => task(scopedAdapter));
1499
+ }
1500
+ const adapter = this.requireAdapter(collectionId);
1501
+ return adapter.runInRegularScope ? adapter.runInRegularScope(
1502
+ (unscheduledAdapter) => this.withWriterLock(() => task(unscheduledAdapter))
1503
+ ) : this.withWriterLock(() => task(adapter));
1504
+ }
1505
+ async withWriterLock(fn) {
1506
+ const lockName = `tsdb:writer:${this.dbName}`;
1507
+ for (let attempt = 0; attempt <= WRITER_LOCK_MAX_RETRIES; attempt++) {
1508
+ const callbackState = { entered: false };
1509
+ try {
1510
+ return await navigator.locks.request(lockName, async () => {
1511
+ callbackState.entered = true;
1512
+ return fn();
1513
+ });
1514
+ } catch (error) {
1515
+ if (callbackState.entered) {
1516
+ throw error;
1517
+ }
1518
+ if (error instanceof DOMException && error.name === `AbortError`) {
1519
+ throw error;
1520
+ }
1521
+ if (attempt < WRITER_LOCK_MAX_RETRIES) {
1522
+ await sleep(WRITER_LOCK_BUSY_RETRY_MS * Math.min(attempt + 1, 5));
1523
+ continue;
1524
+ }
1525
+ throw error;
1526
+ }
1527
+ }
1528
+ throw new Error(`writer lock acquisition failed`);
1529
+ }
1530
+ // -----------------------------------------------------------------------
1531
+ // Helpers
1532
+ // -----------------------------------------------------------------------
1533
+ pruneAppliedEnvelopes() {
1534
+ if (this.appliedEnvelopePruneTimer !== null) {
1535
+ clearTimeout(this.appliedEnvelopePruneTimer);
1536
+ this.appliedEnvelopePruneTimer = null;
1537
+ }
1538
+ const cutoff = Date.now() - RPC_DEDUPE_RETENTION_MS;
1539
+ for (const [key, envelope] of this.appliedEnvelopes) {
1540
+ if (envelope.appliedAt <= cutoff) {
1541
+ this.appliedEnvelopes.delete(key);
1542
+ }
1543
+ }
1544
+ if (this.disposed || this.appliedEnvelopes.size === 0) return;
1545
+ let earliestAppliedAt = Number.POSITIVE_INFINITY;
1546
+ for (const envelope of this.appliedEnvelopes.values()) {
1547
+ earliestAppliedAt = Math.min(earliestAppliedAt, envelope.appliedAt);
1548
+ }
1549
+ const delay = Math.max(
1550
+ 0,
1551
+ earliestAppliedAt + RPC_DEDUPE_RETENTION_MS - Date.now()
1552
+ );
1553
+ this.appliedEnvelopePruneTimer = setTimeout(() => {
1554
+ this.appliedEnvelopePruneTimer = null;
1555
+ this.pruneAppliedEnvelopes();
1556
+ }, delay);
1557
+ }
1558
+ setReleasedRemoteSubsetAcquisition(key, acquisition) {
1559
+ this.inboundRemoteSubsetAcquisitions.set(key, acquisition);
1560
+ this.releasedRemoteSubsetAcquisitionTimes.set(key, Date.now());
1561
+ }
1562
+ pruneReleasedRemoteSubsetAcquisitions() {
1563
+ const cutoff = Date.now() - RPC_DEDUPE_RETENTION_MS;
1564
+ for (const [key, releasedAt] of this.releasedRemoteSubsetAcquisitionTimes) {
1565
+ if (releasedAt < cutoff) {
1566
+ this.releasedRemoteSubsetAcquisitionTimes.delete(key);
1567
+ this.inboundRemoteSubsetAcquisitions.delete(key);
1568
+ }
1569
+ }
1570
+ }
1571
+ }
1572
+ function isProtocolEnvelope(data) {
1573
+ if (!data || typeof data !== `object`) return false;
1574
+ const record = data;
1575
+ return record.v === 1 && typeof record.dbName === `string` && typeof record.collectionId === `string` && typeof record.senderId === `string` && typeof record.ts === `number`;
1576
+ }
1577
+ function isRPCRequest(payload) {
1578
+ if (!payload || typeof payload !== `object`) return false;
1579
+ switch (payload.type) {
1580
+ case `rpc:ensureRemoteSubset:req`:
1581
+ case `rpc:releaseRemoteSubset:req`:
1582
+ case `rpc:ensurePersistedIndex:req`:
1583
+ case `rpc:applyLocalMutations:req`:
1584
+ case `rpc:applyCommittedTx:req`:
1585
+ case `rpc:pullSince:req`:
1586
+ return true;
1587
+ default:
1588
+ return false;
1589
+ }
1590
+ }
1591
+ function sleep(ms) {
1592
+ return new Promise((resolve) => setTimeout(resolve, ms));
1593
+ }
1594
+ function appliedEnvelopeKey(collectionId, envelopeId) {
1595
+ return JSON.stringify([collectionId, envelopeId]);
1596
+ }
1597
+ function remoteSubsetAcquisitionKey(collectionId, acquisitionId) {
1598
+ return JSON.stringify([collectionId, acquisitionId]);
1599
+ }
1600
+ function inboundRemoteSubsetAcquisitionKey(collectionId, requesterId, acquisitionId) {
1601
+ return JSON.stringify([collectionId, requesterId, acquisitionId]);
1602
+ }
1603
+ function createRPCErrorResponse(request, cause) {
1604
+ const error = cause instanceof Error ? cause.message : String(cause);
1605
+ switch (request.type) {
1606
+ case `rpc:ensureRemoteSubset:req`:
1607
+ return {
1608
+ type: `rpc:ensureRemoteSubset:res`,
1609
+ rpcId: request.rpcId,
1610
+ ok: false,
1611
+ error,
1612
+ ...cause instanceof RetryableRemoteSubsetAcquisitionError ? { retryable: true } : {}
1613
+ };
1614
+ case `rpc:releaseRemoteSubset:req`:
1615
+ return {
1616
+ type: `rpc:releaseRemoteSubset:res`,
1617
+ rpcId: request.rpcId,
1618
+ ok: false,
1619
+ error
1620
+ };
1621
+ case `rpc:ensurePersistedIndex:req`:
1622
+ return {
1623
+ type: `rpc:ensurePersistedIndex:res`,
1624
+ rpcId: request.rpcId,
1625
+ ok: false,
1626
+ error
1627
+ };
1628
+ case `rpc:applyLocalMutations:req`:
1629
+ if (cause instanceof PersistedCollectionDurabilityError) {
1630
+ return {
1631
+ type: `rpc:applyLocalMutations:res`,
1632
+ rpcId: request.rpcId,
1633
+ ok: false,
1634
+ code: `PERSISTENCE_ERROR`,
1635
+ error,
1636
+ ...toSafeDurabilityDetails(cause)
1637
+ };
1638
+ }
1639
+ return {
1640
+ type: `rpc:applyLocalMutations:res`,
1641
+ rpcId: request.rpcId,
1642
+ ok: false,
1643
+ code: `CONFLICT`,
1644
+ error
1645
+ };
1646
+ case `rpc:applyCommittedTx:req`:
1647
+ if (cause instanceof PersistedCollectionDurabilityError) {
1648
+ return {
1649
+ type: `rpc:applyCommittedTx:res`,
1650
+ rpcId: request.rpcId,
1651
+ ok: false,
1652
+ code: `PERSISTENCE_ERROR`,
1653
+ error,
1654
+ ...toSafeDurabilityDetails(cause)
1655
+ };
1656
+ }
1657
+ return {
1658
+ type: `rpc:applyCommittedTx:res`,
1659
+ rpcId: request.rpcId,
1660
+ ok: false,
1661
+ code: `CONFLICT`,
1662
+ error
1663
+ };
1664
+ case `rpc:pullSince:req`:
1665
+ return {
1666
+ type: `rpc:pullSince:res`,
1667
+ rpcId: request.rpcId,
1668
+ ok: false,
1669
+ error
1670
+ };
1671
+ }
1672
+ }
1673
+ function isMutatingRPCRequest(request) {
1674
+ return request.type === `rpc:applyLocalMutations:req` || request.type === `rpc:applyCommittedTx:req`;
1675
+ }
1676
+ function toSafeDurabilityDetails(error) {
1677
+ const sourceCode = typeof error.code === `string` || typeof error.code === `number` ? error.code : void 0;
1678
+ const path = typeof error.path === `string` || Array.isArray(error.path) && error.path.every(
1679
+ (part) => typeof part === `string` || typeof part === `number`
1680
+ ) ? error.path : void 0;
1681
+ return {
1682
+ ...sourceCode === void 0 ? {} : { sourceCode },
1683
+ ...path === void 0 ? {} : { path }
1684
+ };
1685
+ }
1686
+ export {
1687
+ BroadcastCollectionCoordinator
1688
+ };
1689
+ //# sourceMappingURL=broadcast-coordinator.js.map