@tanstack/electron-db-sqlite-persistence 0.2.8 → 0.2.10

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/src/main.ts CHANGED
@@ -1,10 +1,14 @@
1
- import { InvalidPersistedCollectionConfigError } from '@tanstack/db-sqlite-persistence-core'
1
+ import {
2
+ InvalidPersistedCollectionConfigError,
3
+ resolvePersistedStorageTarget,
4
+ } from '@tanstack/db-sqlite-persistence-core'
2
5
  import {
3
6
  DEFAULT_ELECTRON_PERSISTENCE_CHANNEL,
4
7
  ELECTRON_PERSISTENCE_PROTOCOL_VERSION,
5
8
  } from './protocol'
6
9
  import type {
7
10
  PersistedCollectionPersistence,
11
+ PersistedStorageTarget,
8
12
  PersistenceAdapter,
9
13
  SQLitePullSinceResult,
10
14
  } from '@tanstack/db-sqlite-persistence-core'
@@ -18,11 +22,13 @@ import type {
18
22
 
19
23
  type ElectronMainPersistenceAdapter = PersistenceAdapter & {
20
24
  loadCollectionMetadata?: (
21
- collectionId: string,
25
+ target: PersistedStorageTarget,
26
+ ctx?: { cacheGenerationClaimId?: string },
22
27
  ) => Promise<Array<{ key: string; value: unknown }>>
23
28
  scanRows?: (
24
- collectionId: string,
29
+ target: PersistedStorageTarget,
25
30
  options?: { metadataOnly?: boolean },
31
+ ctx?: { cacheGenerationClaimId?: string },
26
32
  ) => Promise<
27
33
  Array<{
28
34
  key: ElectronPersistedKey
@@ -31,10 +37,14 @@ type ElectronMainPersistenceAdapter = PersistenceAdapter & {
31
37
  }>
32
38
  >
33
39
  pullSince?: (
34
- collectionId: string,
40
+ target: PersistedStorageTarget,
35
41
  fromRowVersion: number,
42
+ ctx?: { cacheGenerationClaimId?: string },
36
43
  ) => Promise<SQLitePullSinceResult<ElectronPersistedKey>>
37
- getStreamPosition?: (collectionId: string) => Promise<{
44
+ getStreamPosition?: (
45
+ target: PersistedStorageTarget,
46
+ ctx?: { cacheGenerationClaimId?: string },
47
+ ) => Promise<{
38
48
  latestTerm: number
39
49
  latestSeq: number
40
50
  latestRowVersion: number
@@ -52,12 +62,26 @@ function serializeError(error: unknown): ElectronSerializedError {
52
62
  }
53
63
  }
54
64
 
55
- const codedError = error as Error & { code?: unknown }
65
+ const detailedError = error as Error & { code?: unknown; path?: unknown }
66
+ const path = detailedError.path
56
67
  return {
57
68
  name: error.name || `Error`,
58
69
  message: error.message || fallbackMessage,
59
70
  stack: error.stack,
60
- code: typeof codedError.code === `string` ? codedError.code : undefined,
71
+ code:
72
+ typeof detailedError.code === `string` ||
73
+ typeof detailedError.code === `number`
74
+ ? detailedError.code
75
+ : undefined,
76
+ path:
77
+ typeof path === `string` ||
78
+ (Array.isArray(path) &&
79
+ path.every(
80
+ (segment: unknown) =>
81
+ typeof segment === `string` || typeof segment === `number`,
82
+ ))
83
+ ? (path as string | ReadonlyArray<string | number>)
84
+ : undefined,
61
85
  }
62
86
  }
63
87
 
@@ -100,6 +124,25 @@ function assertValidRequest(request: ElectronPersistenceRequestEnvelope): void {
100
124
  }
101
125
  }
102
126
 
127
+ function requireStorageTarget(
128
+ request: ElectronPersistenceRequestEnvelope,
129
+ ): PersistedStorageTarget {
130
+ const target = request.storageTarget
131
+ if (!target) {
132
+ throw new InvalidPersistedCollectionConfigError(
133
+ `Electron persistence data request requires an explicit storage target`,
134
+ )
135
+ }
136
+ if (
137
+ resolvePersistedStorageTarget(target).collectionId !== request.collectionId
138
+ ) {
139
+ throw new InvalidPersistedCollectionConfigError(
140
+ `Electron persistence storage target does not match its collectionId`,
141
+ )
142
+ }
143
+ return target
144
+ }
145
+
103
146
  async function executeRequestAgainstAdapter(
104
147
  request: ElectronPersistenceRequestEnvelope,
105
148
  adapter: ElectronMainPersistenceAdapter,
@@ -107,7 +150,7 @@ async function executeRequestAgainstAdapter(
107
150
  switch (request.method) {
108
151
  case `loadSubset`: {
109
152
  const result = await adapter.loadSubset(
110
- request.collectionId,
153
+ requireStorageTarget(request),
111
154
  request.payload.options,
112
155
  request.payload.ctx,
113
156
  )
@@ -122,7 +165,7 @@ async function executeRequestAgainstAdapter(
122
165
 
123
166
  case `loadResumeSnapshot`: {
124
167
  const result = await adapter.loadResumeSnapshot(
125
- request.collectionId,
168
+ requireStorageTarget(request),
126
169
  request.payload.ctx,
127
170
  )
128
171
  return {
@@ -140,7 +183,10 @@ async function executeRequestAgainstAdapter(
140
183
  `loadCollectionMetadata is not supported by the configured electron persistence adapter`,
141
184
  )
142
185
  }
143
- const result = await adapter.loadCollectionMetadata(request.collectionId)
186
+ const result = await adapter.loadCollectionMetadata(
187
+ requireStorageTarget(request),
188
+ request.payload.ctx,
189
+ )
144
190
  return {
145
191
  v: ELECTRON_PERSISTENCE_PROTOCOL_VERSION,
146
192
  requestId: request.requestId,
@@ -157,8 +203,9 @@ async function executeRequestAgainstAdapter(
157
203
  )
158
204
  }
159
205
  const result = await adapter.scanRows(
160
- request.collectionId,
206
+ requireStorageTarget(request),
161
207
  request.payload.options,
208
+ request.payload.ctx,
162
209
  )
163
210
  return {
164
211
  v: ELECTRON_PERSISTENCE_PROTOCOL_VERSION,
@@ -170,7 +217,10 @@ async function executeRequestAgainstAdapter(
170
217
  }
171
218
 
172
219
  case `applyCommittedTx`: {
173
- await adapter.applyCommittedTx(request.collectionId, request.payload.tx)
220
+ await adapter.applyCommittedTx(
221
+ requireStorageTarget(request),
222
+ request.payload.tx,
223
+ )
174
224
  return {
175
225
  v: ELECTRON_PERSISTENCE_PROTOCOL_VERSION,
176
226
  requestId: request.requestId,
@@ -180,11 +230,29 @@ async function executeRequestAgainstAdapter(
180
230
  }
181
231
  }
182
232
 
233
+ case `reconcileCommittedTx`: {
234
+ const result = adapter.reconcileCommittedTx
235
+ ? await adapter.reconcileCommittedTx(
236
+ requireStorageTarget(request),
237
+ request.payload.tx,
238
+ request.payload.anchor,
239
+ )
240
+ : { kind: `unknown` as const }
241
+ return {
242
+ v: ELECTRON_PERSISTENCE_PROTOCOL_VERSION,
243
+ requestId: request.requestId,
244
+ method: request.method,
245
+ ok: true,
246
+ result,
247
+ }
248
+ }
249
+
183
250
  case `ensureIndex`: {
184
251
  await adapter.ensureIndex(
185
- request.collectionId,
252
+ requireStorageTarget(request),
186
253
  request.payload.signature,
187
254
  request.payload.spec,
255
+ request.payload.ctx,
188
256
  )
189
257
  return {
190
258
  v: ELECTRON_PERSISTENCE_PROTOCOL_VERSION,
@@ -202,8 +270,9 @@ async function executeRequestAgainstAdapter(
202
270
  )
203
271
  }
204
272
  await adapter.markIndexRemoved(
205
- request.collectionId,
273
+ requireStorageTarget(request),
206
274
  request.payload.signature,
275
+ request.payload.ctx,
207
276
  )
208
277
  return {
209
278
  v: ELECTRON_PERSISTENCE_PROTOCOL_VERSION,
@@ -221,8 +290,9 @@ async function executeRequestAgainstAdapter(
221
290
  )
222
291
  }
223
292
  const result = await adapter.pullSince(
224
- request.collectionId,
293
+ requireStorageTarget(request),
225
294
  request.payload.fromRowVersion,
295
+ request.payload.ctx,
226
296
  )
227
297
  return {
228
298
  v: ELECTRON_PERSISTENCE_PROTOCOL_VERSION,
@@ -233,13 +303,36 @@ async function executeRequestAgainstAdapter(
233
303
  }
234
304
  }
235
305
 
306
+ case `reserveLeadershipTerm`: {
307
+ if (!adapter.reserveLeadershipTerm) {
308
+ throw new InvalidPersistedCollectionConfigError(
309
+ `reserveLeadershipTerm is not supported by the configured electron persistence adapter`,
310
+ )
311
+ }
312
+ const position = await adapter.reserveLeadershipTerm(
313
+ requireStorageTarget(request),
314
+ request.payload.observedTerm,
315
+ request.payload.ctx,
316
+ )
317
+ return {
318
+ v: ELECTRON_PERSISTENCE_PROTOCOL_VERSION,
319
+ requestId: request.requestId,
320
+ method: request.method,
321
+ ok: true,
322
+ result: position,
323
+ }
324
+ }
325
+
236
326
  case `getStreamPosition`: {
237
327
  if (!adapter.getStreamPosition) {
238
328
  throw new InvalidPersistedCollectionConfigError(
239
329
  `getStreamPosition is not supported by the configured electron persistence adapter`,
240
330
  )
241
331
  }
242
- const position = await adapter.getStreamPosition(request.collectionId)
332
+ const position = await adapter.getStreamPosition(
333
+ requireStorageTarget(request),
334
+ request.payload.ctx,
335
+ )
243
336
  return {
244
337
  v: ELECTRON_PERSISTENCE_PROTOCOL_VERSION,
245
338
  requestId: request.requestId,
@@ -248,6 +341,78 @@ async function executeRequestAgainstAdapter(
248
341
  result: position,
249
342
  }
250
343
  }
344
+
345
+ case `claimCacheGeneration`: {
346
+ if (!adapter.claimCacheGeneration) {
347
+ throw new InvalidPersistedCollectionConfigError(
348
+ `claimCacheGeneration is not supported by the configured electron persistence adapter`,
349
+ )
350
+ }
351
+ const result = await adapter.claimCacheGeneration(request.collectionId)
352
+ return {
353
+ v: ELECTRON_PERSISTENCE_PROTOCOL_VERSION,
354
+ requestId: request.requestId,
355
+ method: request.method,
356
+ ok: true,
357
+ result,
358
+ }
359
+ }
360
+
361
+ case `rotateCacheGeneration`: {
362
+ if (!adapter.rotateCacheGeneration) {
363
+ throw new InvalidPersistedCollectionConfigError(
364
+ `rotateCacheGeneration is not supported by the configured electron persistence adapter`,
365
+ )
366
+ }
367
+ const result = await adapter.rotateCacheGeneration(
368
+ request.collectionId,
369
+ request.payload.claimId,
370
+ request.payload.resetMetadata,
371
+ request.payload.expectedStorageCollectionId,
372
+ )
373
+ return {
374
+ v: ELECTRON_PERSISTENCE_PROTOCOL_VERSION,
375
+ requestId: request.requestId,
376
+ method: request.method,
377
+ ok: true,
378
+ result,
379
+ }
380
+ }
381
+
382
+ case `renewCacheGenerationClaim`: {
383
+ if (!adapter.renewCacheGenerationClaim) {
384
+ throw new InvalidPersistedCollectionConfigError(
385
+ `renewCacheGenerationClaim is not supported by the configured electron persistence adapter`,
386
+ )
387
+ }
388
+ const result = await adapter.renewCacheGenerationClaim(
389
+ request.payload.storageCollectionId,
390
+ request.payload.claimId,
391
+ )
392
+ return {
393
+ v: ELECTRON_PERSISTENCE_PROTOCOL_VERSION,
394
+ requestId: request.requestId,
395
+ method: request.method,
396
+ ok: true,
397
+ result,
398
+ }
399
+ }
400
+
401
+ case `releaseCacheGenerationClaim`: {
402
+ if (!adapter.releaseCacheGenerationClaim) {
403
+ throw new InvalidPersistedCollectionConfigError(
404
+ `releaseCacheGenerationClaim is not supported by the configured electron persistence adapter`,
405
+ )
406
+ }
407
+ await adapter.releaseCacheGenerationClaim(request.payload.claimId)
408
+ return {
409
+ v: ELECTRON_PERSISTENCE_PROTOCOL_VERSION,
410
+ requestId: request.requestId,
411
+ method: request.method,
412
+ ok: true,
413
+ result: null,
414
+ }
415
+ }
251
416
  }
252
417
  }
253
418
 
@@ -255,11 +420,20 @@ function resolveModeAwarePersistence(
255
420
  persistence: PersistedCollectionPersistence,
256
421
  request: ElectronPersistenceRequestEnvelope,
257
422
  ): PersistedCollectionPersistence {
423
+ if (
424
+ request.storageTarget?.kind === `managed` &&
425
+ !request.resolution?.logicalCollectionId
426
+ ) {
427
+ throw new InvalidPersistedCollectionConfigError(
428
+ `Electron managed cache request requires a logical Collection ID`,
429
+ )
430
+ }
258
431
  const mode = request.resolution?.mode ?? `sync-absent`
259
432
  const schemaVersion = request.resolution?.schemaVersion
260
433
  const collectionAwarePersistence =
261
434
  persistence.resolvePersistenceForCollection?.({
262
- collectionId: request.collectionId,
435
+ collectionId:
436
+ request.resolution?.logicalCollectionId ?? request.collectionId,
263
437
  mode,
264
438
  schemaVersion,
265
439
  })
package/src/protocol.ts CHANGED
@@ -1,13 +1,17 @@
1
1
  import type { LoadSubsetOptions } from '@tanstack/db'
2
2
  import type {
3
+ CommittedTxAnchor,
4
+ PersistedCacheGenerationClaim,
3
5
  PersistedCollectionMode,
4
6
  PersistedIndexSpec,
5
7
  PersistedKeySetEvidence,
8
+ PersistedStorageTarget,
6
9
  PersistedTx,
10
+ ReconciledCommittedTx,
7
11
  SQLitePullSinceResult,
8
12
  } from '@tanstack/db-sqlite-persistence-core'
9
13
 
10
- export const ELECTRON_PERSISTENCE_PROTOCOL_VERSION = 2 as const
14
+ export const ELECTRON_PERSISTENCE_PROTOCOL_VERSION = 6 as const
11
15
  export const DEFAULT_ELECTRON_PERSISTENCE_CHANNEL = `tanstack-db:sqlite-persistence`
12
16
 
13
17
  export type ElectronPersistedRow = Record<string, unknown>
@@ -16,6 +20,7 @@ export type ElectronPersistedKey = string | number
16
20
  export type ElectronPersistenceResolution = {
17
21
  mode: PersistedCollectionMode
18
22
  schemaVersion?: number
23
+ logicalCollectionId?: string
19
24
  }
20
25
 
21
26
  export type ElectronPersistenceMethod =
@@ -24,42 +29,75 @@ export type ElectronPersistenceMethod =
24
29
  | `loadCollectionMetadata`
25
30
  | `scanRows`
26
31
  | `applyCommittedTx`
32
+ | `reconcileCommittedTx`
27
33
  | `ensureIndex`
28
34
  | `markIndexRemoved`
29
35
  | `pullSince`
30
36
  | `getStreamPosition`
37
+ | `claimCacheGeneration`
38
+ | `rotateCacheGeneration`
39
+ | `renewCacheGenerationClaim`
40
+ | `releaseCacheGenerationClaim`
41
+ | `reserveLeadershipTerm`
31
42
 
32
43
  export type ElectronPersistencePayloadMap = {
33
44
  loadSubset: {
34
45
  options: LoadSubsetOptions
35
- ctx?: { requiredIndexSignatures?: ReadonlyArray<string> }
46
+ ctx?: {
47
+ requiredIndexSignatures?: ReadonlyArray<string>
48
+ cacheGenerationClaimId?: string
49
+ }
36
50
  }
37
51
  loadResumeSnapshot: {
38
52
  ctx?: {
39
53
  requiredIndexSignatures?: ReadonlyArray<string>
40
54
  includeRows?: boolean
55
+ cacheGenerationClaimId?: string
41
56
  }
42
57
  }
43
- loadCollectionMetadata: {}
58
+ loadCollectionMetadata: { ctx?: { cacheGenerationClaimId?: string } }
44
59
  scanRows: {
45
60
  options?: {
46
61
  metadataOnly?: boolean
47
62
  }
63
+ ctx?: { cacheGenerationClaimId?: string }
48
64
  }
49
65
  applyCommittedTx: {
50
66
  tx: PersistedTx<ElectronPersistedRow, ElectronPersistedKey>
51
67
  }
68
+ reconcileCommittedTx: {
69
+ tx: PersistedTx<ElectronPersistedRow, ElectronPersistedKey>
70
+ anchor: CommittedTxAnchor
71
+ }
52
72
  ensureIndex: {
53
73
  signature: string
54
74
  spec: PersistedIndexSpec
75
+ ctx?: { cacheGenerationClaimId?: string }
55
76
  }
56
77
  markIndexRemoved: {
57
78
  signature: string
79
+ ctx?: { cacheGenerationClaimId?: string }
58
80
  }
59
81
  pullSince: {
60
82
  fromRowVersion: number
83
+ ctx?: { cacheGenerationClaimId?: string }
84
+ }
85
+ getStreamPosition: { ctx?: { cacheGenerationClaimId?: string } }
86
+ claimCacheGeneration: {}
87
+ rotateCacheGeneration: {
88
+ claimId: string
89
+ resetMetadata?: { key: string; value: unknown }
90
+ expectedStorageCollectionId?: string
91
+ }
92
+ renewCacheGenerationClaim: {
93
+ storageCollectionId: string
94
+ claimId: string
95
+ }
96
+ releaseCacheGenerationClaim: { claimId: string }
97
+ reserveLeadershipTerm: {
98
+ observedTerm: number
99
+ ctx?: { cacheGenerationClaimId?: string }
61
100
  }
62
- getStreamPosition: {}
63
101
  }
64
102
 
65
103
  export type ElectronPersistenceResultMap = {
@@ -84,6 +122,7 @@ export type ElectronPersistenceResultMap = {
84
122
  metadata?: unknown
85
123
  }>
86
124
  applyCommittedTx: null
125
+ reconcileCommittedTx: ReconciledCommittedTx
87
126
  ensureIndex: null
88
127
  markIndexRemoved: null
89
128
  pullSince: SQLitePullSinceResult<ElectronPersistedKey>
@@ -92,13 +131,23 @@ export type ElectronPersistenceResultMap = {
92
131
  latestSeq: number
93
132
  latestRowVersion: number
94
133
  }
134
+ claimCacheGeneration: PersistedCacheGenerationClaim
135
+ rotateCacheGeneration: PersistedCacheGenerationClaim
136
+ renewCacheGenerationClaim: number | undefined
137
+ releaseCacheGenerationClaim: null
138
+ reserveLeadershipTerm: {
139
+ latestTerm: number
140
+ latestSeq: number
141
+ latestRowVersion: number
142
+ }
95
143
  }
96
144
 
97
145
  export type ElectronSerializedError = {
98
146
  name: string
99
147
  message: string
100
148
  stack?: string
101
- code?: string
149
+ code?: string | number
150
+ path?: string | ReadonlyArray<string | number>
102
151
  }
103
152
 
104
153
  export type ElectronPersistenceRequestByMethod = {
@@ -106,6 +155,8 @@ export type ElectronPersistenceRequestByMethod = {
106
155
  v: number
107
156
  requestId: string
108
157
  collectionId: string
158
+ /** Required for data operations; preserves eager or managed identity across IPC. */
159
+ storageTarget?: PersistedStorageTarget
109
160
  resolution?: ElectronPersistenceResolution
110
161
  method: Method
111
162
  payload: ElectronPersistencePayloadMap[Method]