@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.
- package/README.md +75 -1
- package/dist/assets/{opfs-worker-uyjx5Rus.js → opfs-worker-accoEoax.js} +17 -6
- package/dist/assets/{opfs-worker-uyjx5Rus.js.map → opfs-worker-accoEoax.js.map} +1 -1
- package/dist/cjs/browser-coordinator.cjs +5 -534
- package/dist/cjs/browser-coordinator.cjs.map +1 -1
- package/dist/cjs/browser-coordinator.d.cts +3 -65
- package/dist/cjs/browser-persistence.cjs +19 -8
- package/dist/cjs/browser-persistence.cjs.map +1 -1
- package/dist/cjs/index.cjs +16 -0
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +2 -2
- package/dist/cjs/opfs-database.cjs +58 -1
- package/dist/cjs/opfs-database.cjs.map +1 -1
- package/dist/cjs/opfs-database.d.cts +4 -0
- package/dist/cjs/opfs-worker.cjs +1 -1
- package/dist/cjs/wa-sqlite-driver.cjs +38 -14
- package/dist/cjs/wa-sqlite-driver.cjs.map +1 -1
- package/dist/cjs/wa-sqlite-driver.d.cts +3 -3
- package/dist/esm/browser-coordinator.d.ts +3 -65
- package/dist/esm/browser-coordinator.js +5 -534
- package/dist/esm/browser-coordinator.js.map +1 -1
- package/dist/esm/browser-persistence.js +19 -8
- package/dist/esm/browser-persistence.js.map +1 -1
- package/dist/esm/index.d.ts +2 -2
- package/dist/esm/index.js +5 -1
- package/dist/esm/opfs-database.d.ts +4 -0
- package/dist/esm/opfs-database.js +58 -1
- package/dist/esm/opfs-database.js.map +1 -1
- package/dist/esm/opfs-worker.js +1 -1
- package/dist/esm/wa-sqlite-driver.d.ts +3 -3
- package/dist/esm/wa-sqlite-driver.js +39 -15
- package/dist/esm/wa-sqlite-driver.js.map +1 -1
- package/package.json +9 -3
- package/src/browser-coordinator.ts +10 -834
- package/src/browser-persistence.ts +22 -14
- package/src/index.ts +9 -0
- package/src/opfs-database.ts +76 -1
- package/src/opfs-worker.ts +16 -5
- package/src/wa-sqlite-driver.ts +57 -15
- 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
|
|
148
|
-
|
|
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'
|
package/src/opfs-database.ts
CHANGED
|
@@ -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
|
-
|
|
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>({
|
package/src/opfs-worker.ts
CHANGED
|
@@ -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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
package/src/wa-sqlite-driver.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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
|
-
|
|
50
|
-
|
|
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
|
-
|
|
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
|
-
|
|
63
|
-
|
|
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
|
-
|
|
98
|
+
transaction<T>(
|
|
69
99
|
fn: (transactionDriver: SQLiteDriver) => Promise<T>,
|
|
70
100
|
): Promise<T> {
|
|
71
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
)
|