@tanstack/browser-db-sqlite-persistence 0.1.11 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/browser-coordinator.cjs +11 -10
- package/dist/cjs/browser-coordinator.cjs.map +1 -1
- package/dist/cjs/browser-persistence.cjs +2 -2
- package/dist/cjs/browser-persistence.cjs.map +1 -1
- package/dist/cjs/index.cjs +8 -0
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +1 -1
- package/dist/esm/browser-coordinator.js +11 -10
- package/dist/esm/browser-coordinator.js.map +1 -1
- package/dist/esm/browser-persistence.js +3 -3
- package/dist/esm/browser-persistence.js.map +1 -1
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/index.js +3 -1
- package/package.json +2 -2
- package/src/browser-coordinator.ts +11 -10
- package/src/browser-persistence.ts +7 -2
- package/src/index.ts +5 -1
|
@@ -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 BrowserCollectionCoordinator {
|
|
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 BrowserCollectionCoordinator {
|
|
|
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 BrowserCollectionCoordinator {
|
|
|
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 BrowserCollectionCoordinator {
|
|
|
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 BrowserCollectionCoordinator {
|
|
|
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 BrowserCollectionCoordinator {
|
|
|
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":"browser-coordinator.cjs","sources":["../../src/browser-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 BrowserCollectionCoordinatorOptions = {\n dbName: string\n adapter?: AdapterWithPullSince\n}\n\n// ---------------------------------------------------------------------------\n// BrowserCollectionCoordinator\n// ---------------------------------------------------------------------------\n\nexport class BrowserCollectionCoordinator 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 `BrowserCollectionCoordinator: adapter not set. Call setAdapter() before using leader-side operations.`,\n )\n }\n return this.adapter\n }\n\n constructor(options: BrowserCollectionCoordinatorOptions) {\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 `createBrowserWASQLitePersistence` 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 (Workstream E)\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":"browser-coordinator.cjs","sources":["../../src/browser-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 BrowserCollectionCoordinatorOptions = {\n dbName: string\n adapter?: AdapterWithPullSince\n}\n\n// ---------------------------------------------------------------------------\n// BrowserCollectionCoordinator\n// ---------------------------------------------------------------------------\n\nexport class BrowserCollectionCoordinator 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 `BrowserCollectionCoordinator: adapter not set. Call setAdapter() before using leader-side operations.`,\n )\n }\n return this.adapter\n }\n\n constructor(options: BrowserCollectionCoordinatorOptions) {\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 `createBrowserWASQLitePersistence` 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 (Workstream E)\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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"}
|
|
@@ -26,8 +26,8 @@ function createInternalSQLiteDriver(options) {
|
|
|
26
26
|
}
|
|
27
27
|
function resolveAdapterBaseOptions(options) {
|
|
28
28
|
return {
|
|
29
|
-
appliedTxPruneMaxRows: options.appliedTxPruneMaxRows,
|
|
30
|
-
appliedTxPruneMaxAgeSeconds: options.appliedTxPruneMaxAgeSeconds,
|
|
29
|
+
appliedTxPruneMaxRows: options.appliedTxPruneMaxRows ?? dbSqlitePersistenceCore.DEFAULT_APPLIED_TX_PRUNE_MAX_ROWS,
|
|
30
|
+
appliedTxPruneMaxAgeSeconds: options.appliedTxPruneMaxAgeSeconds ?? dbSqlitePersistenceCore.DEFAULT_APPLIED_TX_PRUNE_MAX_AGE_SECONDS,
|
|
31
31
|
pullSinceReloadThreshold: options.pullSinceReloadThreshold
|
|
32
32
|
};
|
|
33
33
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"browser-persistence.cjs","sources":["../../src/browser-persistence.ts"],"sourcesContent":["import {\n SingleProcessCoordinator,\n createSQLiteCorePersistenceAdapter,\n} from '@tanstack/db-sqlite-persistence-core'\nimport { BrowserCollectionCoordinator } from './browser-coordinator'\nimport { BrowserWASQLiteDriver } from './wa-sqlite-driver'\nimport type {\n PersistedCollectionCoordinator,\n PersistedCollectionMode,\n PersistedCollectionPersistence,\n SQLiteCoreAdapterOptions,\n SQLiteDriver,\n} from '@tanstack/db-sqlite-persistence-core'\nimport type { BrowserWASQLiteDatabase } from './wa-sqlite-driver'\n\nexport type { BrowserWASQLiteDatabase } from './wa-sqlite-driver'\n\ntype BrowserSQLiteCoreSchemaMismatchPolicy =\n | `sync-present-reset`\n | `sync-absent-error`\n | `reset`\n\nexport type BrowserWASQLiteSchemaMismatchPolicy =\n | BrowserSQLiteCoreSchemaMismatchPolicy\n | `throw`\n\nexport type BrowserWASQLitePersistenceOptions = Omit<\n SQLiteCoreAdapterOptions,\n `driver` | `schemaVersion` | `schemaMismatchPolicy`\n> & {\n database: BrowserWASQLiteDatabase\n coordinator?: PersistedCollectionCoordinator\n schemaMismatchPolicy?: BrowserWASQLiteSchemaMismatchPolicy\n}\n\nfunction normalizeSchemaMismatchPolicy(\n policy: BrowserWASQLiteSchemaMismatchPolicy,\n): BrowserSQLiteCoreSchemaMismatchPolicy {\n if (policy === `throw`) {\n return `sync-absent-error`\n }\n\n return policy\n}\n\nfunction resolveSchemaMismatchPolicy(\n explicitPolicy: BrowserWASQLiteSchemaMismatchPolicy | undefined,\n mode: PersistedCollectionMode,\n): BrowserSQLiteCoreSchemaMismatchPolicy {\n if (explicitPolicy) {\n return normalizeSchemaMismatchPolicy(explicitPolicy)\n }\n\n return mode === `sync-present` ? `sync-present-reset` : `sync-absent-error`\n}\n\nfunction createAdapterCacheKey(\n schemaMismatchPolicy: BrowserSQLiteCoreSchemaMismatchPolicy,\n schemaVersion: number | undefined,\n): string {\n const schemaVersionKey =\n schemaVersion === undefined ? `schema:default` : `schema:${schemaVersion}`\n return `${schemaMismatchPolicy}|${schemaVersionKey}`\n}\n\nfunction createInternalSQLiteDriver(\n options: BrowserWASQLitePersistenceOptions,\n): SQLiteDriver {\n return new BrowserWASQLiteDriver({\n database: options.database,\n })\n}\n\nfunction resolveAdapterBaseOptions(\n options: BrowserWASQLitePersistenceOptions,\n): Omit<\n SQLiteCoreAdapterOptions,\n `driver` | `schemaVersion` | `schemaMismatchPolicy`\n> {\n return {\n appliedTxPruneMaxRows
|
|
1
|
+
{"version":3,"file":"browser-persistence.cjs","sources":["../../src/browser-persistence.ts"],"sourcesContent":["import {\n DEFAULT_APPLIED_TX_PRUNE_MAX_AGE_SECONDS,\n DEFAULT_APPLIED_TX_PRUNE_MAX_ROWS,\n SingleProcessCoordinator,\n createSQLiteCorePersistenceAdapter,\n} from '@tanstack/db-sqlite-persistence-core'\nimport { BrowserCollectionCoordinator } from './browser-coordinator'\nimport { BrowserWASQLiteDriver } from './wa-sqlite-driver'\nimport type {\n PersistedCollectionCoordinator,\n PersistedCollectionMode,\n PersistedCollectionPersistence,\n SQLiteCoreAdapterOptions,\n SQLiteDriver,\n} from '@tanstack/db-sqlite-persistence-core'\nimport type { BrowserWASQLiteDatabase } from './wa-sqlite-driver'\n\nexport type { BrowserWASQLiteDatabase } from './wa-sqlite-driver'\n\ntype BrowserSQLiteCoreSchemaMismatchPolicy =\n | `sync-present-reset`\n | `sync-absent-error`\n | `reset`\n\nexport type BrowserWASQLiteSchemaMismatchPolicy =\n | BrowserSQLiteCoreSchemaMismatchPolicy\n | `throw`\n\nexport type BrowserWASQLitePersistenceOptions = Omit<\n SQLiteCoreAdapterOptions,\n `driver` | `schemaVersion` | `schemaMismatchPolicy`\n> & {\n database: BrowserWASQLiteDatabase\n coordinator?: PersistedCollectionCoordinator\n schemaMismatchPolicy?: BrowserWASQLiteSchemaMismatchPolicy\n}\n\nfunction normalizeSchemaMismatchPolicy(\n policy: BrowserWASQLiteSchemaMismatchPolicy,\n): BrowserSQLiteCoreSchemaMismatchPolicy {\n if (policy === `throw`) {\n return `sync-absent-error`\n }\n\n return policy\n}\n\nfunction resolveSchemaMismatchPolicy(\n explicitPolicy: BrowserWASQLiteSchemaMismatchPolicy | undefined,\n mode: PersistedCollectionMode,\n): BrowserSQLiteCoreSchemaMismatchPolicy {\n if (explicitPolicy) {\n return normalizeSchemaMismatchPolicy(explicitPolicy)\n }\n\n return mode === `sync-present` ? `sync-present-reset` : `sync-absent-error`\n}\n\nfunction createAdapterCacheKey(\n schemaMismatchPolicy: BrowserSQLiteCoreSchemaMismatchPolicy,\n schemaVersion: number | undefined,\n): string {\n const schemaVersionKey =\n schemaVersion === undefined ? `schema:default` : `schema:${schemaVersion}`\n return `${schemaMismatchPolicy}|${schemaVersionKey}`\n}\n\nfunction createInternalSQLiteDriver(\n options: BrowserWASQLitePersistenceOptions,\n): SQLiteDriver {\n return new BrowserWASQLiteDriver({\n database: options.database,\n })\n}\n\nfunction resolveAdapterBaseOptions(\n options: BrowserWASQLitePersistenceOptions,\n): Omit<\n SQLiteCoreAdapterOptions,\n `driver` | `schemaVersion` | `schemaMismatchPolicy`\n> {\n return {\n appliedTxPruneMaxRows:\n options.appliedTxPruneMaxRows ?? DEFAULT_APPLIED_TX_PRUNE_MAX_ROWS,\n appliedTxPruneMaxAgeSeconds:\n options.appliedTxPruneMaxAgeSeconds ??\n DEFAULT_APPLIED_TX_PRUNE_MAX_AGE_SECONDS,\n pullSinceReloadThreshold: options.pullSinceReloadThreshold,\n }\n}\n\n/**\n * Creates a shared browser wa-sqlite persistence instance that can be reused by\n * many collections on the same database. This is single-tab wiring using\n * SingleProcessCoordinator semantics (no election required).\n */\nexport function createBrowserWASQLitePersistence(\n options: BrowserWASQLitePersistenceOptions,\n): PersistedCollectionPersistence {\n const { coordinator, schemaMismatchPolicy } = options\n const driver = createInternalSQLiteDriver(options)\n const adapterBaseOptions = resolveAdapterBaseOptions(options)\n const resolvedCoordinator = coordinator ?? new SingleProcessCoordinator()\n const adapterCache = new Map<\n string,\n ReturnType<typeof createSQLiteCorePersistenceAdapter>\n >()\n\n const getAdapterForCollection = (\n mode: PersistedCollectionMode,\n schemaVersion: number | undefined,\n ) => {\n const resolvedSchemaMismatchPolicy = resolveSchemaMismatchPolicy(\n schemaMismatchPolicy,\n mode,\n )\n const cacheKey = createAdapterCacheKey(\n resolvedSchemaMismatchPolicy,\n schemaVersion,\n )\n const cachedAdapter = adapterCache.get(cacheKey)\n if (cachedAdapter) {\n return cachedAdapter\n }\n\n const adapter = createSQLiteCorePersistenceAdapter({\n ...adapterBaseOptions,\n driver,\n schemaMismatchPolicy: resolvedSchemaMismatchPolicy,\n ...(schemaVersion === undefined ? {} : { schemaVersion }),\n })\n adapterCache.set(cacheKey, adapter)\n\n // Wire the adapter into the multi-tab coordinator so it can handle\n // leader-side RPCs (applyCommittedTx, pullSince, ensureIndex, etc.)\n if (resolvedCoordinator instanceof BrowserCollectionCoordinator) {\n resolvedCoordinator.setAdapter(adapter)\n }\n\n return adapter\n }\n\n const createCollectionPersistence = (\n mode: PersistedCollectionMode,\n schemaVersion: number | undefined,\n ): PersistedCollectionPersistence => ({\n adapter: getAdapterForCollection(mode, schemaVersion),\n coordinator: resolvedCoordinator,\n })\n\n const defaultPersistence = createCollectionPersistence(\n `sync-absent`,\n undefined,\n )\n\n return {\n ...defaultPersistence,\n resolvePersistenceForCollection: ({ mode, schemaVersion }) =>\n createCollectionPersistence(mode, schemaVersion),\n resolvePersistenceForMode: (mode) =>\n createCollectionPersistence(mode, undefined),\n }\n}\n"],"names":["BrowserWASQLiteDriver","DEFAULT_APPLIED_TX_PRUNE_MAX_ROWS","DEFAULT_APPLIED_TX_PRUNE_MAX_AGE_SECONDS","SingleProcessCoordinator","createSQLiteCorePersistenceAdapter","BrowserCollectionCoordinator"],"mappings":";;;;;AAqCA,SAAS,8BACP,QACuC;AACvC,MAAI,WAAW,SAAS;AACtB,WAAO;AAAA,EACT;AAEA,SAAO;AACT;AAEA,SAAS,4BACP,gBACA,MACuC;AACvC,MAAI,gBAAgB;AAClB,WAAO,8BAA8B,cAAc;AAAA,EACrD;AAEA,SAAO,SAAS,iBAAiB,uBAAuB;AAC1D;AAEA,SAAS,sBACP,sBACA,eACQ;AACR,QAAM,mBACJ,kBAAkB,SAAY,mBAAmB,UAAU,aAAa;AAC1E,SAAO,GAAG,oBAAoB,IAAI,gBAAgB;AACpD;AAEA,SAAS,2BACP,SACc;AACd,SAAO,IAAIA,eAAAA,sBAAsB;AAAA,IAC/B,UAAU,QAAQ;AAAA,EAAA,CACnB;AACH;AAEA,SAAS,0BACP,SAIA;AACA,SAAO;AAAA,IACL,uBACE,QAAQ,yBAAyBC,wBAAAA;AAAAA,IACnC,6BACE,QAAQ,+BACRC,wBAAAA;AAAAA,IACF,0BAA0B,QAAQ;AAAA,EAAA;AAEtC;AAOO,SAAS,iCACd,SACgC;AAChC,QAAM,EAAE,aAAa,qBAAA,IAAyB;AAC9C,QAAM,SAAS,2BAA2B,OAAO;AACjD,QAAM,qBAAqB,0BAA0B,OAAO;AAC5D,QAAM,sBAAsB,eAAe,IAAIC,iDAAA;AAC/C,QAAM,mCAAmB,IAAA;AAKzB,QAAM,0BAA0B,CAC9B,MACA,kBACG;AACH,UAAM,+BAA+B;AAAA,MACnC;AAAA,MACA;AAAA,IAAA;AAEF,UAAM,WAAW;AAAA,MACf;AAAA,MACA;AAAA,IAAA;AAEF,UAAM,gBAAgB,aAAa,IAAI,QAAQ;AAC/C,QAAI,eAAe;AACjB,aAAO;AAAA,IACT;AAEA,UAAM,UAAUC,wBAAAA,mCAAmC;AAAA,MACjD,GAAG;AAAA,MACH;AAAA,MACA,sBAAsB;AAAA,MACtB,GAAI,kBAAkB,SAAY,CAAA,IAAK,EAAE,cAAA;AAAA,IAAc,CACxD;AACD,iBAAa,IAAI,UAAU,OAAO;AAIlC,QAAI,+BAA+BC,mBAAAA,8BAA8B;AAC/D,0BAAoB,WAAW,OAAO;AAAA,IACxC;AAEA,WAAO;AAAA,EACT;AAEA,QAAM,8BAA8B,CAClC,MACA,mBACoC;AAAA,IACpC,SAAS,wBAAwB,MAAM,aAAa;AAAA,IACpD,aAAa;AAAA,EAAA;AAGf,QAAM,qBAAqB;AAAA,IACzB;AAAA,IACA;AAAA,EAAA;AAGF,SAAO;AAAA,IACL,GAAG;AAAA,IACH,iCAAiC,CAAC,EAAE,MAAM,oBACxC,4BAA4B,MAAM,aAAa;AAAA,IACjD,2BAA2B,CAAC,SAC1B,4BAA4B,MAAM,MAAS;AAAA,EAAA;AAEjD;;"}
|
package/dist/cjs/index.cjs
CHANGED
|
@@ -7,6 +7,14 @@ const dbSqlitePersistenceCore = require("@tanstack/db-sqlite-persistence-core");
|
|
|
7
7
|
exports.createBrowserWASQLitePersistence = browserPersistence.createBrowserWASQLitePersistence;
|
|
8
8
|
exports.openBrowserWASQLiteOPFSDatabase = opfsDatabase.openBrowserWASQLiteOPFSDatabase;
|
|
9
9
|
exports.BrowserCollectionCoordinator = browserCoordinator.BrowserCollectionCoordinator;
|
|
10
|
+
Object.defineProperty(exports, "DEFAULT_APPLIED_TX_PRUNE_MAX_AGE_SECONDS", {
|
|
11
|
+
enumerable: true,
|
|
12
|
+
get: () => dbSqlitePersistenceCore.DEFAULT_APPLIED_TX_PRUNE_MAX_AGE_SECONDS
|
|
13
|
+
});
|
|
14
|
+
Object.defineProperty(exports, "DEFAULT_APPLIED_TX_PRUNE_MAX_ROWS", {
|
|
15
|
+
enumerable: true,
|
|
16
|
+
get: () => dbSqlitePersistenceCore.DEFAULT_APPLIED_TX_PRUNE_MAX_ROWS
|
|
17
|
+
});
|
|
10
18
|
Object.defineProperty(exports, "persistedCollectionOptions", {
|
|
11
19
|
enumerable: true,
|
|
12
20
|
get: () => dbSqlitePersistenceCore.persistedCollectionOptions
|
package/dist/cjs/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;"}
|
package/dist/cjs/index.d.cts
CHANGED
|
@@ -4,5 +4,5 @@ export { BrowserCollectionCoordinator } from './browser-coordinator.cjs';
|
|
|
4
4
|
export type { BrowserWASQLiteDatabase, BrowserWASQLitePersistenceOptions, BrowserWASQLiteSchemaMismatchPolicy, } from './browser-persistence.cjs';
|
|
5
5
|
export type { OpenBrowserWASQLiteOPFSDatabaseOptions } from './opfs-database.cjs';
|
|
6
6
|
export type { BrowserCollectionCoordinatorOptions } from './browser-coordinator.cjs';
|
|
7
|
-
export { persistedCollectionOptions } from '@tanstack/db-sqlite-persistence-core';
|
|
7
|
+
export { DEFAULT_APPLIED_TX_PRUNE_MAX_AGE_SECONDS, DEFAULT_APPLIED_TX_PRUNE_MAX_ROWS, persistedCollectionOptions, } from '@tanstack/db-sqlite-persistence-core';
|
|
8
8
|
export type { PersistedCollectionCoordinator, PersistedCollectionPersistence, } from '@tanstack/db-sqlite-persistence-core';
|
|
@@ -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 BrowserCollectionCoordinator {
|
|
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 BrowserCollectionCoordinator {
|
|
|
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 BrowserCollectionCoordinator {
|
|
|
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 BrowserCollectionCoordinator {
|
|
|
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 BrowserCollectionCoordinator {
|
|
|
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 BrowserCollectionCoordinator {
|
|
|
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":"browser-coordinator.js","sources":["../../src/browser-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 BrowserCollectionCoordinatorOptions = {\n dbName: string\n adapter?: AdapterWithPullSince\n}\n\n// ---------------------------------------------------------------------------\n// BrowserCollectionCoordinator\n// ---------------------------------------------------------------------------\n\nexport class BrowserCollectionCoordinator 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 `BrowserCollectionCoordinator: adapter not set. Call setAdapter() before using leader-side operations.`,\n )\n }\n return this.adapter\n }\n\n constructor(options: BrowserCollectionCoordinatorOptions) {\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 `createBrowserWASQLitePersistence` 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 (Workstream E)\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":"browser-coordinator.js","sources":["../../src/browser-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 BrowserCollectionCoordinatorOptions = {\n dbName: string\n adapter?: AdapterWithPullSince\n}\n\n// ---------------------------------------------------------------------------\n// BrowserCollectionCoordinator\n// ---------------------------------------------------------------------------\n\nexport class BrowserCollectionCoordinator 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 `BrowserCollectionCoordinator: adapter not set. Call setAdapter() before using leader-side operations.`,\n )\n }\n return this.adapter\n }\n\n constructor(options: BrowserCollectionCoordinatorOptions) {\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 `createBrowserWASQLitePersistence` 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 (Workstream E)\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,4 +1,4 @@
|
|
|
1
|
-
import { SingleProcessCoordinator, createSQLiteCorePersistenceAdapter } from "@tanstack/db-sqlite-persistence-core";
|
|
1
|
+
import { SingleProcessCoordinator, DEFAULT_APPLIED_TX_PRUNE_MAX_AGE_SECONDS, DEFAULT_APPLIED_TX_PRUNE_MAX_ROWS, createSQLiteCorePersistenceAdapter } from "@tanstack/db-sqlite-persistence-core";
|
|
2
2
|
import { BrowserCollectionCoordinator } from "./browser-coordinator.js";
|
|
3
3
|
import { BrowserWASQLiteDriver } from "./wa-sqlite-driver.js";
|
|
4
4
|
function normalizeSchemaMismatchPolicy(policy) {
|
|
@@ -24,8 +24,8 @@ function createInternalSQLiteDriver(options) {
|
|
|
24
24
|
}
|
|
25
25
|
function resolveAdapterBaseOptions(options) {
|
|
26
26
|
return {
|
|
27
|
-
appliedTxPruneMaxRows: options.appliedTxPruneMaxRows,
|
|
28
|
-
appliedTxPruneMaxAgeSeconds: options.appliedTxPruneMaxAgeSeconds,
|
|
27
|
+
appliedTxPruneMaxRows: options.appliedTxPruneMaxRows ?? DEFAULT_APPLIED_TX_PRUNE_MAX_ROWS,
|
|
28
|
+
appliedTxPruneMaxAgeSeconds: options.appliedTxPruneMaxAgeSeconds ?? DEFAULT_APPLIED_TX_PRUNE_MAX_AGE_SECONDS,
|
|
29
29
|
pullSinceReloadThreshold: options.pullSinceReloadThreshold
|
|
30
30
|
};
|
|
31
31
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"browser-persistence.js","sources":["../../src/browser-persistence.ts"],"sourcesContent":["import {\n SingleProcessCoordinator,\n createSQLiteCorePersistenceAdapter,\n} from '@tanstack/db-sqlite-persistence-core'\nimport { BrowserCollectionCoordinator } from './browser-coordinator'\nimport { BrowserWASQLiteDriver } from './wa-sqlite-driver'\nimport type {\n PersistedCollectionCoordinator,\n PersistedCollectionMode,\n PersistedCollectionPersistence,\n SQLiteCoreAdapterOptions,\n SQLiteDriver,\n} from '@tanstack/db-sqlite-persistence-core'\nimport type { BrowserWASQLiteDatabase } from './wa-sqlite-driver'\n\nexport type { BrowserWASQLiteDatabase } from './wa-sqlite-driver'\n\ntype BrowserSQLiteCoreSchemaMismatchPolicy =\n | `sync-present-reset`\n | `sync-absent-error`\n | `reset`\n\nexport type BrowserWASQLiteSchemaMismatchPolicy =\n | BrowserSQLiteCoreSchemaMismatchPolicy\n | `throw`\n\nexport type BrowserWASQLitePersistenceOptions = Omit<\n SQLiteCoreAdapterOptions,\n `driver` | `schemaVersion` | `schemaMismatchPolicy`\n> & {\n database: BrowserWASQLiteDatabase\n coordinator?: PersistedCollectionCoordinator\n schemaMismatchPolicy?: BrowserWASQLiteSchemaMismatchPolicy\n}\n\nfunction normalizeSchemaMismatchPolicy(\n policy: BrowserWASQLiteSchemaMismatchPolicy,\n): BrowserSQLiteCoreSchemaMismatchPolicy {\n if (policy === `throw`) {\n return `sync-absent-error`\n }\n\n return policy\n}\n\nfunction resolveSchemaMismatchPolicy(\n explicitPolicy: BrowserWASQLiteSchemaMismatchPolicy | undefined,\n mode: PersistedCollectionMode,\n): BrowserSQLiteCoreSchemaMismatchPolicy {\n if (explicitPolicy) {\n return normalizeSchemaMismatchPolicy(explicitPolicy)\n }\n\n return mode === `sync-present` ? `sync-present-reset` : `sync-absent-error`\n}\n\nfunction createAdapterCacheKey(\n schemaMismatchPolicy: BrowserSQLiteCoreSchemaMismatchPolicy,\n schemaVersion: number | undefined,\n): string {\n const schemaVersionKey =\n schemaVersion === undefined ? `schema:default` : `schema:${schemaVersion}`\n return `${schemaMismatchPolicy}|${schemaVersionKey}`\n}\n\nfunction createInternalSQLiteDriver(\n options: BrowserWASQLitePersistenceOptions,\n): SQLiteDriver {\n return new BrowserWASQLiteDriver({\n database: options.database,\n })\n}\n\nfunction resolveAdapterBaseOptions(\n options: BrowserWASQLitePersistenceOptions,\n): Omit<\n SQLiteCoreAdapterOptions,\n `driver` | `schemaVersion` | `schemaMismatchPolicy`\n> {\n return {\n appliedTxPruneMaxRows
|
|
1
|
+
{"version":3,"file":"browser-persistence.js","sources":["../../src/browser-persistence.ts"],"sourcesContent":["import {\n DEFAULT_APPLIED_TX_PRUNE_MAX_AGE_SECONDS,\n DEFAULT_APPLIED_TX_PRUNE_MAX_ROWS,\n SingleProcessCoordinator,\n createSQLiteCorePersistenceAdapter,\n} from '@tanstack/db-sqlite-persistence-core'\nimport { BrowserCollectionCoordinator } from './browser-coordinator'\nimport { BrowserWASQLiteDriver } from './wa-sqlite-driver'\nimport type {\n PersistedCollectionCoordinator,\n PersistedCollectionMode,\n PersistedCollectionPersistence,\n SQLiteCoreAdapterOptions,\n SQLiteDriver,\n} from '@tanstack/db-sqlite-persistence-core'\nimport type { BrowserWASQLiteDatabase } from './wa-sqlite-driver'\n\nexport type { BrowserWASQLiteDatabase } from './wa-sqlite-driver'\n\ntype BrowserSQLiteCoreSchemaMismatchPolicy =\n | `sync-present-reset`\n | `sync-absent-error`\n | `reset`\n\nexport type BrowserWASQLiteSchemaMismatchPolicy =\n | BrowserSQLiteCoreSchemaMismatchPolicy\n | `throw`\n\nexport type BrowserWASQLitePersistenceOptions = Omit<\n SQLiteCoreAdapterOptions,\n `driver` | `schemaVersion` | `schemaMismatchPolicy`\n> & {\n database: BrowserWASQLiteDatabase\n coordinator?: PersistedCollectionCoordinator\n schemaMismatchPolicy?: BrowserWASQLiteSchemaMismatchPolicy\n}\n\nfunction normalizeSchemaMismatchPolicy(\n policy: BrowserWASQLiteSchemaMismatchPolicy,\n): BrowserSQLiteCoreSchemaMismatchPolicy {\n if (policy === `throw`) {\n return `sync-absent-error`\n }\n\n return policy\n}\n\nfunction resolveSchemaMismatchPolicy(\n explicitPolicy: BrowserWASQLiteSchemaMismatchPolicy | undefined,\n mode: PersistedCollectionMode,\n): BrowserSQLiteCoreSchemaMismatchPolicy {\n if (explicitPolicy) {\n return normalizeSchemaMismatchPolicy(explicitPolicy)\n }\n\n return mode === `sync-present` ? `sync-present-reset` : `sync-absent-error`\n}\n\nfunction createAdapterCacheKey(\n schemaMismatchPolicy: BrowserSQLiteCoreSchemaMismatchPolicy,\n schemaVersion: number | undefined,\n): string {\n const schemaVersionKey =\n schemaVersion === undefined ? `schema:default` : `schema:${schemaVersion}`\n return `${schemaMismatchPolicy}|${schemaVersionKey}`\n}\n\nfunction createInternalSQLiteDriver(\n options: BrowserWASQLitePersistenceOptions,\n): SQLiteDriver {\n return new BrowserWASQLiteDriver({\n database: options.database,\n })\n}\n\nfunction resolveAdapterBaseOptions(\n options: BrowserWASQLitePersistenceOptions,\n): Omit<\n SQLiteCoreAdapterOptions,\n `driver` | `schemaVersion` | `schemaMismatchPolicy`\n> {\n return {\n appliedTxPruneMaxRows:\n options.appliedTxPruneMaxRows ?? DEFAULT_APPLIED_TX_PRUNE_MAX_ROWS,\n appliedTxPruneMaxAgeSeconds:\n options.appliedTxPruneMaxAgeSeconds ??\n DEFAULT_APPLIED_TX_PRUNE_MAX_AGE_SECONDS,\n pullSinceReloadThreshold: options.pullSinceReloadThreshold,\n }\n}\n\n/**\n * Creates a shared browser wa-sqlite persistence instance that can be reused by\n * many collections on the same database. This is single-tab wiring using\n * SingleProcessCoordinator semantics (no election required).\n */\nexport function createBrowserWASQLitePersistence(\n options: BrowserWASQLitePersistenceOptions,\n): PersistedCollectionPersistence {\n const { coordinator, schemaMismatchPolicy } = options\n const driver = createInternalSQLiteDriver(options)\n const adapterBaseOptions = resolveAdapterBaseOptions(options)\n const resolvedCoordinator = coordinator ?? new SingleProcessCoordinator()\n const adapterCache = new Map<\n string,\n ReturnType<typeof createSQLiteCorePersistenceAdapter>\n >()\n\n const getAdapterForCollection = (\n mode: PersistedCollectionMode,\n schemaVersion: number | undefined,\n ) => {\n const resolvedSchemaMismatchPolicy = resolveSchemaMismatchPolicy(\n schemaMismatchPolicy,\n mode,\n )\n const cacheKey = createAdapterCacheKey(\n resolvedSchemaMismatchPolicy,\n schemaVersion,\n )\n const cachedAdapter = adapterCache.get(cacheKey)\n if (cachedAdapter) {\n return cachedAdapter\n }\n\n const adapter = createSQLiteCorePersistenceAdapter({\n ...adapterBaseOptions,\n driver,\n schemaMismatchPolicy: resolvedSchemaMismatchPolicy,\n ...(schemaVersion === undefined ? {} : { schemaVersion }),\n })\n adapterCache.set(cacheKey, adapter)\n\n // Wire the adapter into the multi-tab coordinator so it can handle\n // leader-side RPCs (applyCommittedTx, pullSince, ensureIndex, etc.)\n if (resolvedCoordinator instanceof BrowserCollectionCoordinator) {\n resolvedCoordinator.setAdapter(adapter)\n }\n\n return adapter\n }\n\n const createCollectionPersistence = (\n mode: PersistedCollectionMode,\n schemaVersion: number | undefined,\n ): PersistedCollectionPersistence => ({\n adapter: getAdapterForCollection(mode, schemaVersion),\n coordinator: resolvedCoordinator,\n })\n\n const defaultPersistence = createCollectionPersistence(\n `sync-absent`,\n undefined,\n )\n\n return {\n ...defaultPersistence,\n resolvePersistenceForCollection: ({ mode, schemaVersion }) =>\n createCollectionPersistence(mode, schemaVersion),\n resolvePersistenceForMode: (mode) =>\n createCollectionPersistence(mode, undefined),\n }\n}\n"],"names":[],"mappings":";;;AAqCA,SAAS,8BACP,QACuC;AACvC,MAAI,WAAW,SAAS;AACtB,WAAO;AAAA,EACT;AAEA,SAAO;AACT;AAEA,SAAS,4BACP,gBACA,MACuC;AACvC,MAAI,gBAAgB;AAClB,WAAO,8BAA8B,cAAc;AAAA,EACrD;AAEA,SAAO,SAAS,iBAAiB,uBAAuB;AAC1D;AAEA,SAAS,sBACP,sBACA,eACQ;AACR,QAAM,mBACJ,kBAAkB,SAAY,mBAAmB,UAAU,aAAa;AAC1E,SAAO,GAAG,oBAAoB,IAAI,gBAAgB;AACpD;AAEA,SAAS,2BACP,SACc;AACd,SAAO,IAAI,sBAAsB;AAAA,IAC/B,UAAU,QAAQ;AAAA,EAAA,CACnB;AACH;AAEA,SAAS,0BACP,SAIA;AACA,SAAO;AAAA,IACL,uBACE,QAAQ,yBAAyB;AAAA,IACnC,6BACE,QAAQ,+BACR;AAAA,IACF,0BAA0B,QAAQ;AAAA,EAAA;AAEtC;AAOO,SAAS,iCACd,SACgC;AAChC,QAAM,EAAE,aAAa,qBAAA,IAAyB;AAC9C,QAAM,SAAS,2BAA2B,OAAO;AACjD,QAAM,qBAAqB,0BAA0B,OAAO;AAC5D,QAAM,sBAAsB,eAAe,IAAI,yBAAA;AAC/C,QAAM,mCAAmB,IAAA;AAKzB,QAAM,0BAA0B,CAC9B,MACA,kBACG;AACH,UAAM,+BAA+B;AAAA,MACnC;AAAA,MACA;AAAA,IAAA;AAEF,UAAM,WAAW;AAAA,MACf;AAAA,MACA;AAAA,IAAA;AAEF,UAAM,gBAAgB,aAAa,IAAI,QAAQ;AAC/C,QAAI,eAAe;AACjB,aAAO;AAAA,IACT;AAEA,UAAM,UAAU,mCAAmC;AAAA,MACjD,GAAG;AAAA,MACH;AAAA,MACA,sBAAsB;AAAA,MACtB,GAAI,kBAAkB,SAAY,CAAA,IAAK,EAAE,cAAA;AAAA,IAAc,CACxD;AACD,iBAAa,IAAI,UAAU,OAAO;AAIlC,QAAI,+BAA+B,8BAA8B;AAC/D,0BAAoB,WAAW,OAAO;AAAA,IACxC;AAEA,WAAO;AAAA,EACT;AAEA,QAAM,8BAA8B,CAClC,MACA,mBACoC;AAAA,IACpC,SAAS,wBAAwB,MAAM,aAAa;AAAA,IACpD,aAAa;AAAA,EAAA;AAGf,QAAM,qBAAqB;AAAA,IACzB;AAAA,IACA;AAAA,EAAA;AAGF,SAAO;AAAA,IACL,GAAG;AAAA,IACH,iCAAiC,CAAC,EAAE,MAAM,oBACxC,4BAA4B,MAAM,aAAa;AAAA,IACjD,2BAA2B,CAAC,SAC1B,4BAA4B,MAAM,MAAS;AAAA,EAAA;AAEjD;"}
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -4,5 +4,5 @@ export { BrowserCollectionCoordinator } from './browser-coordinator.js';
|
|
|
4
4
|
export type { BrowserWASQLiteDatabase, BrowserWASQLitePersistenceOptions, BrowserWASQLiteSchemaMismatchPolicy, } from './browser-persistence.js';
|
|
5
5
|
export type { OpenBrowserWASQLiteOPFSDatabaseOptions } from './opfs-database.js';
|
|
6
6
|
export type { BrowserCollectionCoordinatorOptions } from './browser-coordinator.js';
|
|
7
|
-
export { persistedCollectionOptions } from '@tanstack/db-sqlite-persistence-core';
|
|
7
|
+
export { DEFAULT_APPLIED_TX_PRUNE_MAX_AGE_SECONDS, DEFAULT_APPLIED_TX_PRUNE_MAX_ROWS, persistedCollectionOptions, } from '@tanstack/db-sqlite-persistence-core';
|
|
8
8
|
export type { PersistedCollectionCoordinator, PersistedCollectionPersistence, } from '@tanstack/db-sqlite-persistence-core';
|
package/dist/esm/index.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { createBrowserWASQLitePersistence } from "./browser-persistence.js";
|
|
2
2
|
import { openBrowserWASQLiteOPFSDatabase } from "./opfs-database.js";
|
|
3
3
|
import { BrowserCollectionCoordinator } from "./browser-coordinator.js";
|
|
4
|
-
import { persistedCollectionOptions } from "@tanstack/db-sqlite-persistence-core";
|
|
4
|
+
import { DEFAULT_APPLIED_TX_PRUNE_MAX_AGE_SECONDS, DEFAULT_APPLIED_TX_PRUNE_MAX_ROWS, persistedCollectionOptions } from "@tanstack/db-sqlite-persistence-core";
|
|
5
5
|
export {
|
|
6
6
|
BrowserCollectionCoordinator,
|
|
7
|
+
DEFAULT_APPLIED_TX_PRUNE_MAX_AGE_SECONDS,
|
|
8
|
+
DEFAULT_APPLIED_TX_PRUNE_MAX_ROWS,
|
|
7
9
|
createBrowserWASQLitePersistence,
|
|
8
10
|
openBrowserWASQLiteOPFSDatabase,
|
|
9
11
|
persistedCollectionOptions
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/browser-db-sqlite-persistence",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Browser wa-sqlite persisted collection adapter for TanStack DB",
|
|
5
5
|
"author": "TanStack Team",
|
|
6
6
|
"license": "MIT",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"src"
|
|
42
42
|
],
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@tanstack/db-sqlite-persistence-core": "0.1
|
|
44
|
+
"@tanstack/db-sqlite-persistence-core": "0.2.1"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
47
|
"@journeyapps/wa-sqlite": "^1.4.1",
|
|
@@ -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 BrowserCollectionCoordinatorOptions = {
|
|
|
118
119
|
// ---------------------------------------------------------------------------
|
|
119
120
|
|
|
120
121
|
export class BrowserCollectionCoordinator 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 BrowserCollectionCoordinator implements PersistedCollectionCoordina
|
|
|
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 BrowserCollectionCoordinator implements PersistedCollectionCoordina
|
|
|
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 BrowserCollectionCoordinator implements PersistedCollectionCoordina
|
|
|
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 BrowserCollectionCoordinator implements PersistedCollectionCoordina
|
|
|
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 BrowserCollectionCoordinator implements PersistedCollectionCoordina
|
|
|
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,
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
|
+
DEFAULT_APPLIED_TX_PRUNE_MAX_AGE_SECONDS,
|
|
3
|
+
DEFAULT_APPLIED_TX_PRUNE_MAX_ROWS,
|
|
2
4
|
SingleProcessCoordinator,
|
|
3
5
|
createSQLiteCorePersistenceAdapter,
|
|
4
6
|
} from '@tanstack/db-sqlite-persistence-core'
|
|
@@ -78,8 +80,11 @@ function resolveAdapterBaseOptions(
|
|
|
78
80
|
`driver` | `schemaVersion` | `schemaMismatchPolicy`
|
|
79
81
|
> {
|
|
80
82
|
return {
|
|
81
|
-
appliedTxPruneMaxRows:
|
|
82
|
-
|
|
83
|
+
appliedTxPruneMaxRows:
|
|
84
|
+
options.appliedTxPruneMaxRows ?? DEFAULT_APPLIED_TX_PRUNE_MAX_ROWS,
|
|
85
|
+
appliedTxPruneMaxAgeSeconds:
|
|
86
|
+
options.appliedTxPruneMaxAgeSeconds ??
|
|
87
|
+
DEFAULT_APPLIED_TX_PRUNE_MAX_AGE_SECONDS,
|
|
83
88
|
pullSinceReloadThreshold: options.pullSinceReloadThreshold,
|
|
84
89
|
}
|
|
85
90
|
}
|
package/src/index.ts
CHANGED
|
@@ -8,7 +8,11 @@ export type {
|
|
|
8
8
|
} from './browser-persistence'
|
|
9
9
|
export type { OpenBrowserWASQLiteOPFSDatabaseOptions } from './opfs-database'
|
|
10
10
|
export type { BrowserCollectionCoordinatorOptions } from './browser-coordinator'
|
|
11
|
-
export {
|
|
11
|
+
export {
|
|
12
|
+
DEFAULT_APPLIED_TX_PRUNE_MAX_AGE_SECONDS,
|
|
13
|
+
DEFAULT_APPLIED_TX_PRUNE_MAX_ROWS,
|
|
14
|
+
persistedCollectionOptions,
|
|
15
|
+
} from '@tanstack/db-sqlite-persistence-core'
|
|
12
16
|
export type {
|
|
13
17
|
PersistedCollectionCoordinator,
|
|
14
18
|
PersistedCollectionPersistence,
|