@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
package/src/errors.ts ADDED
@@ -0,0 +1,110 @@
1
+ import type {
2
+ ElectronPersistenceMethod,
3
+ ElectronSerializedError,
4
+ } from './protocol'
5
+
6
+ type ElectronPersistenceErrorOptions = {
7
+ code?: string
8
+ cause?: unknown
9
+ }
10
+
11
+ export class ElectronPersistenceError extends Error {
12
+ readonly code: string | undefined
13
+
14
+ constructor(message: string, options?: ElectronPersistenceErrorOptions) {
15
+ super(message, { cause: options?.cause })
16
+ this.name = `ElectronPersistenceError`
17
+ this.code = options?.code
18
+ }
19
+ }
20
+
21
+ export class UnknownElectronPersistenceCollectionError extends ElectronPersistenceError {
22
+ readonly collectionId: string
23
+
24
+ constructor(collectionId: string) {
25
+ super(
26
+ `Unknown electron persistence collection "${collectionId}". Register the collection adapter in the main process host.`,
27
+ {
28
+ code: `UNKNOWN_COLLECTION`,
29
+ },
30
+ )
31
+ this.name = `UnknownElectronPersistenceCollectionError`
32
+ this.collectionId = collectionId
33
+ }
34
+ }
35
+
36
+ export class UnsupportedElectronPersistenceMethodError extends ElectronPersistenceError {
37
+ readonly method: ElectronPersistenceMethod
38
+ readonly collectionId: string
39
+
40
+ constructor(method: ElectronPersistenceMethod, collectionId: string) {
41
+ super(
42
+ `Method "${method}" is not supported by the electron persistence adapter for collection "${collectionId}".`,
43
+ {
44
+ code: `UNSUPPORTED_METHOD`,
45
+ },
46
+ )
47
+ this.name = `UnsupportedElectronPersistenceMethodError`
48
+ this.method = method
49
+ this.collectionId = collectionId
50
+ }
51
+ }
52
+
53
+ export class ElectronPersistenceProtocolError extends ElectronPersistenceError {
54
+ constructor(message: string, options?: ElectronPersistenceErrorOptions) {
55
+ super(message, {
56
+ code: options?.code ?? `INVALID_PROTOCOL`,
57
+ cause: options?.cause,
58
+ })
59
+ this.name = `ElectronPersistenceProtocolError`
60
+ }
61
+ }
62
+
63
+ export class ElectronPersistenceTimeoutError extends ElectronPersistenceError {
64
+ constructor(message: string) {
65
+ super(message, {
66
+ code: `TIMEOUT`,
67
+ })
68
+ this.name = `ElectronPersistenceTimeoutError`
69
+ }
70
+ }
71
+
72
+ export class ElectronPersistenceRpcError extends ElectronPersistenceError {
73
+ readonly method: ElectronPersistenceMethod
74
+ readonly collectionId: string
75
+ readonly requestId: string
76
+ readonly remoteName: string
77
+
78
+ constructor(
79
+ method: ElectronPersistenceMethod,
80
+ collectionId: string,
81
+ requestId: string,
82
+ serializedError: ElectronSerializedError,
83
+ ) {
84
+ super(
85
+ `${serializedError.name}: ${serializedError.message} (method=${method}, collection=${collectionId}, request=${requestId})`,
86
+ {
87
+ code: serializedError.code ?? `REMOTE_ERROR`,
88
+ },
89
+ )
90
+ this.name = `ElectronPersistenceRpcError`
91
+ this.method = method
92
+ this.collectionId = collectionId
93
+ this.requestId = requestId
94
+ this.remoteName = serializedError.name
95
+ }
96
+
97
+ static fromSerialized(
98
+ method: ElectronPersistenceMethod,
99
+ collectionId: string,
100
+ requestId: string,
101
+ serializedError: ElectronSerializedError,
102
+ ): ElectronPersistenceRpcError {
103
+ return new ElectronPersistenceRpcError(
104
+ method,
105
+ collectionId,
106
+ requestId,
107
+ serializedError,
108
+ )
109
+ }
110
+ }
package/src/index.ts ADDED
@@ -0,0 +1,17 @@
1
+ export { exposeElectronSQLitePersistence } from './main'
2
+ export type {
3
+ ElectronIpcMainLike,
4
+ ElectronSQLiteMainProcessOptions,
5
+ } from './main'
6
+ export { createElectronSQLitePersistence } from './renderer'
7
+ export type {
8
+ ElectronIpcRendererLike,
9
+ ElectronSQLitePersistenceOptions,
10
+ } from './renderer'
11
+ export { ElectronCollectionCoordinator } from './electron-coordinator'
12
+ export type { ElectronCollectionCoordinatorOptions } from './electron-coordinator'
13
+ export { persistedCollectionOptions } from '@tanstack/db-sqlite-persistence-core'
14
+ export type {
15
+ PersistedCollectionCoordinator,
16
+ PersistedCollectionPersistence,
17
+ } from '@tanstack/db-sqlite-persistence-core'
package/src/main.ts ADDED
@@ -0,0 +1,315 @@
1
+ import { InvalidPersistedCollectionConfigError } from '@tanstack/db-sqlite-persistence-core'
2
+ import {
3
+ DEFAULT_ELECTRON_PERSISTENCE_CHANNEL,
4
+ ELECTRON_PERSISTENCE_PROTOCOL_VERSION,
5
+ } from './protocol'
6
+ import type {
7
+ PersistedCollectionPersistence,
8
+ PersistenceAdapter,
9
+ SQLitePullSinceResult,
10
+ } from '@tanstack/db-sqlite-persistence-core'
11
+ import type {
12
+ ElectronPersistedKey,
13
+ ElectronPersistedRow,
14
+ ElectronPersistenceRequestEnvelope,
15
+ ElectronPersistenceResponseEnvelope,
16
+ ElectronSerializedError,
17
+ } from './protocol'
18
+
19
+ type ElectronMainPersistenceAdapter = PersistenceAdapter<
20
+ ElectronPersistedRow,
21
+ ElectronPersistedKey
22
+ > & {
23
+ loadCollectionMetadata?: (
24
+ collectionId: string,
25
+ ) => Promise<Array<{ key: string; value: unknown }>>
26
+ scanRows?: (
27
+ collectionId: string,
28
+ options?: { metadataOnly?: boolean },
29
+ ) => Promise<
30
+ Array<{
31
+ key: ElectronPersistedKey
32
+ value: ElectronPersistedRow
33
+ metadata?: unknown
34
+ }>
35
+ >
36
+ pullSince?: (
37
+ collectionId: string,
38
+ fromRowVersion: number,
39
+ ) => Promise<SQLitePullSinceResult<ElectronPersistedKey>>
40
+ getStreamPosition?: (collectionId: string) => Promise<{
41
+ latestTerm: number
42
+ latestSeq: number
43
+ latestRowVersion: number
44
+ }>
45
+ }
46
+
47
+ function serializeError(error: unknown): ElectronSerializedError {
48
+ const fallbackMessage = `Unknown electron persistence error`
49
+
50
+ if (!(error instanceof Error)) {
51
+ return {
52
+ name: `Error`,
53
+ message: fallbackMessage,
54
+ code: undefined,
55
+ }
56
+ }
57
+
58
+ const codedError = error as Error & { code?: unknown }
59
+ return {
60
+ name: error.name || `Error`,
61
+ message: error.message || fallbackMessage,
62
+ stack: error.stack,
63
+ code: typeof codedError.code === `string` ? codedError.code : undefined,
64
+ }
65
+ }
66
+
67
+ function createErrorResponse(
68
+ request: ElectronPersistenceRequestEnvelope,
69
+ error: unknown,
70
+ ): ElectronPersistenceResponseEnvelope {
71
+ return {
72
+ v: ELECTRON_PERSISTENCE_PROTOCOL_VERSION,
73
+ requestId: request.requestId,
74
+ method: request.method,
75
+ ok: false,
76
+ error: serializeError(error),
77
+ }
78
+ }
79
+
80
+ function assertValidRequest(request: ElectronPersistenceRequestEnvelope): void {
81
+ if (request.v !== ELECTRON_PERSISTENCE_PROTOCOL_VERSION) {
82
+ throw new InvalidPersistedCollectionConfigError(
83
+ `Unsupported electron persistence protocol version "${request.v}"`,
84
+ )
85
+ }
86
+
87
+ if (
88
+ typeof request.requestId !== `string` ||
89
+ request.requestId.trim().length === 0
90
+ ) {
91
+ throw new InvalidPersistedCollectionConfigError(
92
+ `Electron persistence requestId cannot be empty`,
93
+ )
94
+ }
95
+
96
+ if (
97
+ typeof request.collectionId !== `string` ||
98
+ request.collectionId.trim().length === 0
99
+ ) {
100
+ throw new InvalidPersistedCollectionConfigError(
101
+ `Electron persistence collectionId cannot be empty`,
102
+ )
103
+ }
104
+ }
105
+
106
+ async function executeRequestAgainstAdapter(
107
+ request: ElectronPersistenceRequestEnvelope,
108
+ adapter: ElectronMainPersistenceAdapter,
109
+ ): Promise<ElectronPersistenceResponseEnvelope> {
110
+ switch (request.method) {
111
+ case `loadSubset`: {
112
+ const result = await adapter.loadSubset(
113
+ request.collectionId,
114
+ request.payload.options,
115
+ request.payload.ctx,
116
+ )
117
+ return {
118
+ v: ELECTRON_PERSISTENCE_PROTOCOL_VERSION,
119
+ requestId: request.requestId,
120
+ method: request.method,
121
+ ok: true,
122
+ result,
123
+ }
124
+ }
125
+
126
+ case `loadCollectionMetadata`: {
127
+ if (!adapter.loadCollectionMetadata) {
128
+ throw new InvalidPersistedCollectionConfigError(
129
+ `loadCollectionMetadata is not supported by the configured electron persistence adapter`,
130
+ )
131
+ }
132
+ const result = await adapter.loadCollectionMetadata(request.collectionId)
133
+ return {
134
+ v: ELECTRON_PERSISTENCE_PROTOCOL_VERSION,
135
+ requestId: request.requestId,
136
+ method: request.method,
137
+ ok: true,
138
+ result,
139
+ }
140
+ }
141
+
142
+ case `scanRows`: {
143
+ if (!adapter.scanRows) {
144
+ throw new InvalidPersistedCollectionConfigError(
145
+ `scanRows is not supported by the configured electron persistence adapter`,
146
+ )
147
+ }
148
+ const result = await adapter.scanRows(
149
+ request.collectionId,
150
+ request.payload.options,
151
+ )
152
+ return {
153
+ v: ELECTRON_PERSISTENCE_PROTOCOL_VERSION,
154
+ requestId: request.requestId,
155
+ method: request.method,
156
+ ok: true,
157
+ result,
158
+ }
159
+ }
160
+
161
+ case `applyCommittedTx`: {
162
+ await adapter.applyCommittedTx(request.collectionId, request.payload.tx)
163
+ return {
164
+ v: ELECTRON_PERSISTENCE_PROTOCOL_VERSION,
165
+ requestId: request.requestId,
166
+ method: request.method,
167
+ ok: true,
168
+ result: null,
169
+ }
170
+ }
171
+
172
+ case `ensureIndex`: {
173
+ await adapter.ensureIndex(
174
+ request.collectionId,
175
+ request.payload.signature,
176
+ request.payload.spec,
177
+ )
178
+ return {
179
+ v: ELECTRON_PERSISTENCE_PROTOCOL_VERSION,
180
+ requestId: request.requestId,
181
+ method: request.method,
182
+ ok: true,
183
+ result: null,
184
+ }
185
+ }
186
+
187
+ case `markIndexRemoved`: {
188
+ if (!adapter.markIndexRemoved) {
189
+ throw new InvalidPersistedCollectionConfigError(
190
+ `markIndexRemoved is not supported by the configured electron persistence adapter`,
191
+ )
192
+ }
193
+ await adapter.markIndexRemoved(
194
+ request.collectionId,
195
+ request.payload.signature,
196
+ )
197
+ return {
198
+ v: ELECTRON_PERSISTENCE_PROTOCOL_VERSION,
199
+ requestId: request.requestId,
200
+ method: request.method,
201
+ ok: true,
202
+ result: null,
203
+ }
204
+ }
205
+
206
+ case `pullSince`: {
207
+ if (!adapter.pullSince) {
208
+ throw new InvalidPersistedCollectionConfigError(
209
+ `pullSince is not supported by the configured electron persistence adapter`,
210
+ )
211
+ }
212
+ const result = await adapter.pullSince(
213
+ request.collectionId,
214
+ request.payload.fromRowVersion,
215
+ )
216
+ return {
217
+ v: ELECTRON_PERSISTENCE_PROTOCOL_VERSION,
218
+ requestId: request.requestId,
219
+ method: request.method,
220
+ ok: true,
221
+ result,
222
+ }
223
+ }
224
+
225
+ case `getStreamPosition`: {
226
+ if (!adapter.getStreamPosition) {
227
+ throw new InvalidPersistedCollectionConfigError(
228
+ `getStreamPosition is not supported by the configured electron persistence adapter`,
229
+ )
230
+ }
231
+ const position = await adapter.getStreamPosition(request.collectionId)
232
+ return {
233
+ v: ELECTRON_PERSISTENCE_PROTOCOL_VERSION,
234
+ requestId: request.requestId,
235
+ method: request.method,
236
+ ok: true,
237
+ result: position,
238
+ }
239
+ }
240
+ }
241
+ }
242
+
243
+ function resolveModeAwarePersistence(
244
+ persistence: PersistedCollectionPersistence<
245
+ ElectronPersistedRow,
246
+ ElectronPersistedKey
247
+ >,
248
+ request: ElectronPersistenceRequestEnvelope,
249
+ ): PersistedCollectionPersistence<ElectronPersistedRow, ElectronPersistedKey> {
250
+ const mode = request.resolution?.mode ?? `sync-absent`
251
+ const schemaVersion = request.resolution?.schemaVersion
252
+ const collectionAwarePersistence =
253
+ persistence.resolvePersistenceForCollection?.({
254
+ collectionId: request.collectionId,
255
+ mode,
256
+ schemaVersion,
257
+ })
258
+ if (collectionAwarePersistence) {
259
+ return collectionAwarePersistence
260
+ }
261
+
262
+ const modeAwarePersistence = persistence.resolvePersistenceForMode?.(mode)
263
+ return modeAwarePersistence ?? persistence
264
+ }
265
+
266
+ export type ElectronIpcMainLike = {
267
+ handle: (
268
+ channel: string,
269
+ listener: (
270
+ event: unknown,
271
+ request: ElectronPersistenceRequestEnvelope,
272
+ ) => Promise<ElectronPersistenceResponseEnvelope>,
273
+ ) => void
274
+ removeHandler?: (channel: string) => void
275
+ }
276
+
277
+ export type ElectronSQLiteMainProcessOptions = {
278
+ persistence: PersistedCollectionPersistence<
279
+ ElectronPersistedRow,
280
+ ElectronPersistedKey
281
+ >
282
+ ipcMain: ElectronIpcMainLike
283
+ channel?: string
284
+ }
285
+
286
+ export function exposeElectronSQLitePersistence(
287
+ options: ElectronSQLiteMainProcessOptions,
288
+ ): () => void {
289
+ const channel = options.channel ?? DEFAULT_ELECTRON_PERSISTENCE_CHANNEL
290
+ options.ipcMain.handle(
291
+ channel,
292
+ async (
293
+ _event,
294
+ request: ElectronPersistenceRequestEnvelope,
295
+ ): Promise<ElectronPersistenceResponseEnvelope> => {
296
+ try {
297
+ assertValidRequest(request)
298
+ const modeAwarePersistence = resolveModeAwarePersistence(
299
+ options.persistence,
300
+ request,
301
+ )
302
+ return await executeRequestAgainstAdapter(
303
+ request,
304
+ modeAwarePersistence.adapter,
305
+ )
306
+ } catch (error) {
307
+ return createErrorResponse(request, error)
308
+ }
309
+ },
310
+ )
311
+
312
+ return () => {
313
+ options.ipcMain.removeHandler?.(channel)
314
+ }
315
+ }
@@ -0,0 +1,137 @@
1
+ import type { LoadSubsetOptions } from '@tanstack/db'
2
+ import type {
3
+ PersistedCollectionMode,
4
+ PersistedIndexSpec,
5
+ PersistedTx,
6
+ SQLitePullSinceResult,
7
+ } from '@tanstack/db-sqlite-persistence-core'
8
+
9
+ export const ELECTRON_PERSISTENCE_PROTOCOL_VERSION = 1 as const
10
+ export const DEFAULT_ELECTRON_PERSISTENCE_CHANNEL = `tanstack-db:sqlite-persistence`
11
+
12
+ export type ElectronPersistedRow = Record<string, unknown>
13
+ export type ElectronPersistedKey = string | number
14
+
15
+ export type ElectronPersistenceResolution = {
16
+ mode: PersistedCollectionMode
17
+ schemaVersion?: number
18
+ }
19
+
20
+ export type ElectronPersistenceMethod =
21
+ | `loadSubset`
22
+ | `loadCollectionMetadata`
23
+ | `scanRows`
24
+ | `applyCommittedTx`
25
+ | `ensureIndex`
26
+ | `markIndexRemoved`
27
+ | `pullSince`
28
+ | `getStreamPosition`
29
+
30
+ export type ElectronPersistencePayloadMap = {
31
+ loadSubset: {
32
+ options: LoadSubsetOptions
33
+ ctx?: { requiredIndexSignatures?: ReadonlyArray<string> }
34
+ }
35
+ loadCollectionMetadata: {}
36
+ scanRows: {
37
+ options?: {
38
+ metadataOnly?: boolean
39
+ }
40
+ }
41
+ applyCommittedTx: {
42
+ tx: PersistedTx<ElectronPersistedRow, ElectronPersistedKey>
43
+ }
44
+ ensureIndex: {
45
+ signature: string
46
+ spec: PersistedIndexSpec
47
+ }
48
+ markIndexRemoved: {
49
+ signature: string
50
+ }
51
+ pullSince: {
52
+ fromRowVersion: number
53
+ }
54
+ getStreamPosition: {}
55
+ }
56
+
57
+ export type ElectronPersistenceResultMap = {
58
+ loadSubset: Array<{ key: ElectronPersistedKey; value: ElectronPersistedRow }>
59
+ loadCollectionMetadata: Array<{ key: string; value: unknown }>
60
+ scanRows: Array<{
61
+ key: ElectronPersistedKey
62
+ value: ElectronPersistedRow
63
+ metadata?: unknown
64
+ }>
65
+ applyCommittedTx: null
66
+ ensureIndex: null
67
+ markIndexRemoved: null
68
+ pullSince: SQLitePullSinceResult<ElectronPersistedKey>
69
+ getStreamPosition: {
70
+ latestTerm: number
71
+ latestSeq: number
72
+ latestRowVersion: number
73
+ }
74
+ }
75
+
76
+ export type ElectronSerializedError = {
77
+ name: string
78
+ message: string
79
+ stack?: string
80
+ code?: string
81
+ }
82
+
83
+ export type ElectronPersistenceRequestByMethod = {
84
+ [Method in ElectronPersistenceMethod]: {
85
+ v: number
86
+ requestId: string
87
+ collectionId: string
88
+ resolution?: ElectronPersistenceResolution
89
+ method: Method
90
+ payload: ElectronPersistencePayloadMap[Method]
91
+ }
92
+ }
93
+
94
+ export type ElectronPersistenceRequest<
95
+ TMethod extends ElectronPersistenceMethod = ElectronPersistenceMethod,
96
+ > = ElectronPersistenceRequestByMethod[TMethod]
97
+
98
+ export type ElectronPersistenceRequestEnvelope =
99
+ ElectronPersistenceRequestByMethod[ElectronPersistenceMethod]
100
+
101
+ type ElectronPersistenceSuccessResponseByMethod = {
102
+ [Method in ElectronPersistenceMethod]: {
103
+ v: number
104
+ requestId: string
105
+ method: Method
106
+ ok: true
107
+ result: ElectronPersistenceResultMap[Method]
108
+ }
109
+ }
110
+
111
+ type ElectronPersistenceErrorResponseByMethod = {
112
+ [Method in ElectronPersistenceMethod]: {
113
+ v: number
114
+ requestId: string
115
+ method: Method
116
+ ok: false
117
+ error: ElectronSerializedError
118
+ }
119
+ }
120
+
121
+ export type ElectronPersistenceResponse<
122
+ TMethod extends ElectronPersistenceMethod = ElectronPersistenceMethod,
123
+ > =
124
+ | ElectronPersistenceSuccessResponseByMethod[TMethod]
125
+ | ElectronPersistenceErrorResponseByMethod[TMethod]
126
+
127
+ export type ElectronPersistenceResponseEnvelope =
128
+ ElectronPersistenceResponse<ElectronPersistenceMethod>
129
+
130
+ export type ElectronPersistenceRequestHandler = (
131
+ request: ElectronPersistenceRequestEnvelope,
132
+ ) => Promise<ElectronPersistenceResponseEnvelope>
133
+
134
+ export type ElectronPersistenceInvoke = (
135
+ channel: string,
136
+ request: ElectronPersistenceRequestEnvelope,
137
+ ) => Promise<ElectronPersistenceResponseEnvelope>