@tanstack/browser-db-sqlite-persistence 0.2.23 → 0.2.26

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 +75 -1
  2. package/dist/assets/{opfs-worker-uyjx5Rus.js → opfs-worker-accoEoax.js} +17 -6
  3. package/dist/assets/{opfs-worker-uyjx5Rus.js.map → opfs-worker-accoEoax.js.map} +1 -1
  4. package/dist/cjs/browser-coordinator.cjs +5 -534
  5. package/dist/cjs/browser-coordinator.cjs.map +1 -1
  6. package/dist/cjs/browser-coordinator.d.cts +3 -65
  7. package/dist/cjs/browser-persistence.cjs +19 -8
  8. package/dist/cjs/browser-persistence.cjs.map +1 -1
  9. package/dist/cjs/index.cjs +16 -0
  10. package/dist/cjs/index.cjs.map +1 -1
  11. package/dist/cjs/index.d.cts +2 -2
  12. package/dist/cjs/opfs-database.cjs +58 -1
  13. package/dist/cjs/opfs-database.cjs.map +1 -1
  14. package/dist/cjs/opfs-database.d.cts +4 -0
  15. package/dist/cjs/opfs-worker.cjs +1 -1
  16. package/dist/cjs/wa-sqlite-driver.cjs +38 -14
  17. package/dist/cjs/wa-sqlite-driver.cjs.map +1 -1
  18. package/dist/cjs/wa-sqlite-driver.d.cts +3 -3
  19. package/dist/esm/browser-coordinator.d.ts +3 -65
  20. package/dist/esm/browser-coordinator.js +5 -534
  21. package/dist/esm/browser-coordinator.js.map +1 -1
  22. package/dist/esm/browser-persistence.js +19 -8
  23. package/dist/esm/browser-persistence.js.map +1 -1
  24. package/dist/esm/index.d.ts +2 -2
  25. package/dist/esm/index.js +5 -1
  26. package/dist/esm/opfs-database.d.ts +4 -0
  27. package/dist/esm/opfs-database.js +58 -1
  28. package/dist/esm/opfs-database.js.map +1 -1
  29. package/dist/esm/opfs-worker.js +1 -1
  30. package/dist/esm/wa-sqlite-driver.d.ts +3 -3
  31. package/dist/esm/wa-sqlite-driver.js +39 -15
  32. package/dist/esm/wa-sqlite-driver.js.map +1 -1
  33. package/package.json +9 -3
  34. package/src/browser-coordinator.ts +10 -834
  35. package/src/browser-persistence.ts +22 -14
  36. package/src/index.ts +9 -0
  37. package/src/opfs-database.ts +76 -1
  38. package/src/opfs-worker.ts +16 -5
  39. package/src/wa-sqlite-driver.ts +57 -15
  40. package/src/wa-sqlite-examples.d.ts +1 -0
@@ -130,34 +130,42 @@ export function createBrowserWASQLitePersistence(
130
130
  ...(schemaVersion === undefined ? {} : { schemaVersion }),
131
131
  })
132
132
  adapterCache.set(cacheKey, adapter)
133
-
134
- // Wire the adapter into the multi-tab coordinator so it can handle
135
- // leader-side RPCs (applyCommittedTx, pullSince, ensureIndex, etc.)
136
- if (resolvedCoordinator instanceof BrowserCollectionCoordinator) {
137
- resolvedCoordinator.setAdapter(adapter)
138
- }
139
-
140
133
  return adapter
141
134
  }
142
135
 
143
136
  const createCollectionPersistence = (
137
+ collectionId: string | undefined,
144
138
  mode: PersistedCollectionMode,
145
139
  schemaVersion: number | undefined,
146
- ): PersistedCollectionPersistence => ({
147
- adapter: getAdapterForCollection(mode, schemaVersion),
148
- coordinator: resolvedCoordinator,
149
- })
140
+ ): PersistedCollectionPersistence => {
141
+ const adapter = getAdapterForCollection(mode, schemaVersion)
142
+ if (resolvedCoordinator instanceof BrowserCollectionCoordinator) {
143
+ if (collectionId === undefined) {
144
+ resolvedCoordinator.setAdapter(adapter)
145
+ } else {
146
+ resolvedCoordinator.setAdapterForCollection(collectionId, adapter)
147
+ }
148
+ }
149
+ return {
150
+ adapter,
151
+ coordinator: resolvedCoordinator,
152
+ }
153
+ }
150
154
 
