@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.
- package/README.md +109 -0
- package/dist/cjs/capacitor-persistence.cjs +79 -0
- package/dist/cjs/capacitor-persistence.cjs.map +1 -0
- package/dist/cjs/capacitor-persistence.d.cts +13 -0
- package/dist/cjs/capacitor-sqlite-driver.cjs +220 -0
- package/dist/cjs/capacitor-sqlite-driver.cjs.map +1 -0
- package/dist/cjs/capacitor-sqlite-driver.d.cts +30 -0
- package/dist/cjs/capacitor.cjs +8 -0
- package/dist/cjs/capacitor.cjs.map +1 -0
- package/dist/cjs/capacitor.d.cts +7 -0
- package/dist/cjs/index.cjs +10 -0
- package/dist/cjs/index.cjs.map +1 -0
- package/dist/cjs/index.d.cts +4 -0
- package/dist/esm/capacitor-persistence.d.ts +13 -0
- package/dist/esm/capacitor-persistence.js +79 -0
- package/dist/esm/capacitor-persistence.js.map +1 -0
- package/dist/esm/capacitor-sqlite-driver.d.ts +30 -0
- package/dist/esm/capacitor-sqlite-driver.js +220 -0
- package/dist/esm/capacitor-sqlite-driver.js.map +1 -0
- package/dist/esm/capacitor.d.ts +7 -0
- package/dist/esm/capacitor.js +8 -0
- package/dist/esm/capacitor.js.map +1 -0
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/index.js.map +1 -0
- package/package.json +73 -0
- package/src/capacitor-persistence.ts +163 -0
- package/src/capacitor-sqlite-driver.ts +335 -0
- package/src/capacitor.ts +22 -0
- package/src/index.ts +12 -0
|
@@ -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
|
+
}
|
package/src/capacitor.ts
ADDED
|
@@ -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'
|