@tanstack/electron-db-sqlite-persistence 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +71 -0
  2. package/dist/cjs/electron-coordinator.cjs +541 -0
  3. package/dist/cjs/electron-coordinator.cjs.map +1 -0
  4. package/dist/cjs/electron-coordinator.d.cts +67 -0
  5. package/dist/cjs/errors.d.cts +33 -0
  6. package/dist/cjs/index.cjs +14 -0
  7. package/dist/cjs/index.cjs.map +1 -0
  8. package/dist/cjs/index.d.cts +8 -0
  9. package/dist/cjs/main.cjs +213 -0
  10. package/dist/cjs/main.cjs.map +1 -0
  11. package/dist/cjs/main.d.cts +12 -0
  12. package/dist/cjs/protocol.cjs +7 -0
  13. package/dist/cjs/protocol.cjs.map +1 -0
  14. package/dist/cjs/protocol.d.cts +104 -0
  15. package/dist/cjs/renderer.cjs +221 -0
  16. package/dist/cjs/renderer.cjs.map +1 -0
  17. package/dist/cjs/renderer.d.cts +13 -0
  18. package/dist/esm/electron-coordinator.d.ts +67 -0
  19. package/dist/esm/electron-coordinator.js +541 -0
  20. package/dist/esm/electron-coordinator.js.map +1 -0
  21. package/dist/esm/errors.d.ts +33 -0
  22. package/dist/esm/index.d.ts +8 -0
  23. package/dist/esm/index.js +11 -0
  24. package/dist/esm/index.js.map +1 -0
  25. package/dist/esm/main.d.ts +12 -0
  26. package/dist/esm/main.js +213 -0
  27. package/dist/esm/main.js.map +1 -0
  28. package/dist/esm/protocol.d.ts +104 -0
  29. package/dist/esm/protocol.js +7 -0
  30. package/dist/esm/protocol.js.map +1 -0
  31. package/dist/esm/renderer.d.ts +13 -0
  32. package/dist/esm/renderer.js +221 -0
  33. package/dist/esm/renderer.js.map +1 -0
  34. package/package.json +80 -0
  35. package/src/electron-coordinator.ts +842 -0
  36. package/src/errors.ts +110 -0
  37. package/src/index.ts +17 -0
  38. package/src/main.ts +315 -0
  39. package/src/protocol.ts +137 -0
  40. package/src/renderer.ts +391 -0