151
155
  const defaultPersistence = createCollectionPersistence(
156
+ undefined,
152
157
  `sync-absent`,
153
158
  undefined,
154
159
  )
160
+ if (resolvedCoordinator instanceof BrowserCollectionCoordinator) {
161
+ resolvedCoordinator.setAdapter(defaultPersistence.adapter)
162
+ }
155
163
 
156
164
  return {
157
165
  ...defaultPersistence,
158
- resolvePersistenceForCollection: ({ mode, schemaVersion }) =>
159
- createCollectionPersistence(mode, schemaVersion),
166
+ resolvePersistenceForCollection: ({ collectionId, mode, schemaVersion }) =>
167
+ createCollectionPersistence(collectionId, mode, schemaVersion),
160
168
  resolvePersistenceForMode: (mode) =>
161
- createCollectionPersistence(mode, undefined),
169
+ createCollectionPersistence(undefined, mode, undefined),
162
170
  }
163
171
  }
package/src/index.ts CHANGED
@@ -11,9 +11,18 @@ export type { BrowserCollectionCoordinatorOptions } from './browser-coordinator'
11
11
  export {
12
12
  DEFAULT_APPLIED_TX_PRUNE_MAX_AGE_SECONDS,
13
13
  DEFAULT_APPLIED_TX_PRUNE_MAX_ROWS,
14
+ DuplicateRemoteSubsetOwnerError,
15
+ IndeterminateCommitError,
16
+ PersistedCollectionDurabilityError,
17
+ RemoteSubsetWireValueError,
14
18
  persistedCollectionOptions,
15
19
  } from '@tanstack/db-sqlite-persistence-core'
16
20
  export type {
17
21
  PersistedCollectionCoordinator,
18
22
  PersistedCollectionPersistence,
23
+ IndeterminateCommitRequestType,
24
+ RemoteSubsetOwner,
25
+ RemoteSubsetWireExpression,
26
+ RemoteSubsetWireValue,
27
+ TransportedLoadSubsetOptions,
19
28
  } from '@tanstack/db-sqlite-persistence-core'
@@ -11,6 +11,8 @@ import type {
11
11
  } from './opfs-worker-protocol'
12
12
 
13
13
  const DEFAULT_VFS_NAME = `opfs`
