@tanstack/capacitor-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.
@@ -0,0 +1,335 @@
1
+ import { InvalidPersistedCollectionConfigError } from '@tanstack/db-sqlite-persistence-core'
2
+ import type { SQLiteDriver } from '@tanstack/db-sqlite-persistence-core'
3
+ import type {
4
+ capSQLiteValues as CapacitorSQLiteValues,
5
+ SQLiteDBConnection,
6
+ } from '@capacitor-community/sqlite'
7
+
8
+ export type CapacitorSQLiteDatabaseLike = SQLiteDBConnection
9
+
10
+ type TransactionContextStore = {
11
+ transactionDriver: SQLiteDriver
12
+ }
13
+
14
+ type AsyncLocalStorageLike<TStore> = {
15
+ getStore: () => TStore | undefined
16
+ run: <TResult>(store: TStore, callback: () => TResult) => TResult
17
+ }
18
+
19
+ type AsyncLocalStorageCtor = new <TStore>() => AsyncLocalStorageLike<TStore>
20
+
21
+ let asyncLocalStorageCtorPromise: Promise<AsyncLocalStorageCtor | null> | null =
22
+ null
23
+
24
+ function canAttemptNodeAsyncLocalStorageLoad(): boolean {
25
+ if (typeof process === `undefined`) {
26
+ return false
27
+ }
28
+
29
+ // In Capacitor webviews, process may be polyfilled without versions.
30
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
31
+ return typeof process.versions?.node === `string`
32
+ }
33
+
34
+ function getNodeAsyncHooksSpecifier(): string {
35
+ const moduleName = `async_hooks`
36
+ return `node:${moduleName}`
37
+ }
38
+
39
+ async function resolveAsyncLocalStorageCtor(): Promise<AsyncLocalStorageCtor | null> {
40
+ if (asyncLocalStorageCtorPromise) {
41
+ return asyncLocalStorageCtorPromise
42
+ }
43
+
44
+ asyncLocalStorageCtorPromise = (async () => {
45
+ if (!canAttemptNodeAsyncLocalStorageLoad()) {
46
+ return null
47
+ }
48
+
49
+ try {
50
+ const asyncHooksModule = (await import(getNodeAsyncHooksSpecifier())) as {
51
+ AsyncLocalStorage?: AsyncLocalStorageCtor
52
+ }
53
+
54
+ return typeof asyncHooksModule.AsyncLocalStorage === `function`
55
+ ? asyncHooksModule.AsyncLocalStorage
56
+ : null
57
+ } catch {
58
+ return null
59
+ }
60
+ })()
61
+
62
+ return asyncLocalStorageCtorPromise
63
+ }
64
+
65
+ function assertTransactionCallbackHasDriverArg(
66
+ fn: (transactionDriver: SQLiteDriver) => Promise<unknown>,
67
+ ): void {
68
+ if (fn.length > 0) {
69
+ return
70
+ }
71
+
72
+ throw new InvalidPersistedCollectionConfigError(
73
+ `SQLiteDriver.transaction callback must accept the transaction driver argument`,
74
+ )
75
+ }
76
+
77
+ function isCapacitorSQLiteDatabaseLike(
78
+ value: unknown,
79
+ ): value is CapacitorSQLiteDatabaseLike {
80
+ const candidate = value as Partial<CapacitorSQLiteDatabaseLike>
81
+ return (
82
+ typeof value === `object` &&
83
+ value !== null &&
84
+ typeof candidate.execute === `function` &&
85
+ typeof candidate.query === `function` &&
86
+ typeof candidate.run === `function`
87
+ )
88
+ }
89
+
90
+ function extractQueryRows<T>(
91
+ result: CapacitorSQLiteValues,
92
+ sql: string,
93
+ ): ReadonlyArray<T> {
94
+ if (Array.isArray(result.values)) {
95
+ return result.values as ReadonlyArray<T>
96
+ }
97
+
98
+ throw new InvalidPersistedCollectionConfigError(
99
+ `Unsupported Capacitor SQLite query result shape for SQL "${sql}"`,
100
+ )
101
+ }
102
+
103
+ export type CapacitorSQLiteDriverOptions = {
104
+ database: CapacitorSQLiteDatabaseLike
105
+ }
106
+
107
+ export class CapacitorSQLiteDriver implements SQLiteDriver {
108
+ private readonly database: CapacitorSQLiteDatabaseLike
109
+ private queue: Promise<void> = Promise.resolve()
110
+ private nextSavepointId = 1
111
+ private transactionContextStoragePromise: Promise<AsyncLocalStorageLike<TransactionContextStore> | null> | null =
112
+ null
113
+
114
+ constructor(options: CapacitorSQLiteDriverOptions) {
115
+ if (!isCapacitorSQLiteDatabaseLike(options.database)) {
116
+ throw new InvalidPersistedCollectionConfigError(
117
+ `Capacitor SQLite database object must provide execute/query/run methods`,
118
+ )
119
+ }
120
+
121
+ this.database = options.database
122
+ }
123
+
124
+ async exec(sql: string): Promise<void> {
125
+ const activeTransactionDriver = await this.getActiveTransactionDriver()
126
+ if (activeTransactionDriver) {
127
+ await activeTransactionDriver.exec(sql)
128
+ return
129
+ }
130
+
131
+ await this.enqueue(async () => {
132
+ await this.database.execute(sql, false)
133
+ })
134
+ }
135
+
136
+ async query<T>(
137
+ sql: string,
138
+ params: ReadonlyArray<unknown> = [],
139
+ ): Promise<ReadonlyArray<T>> {
140
+ const activeTransactionDriver = await this.getActiveTransactionDriver()
141
+ if (activeTransactionDriver) {
142
+ return activeTransactionDriver.query<T>(sql, params)
143
+ }
144
+
145
+ return this.enqueue(async () => {
146
+ const result = await this.database.query(sql, [...params])
147
+ return extractQueryRows<T>(result, sql)
148
+ })
149
+ }
150
+
151
+ async run(sql: string, params: ReadonlyArray<unknown> = []): Promise<void> {
152
+ const activeTransactionDriver = await this.getActiveTransactionDriver()
153
+ if (activeTransactionDriver) {
154
+ await activeTransactionDriver.run(sql, params)
155
+ return
156
+ }
157
+
158
+ await this.enqueue(async () => {
159
+ await this.database.run(sql, [...params], false)
160
+ })
161
+ }
162
+
163
+ async transaction<T>(
164
+ fn: (transactionDriver: SQLiteDriver) => Promise<T>,
165
+ ): Promise<T> {
166
+ assertTransactionCallbackHasDriverArg(fn)
167
+
168
+ const activeTransactionDriver = await this.getActiveTransactionDriver()
169
+ if (activeTransactionDriver) {
170
+ return activeTransactionDriver.transaction(fn)
171
+ }
172
+
173
+ return this.transactionWithDriver((transactionDriver) =>
174
+ fn(transactionDriver),
175
+ )
176
+ }
177
+
178
+ async transactionWithDriver<T>(
179
+ fn: (transactionDriver: SQLiteDriver) => Promise<T>,
180
+ ): Promise<T> {
181
+ const activeTransactionDriver = await this.getActiveTransactionDriver()
182
+ if (
183
+ activeTransactionDriver &&
184
+ typeof activeTransactionDriver.transactionWithDriver === `function`
185
+ ) {
186
+ return activeTransactionDriver.transactionWithDriver(fn)
187
+ }
188
+
189
+ return this.enqueue(async () => {
190
+ await this.beginTopLevelTransaction()
191
+ const transactionDriver = this.createTransactionDriver()
192
+ try {
193
+ const result = await this.runWithTransactionContext(
194
+ transactionDriver,
195
+ async () => fn(transactionDriver),
196
+ )
197
+ await this.commitTopLevelTransaction()
198
+ return result
199
+ } catch (error) {
200
+ try {
201
+ await this.rollbackTopLevelTransaction()
202
+ } catch {
203
+ // Preserve the original transaction failure as the primary error.
204
+ }
205
+ throw error
206
+ }
207
+ })
208
+ }
209
+
210
+ async close(): Promise<void> {
211
+ await this.database.close()
212
+ }
213
+
214
+ getDatabase(): CapacitorSQLiteDatabaseLike {
215
+ return this.database
216
+ }
217
+
218
+ private enqueue<T>(operation: () => Promise<T>): Promise<T> {
219
+ const queuedOperation = this.queue.then(operation, operation)
220
+ this.queue = queuedOperation.then(
221
+ () => undefined,
222
+ () => undefined,
223
+ )
224
+ return queuedOperation
225
+ }
226
+
227
+ private async getTransactionContextStorage(): Promise<AsyncLocalStorageLike<TransactionContextStore> | null> {
228
+ if (this.transactionContextStoragePromise) {
229
+ return this.transactionContextStoragePromise
230
+ }
231
+
232
+ this.transactionContextStoragePromise = (async () => {
233
+ const asyncLocalStorageCtor = await resolveAsyncLocalStorageCtor()
234
+ if (!asyncLocalStorageCtor) {
235
+ return null
236
+ }
237
+
238
+ return new asyncLocalStorageCtor<TransactionContextStore>()
239
+ })()
240
+
241
+ return this.transactionContextStoragePromise
242
+ }
243
+
244
+ private async getActiveTransactionDriver(): Promise<SQLiteDriver | null> {
245
+ const transactionContextStorage = await this.getTransactionContextStorage()
246
+ const store = transactionContextStorage?.getStore()
247
+ return store?.transactionDriver ?? null
248
+ }
249
+
250
+ private async runWithTransactionContext<TResult>(
251
+ transactionDriver: SQLiteDriver,
252
+ callback: () => Promise<TResult>,
253
+ ): Promise<TResult> {
254
+ const transactionContextStorage = await this.getTransactionContextStorage()
255
+ if (!transactionContextStorage) {
256
+ return callback()
257
+ }
258
+
259
+ return transactionContextStorage.run({ transactionDriver }, callback)
260
+ }
261
+
262
+ private async beginTopLevelTransaction(): Promise<void> {
263
+ await this.database.execute(`BEGIN IMMEDIATE`, false)
264
+ }
265
+
266
+ private async commitTopLevelTransaction(): Promise<void> {
267
+ await this.database.execute(`COMMIT`, false)
268
+ }
269
+
270
+ private async rollbackTopLevelTransaction(): Promise<void> {
271
+ await this.database.execute(`ROLLBACK`, false)
272
+ }
273
+
274
+ private createTransactionDriver(): SQLiteDriver {
275
+ const transactionDriver: SQLiteDriver = {
276
+ exec: async (sql) => {
277
+ await this.database.execute(sql, false)
278
+ },
279
+ query: async <T>(
280
+ sql: string,
281
+ params: ReadonlyArray<unknown> = [],
282
+ ): Promise<ReadonlyArray<T>> => {
283
+ const result = await this.database.query(sql, [...params])
284
+ return extractQueryRows<T>(result, sql)
285
+ },
286
+ run: async (sql, params = []) => {
287
+ await this.database.run(sql, [...params], false)
288
+ },
289
+ transaction: async <T>(
290
+ fn: (transactionDriver: SQLiteDriver) => Promise<T>,
291
+ ): Promise<T> => {
292
+ assertTransactionCallbackHasDriverArg(fn)
293
+ return this.runNestedTransaction(transactionDriver, async (driver) => {
294
+ return fn(driver)
295
+ })
296
+ },
297
+ transactionWithDriver: async <T>(
298
+ fn: (transactionDriver: SQLiteDriver) => Promise<T>,
299
+ ): Promise<T> => this.runNestedTransaction(transactionDriver, fn),
300
+ }
301
+
302
+ return transactionDriver
303
+ }
304
+
305
+ private async runNestedTransaction<T>(
306
+ transactionDriver: SQLiteDriver,
307
+ fn: (transactionDriver: SQLiteDriver) => Promise<T>,
308
+ ): Promise<T> {
309
+ const savepointName = `tsdb_sp_${this.nextSavepointId}`
310
+ this.nextSavepointId++
311
+ await this.database.execute(`SAVEPOINT ${savepointName}`, false)
312
+
313
+ try {
314
+ const result = await this.runWithTransactionContext(
315
+ transactionDriver,
316
+ async () => fn(transactionDriver),
317
+ )
318
+ await this.database.execute(`RELEASE SAVEPOINT ${savepointName}`, false)
319
+ return result
320
+ } catch (error) {
321
+ await this.database.execute(
322
+ `ROLLBACK TO SAVEPOINT ${savepointName}`,
323
+ false,
324
+ )
325
+ await this.database.execute(`RELEASE SAVEPOINT ${savepointName}`, false)
326
+ throw error
327
+ }
328
+ }
329
+ }
330
+
331
+ export function createCapacitorSQLiteDriver(
332
+ options: CapacitorSQLiteDriverOptions,
333
+ ): CapacitorSQLiteDriver {
334
+ return new CapacitorSQLiteDriver(options)
335
+ }
@@ -0,0 +1,22 @@
1
+ import { createCapacitorSQLitePersistence as createPersistence } from './capacitor-persistence'
2
+ import type {
3
+ CapacitorSQLitePersistenceOptions as CapacitorSQLitePersistenceOptionsBase,
4
+ CapacitorSQLiteSchemaMismatchPolicy as CapacitorSQLiteSchemaMismatchPolicyBase,
5
+ } from './capacitor-persistence'
6
+ import type { PersistedCollectionPersistence } from '@tanstack/db-sqlite-persistence-core'
7
+
8
+ export type CapacitorSQLitePersistenceOptions =
9
+ CapacitorSQLitePersistenceOptionsBase
10
+ export type CapacitorSQLiteSchemaMismatchPolicy =
11
+ CapacitorSQLiteSchemaMismatchPolicyBase
12
+ export type { CapacitorSQLiteDatabaseLike } from './capacitor-persistence'
13
+ export type { SQLiteDBConnection } from './capacitor-persistence'
14
+
15
+ export function createCapacitorSQLitePersistence<
16
+ T extends object,
17
+ TKey extends string | number = string | number,
18
+ >(
19
+ options: CapacitorSQLitePersistenceOptions,
20
+ ): PersistedCollectionPersistence<T, TKey> {
21
+ return createPersistence<T, TKey>(options)
22
+ }
package/src/index.ts ADDED
@@ -0,0 +1,12 @@
1
+ export { createCapacitorSQLitePersistence } from './capacitor'
2
+ export type {
3
+ CapacitorSQLiteDatabaseLike,
4
+ CapacitorSQLitePersistenceOptions,
5
+ CapacitorSQLiteSchemaMismatchPolicy,
6
+ SQLiteDBConnection,
7
+ } from './capacitor'
8
+ export { persistedCollectionOptions } from '@tanstack/db-sqlite-persistence-core'
9
+ export type {
10
+ PersistedCollectionCoordinator,
11
+ PersistedCollectionPersistence,
12
+ } from '@tanstack/db-sqlite-persistence-core'