@@ -0,0 +1,391 @@
1
+ import {
2
+ InvalidPersistedCollectionConfigError,
3
+ SingleProcessCoordinator,
4
+ } from '@tanstack/db-sqlite-persistence-core'
5
+ import { ElectronCollectionCoordinator } from './electron-coordinator'
6
+ import {
7
+ DEFAULT_ELECTRON_PERSISTENCE_CHANNEL,
8
+ ELECTRON_PERSISTENCE_PROTOCOL_VERSION,
9
+ } from './protocol'
10
+ import type {
11
+ PersistedCollectionCoordinator,
12
+ PersistedCollectionMode,
13
+ PersistedCollectionPersistence,
14
+ PersistedIndexSpec,
15
+ PersistedTx,
16
+ SQLitePullSinceResult,
17
+ } from '@tanstack/db-sqlite-persistence-core'
18
+ import type {
19
+ ElectronPersistedKey,
20
+ ElectronPersistedRow,
21
+ ElectronPersistenceInvoke,
22
+ ElectronPersistenceMethod,
23
+ ElectronPersistencePayloadMap,
24
+ ElectronPersistenceRequest,
25
+ ElectronPersistenceRequestEnvelope,
26
+ ElectronPersistenceResolution,
27
+ ElectronPersistenceResponseEnvelope,
28
+ ElectronPersistenceResultMap,
29
+ } from './protocol'
30
+ import type { LoadSubsetOptions } from '@tanstack/db'
31
+
32
+ const DEFAULT_REQUEST_TIMEOUT_MS = 5_000
33
+ let nextRequestId = 1
34
+
35
+ function createRequestId(): string {
36
+ const requestId = nextRequestId
37
+ nextRequestId++
38
+ return `electron-persistence-${requestId}`
39
+ }
40
+
41
+ function withTimeout<T>(
42
+ promise: Promise<T>,
43
+ timeoutMs: number,
44
+ timeoutMessage: string,
45
+ ): Promise<T> {
46
+ if (timeoutMs <= 0) {
47
+ return promise
48
+ }
49
+
50
+ return new Promise<T>((resolve, reject) => {
51
+ const timer = setTimeout(() => {
52
+ reject(new InvalidPersistedCollectionConfigError(timeoutMessage))
53
+ }, timeoutMs)
54
+
55
+ promise.then(
56
+ (value) => {
57
+ clearTimeout(timer)
58
+ resolve(value)
59
+ },
60
+ (error: unknown) => {
61
+ clearTimeout(timer)
62
+ reject(error)
63
+ },
64
+ )
65
+ })
66
+ }
67
+
68
+ function assertValidResponse(
69
+ response: ElectronPersistenceResponseEnvelope,
70
+ request: ElectronPersistenceRequestEnvelope,
71
+ ): void {
72
+ if (response.v !== ELECTRON_PERSISTENCE_PROTOCOL_VERSION) {
73
+ throw new InvalidPersistedCollectionConfigError(
74
+ `Unexpected electron persistence protocol version "${response.v}" in response`,
75
+ )
76
+ }
77
+
78
+ if (response.requestId !== request.requestId) {
79
+ throw new InvalidPersistedCollectionConfigError(
80
+ `Mismatched electron persistence response request id. Expected "${request.requestId}", received "${response.requestId}"`,
81
+ )
82
+ }
83
+
84
+ if (response.method !== request.method) {
85
+ throw new InvalidPersistedCollectionConfigError(
86
+ `Mismatched electron persistence response method. Expected "${request.method}", received "${response.method}"`,
87
+ )
88
+ }
89
+ }
90
+
91
+ function createSerializableLoadSubsetOptions(
92
+ subsetOptions: LoadSubsetOptions,
93
+ ): LoadSubsetOptions {
94
+ const { subscription: _subscription, ...serializableOptions } = subsetOptions
95
+ return serializableOptions
96
+ }
97
+
98
+ type RendererRequestExecutor = <TMethod extends ElectronPersistenceMethod>(
99
+ method: TMethod,
100
+ collectionId: string,
101
+ payload: ElectronPersistencePayloadMap[TMethod],
102
+ resolution?: ElectronPersistenceResolution,
103
+ ) => Promise<ElectronPersistenceResultMap[TMethod]>
104
+
105
+ function createRendererRequestExecutor(options: {
106
+ invoke: ElectronPersistenceInvoke
107
+ channel?: string
108
+ timeoutMs?: number
109
+ }): RendererRequestExecutor {
110
+ const channel = options.channel ?? DEFAULT_ELECTRON_PERSISTENCE_CHANNEL
111
+ const timeoutMs = options.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS
112
+
113
+ return async <TMethod extends ElectronPersistenceMethod>(
114
+ method: TMethod,
115
+ collectionId: string,
116
+ payload: ElectronPersistencePayloadMap[TMethod],
117
+ resolution?: ElectronPersistenceResolution,
118
+ ) => {
119
+ const request = {
120
+ v: ELECTRON_PERSISTENCE_PROTOCOL_VERSION,
121
+ requestId: createRequestId(),
122
+ collectionId,
123
+ method,
124
+ resolution,
125
+ payload,
126
+ } as ElectronPersistenceRequest
127
+
128
+ const response = await withTimeout(
129
+ options.invoke(channel, request),
130
+ timeoutMs,
131
+ `Electron persistence request timed out (method=${method}, collection=${collectionId}, timeoutMs=${timeoutMs})`,
132
+ )
133
+ assertValidResponse(response, request)
134
+
135
+ if (!response.ok) {
136
+ const remoteError = new InvalidPersistedCollectionConfigError(
137
+ `${response.error.name}: ${response.error.message}`,
138
+ )
139
+ if (typeof response.error.stack === `string`) {
140
+ remoteError.stack = response.error.stack
141
+ }
142
+ if (typeof response.error.code === `string`) {
143
+ ;(remoteError as Error & { code?: string }).code = response.error.code
144
+ }
145
+ throw remoteError
146
+ }
147
+
148
+ return response.result as ElectronPersistenceResultMap[TMethod]
149
+ }
150
+ }
151
+
152
+ type ElectronRendererResolvedAdapter<
153
+ T extends object,
154
+ TKey extends string | number = string | number,
155
+ > = PersistedCollectionPersistence<T, TKey>[`adapter`] & {
156
+ loadCollectionMetadata: (
157
+ collectionId: string,
158
+ ) => Promise<Array<{ key: string; value: unknown }>>
159
+ scanRows: (
160
+ collectionId: string,
161
+ options?: { metadataOnly?: boolean },
162
+ ) => Promise<Array<{ key: TKey; value: T; metadata?: unknown }>>
163
+ pullSince: (
164
+ collectionId: string,
165
+ fromRowVersion: number,
166
+ ) => Promise<SQLitePullSinceResult<TKey>>
167
+ getStreamPosition: (collectionId: string) => Promise<{
168
+ latestTerm: number
169
+ latestSeq: number
170
+ latestRowVersion: number
171
+ }>
172
+ }
173
+
174
+ function createResolvedRendererAdapter<
175
+ T extends object,
176
+ TKey extends string | number = string | number,
177
+ >(
178
+ executeRequest: RendererRequestExecutor,
179
+ resolution?: ElectronPersistenceResolution,
180
+ ): ElectronRendererResolvedAdapter<T, TKey> {
181
+ return {
182
+ loadSubset: async (
183
+ collectionId: string,
184
+ subsetOptions: LoadSubsetOptions,
185
+ ctx?: { requiredIndexSignatures?: ReadonlyArray<string> },
186
+ ) => {
187
+ const result = await executeRequest(
188
+ `loadSubset`,
189
+ collectionId,
190
+ {
191
+ options: createSerializableLoadSubsetOptions(subsetOptions),
192
+ ctx,
193
+ },
194
+ resolution,
195
+ )
196
+
197
+ return result as Array<{ key: TKey; value: T }>
198
+ },
199
+ applyCommittedTx: async (
200
+ collectionId: string,
201
+ tx: PersistedTx<T, TKey>,
202
+ ): Promise<void> => {
203
+ await executeRequest(
204
+ `applyCommittedTx`,
205
+ collectionId,
206
+ {
207
+ tx: tx as PersistedTx<ElectronPersistedRow, ElectronPersistedKey>,
208
+ },
209
+ resolution,
210
+ )
211
+ },
212
+ loadCollectionMetadata: async (
213
+ collectionId: string,
214
+ ): Promise<Array<{ key: string; value: unknown }>> => {
215
+ return executeRequest(
216
+ `loadCollectionMetadata`,
217
+ collectionId,
218
+ {},
219
+ resolution,
220
+ )
221
+ },
222
+ scanRows: async (
223
+ collectionId: string,
224
+ options?: { metadataOnly?: boolean },
225
+ ): Promise<Array<{ key: TKey; value: T; metadata?: unknown }>> => {
226
+ const result = await executeRequest(
227
+ `scanRows`,
228
+ collectionId,
229
+ { options },
230
+ resolution,
231
+ )
232
+ return result as Array<{ key: TKey; value: T; metadata?: unknown }>
233
+ },
234
+ ensureIndex: async (
235
+ collectionId: string,
236
+ signature: string,
237
+ spec: PersistedIndexSpec,
238
+ ): Promise<void> => {
239
+ await executeRequest(
240
+ `ensureIndex`,
241
+ collectionId,
242
+ {
243
+ signature,
244
+ spec,
245
+ },
246
+ resolution,
247
+ )
248
+ },
249
+ markIndexRemoved: async (
250
+ collectionId: string,
251
+ signature: string,
252
+ ): Promise<void> => {
253
+ await executeRequest(
254
+ `markIndexRemoved`,
255
+ collectionId,
256
+ {
257
+ signature,
258
+ },
259
+ resolution,
260
+ )
261
+ },
262
+ pullSince: async (
263
+ collectionId: string,
264
+ fromRowVersion: number,
265
+ ): Promise<SQLitePullSinceResult<TKey>> => {
266
+ const result = await executeRequest(
267
+ `pullSince`,
268
+ collectionId,
269
+ {
270
+ fromRowVersion,
271
+ },
272
+ resolution,
273
+ )
274
+ return result as SQLitePullSinceResult<TKey>
275
+ },
276
+ getStreamPosition: async (
277
+ collectionId: string,
278
+ ): Promise<{
279
+ latestTerm: number
280
+ latestSeq: number
281
+ latestRowVersion: number
282
+ }> => {
283
+ return executeRequest(`getStreamPosition`, collectionId, {}, resolution)
284
+ },
285
+ }
286
+ }
287
+
288
+ export type ElectronIpcRendererLike = {
289
+ invoke: (
290
+ channel: string,
291
+ request: ElectronPersistenceRequestEnvelope,
292
+ ) => Promise<ElectronPersistenceResponseEnvelope>
293
+ }
294
+
295
+ export type ElectronSQLitePersistenceOptions = {
296
+ invoke?: ElectronPersistenceInvoke
297
+ ipcRenderer?: ElectronIpcRendererLike
298
+ channel?: string
299
+ timeoutMs?: number
300
+ coordinator?: PersistedCollectionCoordinator
301
+ }
302
+
303
+ function resolveInvoke(
304
+ options: ElectronSQLitePersistenceOptions,
305
+ ): ElectronPersistenceInvoke {
306
+ if (options.invoke) {
307
+ return options.invoke
308
+ }
309
+
310
+ if (options.ipcRenderer) {
311
+ return (channel, request) => options.ipcRenderer!.invoke(channel, request)
312
+ }
313
+
314
+ throw new InvalidPersistedCollectionConfigError(
315
+ `Electron renderer persistence requires either invoke or ipcRenderer`,
316
+ )
317
+ }
318
+
319
+ export function createElectronSQLitePersistence<
320
+ T extends object,
321
+ TKey extends string | number = string | number,
322
+ >(
323
+ options: ElectronSQLitePersistenceOptions,
324
+ ): PersistedCollectionPersistence<T, TKey> {
325
+ const invoke = resolveInvoke(options)
326
+ const coordinator = options.coordinator ?? new SingleProcessCoordinator()
327
+ const executeRequest = createRendererRequestExecutor({
328
+ invoke,
329
+ channel: options.channel,
330
+ timeoutMs: options.timeoutMs,
331
+ })
332
+ const adapterCache = new Map<
333
+ string,
334
+ ElectronRendererResolvedAdapter<Record<string, unknown>, string | number>
335
+ >()
336
+
337
+ const getAdapterForCollection = (
338
+ mode: PersistedCollectionMode,
339
+ schemaVersion: number | undefined,
340
+ ) => {
341
+ const schemaVersionKey =
342
+ schemaVersion === undefined ? `schema:default` : `schema:${schemaVersion}`
343
+ const cacheKey = `mode:${mode}|${schemaVersionKey}`
344
+ const cachedAdapter = adapterCache.get(cacheKey)
345
+ if (cachedAdapter) {
346
+ return cachedAdapter
347
+ }
348
+
349
+ const adapter = createResolvedRendererAdapter<
350
+ Record<string, unknown>,
351
+ string | number
352
+ >(executeRequest, {
353
+ mode,
354
+ schemaVersion,
355
+ })
356
+ adapterCache.set(cacheKey, adapter)
357
+
358
+ // Wire the adapter into the coordinator so it can handle
359
+ // leader-side RPCs (applyCommittedTx, pullSince, getStreamPosition, etc.)
360
+ if (coordinator instanceof ElectronCollectionCoordinator) {
361
+ coordinator.setAdapter(adapter)
362
+ }
363
+
364
+ return adapter
365
+ }
366
+
367
+ const createCollectionPersistence = (
368
+ mode: PersistedCollectionMode,
369
+ schemaVersion: number | undefined,
370
+ ): PersistedCollectionPersistence<T, TKey> => ({
371
+ adapter: getAdapterForCollection(
372
+ mode,
373
+ schemaVersion,
374
+ ) as unknown as PersistedCollectionPersistence<T, TKey>[`adapter`],
375
+ coordinator,
376
+ })
377
+
378
+ const defaultPersistence = createCollectionPersistence(
379
+ `sync-absent`,
380
+ undefined,
381
+ )
382
+
383
+ return {
384
+ ...defaultPersistence,
385
+ resolvePersistenceForCollection: ({ mode, schemaVersion }) =>
386
+ createCollectionPersistence(mode, schemaVersion),
387
+ // Backward compatible fallback for older callers.
388
+ resolvePersistenceForMode: (mode) =>
389
+ createCollectionPersistence(mode, undefined),
390
+ }
391
+ }