@tanstack/electron-db-sqlite-persistence 0.1.35 → 0.2.2

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.
@@ -1,540 +1,11 @@
1
- import { safeRandomUUID } from "@tanstack/db-sqlite-persistence-core";
2
- const HEARTBEAT_INTERVAL_MS = 3e3;
3
- const RPC_TIMEOUT_MS = 1e4;
4
- const RPC_RETRY_ATTEMPTS = 2;
5
- const RPC_RETRY_DELAY_MS = 200;
6
- const WRITER_LOCK_BUSY_RETRY_MS = 50;
7
- const WRITER_LOCK_MAX_RETRIES = 20;
8
- class ElectronCollectionCoordinator {
1
+ import { BroadcastCollectionCoordinator } from "@tanstack/db-sqlite-persistence-core/broadcast-coordinator";
2
+ class ElectronCollectionCoordinator extends BroadcastCollectionCoordinator {
9
3
  constructor(options) {
10
- this.nodeId = safeRandomUUID();
11
- this.collections = /* @__PURE__ */ new Map();
12
- this.pendingRPCs = /* @__PURE__ */ new Map();
13
- this.appliedEnvelopeIds = /* @__PURE__ */ new Map();
14
- this.disposed = false;
15
- this.dbName = options.dbName;
16
- this.adapter = options.adapter ?? null;
17
- this.channel = new BroadcastChannel(`tsdb:coord:${this.dbName}`);
18
- this.channel.onmessage = (event) => {
19
- this.onChannelMessage(event.data);
20
- };
21
- }
22
- /** Method indirection to prevent TypeScript from narrowing `disposed` across awaits */
23
- isDisposed() {
24
- return this.disposed;
25
- }
26
- requireAdapter() {
27
- if (!this.adapter) {
28
- throw new Error(
29
- `ElectronCollectionCoordinator: adapter not set. Call setAdapter() before using leader-side operations.`
30
- );
31
- }
32
- return this.adapter;
33
- }
34
- /**
35
- * Set or replace the persistence adapter used for leader-side RPC handling.
36
- * Called by `createElectronSQLitePersistence` to wire the internally-created
37
- * adapter into the coordinator.
38
- */
39
- setAdapter(adapter) {
40
- this.adapter = adapter;
41
- }
42
- // -----------------------------------------------------------------------
43
- // PersistedCollectionCoordinator interface
44
- // -----------------------------------------------------------------------
45
- getNodeId() {
46
- return this.nodeId;
47
- }
48
- subscribe(collectionId, onMessage) {
49
- const state = this.ensureCollectionState(collectionId);
50
- state.subscribers.add(onMessage);
51
- return () => {
52
- state.subscribers.delete(onMessage);
53
- };
54
- }
55
- publish(_collectionId, message) {
56
- this.channel.postMessage(message);
57
- }
58
- isLeader(collectionId) {
59
- return this.collections.get(collectionId)?.isLeader ?? false;
60
- }
61
- async ensureLeadership(collectionId) {
62
- const state = this.ensureCollectionState(collectionId);
63
- if (state.isLeader) return;
64
- await this.acquireLeadership(collectionId, state);
65
- }
66
- async requestEnsureRemoteSubset(collectionId, options) {
67
- if (this.isLeader(collectionId)) return;
68
- const response = await this.sendRPC(collectionId, {
69
- type: `rpc:ensureRemoteSubset:req`,
70
- rpcId: safeRandomUUID(),
71
- options
4
+ super({
5
+ ...options,
6
+ coordinatorName: `ElectronCollectionCoordinator`
72
7
  });
73
- if (!response.ok) {
74
- throw new Error(
75
- `ensureRemoteSubset failed: ${response.error ?? `unknown error`}`
76
- );
77
- }
78
- }
79
- async requestEnsurePersistedIndex(collectionId, signature, spec) {
80
- if (this.isLeader(collectionId)) {
81
- await this.requireAdapter().ensureIndex(collectionId, signature, spec);
82
- return;
83
- }
84
- const response = await this.sendRPC(collectionId, {
85
- type: `rpc:ensurePersistedIndex:req`,
86
- rpcId: safeRandomUUID(),
87
- signature,
88
- spec
89
- });
90
- if (!response.ok) {
91
- throw new Error(
92
- `ensurePersistedIndex failed: ${response.error ?? `unknown error`}`
93
- );
94
- }
95
- }
96
- async requestApplyLocalMutations(collectionId, mutations) {
97
- if (this.isLeader(collectionId)) {
98
- return this.handleApplyLocalMutations(collectionId, {
99
- type: `rpc:applyLocalMutations:req`,
100
- rpcId: safeRandomUUID(),
101
- envelopeId: safeRandomUUID(),
102
- mutations
103
- });
104
- }
105
- return this.sendRPC(collectionId, {
106
- type: `rpc:applyLocalMutations:req`,
107
- rpcId: safeRandomUUID(),
108
- envelopeId: safeRandomUUID(),
109
- mutations
110
- });
111
- }
112
- async pullSince(collectionId, fromRowVersion) {
113
- if (this.isLeader(collectionId)) {
114
- return this.handlePullSince(collectionId, {
115
- type: `rpc:pullSince:req`,
116
- rpcId: safeRandomUUID(),
117
- fromRowVersion
118
- });
119
- }
120
- return this.sendRPC(collectionId, {
121
- type: `rpc:pullSince:req`,
122
- rpcId: safeRandomUUID(),
123
- fromRowVersion
124
- });
125
- }
126
- // -----------------------------------------------------------------------
127
- // Lifecycle
128
- // -----------------------------------------------------------------------
129
- dispose() {
130
- this.disposed = true;
131
- for (const [collectionId, state] of this.collections) {
132
- this.releaseLeadership(collectionId, state);
133
- }
134
- for (const [, pending] of this.pendingRPCs) {
135
- clearTimeout(pending.timer);
136
- pending.reject(new Error(`coordinator disposed`));
137
- }
138
- this.pendingRPCs.clear();
139
- this.channel.close();
140
- this.collections.clear();
141
- }
142
- // -----------------------------------------------------------------------
143
- // Leadership via Web Locks
144
- // -----------------------------------------------------------------------
145
- ensureCollectionState(collectionId) {
146
- let state = this.collections.get(collectionId);
147
- if (!state) {
148
- state = {
149
- isLeader: false,
150
- lockAbortController: null,
151
- heartbeatTimer: null,
152
- latestTerm: 0,
153
- latestSeq: 0,
154
- latestRowVersion: 0,
155
- subscribers: /* @__PURE__ */ new Set()
156
- };
157
- this.collections.set(collectionId, state);
158
- void this.acquireLeadership(collectionId, state);
159
- }
160
- return state;
161
8
  }
162
- async acquireLeadership(collectionId, state) {
163
- if (this.disposed || state.isLeader) return;
164
- const lockName = `tsdb:leader:${this.dbName}:${collectionId}`;
165
- const abortController = new AbortController();
166
- state.lockAbortController = abortController;
167
- try {
168
- await navigator.locks.request(
169
- lockName,
170
- { signal: abortController.signal },
171
- async () => {
172
- if (this.isDisposed()) return;
173
- try {
174
- const adapter = this.requireAdapter();
175
- if (adapter.getStreamPosition) {
176
- const pos = await adapter.getStreamPosition(collectionId);
177
- state.latestTerm = pos.latestTerm;
178
- state.latestSeq = pos.latestSeq;
179
- state.latestRowVersion = pos.latestRowVersion;
180
- }
181
- state.latestTerm++;
182
- state.isLeader = true;
183
- this.emitHeartbeat(collectionId, state);
184
- state.heartbeatTimer = setInterval(() => {
185
- this.emitHeartbeat(collectionId, state);
186
- }, HEARTBEAT_INTERVAL_MS);
187
- await new Promise((resolve) => {
188
- const onAbort = () => {
189
- abortController.signal.removeEventListener(`abort`, onAbort);
190
- resolve();
191
- };
192
- if (abortController.signal.aborted) {
193
- resolve();
194
- return;
195
- }
196
- abortController.signal.addEventListener(`abort`, onAbort);
197
- });
198
- } finally {
199
- state.isLeader = false;
200
- if (state.heartbeatTimer) {
201
- clearInterval(state.heartbeatTimer);
202
- state.heartbeatTimer = null;
203
- }
204
- }
205
- }
206
- );
207
- } catch (error) {
208
- if (error instanceof DOMException && error.name === `AbortError`) {
209
- return;
210
- }
211
- console.warn(`Failed to acquire leadership for ${collectionId}:`, error);
212
- }
213
- if (!this.isDisposed()) {
214
- void this.acquireLeadership(collectionId, state);
215
- }
216
- }
217
- releaseLeadership(_collectionId, state) {
218
- if (state.lockAbortController) {
219
- state.lockAbortController.abort();
220
- state.lockAbortController = null;
221
- }
222
- if (state.heartbeatTimer) {
223
- clearInterval(state.heartbeatTimer);
224
- state.heartbeatTimer = null;
225
- }
226
- state.isLeader = false;
227
- }
228
- emitHeartbeat(collectionId, state) {
229
- const envelope = {
230
- v: 1,
231
- dbName: this.dbName,
232
- collectionId,
233
- senderId: this.nodeId,
234
- ts: Date.now(),
235
- payload: {
236
- type: `leader:heartbeat`,
237
- term: state.latestTerm,
238
- leaderId: this.nodeId,
239
- latestSeq: state.latestSeq,
240
- latestRowVersion: state.latestRowVersion
241
- }
242
- };
243
- this.channel.postMessage(envelope);
244
- }
245
- // -----------------------------------------------------------------------
246
- // BroadcastChannel message handling
247
- // -----------------------------------------------------------------------
248
- onChannelMessage(data) {
249
- if (!isProtocolEnvelope(data)) return;
250
- const envelope = data;
251
- if (envelope.senderId === this.nodeId) return;
252
- const payload = envelope.payload;
253
- if (!payload || typeof payload !== `object`) return;
254
- const type = payload.type;
255
- if (type && type.endsWith(`:res`)) {
256
- const rpcId = payload.rpcId;
257
- if (rpcId && this.pendingRPCs.has(rpcId)) {
258
- const pending = this.pendingRPCs.get(rpcId);
259
- this.pendingRPCs.delete(rpcId);
260
- clearTimeout(pending.timer);
261
- pending.resolve(payload);
262
- return;
263
- }
264
- }
265
- if (type && type.endsWith(`:req`)) {
266
- const collectionId = envelope.collectionId;
267
- if (this.isLeader(collectionId)) {
268
- void this.handleRPCRequest(collectionId, payload);
269
- }
270
- return;
271
- }
272
- const state = this.collections.get(envelope.collectionId);
273
- if (state) {
274
- for (const subscriber of state.subscribers) {
275
- subscriber(envelope);
276
- }
277
- }
278
- }
279
- // -----------------------------------------------------------------------
280
- // RPC - Outbound (follower side)
281
- // -----------------------------------------------------------------------
282
- async sendRPC(collectionId, request) {
283
- let lastError;
284
- for (let attempt = 0; attempt <= RPC_RETRY_ATTEMPTS; attempt++) {
285
- if (attempt > 0) {
286
- await sleep(RPC_RETRY_DELAY_MS * attempt);
287
- }
288
- try {
289
- return await this.sendRPCOnce(collectionId, request);
290
- } catch (error) {
291
- lastError = error instanceof Error ? error : new Error(String(error));
292
- }
293
- }
294
- throw lastError ?? new Error(`RPC failed after retries`);
295
- }
296
- sendRPCOnce(collectionId, request) {
297
- return new Promise((resolve, reject) => {
298
- const rpcId = request.rpcId;
299
- const timer = setTimeout(() => {
300
- this.pendingRPCs.delete(rpcId);
301
- reject(
302
- new Error(`RPC ${request.type} timed out after ${RPC_TIMEOUT_MS}ms`)
303
- );
304
- }, RPC_TIMEOUT_MS);
305
- this.pendingRPCs.set(rpcId, {
306
- resolve,
307
- reject,
308
- timer
309
- });
310
- const envelope = {
311
- v: 1,
312
- dbName: this.dbName,
313
- collectionId,
314
- senderId: this.nodeId,
315
- ts: Date.now(),
316
- payload: request
317
- };
318
- this.channel.postMessage(envelope);
319
- });
320
- }
321
- // -----------------------------------------------------------------------
322
- // RPC - Inbound (leader side)
323
- // -----------------------------------------------------------------------
324
- async handleRPCRequest(collectionId, request) {
325
- let response;
326
- try {
327
- switch (request.type) {
328
- case `rpc:ensureRemoteSubset:req`:
329
- response = await this.handleEnsureRemoteSubset(collectionId, request);
330
- break;
331
- case `rpc:ensurePersistedIndex:req`:
332
- response = await this.handleEnsurePersistedIndex(
333
- collectionId,
334
- request
335
- );
336
- break;
337
- case `rpc:applyLocalMutations:req`:
338
- response = await this.handleApplyLocalMutations(collectionId, request);
339
- break;
340
- case `rpc:pullSince:req`:
341
- response = await this.handlePullSince(collectionId, request);
342
- break;
343
- default:
344
- return;
345
- }
346
- } catch (error) {
347
- const errorMessage = error instanceof Error ? error.message : String(error);
348
- response = {
349
- type: request.type.replace(`:req`, `:res`),
350
- rpcId: request.rpcId,
351
- ok: false,
352
- error: errorMessage
353
- };
354
- }
355
- const envelope = {
356
- v: 1,
357
- dbName: this.dbName,
358
- collectionId,
359
- senderId: this.nodeId,
360
- ts: Date.now(),
361
- payload: response
362
- };
363
- this.channel.postMessage(envelope);
364
- }
365
- handleEnsureRemoteSubset(_collectionId, request) {
366
- return {
367
- type: `rpc:ensureRemoteSubset:res`,
368
- rpcId: request.rpcId,
369
- ok: true
370
- };
371
- }
372
- async handleEnsurePersistedIndex(collectionId, request) {
373
- await this.withWriterLock(
374
- () => this.requireAdapter().ensureIndex(
375
- collectionId,
376
- request.signature,
377
- request.spec
378
- )
379
- );
380
- return {
381
- type: `rpc:ensurePersistedIndex:res`,
382
- rpcId: request.rpcId,
383
- ok: true
384
- };
385
- }
386
- async handleApplyLocalMutations(collectionId, request) {
387
- if (this.appliedEnvelopeIds.has(request.envelopeId)) {
388
- return {
389
- type: `rpc:applyLocalMutations:res`,
390
- rpcId: request.rpcId,
391
- ok: false,
392
- code: `CONFLICT`,
393
- error: `envelope ${request.envelopeId} already applied`
394
- };
395
- }
396
- const state = this.collections.get(collectionId);
397
- if (!state || !state.isLeader) {
398
- return {
399
- type: `rpc:applyLocalMutations:res`,
400
- rpcId: request.rpcId,
401
- ok: false,
402
- code: `NOT_LEADER`,
403
- error: `not the leader for ${collectionId}`
404
- };
405
- }
406
- state.latestSeq++;
407
- state.latestRowVersion++;
408
- const term = state.latestTerm;
409
- const seq = state.latestSeq;
410
- const rowVersion = state.latestRowVersion;
411
- const tx = {
412
- txId: safeRandomUUID(),
413
- term,
414
- seq,
415
- rowVersion,
416
- mutations: request.mutations.map((m) => ({
417
- type: m.type,
418
- key: m.key,
419
- value: m.value
420
- }))
421
- };
422
- await this.withWriterLock(
423
- () => this.requireAdapter().applyCommittedTx(collectionId, tx)
424
- );
425
- this.appliedEnvelopeIds.set(request.envelopeId, Date.now());
426
- this.pruneAppliedEnvelopeIds();
427
- const changedRows = request.mutations.filter((m) => m.type !== `delete`).map((m) => ({ key: m.key, value: m.value }));
428
- const deletedKeys = request.mutations.filter((m) => m.type === `delete`).map((m) => m.key);
429
- const txCommitted = {
430
- v: 1,
431
- dbName: this.dbName,
432
- collectionId,
433
- senderId: this.nodeId,
434
- ts: Date.now(),
435
- payload: {
436
- type: `tx:committed`,
437
- term,
438
- seq,
439
- txId: tx.txId,
440
- latestRowVersion: rowVersion,
441
- requiresFullReload: false,
442
- changedRows,
443
- deletedKeys
444
- }
445
- };
446
- this.channel.postMessage(txCommitted);
447
- for (const subscriber of state.subscribers) {
448
- subscriber(txCommitted);
449
- }
450
- return {
451
- type: `rpc:applyLocalMutations:res`,
452
- rpcId: request.rpcId,
453
- ok: true,
454
- term,
455
- seq,
456
- latestRowVersion: rowVersion,
457
- acceptedMutationIds: request.mutations.map((m) => m.mutationId)
458
- };
459
- }
460
- async handlePullSince(collectionId, request) {
461
- const state = this.collections.get(collectionId);
462
- const adapter = this.requireAdapter();
463
- if (!adapter.pullSince) {
464
- return {
465
- type: `rpc:pullSince:res`,
466
- rpcId: request.rpcId,
467
- ok: true,
468
- latestTerm: state?.latestTerm ?? 0,
469
- latestSeq: state?.latestSeq ?? 0,
470
- latestRowVersion: state?.latestRowVersion ?? 0,
471
- requiresFullReload: true
472
- };
473
- }
474
- const result = await adapter.pullSince(collectionId, request.fromRowVersion);
475
- if (result.requiresFullReload) {
476
- return {
477
- type: `rpc:pullSince:res`,
478
- rpcId: request.rpcId,
479
- ok: true,
480
- latestTerm: state?.latestTerm ?? 0,
481
- latestSeq: state?.latestSeq ?? 0,
482
- latestRowVersion: result.latestRowVersion,
483
- requiresFullReload: true
484
- };
485
- }
486
- return {
487
- type: `rpc:pullSince:res`,
488
- rpcId: request.rpcId,
489
- ok: true,
490
- latestTerm: state?.latestTerm ?? 0,
491
- latestSeq: state?.latestSeq ?? 0,
492
- latestRowVersion: result.latestRowVersion,
493
- requiresFullReload: false,
494
- changedKeys: result.changedKeys,
495
- deletedKeys: result.deletedKeys
496
- };
497
- }
498
- // -----------------------------------------------------------------------
499
- // DB Writer Lock
500
- // -----------------------------------------------------------------------
501
- async withWriterLock(fn) {
502
- const lockName = `tsdb:writer:${this.dbName}`;
503
- for (let attempt = 0; attempt <= WRITER_LOCK_MAX_RETRIES; attempt++) {
504
- try {
505
- return await navigator.locks.request(lockName, async () => fn());
506
- } catch (error) {
507
- if (error instanceof DOMException && error.name === `AbortError`) {
508
- throw error;
509
- }
510
- if (attempt < WRITER_LOCK_MAX_RETRIES) {
511
- await sleep(WRITER_LOCK_BUSY_RETRY_MS * Math.min(attempt + 1, 5));
512
- continue;
513
- }
514
- throw error;
515
- }
516
- }
517
- throw new Error(`writer lock acquisition failed`);
518
- }
519
- // -----------------------------------------------------------------------
520
- // Helpers
521
- // -----------------------------------------------------------------------
522
- pruneAppliedEnvelopeIds() {
523
- const cutoff = Date.now() - 6e4;
524
- for (const [id, ts] of this.appliedEnvelopeIds) {
525
- if (ts < cutoff) {
526
- this.appliedEnvelopeIds.delete(id);
527
- }
528
- }
529
- }
530
- }
531
- function isProtocolEnvelope(data) {
532
- if (!data || typeof data !== `object`) return false;
533
- const record = data;
534
- return record.v === 1 && typeof record.dbName === `string` && typeof record.collectionId === `string` && typeof record.senderId === `string` && typeof record.ts === `number`;
535
- }
536
- function sleep(ms) {
537
- return new Promise((resolve) => setTimeout(resolve, ms));
538
9
  }
539
10
  export {
540
11
  ElectronCollectionCoordinator
@@ -1 +1 @@
1
- {"version":3,"file":"electron-coordinator.js","sources":["../../src/electron-coordinator.ts"],"sourcesContent":["import { safeRandomUUID } from '@tanstack/db-sqlite-persistence-core'\nimport type {\n ApplyLocalMutationsResponse,\n PersistedCollectionCoordinator,\n PersistedIndexSpec,\n PersistedMutationEnvelope,\n PersistenceAdapter,\n ProtocolEnvelope,\n PullSinceResponse,\n} from '@tanstack/db-sqlite-persistence-core'\nimport type { LoadSubsetOptions } from '@tanstack/db'\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst HEARTBEAT_INTERVAL_MS = 3_000\nconst RPC_TIMEOUT_MS = 10_000\nconst RPC_RETRY_ATTEMPTS = 2\nconst RPC_RETRY_DELAY_MS = 200\nconst WRITER_LOCK_BUSY_RETRY_MS = 50\nconst WRITER_LOCK_MAX_RETRIES = 20\n\n// ---------------------------------------------------------------------------\n// Internal types\n// ---------------------------------------------------------------------------\n\ntype RPCRequest =\n | {\n type: `rpc:ensureRemoteSubset:req`\n rpcId: string\n options: LoadSubsetOptions\n }\n | {\n type: `rpc:ensurePersistedIndex:req`\n rpcId: string\n signature: string\n spec: PersistedIndexSpec\n }\n | {\n type: `rpc:applyLocalMutations:req`\n rpcId: string\n envelopeId: string\n mutations: Array<PersistedMutationEnvelope>\n }\n | {\n type: `rpc:pullSince:req`\n rpcId: string\n fromRowVersion: number\n }\n\ntype RPCResponse =\n | {\n type: `rpc:ensureRemoteSubset:res`\n rpcId: string\n ok: boolean\n error?: string\n }\n | {\n type: `rpc:ensurePersistedIndex:res`\n rpcId: string\n ok: boolean\n error?: string\n }\n | ApplyLocalMutationsResponse\n | PullSinceResponse\n\ntype PendingRPC = {\n resolve: (response: RPCResponse) => void\n reject: (error: Error) => void\n timer: ReturnType<typeof setTimeout>\n}\n\ntype CollectionState = {\n isLeader: boolean\n lockAbortController: AbortController | null\n heartbeatTimer: ReturnType<typeof setInterval> | null\n latestTerm: number\n latestSeq: number\n latestRowVersion: number\n subscribers: Set<(message: ProtocolEnvelope<unknown>) => void>\n}\n\n// Adapter with pullSince support\ntype AdapterWithPullSince = PersistenceAdapter & {\n pullSince?: (\n collectionId: string,\n fromRowVersion: number,\n ) => Promise<\n | {\n latestRowVersion: number\n requiresFullReload: true\n }\n | {\n latestRowVersion: number\n requiresFullReload: false\n changedKeys: Array<string | number>\n deletedKeys: Array<string | number>\n }\n >\n getStreamPosition?: (collectionId: string) => Promise<{\n latestTerm: number\n latestSeq: number\n latestRowVersion: number\n }>\n}\n\n// ---------------------------------------------------------------------------\n// Options\n// ---------------------------------------------------------------------------\n\nexport type ElectronCollectionCoordinatorOptions = {\n dbName: string\n adapter?: AdapterWithPullSince\n}\n\n// ---------------------------------------------------------------------------\n// ElectronCollectionCoordinator\n// ---------------------------------------------------------------------------\n\nexport class ElectronCollectionCoordinator implements PersistedCollectionCoordinator {\n private readonly nodeId = safeRandomUUID()\n private readonly dbName: string\n private adapter: AdapterWithPullSince | null\n private readonly channel: BroadcastChannel\n private readonly collections = new Map<string, CollectionState>()\n private readonly pendingRPCs = new Map<string, PendingRPC>()\n private readonly appliedEnvelopeIds = new Map<string, number>()\n private disposed = false\n\n /** Method indirection to prevent TypeScript from narrowing `disposed` across awaits */\n private isDisposed(): boolean {\n return this.disposed\n }\n\n private requireAdapter(): AdapterWithPullSince {\n if (!this.adapter) {\n throw new Error(\n `ElectronCollectionCoordinator: adapter not set. Call setAdapter() before using leader-side operations.`,\n )\n }\n return this.adapter\n }\n\n constructor(options: ElectronCollectionCoordinatorOptions) {\n this.dbName = options.dbName\n this.adapter = options.adapter ?? null\n this.channel = new BroadcastChannel(`tsdb:coord:${this.dbName}`)\n this.channel.onmessage = (event: MessageEvent) => {\n this.onChannelMessage(event.data)\n }\n }\n\n /**\n * Set or replace the persistence adapter used for leader-side RPC handling.\n * Called by `createElectronSQLitePersistence` to wire the internally-created\n * adapter into the coordinator.\n */\n setAdapter(adapter: AdapterWithPullSince): void {\n this.adapter = adapter\n }\n\n // -----------------------------------------------------------------------\n // PersistedCollectionCoordinator interface\n // -----------------------------------------------------------------------\n\n getNodeId(): string {\n return this.nodeId\n }\n\n subscribe(\n collectionId: string,\n onMessage: (message: ProtocolEnvelope<unknown>) => void,\n ): () => void {\n const state = this.ensureCollectionState(collectionId)\n state.subscribers.add(onMessage)\n return () => {\n state.subscribers.delete(onMessage)\n }\n }\n\n publish(_collectionId: string, message: ProtocolEnvelope<unknown>): void {\n this.channel.postMessage(message)\n }\n\n isLeader(collectionId: string): boolean {\n return this.collections.get(collectionId)?.isLeader ?? false\n }\n\n async ensureLeadership(collectionId: string): Promise<void> {\n const state = this.ensureCollectionState(collectionId)\n if (state.isLeader) return\n await this.acquireLeadership(collectionId, state)\n }\n\n async requestEnsureRemoteSubset(\n collectionId: string,\n options: LoadSubsetOptions,\n ): Promise<void> {\n if (this.isLeader(collectionId)) return\n\n const response = await this.sendRPC<{\n type: `rpc:ensureRemoteSubset:res`\n rpcId: string\n ok: boolean\n error?: string\n }>(collectionId, {\n type: `rpc:ensureRemoteSubset:req`,\n rpcId: safeRandomUUID(),\n options,\n })\n\n if (!response.ok) {\n throw new Error(\n `ensureRemoteSubset failed: ${response.error ?? `unknown error`}`,\n )\n }\n }\n\n async requestEnsurePersistedIndex(\n collectionId: string,\n signature: string,\n spec: PersistedIndexSpec,\n ): Promise<void> {\n if (this.isLeader(collectionId)) {\n await this.requireAdapter().ensureIndex(collectionId, signature, spec)\n return\n }\n\n const response = await this.sendRPC<{\n type: `rpc:ensurePersistedIndex:res`\n rpcId: string\n ok: boolean\n error?: string\n }>(collectionId, {\n type: `rpc:ensurePersistedIndex:req`,\n rpcId: safeRandomUUID(),\n signature,\n spec,\n })\n\n if (!response.ok) {\n throw new Error(\n `ensurePersistedIndex failed: ${response.error ?? `unknown error`}`,\n )\n }\n }\n\n async requestApplyLocalMutations(\n collectionId: string,\n mutations: Array<PersistedMutationEnvelope>,\n ): Promise<ApplyLocalMutationsResponse> {\n if (this.isLeader(collectionId)) {\n return this.handleApplyLocalMutations(collectionId, {\n type: `rpc:applyLocalMutations:req`,\n rpcId: safeRandomUUID(),\n envelopeId: safeRandomUUID(),\n mutations,\n })\n }\n\n return this.sendRPC<ApplyLocalMutationsResponse>(collectionId, {\n type: `rpc:applyLocalMutations:req`,\n rpcId: safeRandomUUID(),\n envelopeId: safeRandomUUID(),\n mutations,\n })\n }\n\n async pullSince(\n collectionId: string,\n fromRowVersion: number,\n ): Promise<PullSinceResponse> {\n if (this.isLeader(collectionId)) {\n return this.handlePullSince(collectionId, {\n type: `rpc:pullSince:req`,\n rpcId: safeRandomUUID(),\n fromRowVersion,\n })\n }\n\n return this.sendRPC<PullSinceResponse>(collectionId, {\n type: `rpc:pullSince:req`,\n rpcId: safeRandomUUID(),\n fromRowVersion,\n })\n }\n\n // -----------------------------------------------------------------------\n // Lifecycle\n // -----------------------------------------------------------------------\n\n dispose(): void {\n this.disposed = true\n\n for (const [collectionId, state] of this.collections) {\n this.releaseLeadership(collectionId, state)\n }\n\n for (const [, pending] of this.pendingRPCs) {\n clearTimeout(pending.timer)\n pending.reject(new Error(`coordinator disposed`))\n }\n this.pendingRPCs.clear()\n\n this.channel.close()\n this.collections.clear()\n }\n\n // -----------------------------------------------------------------------\n // Leadership via Web Locks\n // -----------------------------------------------------------------------\n\n private ensureCollectionState(collectionId: string): CollectionState {\n let state = this.collections.get(collectionId)\n if (!state) {\n state = {\n isLeader: false,\n lockAbortController: null,\n heartbeatTimer: null,\n latestTerm: 0,\n latestSeq: 0,\n latestRowVersion: 0,\n subscribers: new Set(),\n }\n this.collections.set(collectionId, state)\n void this.acquireLeadership(collectionId, state)\n }\n return state\n }\n\n private async acquireLeadership(\n collectionId: string,\n state: CollectionState,\n ): Promise<void> {\n if (this.disposed || state.isLeader) return\n\n const lockName = `tsdb:leader:${this.dbName}:${collectionId}`\n const abortController = new AbortController()\n state.lockAbortController = abortController\n\n try {\n await navigator.locks.request(\n lockName,\n { signal: abortController.signal },\n async () => {\n if (this.isDisposed()) return\n\n try {\n // Restore stream position from DB before claiming leadership\n const adapter = this.requireAdapter()\n if (adapter.getStreamPosition) {\n const pos = await adapter.getStreamPosition(collectionId)\n state.latestTerm = pos.latestTerm\n state.latestSeq = pos.latestSeq\n state.latestRowVersion = pos.latestRowVersion\n }\n\n state.latestTerm++\n state.isLeader = true\n\n this.emitHeartbeat(collectionId, state)\n state.heartbeatTimer = setInterval(() => {\n this.emitHeartbeat(collectionId, state)\n }, HEARTBEAT_INTERVAL_MS)\n\n // Hold the lock until disposed or aborted\n await new Promise<void>((resolve) => {\n const onAbort = () => {\n abortController.signal.removeEventListener(`abort`, onAbort)\n resolve()\n }\n if (abortController.signal.aborted) {\n resolve()\n return\n }\n abortController.signal.addEventListener(`abort`, onAbort)\n })\n } finally {\n state.isLeader = false\n if (state.heartbeatTimer) {\n clearInterval(state.heartbeatTimer)\n state.heartbeatTimer = null\n }\n }\n },\n )\n } catch (error) {\n if (error instanceof DOMException && error.name === `AbortError`) {\n return\n }\n console.warn(`Failed to acquire leadership for ${collectionId}:`, error)\n }\n\n // Re-acquire if not disposed (leadership was released by another means)\n if (!this.isDisposed()) {\n void this.acquireLeadership(collectionId, state)\n }\n }\n\n private releaseLeadership(\n _collectionId: string,\n state: CollectionState,\n ): void {\n if (state.lockAbortController) {\n state.lockAbortController.abort()\n state.lockAbortController = null\n }\n if (state.heartbeatTimer) {\n clearInterval(state.heartbeatTimer)\n state.heartbeatTimer = null\n }\n state.isLeader = false\n }\n\n private emitHeartbeat(collectionId: string, state: CollectionState): void {\n const envelope: ProtocolEnvelope<unknown> = {\n v: 1,\n dbName: this.dbName,\n collectionId,\n senderId: this.nodeId,\n ts: Date.now(),\n payload: {\n type: `leader:heartbeat`,\n term: state.latestTerm,\n leaderId: this.nodeId,\n latestSeq: state.latestSeq,\n latestRowVersion: state.latestRowVersion,\n },\n }\n this.channel.postMessage(envelope)\n }\n\n // -----------------------------------------------------------------------\n // BroadcastChannel message handling\n // -----------------------------------------------------------------------\n\n private onChannelMessage(data: unknown): void {\n if (!isProtocolEnvelope(data)) return\n\n const envelope = data\n\n // Ignore own messages\n if (envelope.senderId === this.nodeId) return\n\n const payload = envelope.payload\n if (!payload || typeof payload !== `object`) return\n\n const type = (payload as Record<string, unknown>).type as string | undefined\n\n // Handle RPC responses (for pending outbound RPCs)\n if (type && type.endsWith(`:res`)) {\n const rpcId = (payload as { rpcId?: string }).rpcId\n if (rpcId && this.pendingRPCs.has(rpcId)) {\n const pending = this.pendingRPCs.get(rpcId)!\n this.pendingRPCs.delete(rpcId)\n clearTimeout(pending.timer)\n pending.resolve(payload as RPCResponse)\n return\n }\n }\n\n // Handle RPC requests (leader only)\n if (type && type.endsWith(`:req`)) {\n const collectionId = envelope.collectionId\n if (this.isLeader(collectionId)) {\n void this.handleRPCRequest(collectionId, payload as RPCRequest)\n }\n return\n }\n\n // Forward protocol messages to subscribers\n const state = this.collections.get(envelope.collectionId)\n if (state) {\n for (const subscriber of state.subscribers) {\n subscriber(envelope)\n }\n }\n }\n\n // -----------------------------------------------------------------------\n // RPC - Outbound (follower side)\n // -----------------------------------------------------------------------\n\n private async sendRPC<T extends RPCResponse>(\n collectionId: string,\n request: RPCRequest,\n ): Promise<T> {\n let lastError: Error | undefined\n\n for (let attempt = 0; attempt <= RPC_RETRY_ATTEMPTS; attempt++) {\n if (attempt > 0) {\n await sleep(RPC_RETRY_DELAY_MS * attempt)\n }\n\n try {\n return await this.sendRPCOnce<T>(collectionId, request)\n } catch (error) {\n lastError = error instanceof Error ? error : new Error(String(error))\n }\n }\n\n throw lastError ?? new Error(`RPC failed after retries`)\n }\n\n private sendRPCOnce<T extends RPCResponse>(\n collectionId: string,\n request: RPCRequest,\n ): Promise<T> {\n return new Promise<T>((resolve, reject) => {\n const rpcId = request.rpcId\n\n const timer = setTimeout(() => {\n this.pendingRPCs.delete(rpcId)\n reject(\n new Error(`RPC ${request.type} timed out after ${RPC_TIMEOUT_MS}ms`),\n )\n }, RPC_TIMEOUT_MS)\n\n this.pendingRPCs.set(rpcId, {\n resolve: resolve as (response: RPCResponse) => void,\n reject,\n timer,\n })\n\n const envelope: ProtocolEnvelope<unknown> = {\n v: 1,\n dbName: this.dbName,\n collectionId,\n senderId: this.nodeId,\n ts: Date.now(),\n payload: request,\n }\n this.channel.postMessage(envelope)\n })\n }\n\n // -----------------------------------------------------------------------\n // RPC - Inbound (leader side)\n // -----------------------------------------------------------------------\n\n private async handleRPCRequest(\n collectionId: string,\n request: RPCRequest,\n ): Promise<void> {\n let response: RPCResponse\n\n try {\n switch (request.type) {\n case `rpc:ensureRemoteSubset:req`:\n response = await this.handleEnsureRemoteSubset(collectionId, request)\n break\n case `rpc:ensurePersistedIndex:req`:\n response = await this.handleEnsurePersistedIndex(\n collectionId,\n request,\n )\n break\n case `rpc:applyLocalMutations:req`:\n response = await this.handleApplyLocalMutations(collectionId, request)\n break\n case `rpc:pullSince:req`:\n response = await this.handlePullSince(collectionId, request)\n break\n default:\n return\n }\n } catch (error) {\n const errorMessage =\n error instanceof Error ? error.message : String(error)\n response = {\n type: request.type.replace(`:req`, `:res`) as RPCResponse[`type`],\n rpcId: request.rpcId,\n ok: false,\n error: errorMessage,\n } as RPCResponse\n }\n\n const envelope: ProtocolEnvelope<unknown> = {\n v: 1,\n dbName: this.dbName,\n collectionId,\n senderId: this.nodeId,\n ts: Date.now(),\n payload: response,\n }\n this.channel.postMessage(envelope)\n }\n\n private handleEnsureRemoteSubset(\n _collectionId: string,\n request: { type: `rpc:ensureRemoteSubset:req`; rpcId: string },\n ): RPCResponse {\n // Leader doesn't need to do anything special — the remote subset\n // is ensured by the leader's own sync connection\n return {\n type: `rpc:ensureRemoteSubset:res`,\n rpcId: request.rpcId,\n ok: true,\n }\n }\n\n private async handleEnsurePersistedIndex(\n collectionId: string,\n request: {\n type: `rpc:ensurePersistedIndex:req`\n rpcId: string\n signature: string\n spec: PersistedIndexSpec\n },\n ): Promise<RPCResponse> {\n await this.withWriterLock(() =>\n this.requireAdapter().ensureIndex(\n collectionId,\n request.signature,\n request.spec,\n ),\n )\n return {\n type: `rpc:ensurePersistedIndex:res`,\n rpcId: request.rpcId,\n ok: true,\n }\n }\n\n private async handleApplyLocalMutations(\n collectionId: string,\n request: {\n type: `rpc:applyLocalMutations:req`\n rpcId: string\n envelopeId: string\n mutations: Array<PersistedMutationEnvelope>\n },\n ): Promise<ApplyLocalMutationsResponse> {\n // Dedupe by envelopeId\n if (this.appliedEnvelopeIds.has(request.envelopeId)) {\n return {\n type: `rpc:applyLocalMutations:res`,\n rpcId: request.rpcId,\n ok: false,\n code: `CONFLICT`,\n error: `envelope ${request.envelopeId} already applied`,\n }\n }\n\n const state = this.collections.get(collectionId)\n if (!state || !state.isLeader) {\n return {\n type: `rpc:applyLocalMutations:res`,\n rpcId: request.rpcId,\n ok: false,\n code: `NOT_LEADER`,\n error: `not the leader for ${collectionId}`,\n }\n }\n\n // Assign stream position\n state.latestSeq++\n state.latestRowVersion++\n\n const term = state.latestTerm\n const seq = state.latestSeq\n const rowVersion = state.latestRowVersion\n\n // Build and apply the persisted transaction\n const tx = {\n txId: safeRandomUUID(),\n term,\n seq,\n rowVersion,\n mutations: request.mutations.map((m) => ({\n type: m.type,\n key: m.key,\n value: m.value,\n })),\n }\n\n await this.withWriterLock(() =>\n this.requireAdapter().applyCommittedTx(collectionId, tx),\n )\n\n // Track envelope for dedup\n this.appliedEnvelopeIds.set(request.envelopeId, Date.now())\n this.pruneAppliedEnvelopeIds()\n\n // Broadcast tx:committed to all tabs\n const changedRows = request.mutations\n .filter((m) => m.type !== `delete`)\n .map((m) => ({ key: m.key, value: m.value }))\n const deletedKeys = request.mutations\n .filter((m) => m.type === `delete`)\n .map((m) => m.key)\n\n const txCommitted: ProtocolEnvelope<unknown> = {\n v: 1,\n dbName: this.dbName,\n collectionId,\n senderId: this.nodeId,\n ts: Date.now(),\n payload: {\n type: `tx:committed`,\n term,\n seq,\n txId: tx.txId,\n latestRowVersion: rowVersion,\n requiresFullReload: false,\n changedRows,\n deletedKeys,\n },\n }\n this.channel.postMessage(txCommitted)\n\n // Deliver to local subscribers too\n for (const subscriber of state.subscribers) {\n subscriber(txCommitted)\n }\n\n return {\n type: `rpc:applyLocalMutations:res`,\n rpcId: request.rpcId,\n ok: true,\n term,\n seq,\n latestRowVersion: rowVersion,\n acceptedMutationIds: request.mutations.map((m) => m.mutationId),\n }\n }\n\n private async handlePullSince(\n collectionId: string,\n request: {\n type: `rpc:pullSince:req`\n rpcId: string\n fromRowVersion: number\n },\n ): Promise<PullSinceResponse> {\n const state = this.collections.get(collectionId)\n\n const adapter = this.requireAdapter()\n if (!adapter.pullSince) {\n return {\n type: `rpc:pullSince:res`,\n rpcId: request.rpcId,\n ok: true,\n latestTerm: state?.latestTerm ?? 0,\n latestSeq: state?.latestSeq ?? 0,\n latestRowVersion: state?.latestRowVersion ?? 0,\n requiresFullReload: true,\n }\n }\n\n const result = await adapter.pullSince(collectionId, request.fromRowVersion)\n\n if (result.requiresFullReload) {\n return {\n type: `rpc:pullSince:res`,\n rpcId: request.rpcId,\n ok: true,\n latestTerm: state?.latestTerm ?? 0,\n latestSeq: state?.latestSeq ?? 0,\n latestRowVersion: result.latestRowVersion,\n requiresFullReload: true,\n }\n }\n\n return {\n type: `rpc:pullSince:res`,\n rpcId: request.rpcId,\n ok: true,\n latestTerm: state?.latestTerm ?? 0,\n latestSeq: state?.latestSeq ?? 0,\n latestRowVersion: result.latestRowVersion,\n requiresFullReload: false,\n changedKeys: result.changedKeys,\n deletedKeys: result.deletedKeys,\n }\n }\n\n // -----------------------------------------------------------------------\n // DB Writer Lock\n // -----------------------------------------------------------------------\n\n private async withWriterLock<T>(fn: () => Promise<T>): Promise<T> {\n const lockName = `tsdb:writer:${this.dbName}`\n\n for (let attempt = 0; attempt <= WRITER_LOCK_MAX_RETRIES; attempt++) {\n try {\n return await navigator.locks.request(lockName, async () => fn())\n } catch (error) {\n if (error instanceof DOMException && error.name === `AbortError`) {\n throw error\n }\n\n if (attempt < WRITER_LOCK_MAX_RETRIES) {\n await sleep(WRITER_LOCK_BUSY_RETRY_MS * Math.min(attempt + 1, 5))\n continue\n }\n\n throw error\n }\n }\n\n // Unreachable but satisfies TypeScript\n throw new Error(`writer lock acquisition failed`)\n }\n\n // -----------------------------------------------------------------------\n // Helpers\n // -----------------------------------------------------------------------\n\n private pruneAppliedEnvelopeIds(): void {\n // Keep envelopes for 60 seconds for dedup\n const cutoff = Date.now() - 60_000\n for (const [id, ts] of this.appliedEnvelopeIds) {\n if (ts < cutoff) {\n this.appliedEnvelopeIds.delete(id)\n }\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Utilities\n// ---------------------------------------------------------------------------\n\nfunction isProtocolEnvelope(data: unknown): data is ProtocolEnvelope<unknown> {\n if (!data || typeof data !== `object`) return false\n const record = data as Record<string, unknown>\n return (\n record.v === 1 &&\n typeof record.dbName === `string` &&\n typeof record.collectionId === `string` &&\n typeof record.senderId === `string` &&\n typeof record.ts === `number`\n )\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms))\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"electron-coordinator.js","sources":["../../src/electron-coordinator.ts"],"sourcesContent":["import { BroadcastCollectionCoordinator } from '@tanstack/db-sqlite-persistence-core/broadcast-coordinator'\nimport type { BroadcastCollectionCoordinatorOptions } from '@tanstack/db-sqlite-persistence-core/broadcast-coordinator'\n\nexport type ElectronCollectionCoordinatorOptions = Omit<\n BroadcastCollectionCoordinatorOptions,\n `coordinatorName`\n>\n\nexport class ElectronCollectionCoordinator extends BroadcastCollectionCoordinator {\n constructor(options: ElectronCollectionCoordinatorOptions) {\n super({\n ...options,\n coordinatorName: `ElectronCollectionCoordinator`,\n })\n }\n}\n"],"names":[],"mappings":";AAQO,MAAM,sCAAsC,+BAA+B;AAAA,EAChF,YAAY,SAA+C;AACzD,UAAM;AAAA,MACJ,GAAG;AAAA,MACH,iBAAiB;AAAA,IAAA,CAClB;AAAA,EACH;AACF;"}
@@ -4,5 +4,5 @@ export { createElectronSQLitePersistence } from './renderer.js';
4
4
  export type { ElectronIpcRendererLike, ElectronSQLitePersistenceOptions, } from './renderer.js';
5
5
  export { ElectronCollectionCoordinator } from './electron-coordinator.js';
6
6
  export type { ElectronCollectionCoordinatorOptions } from './electron-coordinator.js';
7
- export { persistedCollectionOptions } from '@tanstack/db-sqlite-persistence-core';
8
- export type { PersistedCollectionCoordinator, PersistedCollectionPersistence, } from '@tanstack/db-sqlite-persistence-core';
7
+ export { DuplicateRemoteSubsetOwnerError, IndeterminateCommitError, PersistedCollectionDurabilityError, RemoteSubsetWireValueError, persistedCollectionOptions, } from '@tanstack/db-sqlite-persistence-core';
8
+ export type { PersistedCollectionCoordinator, PersistedCollectionPersistence, IndeterminateCommitRequestType, RemoteSubsetOwner, RemoteSubsetWireExpression, RemoteSubsetWireValue, TransportedLoadSubsetOptions, } from '@tanstack/db-sqlite-persistence-core';
package/dist/esm/index.js CHANGED
@@ -1,9 +1,13 @@
1
1
  import { exposeElectronSQLitePersistence } from "./main.js";
2
2
  import { createElectronSQLitePersistence } from "./renderer.js";
3
3
  import { ElectronCollectionCoordinator } from "./electron-coordinator.js";
4
- import { persistedCollectionOptions } from "@tanstack/db-sqlite-persistence-core";
4
+ import { DuplicateRemoteSubsetOwnerError, IndeterminateCommitError, PersistedCollectionDurabilityError, RemoteSubsetWireValueError, persistedCollectionOptions } from "@tanstack/db-sqlite-persistence-core";
5
5
  export {
6
+ DuplicateRemoteSubsetOwnerError,
6
7
  ElectronCollectionCoordinator,
8
+ IndeterminateCommitError,
9
+ PersistedCollectionDurabilityError,
10
+ RemoteSubsetWireValueError,
7
11
  createElectronSQLitePersistence,
8
12
  exposeElectronSQLitePersistence,
9
13
  persistedCollectionOptions
package/dist/esm/main.js CHANGED
@@ -59,6 +59,19 @@ async function executeRequestAgainstAdapter(request, adapter) {
59
59
  result
60
60
  };
61
61
  }
62
+ case `loadResumeSnapshot`: {
63
+ const result = await adapter.loadResumeSnapshot(
64
+ request.collectionId,
65
+ request.payload.ctx
66
+ );
67
+ return {
68
+ v: ELECTRON_PERSISTENCE_PROTOCOL_VERSION,
69
+ requestId: request.requestId,
70
+ method: request.method,
71
+ ok: true,
72
+ result
73
+ };
74
+ }
62
75
  case `loadCollectionMetadata`: {
63
76
  if (!adapter.loadCollectionMetadata) {
64
77
  throw new InvalidPersistedCollectionConfigError(