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