@tanstack/browser-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 (51) hide show
  1. package/README.md +54 -0
  2. package/dist/assets/opfs-worker-CCciqEMo.js +5879 -0
  3. package/dist/assets/opfs-worker-CCciqEMo.js.map +1 -0
  4. package/dist/cjs/browser-coordinator.cjs +541 -0
  5. package/dist/cjs/browser-coordinator.cjs.map +1 -0
  6. package/dist/cjs/browser-coordinator.d.cts +67 -0
  7. package/dist/cjs/browser-persistence.cjs +83 -0
  8. package/dist/cjs/browser-persistence.cjs.map +1 -0
  9. package/dist/cjs/browser-persistence.d.cts +16 -0
  10. package/dist/cjs/index.cjs +14 -0
  11. package/dist/cjs/index.cjs.map +1 -0
  12. package/dist/cjs/index.d.cts +8 -0
  13. package/dist/cjs/opfs-database.cjs +175 -0
  14. package/dist/cjs/opfs-database.cjs.map +1 -0
  15. package/dist/cjs/opfs-database.d.cts +10 -0
  16. package/dist/cjs/opfs-worker-protocol.d.cts +32 -0
  17. package/dist/cjs/opfs-worker.cjs +11 -0
  18. package/dist/cjs/opfs-worker.cjs.map +1 -0
  19. package/dist/cjs/opfs-worker.d.cts +1 -0
  20. package/dist/cjs/wa-sqlite-driver.cjs +106 -0
  21. package/dist/cjs/wa-sqlite-driver.cjs.map +1 -0
  22. package/dist/cjs/wa-sqlite-driver.d.cts +25 -0
  23. package/dist/esm/browser-coordinator.d.ts +67 -0
  24. package/dist/esm/browser-coordinator.js +541 -0
  25. package/dist/esm/browser-coordinator.js.map +1 -0
  26. package/dist/esm/browser-persistence.d.ts +16 -0
  27. package/dist/esm/browser-persistence.js +83 -0
  28. package/dist/esm/browser-persistence.js.map +1 -0
  29. package/dist/esm/index.d.ts +8 -0
  30. package/dist/esm/index.js +11 -0
  31. package/dist/esm/index.js.map +1 -0
  32. package/dist/esm/opfs-database.d.ts +10 -0
  33. package/dist/esm/opfs-database.js +175 -0
  34. package/dist/esm/opfs-database.js.map +1 -0
  35. package/dist/esm/opfs-worker-protocol.d.ts +32 -0
  36. package/dist/esm/opfs-worker.d.ts +1 -0
  37. package/dist/esm/opfs-worker.js +12 -0
  38. package/dist/esm/opfs-worker.js.map +1 -0
  39. package/dist/esm/wa-sqlite-driver.d.ts +25 -0
  40. package/dist/esm/wa-sqlite-driver.js +106 -0
  41. package/dist/esm/wa-sqlite-driver.js.map +1 -0
  42. package/package.json +63 -0
  43. package/src/browser-coordinator.ts +842 -0
  44. package/src/browser-persistence.ts +172 -0
  45. package/src/index.ts +15 -0
  46. package/src/opfs-database.ts +258 -0
  47. package/src/opfs-worker-protocol.ts +47 -0
  48. package/src/opfs-worker.ts +305 -0
  49. package/src/vite-env.d.ts +1 -0
  50. package/src/wa-sqlite-driver.ts +153 -0
  51. package/src/wa-sqlite-examples.d.ts +10 -0
