@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,305 @@
1
+ import SQLiteESMFactory from '@journeyapps/wa-sqlite/dist/wa-sqlite.mjs'
2
+ import * as SQLite from '@journeyapps/wa-sqlite'
3
+ import { OPFSCoopSyncVFS } from '@journeyapps/wa-sqlite/src/examples/OPFSCoopSyncVFS.js'
4
+ import {
5
+ InvalidPersistedCollectionConfigError,
6
+ PersistenceUnavailableError,
7
+ } from '@tanstack/db-sqlite-persistence-core'
8
+ import type {
9
+ BrowserOPFSWorkerErrorCode,
10
+ BrowserOPFSWorkerRequest,
11
+ BrowserOPFSWorkerResponse,
12
+ } from './opfs-worker-protocol'
13
+
14
+ const workerGlobal = globalThis as typeof globalThis & {
15
+ postMessage: (message: BrowserOPFSWorkerResponse) => void
16
+ addEventListener: (
17
+ type: `message`,
18
+ listener: (event: MessageEvent<BrowserOPFSWorkerRequest>) => void,
19
+ ) => void
20
+ }
21
+
22
+ const SQLITE_ROW = 100
23
+ const SQLITE_DONE = 101
24
+
25
+ type SQLiteCompatibleBinding =
26
+ | null
27
+ | string
28
+ | number
29
+ | bigint
30
+ | Uint8Array
31
+ | Array<number>
32
+
33
+ type WorkerOPFSGlobal = {
34
+ navigator?: {
35
+ storage?: {
36
+ getDirectory?: () => Promise<unknown>
37
+ }
38
+ }
39
+ FileSystemFileHandle?: {
40
+ prototype?: {
41
+ createSyncAccessHandle?: () => Promise<unknown>
42
+ }
43
+ }
44
+ }
45
+
46
+ let sqlite3Instance: ReturnType<typeof SQLite.Factory> | null = null
47
+ let sqliteDatabaseHandle: number | null = null
48
+ let opfsVfsInstance: { close?: () => Promise<void> | void } | null = null
49
+
50
+ function hasWorkerOPFSSyncAccessSupport(globalObject: unknown): boolean {
51
+ const candidate = globalObject as WorkerOPFSGlobal
52
+ const getDirectory = candidate.navigator?.storage?.getDirectory
53
+ const createSyncAccessHandle =
54
+ candidate.FileSystemFileHandle?.prototype?.createSyncAccessHandle
55
+
56
+ return (
57
+ typeof getDirectory === `function` &&
58
+ typeof createSyncAccessHandle === `function`
59
+ )
60
+ }
61
+
62
+ function toBindableValue(value: unknown): SQLiteCompatibleBinding {
63
+ if (value === null || value === undefined) {
64
+ return null
65
+ }
66
+
67
+ if (typeof value === `string`) {
68
+ return value
69
+ }
70
+
71
+ if (typeof value === `number`) {
72
+ return Number.isFinite(value) ? value : null
73
+ }
74
+
75
+ if (typeof value === `bigint`) {
76
+ return value
77
+ }
78
+
79
+ if (typeof value === `boolean`) {
80
+ return value ? 1 : 0
81
+ }
82
+
83
+ if (value instanceof Date) {
84
+ return value.toISOString()
85
+ }
86
+
87
+ if (value instanceof Uint8Array) {
88
+ return value
89
+ }
90
+
91
+ if (
92
+ Array.isArray(value) &&
93
+ value.every((entry) => typeof entry === `number` && Number.isFinite(entry))
94
+ ) {
95
+ return [...value]
96
+ }
97
+
98
+ throw new InvalidPersistedCollectionConfigError(
99
+ `Unsupported parameter type for wa-sqlite binding`,
100
+ )
101
+ }
102
+
103
+ function toErrorCode(error: unknown): BrowserOPFSWorkerErrorCode {
104
+ if (error instanceof PersistenceUnavailableError) {
105
+ return `PERSISTENCE_UNAVAILABLE`
106
+ }
107
+ if (error instanceof InvalidPersistedCollectionConfigError) {
108
+ return `INVALID_CONFIG`
109
+ }
110
+ return `INTERNAL`
111
+ }
112
+
113
+ async function closeWorkerDatabase(): Promise<void> {
114
+ const sqlite3 = sqlite3Instance
115
+ const dbHandle = sqliteDatabaseHandle
116
+ const vfs = opfsVfsInstance
117
+
118
+ sqlite3Instance = null
119
+ sqliteDatabaseHandle = null
120
+ opfsVfsInstance = null
121
+
122
+ let closeError: unknown
123
+ if (sqlite3 && dbHandle !== null) {
124
+ try {
125
+ await sqlite3.close(dbHandle)
126
+ } catch (error) {
127
+ closeError = error
128
+ }
129
+ }
130
+
131
+ try {
132
+ await Promise.resolve(vfs?.close?.())
133
+ } catch (error) {
134
+ closeError ??= error
135
+ }
136
+
137
+ if (closeError) {
138
+ throw closeError
139
+ }
140
+ }
141
+
142
+ async function initializeWorkerDatabase(request: {
143
+ databaseName: string
144
+ vfsName: string
145
+ }): Promise<void> {
146
+ if (!hasWorkerOPFSSyncAccessSupport(workerGlobal)) {
147
+ throw new PersistenceUnavailableError(
148
+ `Browser OPFS sync access is not available in this worker runtime`,
149
+ )
150
+ }
151
+
152
+ await closeWorkerDatabase()
153
+
154
+ const sqliteModule = await SQLiteESMFactory()
155
+ const sqlite3 = SQLite.Factory(sqliteModule)
156
+ const opfsVfs = await OPFSCoopSyncVFS.create(request.vfsName, sqliteModule)
157
+ sqlite3.vfs_register(opfsVfs as never, true)
158
+
159
+ const openFlags =
160
+ SQLite.SQLITE_OPEN_CREATE |
161
+ SQLite.SQLITE_OPEN_READWRITE |
162
+ SQLite.SQLITE_OPEN_URI
163
+ const databaseFileUri = `file:${request.databaseName}?vfs=${encodeURIComponent(request.vfsName)}`
164
+ const dbHandle = await sqlite3.open_v2(
165
+ databaseFileUri,
166
+ openFlags,
167
+ request.vfsName,
168
+ )
169
+
170
+ sqlite3Instance = sqlite3
171
+ sqliteDatabaseHandle = dbHandle
172
+ opfsVfsInstance = opfsVfs
173
+ }
174
+
175
+ function getInitializedDatabaseState(): {
176
+ sqlite3: ReturnType<typeof SQLite.Factory>
177
+ dbHandle: number
178
+ } {
179
+ if (!sqlite3Instance || sqliteDatabaseHandle === null) {
180
+ throw new InvalidPersistedCollectionConfigError(
181
+ `OPFS worker database has not been initialized`,
182
+ )
183
+ }
184
+
185
+ return {
186
+ sqlite3: sqlite3Instance,
187
+ dbHandle: sqliteDatabaseHandle,
188
+ }
189
+ }
190
+
191
+ async function executeSqlInWorker(
192
+ sql: string,
193
+ params: ReadonlyArray<unknown>,
194
+ ): Promise<ReadonlyArray<Record<string, unknown>>> {
195
+ const { sqlite3, dbHandle } = getInitializedDatabaseState()
196
+ const rows = new Array<Record<string, unknown>>()
197
+ let parametersBound = false
198
+
199
+ for await (const statement of sqlite3.statements(dbHandle, sql)) {
200
+ if (params.length > 0) {
201
+ if (parametersBound) {
202
+ throw new InvalidPersistedCollectionConfigError(
203
+ `wa-sqlite worker only supports parameter binding for a single SQL statement`,
204
+ )
205
+ }
206
+
207
+ sqlite3.bind_collection(
208
+ statement,
209
+ params.map((param) => toBindableValue(param)),
210
+ )
211
+ parametersBound = true
212
+ }
213
+
214
+ let columns = [...sqlite3.column_names(statement)]
215
+ for (;;) {
216
+ const stepResult = await sqlite3.step(statement)
217
+
218
+ if (stepResult === SQLITE_ROW) {
219
+ if (columns.length === 0) {
220
+ columns = [...sqlite3.column_names(statement)]
221
+ }
222
+ const values = sqlite3.row(statement)
223
+ const row: Record<string, unknown> = {}
224
+ for (let columnIndex = 0; columnIndex < columns.length; columnIndex++) {
225
+ const columnName = columns[columnIndex]
226
+ if (!columnName) {
227
+ continue
228
+ }
229
+ row[columnName] = values[columnIndex]
230
+ }
231
+ rows.push(row)
232
+ continue
233
+ }
234
+
235
+ if (stepResult === SQLITE_DONE) {
236
+ break
237
+ }
238
+
239
+ throw new InvalidPersistedCollectionConfigError(
240
+ `wa-sqlite step returned unexpected result code: ${String(stepResult)}`,
241
+ )
242
+ }
243
+ }
244
+
245
+ if (params.length > 0 && !parametersBound) {
246
+ throw new InvalidPersistedCollectionConfigError(
247
+ `SQL query parameters were provided but no statement accepted bindings`,
248
+ )
249
+ }
250
+
251
+ return rows
252
+ }
253
+
254
+ async function handleWorkerRequest(
255
+ request: BrowserOPFSWorkerRequest,
256
+ ): Promise<BrowserOPFSWorkerResponse> {
257
+ try {
258
+ switch (request.type) {
259
+ case `init`:
260
+ await initializeWorkerDatabase({
261
+ databaseName: request.databaseName,
262
+ vfsName: request.vfsName,
263
+ })
264
+ return {
265
+ type: `response`,
266
+ requestId: request.requestId,
267
+ ok: true,
268
+ }
269
+ case `execute`: {
270
+ const rows = await executeSqlInWorker(request.sql, request.params)
271
+ return {
272
+ type: `response`,
273
+ requestId: request.requestId,
274
+ ok: true,
275
+ rows,
276
+ }
277
+ }
278
+ case `close`:
279
+ await closeWorkerDatabase()
280
+ return {
281
+ type: `response`,
282
+ requestId: request.requestId,
283
+ ok: true,
284
+ }
285
+ default:
286
+ throw new InvalidPersistedCollectionConfigError(
287
+ `Unknown OPFS worker request type`,
288
+ )
289
+ }
290
+ } catch (error) {
291
+ return {
292
+ type: `response`,
293
+ requestId: request.requestId,
294
+ ok: false,
295
+ code: toErrorCode(error),
296
+ error: (error as Error).message,
297
+ }
298
+ }
299
+ }
300
+
301
+ workerGlobal.addEventListener(`message`, (event) => {
302
+ void handleWorkerRequest(event.data).then((response) => {
303
+ workerGlobal.postMessage(response)
304
+ })
305
+ })
@@ -0,0 +1 @@
1
+ /// <reference types="vite/client" />
@@ -0,0 +1,153 @@
1
+ import { InvalidPersistedCollectionConfigError } from '@tanstack/db-sqlite-persistence-core'
2
+ import type { SQLiteDriver } from '@tanstack/db-sqlite-persistence-core'
3
+
4
+ export type BrowserWASQLiteDatabase = {
5
+ execute: <TRow = unknown>(
6
+ sql: string,
7
+ params?: ReadonlyArray<unknown>,
8
+ ) => Promise<ReadonlyArray<TRow>>
9
+ close?: () => Promise<void> | void
10
+ }
11
+
12
+ export type BrowserWASQLiteDriverOptions = {
13
+ database: BrowserWASQLiteDatabase
14
+ }
15
+
16
+ function assertTransactionCallbackHasDriverArg(
17
+ fn: (transactionDriver: SQLiteDriver) => Promise<unknown>,
18
+ ): void {
19
+ if (fn.length > 0) {
20
+ return
21
+ }
22
+
23
+ throw new InvalidPersistedCollectionConfigError(
24
+ `SQLiteDriver.transaction callback must accept the transaction driver argument`,
25
+ )
26
+ }
27
+
28
+ function assertDatabaseShape(
29
+ database: BrowserWASQLiteDatabase,
30
+ ): asserts database is BrowserWASQLiteDatabase {
31
+ if (typeof database.execute !== `function`) {
32
+ throw new InvalidPersistedCollectionConfigError(
33
+ `Browser wa-sqlite database handle must provide execute(sql, params?)`,
34
+ )
35
+ }
36
+ }
37
+
38
+ export class BrowserWASQLiteDriver implements SQLiteDriver {
39
+ private readonly database: BrowserWASQLiteDatabase
40
+ private queue: Promise<void> = Promise.resolve()
41
+ private nextSavepointId = 1
42
+ private closed = false
43
+
44
+ constructor(options: BrowserWASQLiteDriverOptions) {
45
+ assertDatabaseShape(options.database)
46
+ this.database = options.database
47
+ }
48
+
49
+ async exec(sql: string): Promise<void> {
50
+ await this.enqueue(async () => {
51
+ await this.database.execute(sql)
52
+ })
53
+ }
54
+
55
+ async query<T>(
56
+ sql: string,
57
+ params: ReadonlyArray<unknown> = [],
58
+ ): Promise<ReadonlyArray<T>> {
59
+ return this.enqueue(() => this.database.execute<T>(sql, params))
60
+ }
61
+
62
+ async run(sql: string, params: ReadonlyArray<unknown> = []): Promise<void> {
63
+ await this.enqueue(async () => {
64
+ await this.database.execute(sql, params)
65
+ })
66
+ }
67
+
68
+ async transaction<T>(
69
+ fn: (transactionDriver: SQLiteDriver) => Promise<T>,
70
+ ): Promise<T> {
71
+ assertTransactionCallbackHasDriverArg(fn)
72
+
73
+ return this.enqueue(async () => {
74
+ await this.database.execute(`BEGIN IMMEDIATE`)
75
+ try {
76
+ const result = await fn(this.createTransactionDriver())
77
+ await this.database.execute(`COMMIT`)
78
+ return result
79
+ } catch (error) {
80
+ try {
81
+ await this.database.execute(`ROLLBACK`)
82
+ } catch {
83
+ // Preserve original transaction error.
84
+ }
85
+ throw error
86
+ }
87
+ })
88
+ }
89
+
90
+ async transactionWithDriver<T>(
91
+ fn: (transactionDriver: SQLiteDriver) => Promise<T>,
92
+ ): Promise<T> {
93
+ return this.transaction(fn)
94
+ }
95
+
96
+ async close(): Promise<void> {
97
+ if (this.closed) {
98
+ return
99
+ }
100
+ this.closed = true
101
+
102
+ if (typeof this.database.close === `function`) {
103
+ await Promise.resolve(this.database.close())
104
+ }
105
+ }
106
+
107
+ getDatabase(): BrowserWASQLiteDatabase {
108
+ return this.database
109
+ }
110
+
111
+ private createTransactionDriver(): SQLiteDriver {
112
+ return {
113
+ exec: (sql) => this.database.execute(sql).then(() => undefined),
114
+ query: <T>(sql: string, params: ReadonlyArray<unknown> = []) =>
115
+ this.database.execute<T>(sql, params),
116
+ run: (sql: string, params: ReadonlyArray<unknown> = []) =>
117
+ this.database.execute(sql, params).then(() => undefined),
118
+ transaction: <T>(fn: (transactionDriver: SQLiteDriver) => Promise<T>) =>
119
+ this.runNestedTransaction(fn),
120
+ transactionWithDriver: <T>(
121
+ fn: (transactionDriver: SQLiteDriver) => Promise<T>,
122
+ ) => this.runNestedTransaction(fn),
123
+ }
124
+ }
125
+
126
+ private async runNestedTransaction<T>(
127
+ fn: (transactionDriver: SQLiteDriver) => Promise<T>,
128
+ ): Promise<T> {
129
+ assertTransactionCallbackHasDriverArg(fn)
130
+
131
+ const savepointName = `tsdb_sp_${this.nextSavepointId}`
132
+ this.nextSavepointId++
133
+ await this.database.execute(`SAVEPOINT ${savepointName}`)
134
+ try {
135
+ const result = await fn(this.createTransactionDriver())
136
+ await this.database.execute(`RELEASE SAVEPOINT ${savepointName}`)
137
+ return result
138
+ } catch (error) {
139
+ await this.database.execute(`ROLLBACK TO SAVEPOINT ${savepointName}`)
140
+ await this.database.execute(`RELEASE SAVEPOINT ${savepointName}`)
141
+ throw error
142
+ }
143
+ }
144
+
145
+ private enqueue<T>(operation: () => Promise<T> | T): Promise<T> {
146
+ const queuedOperation = this.queue.then(operation, operation)
147
+ this.queue = queuedOperation.then(
148
+ () => undefined,
149
+ () => undefined,
150
+ )
151
+ return queuedOperation
152
+ }
153
+ }
@@ -0,0 +1,10 @@
1
+ declare module '@journeyapps/wa-sqlite/src/examples/OPFSCoopSyncVFS.js' {
2
+ export class OPFSCoopSyncVFS {
3
+ static create(
4
+ name: string,
5
+ module: unknown,
6
+ ): Promise<{
7
+ close?: () => Promise<void> | void
8
+ }>
9
+ }
10
+ }