@tanstack/electron-db-sqlite-persistence 0.1.12 → 0.1.13

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,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const dbSqlitePersistenceCore = require("@tanstack/db-sqlite-persistence-core");
3
4
  const HEARTBEAT_INTERVAL_MS = 3e3;
4
5
  const RPC_TIMEOUT_MS = 1e4;
5
6
  const RPC_RETRY_ATTEMPTS = 2;
@@ -8,7 +9,7 @@ const WRITER_LOCK_BUSY_RETRY_MS = 50;
8
9
  const WRITER_LOCK_MAX_RETRIES = 20;
9
10
  class ElectronCollectionCoordinator {
10
11
  constructor(options) {
11
- this.nodeId = crypto.randomUUID();
12
+ this.nodeId = dbSqlitePersistenceCore.safeRandomUUID();
12
13
  this.collections = /* @__PURE__ */ new Map();
13
14
  this.pendingRPCs = /* @__PURE__ */ new Map();
14
15
  this.appliedEnvelopeIds = /* @__PURE__ */ new Map();
@@ -68,7 +69,7 @@ class ElectronCollectionCoordinator {
68
69
  if (this.isLeader(collectionId)) return;
69
70
  const response = await this.sendRPC(collectionId, {
70
71
  type: `rpc:ensureRemoteSubset:req`,
71
- rpcId: crypto.randomUUID(),
72
+ rpcId: dbSqlitePersistenceCore.safeRandomUUID(),
72
73
  options
73
74
  });
74
75
  if (!response.ok) {
@@ -84,7 +85,7 @@ class ElectronCollectionCoordinator {
84
85
  }
85
86
  const response = await this.sendRPC(collectionId, {
86
87
  type: `rpc:ensurePersistedIndex:req`,
87
- rpcId: crypto.randomUUID(),
88
+ rpcId: dbSqlitePersistenceCore.safeRandomUUID(),
88
89
  signature,
89
90
  spec
90
91
  });
@@ -98,15 +99,15 @@ class ElectronCollectionCoordinator {
98
99
  if (this.isLeader(collectionId)) {
99
100
  return this.handleApplyLocalMutations(collectionId, {
100
101
  type: `rpc:applyLocalMutations:req`,
101
- rpcId: crypto.randomUUID(),
102
- envelopeId: crypto.randomUUID(),
102
+ rpcId: dbSqlitePersistenceCore.safeRandomUUID(),
103
+ envelopeId: dbSqlitePersistenceCore.safeRandomUUID(),
103
104
  mutations
104
105
  });
105
106
  }
106
107
  return this.sendRPC(collectionId, {
107
108
  type: `rpc:applyLocalMutations:req`,
108
- rpcId: crypto.randomUUID(),
109
- envelopeId: crypto.randomUUID(),
109
+ rpcId: dbSqlitePersistenceCore.safeRandomUUID(),
110
+ envelopeId: dbSqlitePersistenceCore.safeRandomUUID(),
110
111
  mutations
111
112
  });
112
113
  }
@@ -114,13 +115,13 @@ class ElectronCollectionCoordinator {
114
115
  if (this.isLeader(collectionId)) {
115
116
  return this.handlePullSince(collectionId, {
116
117
  type: `rpc:pullSince:req`,
117
- rpcId: crypto.randomUUID(),
118
+ rpcId: dbSqlitePersistenceCore.safeRandomUUID(),
118
119
  fromRowVersion
119
120
  });
120
121
  }
121
122
  return this.sendRPC(collectionId, {
122
123
  type: `rpc:pullSince:req`,
123
- rpcId: crypto.randomUUID(),
124
+ rpcId: dbSqlitePersistenceCore.safeRandomUUID(),
124
125
  fromRowVersion
125
126
  });
126
127
  }
@@ -410,7 +411,7 @@ class ElectronCollectionCoordinator {
410
411
  const seq = state.latestSeq;
411
412
  const rowVersion = state.latestRowVersion;
412
413
  const tx = {
413
- txId: crypto.randomUUID(),
414
+ txId: dbSqlitePersistenceCore.safeRandomUUID(),
414
415
  term,
415
416
  seq,
416
417
  rowVersion,
@@ -1 +1 @@
1
- {"version":3,"file":"electron-coordinator.cjs","sources":["../../src/electron-coordinator.ts"],"sourcesContent":["import 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 = crypto.randomUUID()\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: crypto.randomUUID(),\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: crypto.randomUUID(),\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: crypto.randomUUID(),\n envelopeId: crypto.randomUUID(),\n mutations,\n })\n }\n\n return this.sendRPC<ApplyLocalMutationsResponse>(collectionId, {\n type: `rpc:applyLocalMutations:req`,\n rpcId: crypto.randomUUID(),\n envelopeId: crypto.randomUUID(),\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: crypto.randomUUID(),\n fromRowVersion,\n })\n }\n\n return this.sendRPC<PullSinceResponse>(collectionId, {\n type: `rpc:pullSince:req`,\n rpcId: crypto.randomUUID(),\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: crypto.randomUUID(),\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.cjs","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":["safeRandomUUID"],"mappings":"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"}
@@ -1,3 +1,4 @@
1
+ import { safeRandomUUID } from "@tanstack/db-sqlite-persistence-core";
1
2
  const HEARTBEAT_INTERVAL_MS = 3e3;
2
3
  const RPC_TIMEOUT_MS = 1e4;
3
4
  const RPC_RETRY_ATTEMPTS = 2;
@@ -6,7 +7,7 @@ const WRITER_LOCK_BUSY_RETRY_MS = 50;
6
7
  const WRITER_LOCK_MAX_RETRIES = 20;
7
8
  class ElectronCollectionCoordinator {
8
9
  constructor(options) {
9
- this.nodeId = crypto.randomUUID();
10
+ this.nodeId = safeRandomUUID();
10
11
  this.collections = /* @__PURE__ */ new Map();
11
12
  this.pendingRPCs = /* @__PURE__ */ new Map();
12
13
  this.appliedEnvelopeIds = /* @__PURE__ */ new Map();
@@ -66,7 +67,7 @@ class ElectronCollectionCoordinator {
66
67
  if (this.isLeader(collectionId)) return;
67
68
  const response = await this.sendRPC(collectionId, {
68
69
  type: `rpc:ensureRemoteSubset:req`,
69
- rpcId: crypto.randomUUID(),
70
+ rpcId: safeRandomUUID(),
70
71
  options
71
72
  });
72
73
  if (!response.ok) {
@@ -82,7 +83,7 @@ class ElectronCollectionCoordinator {
82
83
  }
83
84
  const response = await this.sendRPC(collectionId, {
84
85
  type: `rpc:ensurePersistedIndex:req`,
85
- rpcId: crypto.randomUUID(),
86
+ rpcId: safeRandomUUID(),
86
87
  signature,
87
88
  spec
88
89
  });
@@ -96,15 +97,15 @@ class ElectronCollectionCoordinator {
96
97
  if (this.isLeader(collectionId)) {
97
98
  return this.handleApplyLocalMutations(collectionId, {
98
99
  type: `rpc:applyLocalMutations:req`,
99
- rpcId: crypto.randomUUID(),
100
- envelopeId: crypto.randomUUID(),
100
+ rpcId: safeRandomUUID(),
101
+ envelopeId: safeRandomUUID(),
101
102
  mutations
102
103
  });
103
104
  }
104
105
  return this.sendRPC(collectionId, {
105
106
  type: `rpc:applyLocalMutations:req`,
106
- rpcId: crypto.randomUUID(),
107
- envelopeId: crypto.randomUUID(),
107
+ rpcId: safeRandomUUID(),
108
+ envelopeId: safeRandomUUID(),
108
109
  mutations
109
110
  });
110
111
  }
@@ -112,13 +113,13 @@ class ElectronCollectionCoordinator {
112
113
  if (this.isLeader(collectionId)) {
113
114
  return this.handlePullSince(collectionId, {
114
115
  type: `rpc:pullSince:req`,
115
- rpcId: crypto.randomUUID(),
116
+ rpcId: safeRandomUUID(),
116
117
  fromRowVersion
117
118
  });
118
119
  }
119
120
  return this.sendRPC(collectionId, {
120
121
  type: `rpc:pullSince:req`,
121
- rpcId: crypto.randomUUID(),
122
+ rpcId: safeRandomUUID(),
122
123
  fromRowVersion
123
124
  });
124
125
  }
@@ -408,7 +409,7 @@ class ElectronCollectionCoordinator {
408
409
  const seq = state.latestSeq;
409
410
  const rowVersion = state.latestRowVersion;
410
411
  const tx = {
411
- txId: crypto.randomUUID(),
412
+ txId: safeRandomUUID(),
412
413
  term,
413
414
  seq,
414
415
  rowVersion,
@@ -1 +1 @@
1
- {"version":3,"file":"electron-coordinator.js","sources":["../../src/electron-coordinator.ts"],"sourcesContent":["import 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 = crypto.randomUUID()\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: crypto.randomUUID(),\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: crypto.randomUUID(),\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: crypto.randomUUID(),\n envelopeId: crypto.randomUUID(),\n mutations,\n })\n }\n\n return this.sendRPC<ApplyLocalMutationsResponse>(collectionId, {\n type: `rpc:applyLocalMutations:req`,\n rpcId: crypto.randomUUID(),\n envelopeId: crypto.randomUUID(),\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: crypto.randomUUID(),\n fromRowVersion,\n })\n }\n\n return this.sendRPC<PullSinceResponse>(collectionId, {\n type: `rpc:pullSince:req`,\n rpcId: crypto.randomUUID(),\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: crypto.randomUUID(),\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 { 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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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/electron-db-sqlite-persistence",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "description": "Electron SQLite persisted collection bridge for TanStack DB",
5
5
  "author": "TanStack Team",
6
6
  "license": "MIT",
@@ -60,7 +60,7 @@
60
60
  "src"
61
61
  ],
62
62
  "dependencies": {
63
- "@tanstack/db-sqlite-persistence-core": "0.2.0"
63
+ "@tanstack/db-sqlite-persistence-core": "0.2.1"
64
64
  },
65
65
  "peerDependencies": {
66
66
  "typescript": ">=4.7"
@@ -1,3 +1,4 @@
1
+ import { safeRandomUUID } from '@tanstack/db-sqlite-persistence-core'
1
2
  import type {
2
3
  ApplyLocalMutationsResponse,
3
4
  PersistedCollectionCoordinator,
@@ -118,7 +119,7 @@ export type ElectronCollectionCoordinatorOptions = {
118
119
  // ---------------------------------------------------------------------------
119
120
 
120
121
  export class ElectronCollectionCoordinator implements PersistedCollectionCoordinator {
121
- private readonly nodeId = crypto.randomUUID()
122
+ private readonly nodeId = safeRandomUUID()
122
123
  private readonly dbName: string
123
124
  private adapter: AdapterWithPullSince | null
124
125
  private readonly channel: BroadcastChannel
@@ -205,7 +206,7 @@ export class ElectronCollectionCoordinator implements PersistedCollectionCoordin
205
206
  error?: string
206
207
  }>(collectionId, {
207
208
  type: `rpc:ensureRemoteSubset:req`,
208
- rpcId: crypto.randomUUID(),
209
+ rpcId: safeRandomUUID(),
209
210
  options,
210
211
  })
211
212
 
@@ -233,7 +234,7 @@ export class ElectronCollectionCoordinator implements PersistedCollectionCoordin
233
234
  error?: string
234
235
  }>(collectionId, {
235
236
  type: `rpc:ensurePersistedIndex:req`,
236
- rpcId: crypto.randomUUID(),
237
+ rpcId: safeRandomUUID(),
237
238
  signature,
238
239
  spec,
239
240
  })
@@ -252,16 +253,16 @@ export class ElectronCollectionCoordinator implements PersistedCollectionCoordin
252
253
  if (this.isLeader(collectionId)) {
253
254
  return this.handleApplyLocalMutations(collectionId, {
254
255
  type: `rpc:applyLocalMutations:req`,
255
- rpcId: crypto.randomUUID(),
256
- envelopeId: crypto.randomUUID(),
256
+ rpcId: safeRandomUUID(),
257
+ envelopeId: safeRandomUUID(),
257
258
  mutations,
258
259
  })
259
260
  }
260
261
 
261
262
  return this.sendRPC<ApplyLocalMutationsResponse>(collectionId, {
262
263
  type: `rpc:applyLocalMutations:req`,
263
- rpcId: crypto.randomUUID(),
264
- envelopeId: crypto.randomUUID(),
264
+ rpcId: safeRandomUUID(),
265
+ envelopeId: safeRandomUUID(),
265
266
  mutations,
266
267
  })
267
268
  }
@@ -273,14 +274,14 @@ export class ElectronCollectionCoordinator implements PersistedCollectionCoordin
273
274
  if (this.isLeader(collectionId)) {
274
275
  return this.handlePullSince(collectionId, {
275
276
  type: `rpc:pullSince:req`,
276
- rpcId: crypto.randomUUID(),
277
+ rpcId: safeRandomUUID(),
277
278
  fromRowVersion,
278
279
  })
279
280
  }
280
281
 
281
282
  return this.sendRPC<PullSinceResponse>(collectionId, {
282
283
  type: `rpc:pullSince:req`,
283
- rpcId: crypto.randomUUID(),
284
+ rpcId: safeRandomUUID(),
284
285
  fromRowVersion,
285
286
  })
286
287
  }
@@ -663,7 +664,7 @@ export class ElectronCollectionCoordinator implements PersistedCollectionCoordin
663
664
 
664
665
  // Build and apply the persisted transaction
665
666
  const tx = {
666
- txId: crypto.randomUUID(),
667
+ txId: safeRandomUUID(),
667
668
  term,
668
669
  seq,
669
670
  rowVersion,