@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.
- package/README.md +71 -0
- package/dist/cjs/electron-coordinator.cjs +541 -0
- package/dist/cjs/electron-coordinator.cjs.map +1 -0
- package/dist/cjs/electron-coordinator.d.cts +67 -0
- package/dist/cjs/errors.d.cts +33 -0
- package/dist/cjs/index.cjs +14 -0
- package/dist/cjs/index.cjs.map +1 -0
- package/dist/cjs/index.d.cts +8 -0
- package/dist/cjs/main.cjs +213 -0
- package/dist/cjs/main.cjs.map +1 -0
- package/dist/cjs/main.d.cts +12 -0
- package/dist/cjs/protocol.cjs +7 -0
- package/dist/cjs/protocol.cjs.map +1 -0
- package/dist/cjs/protocol.d.cts +104 -0
- package/dist/cjs/renderer.cjs +221 -0
- package/dist/cjs/renderer.cjs.map +1 -0
- package/dist/cjs/renderer.d.cts +13 -0
- package/dist/esm/electron-coordinator.d.ts +67 -0
- package/dist/esm/electron-coordinator.js +541 -0
- package/dist/esm/electron-coordinator.js.map +1 -0
- package/dist/esm/errors.d.ts +33 -0
- package/dist/esm/index.d.ts +8 -0
- package/dist/esm/index.js +11 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/main.d.ts +12 -0
- package/dist/esm/main.js +213 -0
- package/dist/esm/main.js.map +1 -0
- package/dist/esm/protocol.d.ts +104 -0
- package/dist/esm/protocol.js +7 -0
- package/dist/esm/protocol.js.map +1 -0
- package/dist/esm/renderer.d.ts +13 -0
- package/dist/esm/renderer.js +221 -0
- package/dist/esm/renderer.js.map +1 -0
- package/package.json +80 -0
- package/src/electron-coordinator.ts +842 -0
- package/src/errors.ts +110 -0
- package/src/index.ts +17 -0
- package/src/main.ts +315 -0
- package/src/protocol.ts +137 -0
- package/src/renderer.ts +391 -0
package/src/renderer.ts
ADDED
|
@@ -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
|
+
}
|