@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 =
|
|
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:
|
|
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:
|
|
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:
|
|
102
|
-
envelopeId:
|
|
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:
|
|
109
|
-
envelopeId:
|
|
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:
|
|
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:
|
|
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:
|
|
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 =
|
|
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:
|
|
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:
|
|
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:
|
|
100
|
-
envelopeId:
|
|
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:
|
|
107
|
-
envelopeId:
|
|
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:
|
|
116
|
+
rpcId: safeRandomUUID(),
|
|
116
117
|
fromRowVersion
|
|
117
118
|
});
|
|
118
119
|
}
|
|
119
120
|
return this.sendRPC(collectionId, {
|
|
120
121
|
type: `rpc:pullSince:req`,
|
|
121
|
-
rpcId:
|
|
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:
|
|
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.
|
|
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.
|
|
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 =
|
|
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:
|
|
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:
|
|
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:
|
|
256
|
-
envelopeId:
|
|
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:
|
|
264
|
-
envelopeId:
|
|
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:
|
|
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:
|
|
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:
|
|
667
|
+
txId: safeRandomUUID(),
|
|
667
668
|
term,
|
|
668
669
|
seq,
|
|
669
670
|
rowVersion,
|