14
+ const DEFAULT_OPEN_TIMEOUT_MS = 30_000
15
+ const MAX_OPEN_TIMEOUT_MS = 2_147_483_647
14
16
  type BrowserOPFSFeatureGlobal = {
15
17
  navigator?: {
16
18
  storage?: {
@@ -23,6 +25,10 @@ type BrowserOPFSFeatureGlobal = {
23
25
  export type OpenBrowserWASQLiteOPFSDatabaseOptions = {
24
26
  databaseName: string
25
27
  vfsName?: string
28
+ /** Defaults to 30 seconds. Set to 0 to wait without a deadline. */
29
+ timeoutMs?: number
30
+ /** Cancels only the database open, not an established connection. */
31
+ signal?: AbortSignal
26
32
  }
27
33
 
28
34
  type BrowserOPFSWorkerLike = Pick<
@@ -99,6 +105,16 @@ function createOPFSWorkerInstance(): BrowserOPFSWorkerLike {
99
105
  return new (OPFSWorkerConstructor as unknown as new () => BrowserOPFSWorkerLike)()
100
106
  }
101
107
 
108
+ function getOpenAbortError(signal: AbortSignal): Error {
109
+ const reason: unknown = signal.reason
110
+ return reason instanceof Error
111
+ ? reason
112
+ : new DOMException(
113
+ `Opening browser OPFS database was aborted`,
114
+ `AbortError`,
115
+ )
116
+ }
117
+
102
118
  /**
103
119
  * Creates a browser wa-sqlite database handle backed by OPFS and
104
120
  * OPFSCoopSyncVFS.
@@ -113,6 +129,22 @@ export async function openBrowserWASQLiteOPFSDatabase(
113
129
  )
114
130
  }
115
131
 
132
+ const timeoutMs = options.timeoutMs ?? DEFAULT_OPEN_TIMEOUT_MS
133
+ if (
134
+ !Number.isSafeInteger(timeoutMs) ||
135
+ timeoutMs < 0 ||
136
+ timeoutMs > MAX_OPEN_TIMEOUT_MS
137
+ ) {
138
+ throw new InvalidPersistedCollectionConfigError(
139
+ `Browser wa-sqlite timeoutMs must be an integer between 0 and ${MAX_OPEN_TIMEOUT_MS}`,
140
+ )
141
+ }
142
+
143
+ const signal = options.signal
144
+ if (signal?.aborted) {
145
+ throw getOpenAbortError(signal)
146
+ }
147
+
116
148
  if (!hasOPFSBrowserPrerequisites(globalThis)) {
117
149
  throw new PersistenceUnavailableError(
118
150
  `Browser OPFS prerequisites are not available in this runtime`,
@@ -130,6 +162,7 @@ export async function openBrowserWASQLiteOPFSDatabase(
130
162
  return
131
163
  }
132
164
  disposed = true
165
+ globalThis.removeEventListener(`pagehide`, onPageHide)
133
166
  worker.removeEventListener(`message`, onMessage)
134
167
  worker.removeEventListener(`error`, onError)
135
168
  worker.removeEventListener(`messageerror`, onMessageError)
@@ -177,9 +210,22 @@ export async function openBrowserWASQLiteOPFSDatabase(
177
210
  disposeWorker()
178
211
  }
179
212
 
213
+ const onPageHide = (): void => {
214
+ // A document entering the back/forward cache can be frozen before an
215
+ // asynchronous close completes, including while initialization is pending.
216
+ rejectAllPendingRequests(
217
+ new DOMException(
218
+ `The page is closing its SQLite connection`,
219
+ `AbortError`,
220
+ ),
221
+ )
222
+ disposeWorker()
223
+ }
224
+
180
225
  worker.addEventListener(`message`, onMessage)
181
226
  worker.addEventListener(`error`, onError)
182
227
  worker.addEventListener(`messageerror`, onMessageError)
228
+ globalThis.addEventListener(`pagehide`, onPageHide)
183
229
 
184
230
  const sendWorkerRequest = <T>(
185
231
  request: BrowserOPFSWorkerRequestWithoutId,
@@ -217,15 +263,40 @@ export async function openBrowserWASQLiteOPFSDatabase(
217
263
  })
218
264
  }
219
265
 
266
+ let openTimeout: ReturnType<typeof setTimeout> | undefined
267
+ const onAbort = (): void => {
268
+ if (!signal) return
269
+ rejectAllPendingRequests(getOpenAbortError(signal))
270
+ disposeWorker()
271
+ }
272
+ const onTimeout = (): void => {
273
+ rejectAllPendingRequests(
274
+ new DOMException(
275
+ `Opening browser OPFS database timed out after ${timeoutMs} ms`,
276
+ `TimeoutError`,
277
+ ),
278
+ )
279
+ disposeWorker()
280
+ }
281
+
220
282
  try {
221
- await sendWorkerRequest<void>({
283
+ const initialization = sendWorkerRequest<void>({
222
284
  type: `init`,
223
285
  databaseName,
224
286
  vfsName,
225
287
  })
288
+ signal?.addEventListener(`abort`, onAbort, { once: true })
289
+ if (signal?.aborted) onAbort()
290
+ if (timeoutMs > 0) {
291
+ openTimeout = setTimeout(onTimeout, timeoutMs)
292
+ }
293
+ await initialization
226
294
  } catch (error) {
227
295
  disposeWorker()
228
296
  throw error
297
+ } finally {
298
+ if (openTimeout !== undefined) clearTimeout(openTimeout)
299
+ signal?.removeEventListener(`abort`, onAbort)
229
300
  }
230
301
 
231
302
  return {
@@ -239,6 +310,10 @@ export async function openBrowserWASQLiteOPFSDatabase(
239
310
  params,
240
311
  }),
241
312
  close: async () => {
313
+ if (disposed) {
314
+ return
315
+ }
316
+
242
317
  let closeError: unknown
243
318
  try {
244
319
  await sendWorkerRequest<void>({
@@ -161,11 +161,22 @@ async function initializeWorkerDatabase(request: {
161
161
  SQLite.SQLITE_OPEN_READWRITE |
162
162
  SQLite.SQLITE_OPEN_URI
163
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
- )
164
+ let dbHandle: number
165
+ try {
166
+ dbHandle = await sqlite3.open_v2(
167
+ databaseFileUri,
168
+ openFlags,
169
+ request.vfsName,
170
+ )
171
+ } catch (error) {
172
+ const cause = opfsVfs.lastError
173
+ if (error instanceof Error && cause instanceof Error) {
174
+ throw new Error(`${error.message}: ${cause.name}: ${cause.message}`, {
175
+ cause,
176
+ })
177
+ }
178
+ throw error
179
+ }
169
180
 
170
181
  sqlite3Instance = sqlite3
171
182
  sqliteDatabaseHandle = dbHandle
@@ -1,4 +1,7 @@
1
- import { InvalidPersistedCollectionConfigError } from '@tanstack/db-sqlite-persistence-core'
1
+ import {
2
+ InvalidPersistedCollectionConfigError,
3
+ SQLITE_DRIVER_SHARED_LOGICAL_SCHEDULING_KEY,
4
+ } from '@tanstack/db-sqlite-persistence-core'
2
5
  import type { SQLiteDriver } from '@tanstack/db-sqlite-persistence-core'
3
6
 
4
7
  export type BrowserWASQLiteDatabase = {
@@ -13,6 +16,32 @@ export type BrowserWASQLiteDriverOptions = {
13
16
  database: BrowserWASQLiteDatabase
14
17
  }
15
18
 
19
+ type DatabaseExecutionState = {
20
+ queue: Promise<void>
21
+ nextSavepointId: number
22
+ }
23
+
24
+ const databaseExecutionStates = new WeakMap<
25
+ BrowserWASQLiteDatabase,
26
+ DatabaseExecutionState
27
+ >()
28
+
29
+ function getDatabaseExecutionState(
30
+ database: BrowserWASQLiteDatabase,
31
+ ): DatabaseExecutionState {
32
+ const existing = databaseExecutionStates.get(database)
33
+ if (existing) {
34
+ return existing
35
+ }
36
+
37
+ const state: DatabaseExecutionState = {
38
+ queue: Promise.resolve(),
39
+ nextSavepointId: 1,
40
+ }
41
+ databaseExecutionStates.set(database, state)
42
+ return state
43
+ }
44
+
16
45
  function assertTransactionCallbackHasDriverArg(
17
46
  fn: (transactionDriver: SQLiteDriver) => Promise<unknown>,
18
47
  ): void {
@@ -36,39 +65,44 @@ function assertDatabaseShape(
36
65
  }
37
66
 
38
67
  export class BrowserWASQLiteDriver implements SQLiteDriver {
68
+ readonly [SQLITE_DRIVER_SHARED_LOGICAL_SCHEDULING_KEY] = {}
39
69
  private readonly database: BrowserWASQLiteDatabase
40
- private queue: Promise<void> = Promise.resolve()
41
- private nextSavepointId = 1
70
+ private readonly executionState: DatabaseExecutionState
42
71
  private closed = false
43
72
 
44
73
  constructor(options: BrowserWASQLiteDriverOptions) {
45
74
  assertDatabaseShape(options.database)
46
75
  this.database = options.database
76
+ this.executionState = getDatabaseExecutionState(this.database)
47
77
  }
48
78
 
49
- async exec(sql: string): Promise<void> {
50
- await this.enqueue(async () => {
79
+ exec(sql: string): Promise<void> {
80
+ return this.enqueue(async () => {
51
81
  await this.database.execute(sql)
52
82
  })
53
83
  }
54
84
 
55
- async query<T>(
85
+ query<T>(
56
86
  sql: string,
57
87
  params: ReadonlyArray<unknown> = [],
58
88
  ): Promise<ReadonlyArray<T>> {
59
89
  return this.enqueue(() => this.database.execute<T>(sql, params))
60
90
  }
61
91
 
62
- async run(sql: string, params: ReadonlyArray<unknown> = []): Promise<void> {
63
- await this.enqueue(async () => {
92
+ run(sql: string, params: ReadonlyArray<unknown> = []): Promise<void> {
93
+ return this.enqueue(async () => {
64
94
  await this.database.execute(sql, params)
65
95
  })
66
96
  }
67
97
 
68
- async transaction<T>(
98
+ transaction<T>(
69
99
  fn: (transactionDriver: SQLiteDriver) => Promise<T>,
70
100
  ): Promise<T> {
71
- assertTransactionCallbackHasDriverArg(fn)
101
+ try {
102
+ assertTransactionCallbackHasDriverArg(fn)
103
+ } catch (error) {
104
+ return Promise.reject(error)
105
+ }
72
106
 
73
107
  return this.enqueue(async () => {
74
108
  await this.database.execute(`BEGIN IMMEDIATE`)
@@ -87,7 +121,7 @@ export class BrowserWASQLiteDriver implements SQLiteDriver {
87
121
  })
88
122
  }
89
123
 
90
- async transactionWithDriver<T>(
124
+ transactionWithDriver<T>(
91
125
  fn: (transactionDriver: SQLiteDriver) => Promise<T>,
92
126
  ): Promise<T> {
93
127
  return this.transaction(fn)
@@ -128,8 +162,8 @@ export class BrowserWASQLiteDriver implements SQLiteDriver {
128
162
  ): Promise<T> {
129
163
  assertTransactionCallbackHasDriverArg(fn)
130
164
 
131
- const savepointName = `tsdb_sp_${this.nextSavepointId}`
132
- this.nextSavepointId++
165
+ const savepointName = `tsdb_sp_${this.executionState.nextSavepointId}`
166
+ this.executionState.nextSavepointId++
133
167
  await this.database.execute(`SAVEPOINT ${savepointName}`)
134
168
  try {
135
169
  const result = await fn(this.createTransactionDriver())
@@ -143,8 +177,16 @@ export class BrowserWASQLiteDriver implements SQLiteDriver {
143
177
  }
144
178
 
145
179
  private enqueue<T>(operation: () => Promise<T> | T): Promise<T> {
146
- const queuedOperation = this.queue.then(operation, operation)
147
- this.queue = queuedOperation.then(
180
+ const queuedOperation = this.executionState.queue.then(operation, operation)
181
+ // Brand the exact Promise returned to callers. Transparent wrappers may
182
+ // preserve this identity for late discovery; wrappers that create a new
183
+ // Promise must forward the driver key before adapter construction.
184
+ Object.defineProperty(
185
+ queuedOperation,
186
+ SQLITE_DRIVER_SHARED_LOGICAL_SCHEDULING_KEY,
187
+ { value: this[SQLITE_DRIVER_SHARED_LOGICAL_SCHEDULING_KEY] },
188
+ )
189
+ this.executionState.queue = queuedOperation.then(
148
190
  () => undefined,
149
191
  () => undefined,
150
192
  )
@@ -5,6 +5,7 @@ declare module '@journeyapps/wa-sqlite/src/examples/OPFSCoopSyncVFS.js' {
5
5
  module: unknown,
6
6
  ): Promise<{
7
7
  close?: () => Promise<void> | void
8
+ lastError?: unknown
8
9
  }>
9
10
  }
10
11
  }