@@ -0,0 +1,172 @@
1
+ import {
2
+ SingleProcessCoordinator,
3
+ createSQLiteCorePersistenceAdapter,
4
+ } from '@tanstack/db-sqlite-persistence-core'
5
+ import { BrowserCollectionCoordinator } from './browser-coordinator'
6
+ import { BrowserWASQLiteDriver } from './wa-sqlite-driver'
7
+ import type {
8
+ PersistedCollectionCoordinator,
9
+ PersistedCollectionMode,
10
+ PersistedCollectionPersistence,
11
+ SQLiteCoreAdapterOptions,
12
+ SQLiteDriver,
13
+ } from '@tanstack/db-sqlite-persistence-core'
14
+ import type { BrowserWASQLiteDatabase } from './wa-sqlite-driver'
15
+
16
+ export type { BrowserWASQLiteDatabase } from './wa-sqlite-driver'
17
+
18
+ type BrowserSQLiteCoreSchemaMismatchPolicy =
19
+ | `sync-present-reset`
20
+ | `sync-absent-error`
21
+ | `reset`
22
+
23
+ export type BrowserWASQLiteSchemaMismatchPolicy =
24
+ | BrowserSQLiteCoreSchemaMismatchPolicy
25
+ | `throw`
26
+
27
+ export type BrowserWASQLitePersistenceOptions = Omit<
28
+ SQLiteCoreAdapterOptions,
29
+ `driver` | `schemaVersion` | `schemaMismatchPolicy`
30
+ > & {
31
+ database: BrowserWASQLiteDatabase
32
+ coordinator?: PersistedCollectionCoordinator
33
+ schemaMismatchPolicy?: BrowserWASQLiteSchemaMismatchPolicy
34
+ }
35
+
36
+ function normalizeSchemaMismatchPolicy(
37
+ policy: BrowserWASQLiteSchemaMismatchPolicy,
38
+ ): BrowserSQLiteCoreSchemaMismatchPolicy {
39
+ if (policy === `throw`) {
40
+ return `sync-absent-error`
41
+ }
42
+
43
+ return policy
44
+ }
45
+
46
+ function resolveSchemaMismatchPolicy(
47
+ explicitPolicy: BrowserWASQLiteSchemaMismatchPolicy | undefined,
48
+ mode: PersistedCollectionMode,
49
+ ): BrowserSQLiteCoreSchemaMismatchPolicy {
50
+ if (explicitPolicy) {
51
+ return normalizeSchemaMismatchPolicy(explicitPolicy)
52
+ }
53
+
54
+ return mode === `sync-present` ? `sync-present-reset` : `sync-absent-error`
55
+ }
56
+
57
+ function createAdapterCacheKey(
58
+ schemaMismatchPolicy: BrowserSQLiteCoreSchemaMismatchPolicy,
59
+ schemaVersion: number | undefined,
60
+ ): string {
61
+ const schemaVersionKey =
62
+ schemaVersion === undefined ? `schema:default` : `schema:${schemaVersion}`
63
+ return `${schemaMismatchPolicy}|${schemaVersionKey}`
64
+ }
65
+
66
+ function createInternalSQLiteDriver(
67
+ options: BrowserWASQLitePersistenceOptions,
68
+ ): SQLiteDriver {
69
+ return new BrowserWASQLiteDriver({
70
+ database: options.database,
71
+ })
72
+ }
73
+
74
+ function resolveAdapterBaseOptions(
75
+ options: BrowserWASQLitePersistenceOptions,
76
+ ): Omit<
77
+ SQLiteCoreAdapterOptions,
78
+ `driver` | `schemaVersion` | `schemaMismatchPolicy`
79
+ > {
80
+ return {
81
+ appliedTxPruneMaxRows: options.appliedTxPruneMaxRows,
82
+ appliedTxPruneMaxAgeSeconds: options.appliedTxPruneMaxAgeSeconds,
83
+ pullSinceReloadThreshold: options.pullSinceReloadThreshold,
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Creates a shared browser wa-sqlite persistence instance that can be reused by
89
+ * many collections on the same database. This is single-tab wiring using
90
+ * SingleProcessCoordinator semantics (no election required).
91
+ */
92
+ export function createBrowserWASQLitePersistence<
93
+ T extends object,
94
+ TKey extends string | number = string | number,
95
+ >(
96
+ options: BrowserWASQLitePersistenceOptions,
97
+ ): PersistedCollectionPersistence<T, TKey> {
98
+ const { coordinator, schemaMismatchPolicy } = options
99
+ const driver = createInternalSQLiteDriver(options)
100
+ const adapterBaseOptions = resolveAdapterBaseOptions(options)
101
+ const resolvedCoordinator = coordinator ?? new SingleProcessCoordinator()
102
+ const adapterCache = new Map<
103
+ string,
104
+ ReturnType<
105
+ typeof createSQLiteCorePersistenceAdapter<
106
+ Record<string, unknown>,
107
+ string | number
108
+ >
109
+ >
110
+ >()
111
+
112
+ const getAdapterForCollection = (
113
+ mode: PersistedCollectionMode,
114
+ schemaVersion: number | undefined,
115
+ ) => {
116
+ const resolvedSchemaMismatchPolicy = resolveSchemaMismatchPolicy(
117
+ schemaMismatchPolicy,
118
+ mode,
119
+ )
120
+ const cacheKey = createAdapterCacheKey(
121
+ resolvedSchemaMismatchPolicy,
122
+ schemaVersion,
123
+ )
124
+ const cachedAdapter = adapterCache.get(cacheKey)
125
+ if (cachedAdapter) {
126
+ return cachedAdapter
127
+ }
128
+
129
+ const adapter = createSQLiteCorePersistenceAdapter<
130
+ Record<string, unknown>,
131
+ string | number
132
+ >({
133
+ ...adapterBaseOptions,
134
+ driver,
135
+ schemaMismatchPolicy: resolvedSchemaMismatchPolicy,
136
+ ...(schemaVersion === undefined ? {} : { schemaVersion }),
137
+ })
138
+ adapterCache.set(cacheKey, adapter)
139
+
140
+ // Wire the adapter into the multi-tab coordinator so it can handle
141
+ // leader-side RPCs (applyCommittedTx, pullSince, ensureIndex, etc.)
142
+ if (resolvedCoordinator instanceof BrowserCollectionCoordinator) {
143
+ resolvedCoordinator.setAdapter(adapter)
144
+ }
145
+
146
+ return adapter
147
+ }
148
+
149
+ const createCollectionPersistence = (
150
+ mode: PersistedCollectionMode,
151
+ schemaVersion: number | undefined,
152
+ ): PersistedCollectionPersistence<T, TKey> => ({
153
+ adapter: getAdapterForCollection(
154
+ mode,
155
+ schemaVersion,
156
+ ) as unknown as PersistedCollectionPersistence<T, TKey>[`adapter`],
157
+ coordinator: resolvedCoordinator,
158
+ })
159
+
160
+ const defaultPersistence = createCollectionPersistence(
161
+ `sync-absent`,
162
+ undefined,
163
+ )
164
+
165
+ return {
166
+ ...defaultPersistence,
167
+ resolvePersistenceForCollection: ({ mode, schemaVersion }) =>
168
+ createCollectionPersistence(mode, schemaVersion),
169
+ resolvePersistenceForMode: (mode) =>
170
+ createCollectionPersistence(mode, undefined),
171
+ }
172
+ }
package/src/index.ts ADDED
@@ -0,0 +1,15 @@
1
+ export { createBrowserWASQLitePersistence } from './browser-persistence'
2
+ export { openBrowserWASQLiteOPFSDatabase } from './opfs-database'
3
+ export { BrowserCollectionCoordinator } from './browser-coordinator'
4
+ export type {
5
+ BrowserWASQLiteDatabase,
6
+ BrowserWASQLitePersistenceOptions,
7
+ BrowserWASQLiteSchemaMismatchPolicy,
8
+ } from './browser-persistence'
9
+ export type { OpenBrowserWASQLiteOPFSDatabaseOptions } from './opfs-database'
10
+ export type { BrowserCollectionCoordinatorOptions } from './browser-coordinator'
11
+ export { persistedCollectionOptions } from '@tanstack/db-sqlite-persistence-core'
12
+ export type {
13
+ PersistedCollectionCoordinator,
14
+ PersistedCollectionPersistence,
15
+ } from '@tanstack/db-sqlite-persistence-core'
@@ -0,0 +1,258 @@
1
+ import {
2
+ InvalidPersistedCollectionConfigError,
3
+ PersistenceUnavailableError,
4
+ } from '@tanstack/db-sqlite-persistence-core'
5
+ import OPFSWorkerConstructor from './opfs-worker?worker'
6
+ import type { BrowserWASQLiteDatabase } from './wa-sqlite-driver'
7
+ import type {
8
+ BrowserOPFSWorkerErrorCode,
9
+ BrowserOPFSWorkerRequest,
10
+ BrowserOPFSWorkerResponse,
11
+ } from './opfs-worker-protocol'
12
+
13
+ const DEFAULT_VFS_NAME = `opfs`
14
+ type BrowserOPFSFeatureGlobal = {
15
+ navigator?: {
16
+ storage?: {
17
+ getDirectory?: () => Promise<unknown>
18
+ }
19
+ }
20
+ Worker?: typeof Worker
21
+ }
22
+
23
+ export type OpenBrowserWASQLiteOPFSDatabaseOptions = {
24
+ databaseName: string
25
+ vfsName?: string
26
+ }
27
+
28
+ type BrowserOPFSWorkerLike = Pick<
29
+ Worker,
30
+ `postMessage` | `terminate` | `addEventListener` | `removeEventListener`
31
+ >
32
+
33
+ type PendingWorkerRequest = {
34
+ resolve: (value: unknown) => void
35
+ reject: (error: Error) => void
36
+ }
37
+
38
+ type BrowserOPFSWorkerRequestWithoutId =
39
+ BrowserOPFSWorkerRequest extends infer TRequest
40
+ ? TRequest extends { requestId: string }
41
+ ? Omit<TRequest, `requestId`>
42
+ : never
43
+ : never
44
+
45
+ class OPFSWorkerRequestError extends Error {
46
+ constructor(
47
+ readonly code: BrowserOPFSWorkerErrorCode,
48
+ message: string,
49
+ ) {
50
+ super(message)
51
+ this.name = `OPFSWorkerRequestError`
52
+ }
53
+ }
54
+
55
+ function hasOPFSBrowserPrerequisites(globalObject: unknown): boolean {
56
+ const candidate = globalObject as BrowserOPFSFeatureGlobal
57
+ const getDirectory = candidate.navigator?.storage?.getDirectory
58
+ const WorkerConstructor = candidate.Worker
59
+
60
+ return (
61
+ typeof getDirectory === `function` &&
62
+ typeof WorkerConstructor === `function`
63
+ )
64
+ }
65
+
66
+ function createWorkerError(
67
+ code: BrowserOPFSWorkerErrorCode,
68
+ message: string,
69
+ ): Error {
70
+ if (code === `PERSISTENCE_UNAVAILABLE`) {
71
+ return new PersistenceUnavailableError(message)
72
+ }
73
+ if (code === `INVALID_CONFIG`) {
74
+ return new InvalidPersistedCollectionConfigError(message)
75
+ }
76
+ return new OPFSWorkerRequestError(code, message)
77
+ }
78
+
79
+ function createWorkerRequestIdFactory(): () => string {
80
+ let sequence = 0
81
+ const prefix = `opfs-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`
82
+
83
+ return () => {
84
+ sequence++
85
+ return `${prefix}-${sequence}`
86
+ }
87
+ }
88
+
89
+ function createOPFSWorkerInstance(): BrowserOPFSWorkerLike {
90
+ const WorkerConstructor = (
91
+ globalThis as typeof globalThis & { Worker?: typeof Worker }
92
+ ).Worker
93
+ if (typeof WorkerConstructor !== `function`) {
94
+ throw new PersistenceUnavailableError(
95
+ `Web Worker support is required for browser OPFS persistence`,
96
+ )
97
+ }
98
+
99
+ return new (OPFSWorkerConstructor as unknown as new () => BrowserOPFSWorkerLike)()
100
+ }
101
+
102
+ /**
103
+ * Creates a browser wa-sqlite database handle backed by OPFS and
104
+ * OPFSCoopSyncVFS.
105
+ */
106
+ export async function openBrowserWASQLiteOPFSDatabase(
107
+ options: OpenBrowserWASQLiteOPFSDatabaseOptions,
108
+ ): Promise<BrowserWASQLiteDatabase> {
109
+ const databaseName = options.databaseName.trim()
110
+ if (databaseName.length === 0) {
111
+ throw new InvalidPersistedCollectionConfigError(
112
+ `Browser wa-sqlite databaseName cannot be empty`,
113
+ )
114
+ }
115
+
116
+ if (!hasOPFSBrowserPrerequisites(globalThis)) {
117
+ throw new PersistenceUnavailableError(
118
+ `Browser OPFS prerequisites are not available in this runtime`,
119
+ )
120
+ }
121
+
122
+ const vfsName = options.vfsName ?? DEFAULT_VFS_NAME
123
+ const worker = createOPFSWorkerInstance()
124
+ const nextRequestId = createWorkerRequestIdFactory()
125
+ const pendingRequests = new Map<string, PendingWorkerRequest>()
126
+ let disposed = false
127
+
128
+ const disposeWorker = (): void => {
129
+ if (disposed) {
130
+ return
131
+ }
132
+ disposed = true
133
+ worker.removeEventListener(`message`, onMessage)
134
+ worker.removeEventListener(`error`, onError)
135
+ worker.removeEventListener(`messageerror`, onMessageError)
136
+ worker.terminate()
137
+ }
138
+
139
+ const rejectAllPendingRequests = (error: Error): void => {
140
+ for (const pendingRequest of pendingRequests.values()) {
141
+ pendingRequest.reject(error)
142
+ }
143
+ pendingRequests.clear()
144
+ }
145
+
146
+ const onMessage = (event: Event): void => {
147
+ const messageEvent = event as MessageEvent<BrowserOPFSWorkerResponse>
148
+ const response = messageEvent.data
149
+
150
+ const pendingRequest = pendingRequests.get(response.requestId)
151
+ if (!pendingRequest) {
152
+ return
153
+ }
154
+ pendingRequests.delete(response.requestId)
155
+
156
+ if (response.ok) {
157
+ pendingRequest.resolve(response.rows ?? [])
158
+ return
159
+ }
160
+
161
+ pendingRequest.reject(createWorkerError(response.code, response.error))
162
+ }
163
+
164
+ const onError = (): void => {
165
+ rejectAllPendingRequests(
166
+ new PersistenceUnavailableError(`OPFS worker terminated unexpectedly`),
167
+ )
168
+ disposeWorker()
169
+ }
170
+
171
+ const onMessageError = (): void => {
172
+ rejectAllPendingRequests(
173
+ new PersistenceUnavailableError(
174
+ `OPFS worker message serialization failed`,
175
+ ),
176
+ )
177
+ disposeWorker()
178
+ }
179
+
180
+ worker.addEventListener(`message`, onMessage)
181
+ worker.addEventListener(`error`, onError)
182
+ worker.addEventListener(`messageerror`, onMessageError)
183
+
184
+ const sendWorkerRequest = <T>(
185
+ request: BrowserOPFSWorkerRequestWithoutId,
186
+ ): Promise<T> => {
187
+ if (disposed) {
188
+ return Promise.reject(
189
+ new InvalidPersistedCollectionConfigError(
190
+ `Browser OPFS worker connection is closed`,
191
+ ),
192
+ )
193
+ }
194
+
195
+ const requestId = nextRequestId()
196
+ return new Promise<T>((resolve, reject) => {
197
+ pendingRequests.set(requestId, {
198
+ resolve: (value) => {
199
+ resolve(value as T)
200
+ },
201
+ reject,
202
+ })
203
+ try {
204
+ const payload = {
205
+ ...request,
206
+ requestId,
207
+ } as BrowserOPFSWorkerRequest
208
+ worker.postMessage(payload)
209
+ } catch (error) {
210
+ pendingRequests.delete(requestId)
211
+ reject(
212
+ new PersistenceUnavailableError(
213
+ `Failed to send message to OPFS worker: ${(error as Error).message}`,
214
+ ),
215
+ )
216
+ }
217
+ })
218
+ }
219
+
220
+ try {
221
+ await sendWorkerRequest<void>({
222
+ type: `init`,
223
+ databaseName,
224
+ vfsName,
225
+ })
226
+ } catch (error) {
227
+ disposeWorker()
228
+ throw error
229
+ }
230
+
231
+ return {
232
+ execute: <TRow = unknown>(
233
+ sql: string,
234
+ params: ReadonlyArray<unknown> = [],
235
+ ) =>
236
+ sendWorkerRequest<ReadonlyArray<TRow>>({
237
+ type: `execute`,
238
+ sql,
239
+ params,
240
+ }),
241
+ close: async () => {
242
+ let closeError: unknown
243
+ try {
244
+ await sendWorkerRequest<void>({
245
+ type: `close`,
246
+ })
247
+ } catch (error) {
248
+ closeError = error
249
+ } finally {
250
+ disposeWorker()
251
+ }
252
+
253
+ if (closeError) {
254
+ throw closeError
255
+ }
256
+ },
257
+ }
258
+ }
@@ -0,0 +1,47 @@
1
+ export type BrowserOPFSWorkerErrorCode =
2
+ | `INVALID_CONFIG`
3
+ | `PERSISTENCE_UNAVAILABLE`
4
+ | `INTERNAL`
5
+
6
+ export type BrowserOPFSWorkerInitRequest = {
7
+ type: `init`
8
+ requestId: string
9
+ databaseName: string
10
+ vfsName: string
11
+ }
12
+
13
+ export type BrowserOPFSWorkerExecuteRequest = {
14
+ type: `execute`
15
+ requestId: string
16
+ sql: string
17
+ params: ReadonlyArray<unknown>
18
+ }
19
+
20
+ export type BrowserOPFSWorkerCloseRequest = {
21
+ type: `close`
22
+ requestId: string
23
+ }
24
+
25
+ export type BrowserOPFSWorkerRequest =
26
+ | BrowserOPFSWorkerInitRequest
27
+ | BrowserOPFSWorkerExecuteRequest
28
+ | BrowserOPFSWorkerCloseRequest
29
+
30
+ export type BrowserOPFSWorkerSuccessResponse = {
31
+ type: `response`
32
+ requestId: string
33
+ ok: true
34
+ rows?: ReadonlyArray<Record<string, unknown>>
35
+ }
36
+
37
+ export type BrowserOPFSWorkerErrorResponse = {
38
+ type: `response`
39
+ requestId: string
40
+ ok: false
41
+ code: BrowserOPFSWorkerErrorCode
42
+ error: string
43
+ }
44
+
45
+ export type BrowserOPFSWorkerResponse =
46
+ | BrowserOPFSWorkerSuccessResponse
47
+ | BrowserOPFSWorkerErrorResponse