@symbiote-native/sqlite 0.1.0
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/LICENSE +21 -0
- package/README.md +283 -0
- package/build/angular/index.d.ts +2 -0
- package/build/angular/index.js +2 -0
- package/build/angular/sqlite.service.d.ts +41 -0
- package/build/angular/sqlite.service.js +127 -0
- package/build/core/index.d.ts +10 -0
- package/build/core/index.js +6 -0
- package/build/core/native-database.d.ts +65 -0
- package/build/core/native-database.js +13 -0
- package/build/core/native-module.d.ts +25 -0
- package/build/core/native-module.js +17 -0
- package/build/core/native-session.d.ts +20 -0
- package/build/core/native-session.js +1 -0
- package/build/core/native-statement.d.ts +45 -0
- package/build/core/native-statement.js +6 -0
- package/build/core/param-utils.d.ts +28 -0
- package/build/core/param-utils.js +126 -0
- package/build/core/path-utils.d.ts +6 -0
- package/build/core/path-utils.js +29 -0
- package/build/core/query-utils.d.ts +17 -0
- package/build/core/query-utils.js +37 -0
- package/build/core/sqlite-database.d.ts +246 -0
- package/build/core/sqlite-database.js +427 -0
- package/build/core/sqlite-session.d.ts +84 -0
- package/build/core/sqlite-session.js +121 -0
- package/build/core/sqlite-statement.d.ts +96 -0
- package/build/core/sqlite-statement.js +321 -0
- package/build/core/sqlite-tagged-query.d.ts +73 -0
- package/build/core/sqlite-tagged-query.js +119 -0
- package/build/core/storage.d.ts +107 -0
- package/build/core/storage.js +382 -0
- package/build/core/types.d.ts +30 -0
- package/build/core/types.js +1 -0
- package/build/react/index.d.ts +2 -0
- package/build/react/index.js +2 -0
- package/build/react/sqlite-context.d.ts +25 -0
- package/build/react/sqlite-context.js +129 -0
- package/build/solid/index.d.ts +2 -0
- package/build/solid/index.js +2 -0
- package/build/solid/sqlite-context.d.ts +23 -0
- package/build/solid/sqlite-context.js +106 -0
- package/build/svelte/SQLiteProvider.svelte +88 -0
- package/build/svelte/SQLiteProvider.svelte.d.ts +13 -0
- package/build/svelte/index.d.ts +3 -0
- package/build/svelte/index.js +5 -0
- package/build/svelte/sqlite-context.d.ts +11 -0
- package/build/svelte/sqlite-context.js +23 -0
- package/build/vue/index.d.ts +2 -0
- package/build/vue/index.js +2 -0
- package/build/vue/sqlite-context.d.ts +21 -0
- package/build/vue/sqlite-context.js +69 -0
- package/build-ngc/angular/index.d.ts +2 -0
- package/build-ngc/angular/index.js +3 -0
- package/build-ngc/angular/index.js.map +1 -0
- package/build-ngc/angular/sqlite.service.d.ts +44 -0
- package/build-ngc/angular/sqlite.service.js +85 -0
- package/build-ngc/angular/sqlite.service.js.map +1 -0
- package/build-ngc/core/index.d.ts +10 -0
- package/build-ngc/core/index.js +7 -0
- package/build-ngc/core/index.js.map +1 -0
- package/build-ngc/core/native-database.d.ts +65 -0
- package/build-ngc/core/native-database.js +14 -0
- package/build-ngc/core/native-database.js.map +1 -0
- package/build-ngc/core/native-module.d.ts +25 -0
- package/build-ngc/core/native-module.js +18 -0
- package/build-ngc/core/native-module.js.map +1 -0
- package/build-ngc/core/native-session.d.ts +20 -0
- package/build-ngc/core/native-session.js +2 -0
- package/build-ngc/core/native-session.js.map +1 -0
- package/build-ngc/core/native-statement.d.ts +45 -0
- package/build-ngc/core/native-statement.js +7 -0
- package/build-ngc/core/native-statement.js.map +1 -0
- package/build-ngc/core/param-utils.d.ts +28 -0
- package/build-ngc/core/param-utils.js +127 -0
- package/build-ngc/core/param-utils.js.map +1 -0
- package/build-ngc/core/path-utils.d.ts +6 -0
- package/build-ngc/core/path-utils.js +30 -0
- package/build-ngc/core/path-utils.js.map +1 -0
- package/build-ngc/core/query-utils.d.ts +17 -0
- package/build-ngc/core/query-utils.js +38 -0
- package/build-ngc/core/query-utils.js.map +1 -0
- package/build-ngc/core/sqlite-database.d.ts +246 -0
- package/build-ngc/core/sqlite-database.js +428 -0
- package/build-ngc/core/sqlite-database.js.map +1 -0
- package/build-ngc/core/sqlite-session.d.ts +84 -0
- package/build-ngc/core/sqlite-session.js +122 -0
- package/build-ngc/core/sqlite-session.js.map +1 -0
- package/build-ngc/core/sqlite-statement.d.ts +96 -0
- package/build-ngc/core/sqlite-statement.js +322 -0
- package/build-ngc/core/sqlite-statement.js.map +1 -0
- package/build-ngc/core/sqlite-tagged-query.d.ts +73 -0
- package/build-ngc/core/sqlite-tagged-query.js +120 -0
- package/build-ngc/core/sqlite-tagged-query.js.map +1 -0
- package/build-ngc/core/storage.d.ts +107 -0
- package/build-ngc/core/storage.js +383 -0
- package/build-ngc/core/storage.js.map +1 -0
- package/build-ngc/core/types.d.ts +30 -0
- package/build-ngc/core/types.js +2 -0
- package/build-ngc/core/types.js.map +1 -0
- package/native-link.json +12 -0
- package/package.json +141 -0
- package/src/angular/index.ts +2 -0
- package/src/angular/sqlite.service.ts +114 -0
- package/src/core/index.ts +40 -0
- package/src/core/native-database.ts +118 -0
- package/src/core/native-module.ts +57 -0
- package/src/core/native-session.ts +64 -0
- package/src/core/native-statement.ts +85 -0
- package/src/core/param-utils.ts +163 -0
- package/src/core/path-utils.ts +38 -0
- package/src/core/query-utils.ts +45 -0
- package/src/core/sqlite-database.ts +676 -0
- package/src/core/sqlite-session.ts +165 -0
- package/src/core/sqlite-statement.ts +578 -0
- package/src/core/sqlite-tagged-query.ts +160 -0
- package/src/core/storage.ts +492 -0
- package/src/core/types.ts +33 -0
- package/src/react/index.ts +2 -0
- package/src/react/sqlite-context.tsx +244 -0
- package/src/solid/index.ts +2 -0
- package/src/solid/sqlite-context.ts +156 -0
- package/src/svelte/SQLiteProvider.svelte +88 -0
- package/src/svelte/index.ts +6 -0
- package/src/svelte/sqlite-context.ts +32 -0
- package/src/svelte/svelte-compile.test-helper.ts +135 -0
- package/src/vue/index.ts +2 -0
- package/src/vue/sqlite-context.ts +105 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Update function for `setItemAsync()`/`setItemSync()`. Computes the new value from the
|
|
3
|
+
* previous one (`null` when the key was unset) and returns the value to store.
|
|
4
|
+
*/
|
|
5
|
+
export type ISQLiteStorageSetItemUpdateFunction = (prevValue: string | null) => string;
|
|
6
|
+
/**
|
|
7
|
+
* A key-value store backed by SQLite. The constructor's `databaseName` is the database file
|
|
8
|
+
* name used for storage.
|
|
9
|
+
*/
|
|
10
|
+
export declare class SQLiteStorage {
|
|
11
|
+
private readonly databaseName;
|
|
12
|
+
private db;
|
|
13
|
+
private readonly awaitLock;
|
|
14
|
+
constructor(databaseName: string);
|
|
15
|
+
/** Retrieves the value for the given key. */
|
|
16
|
+
getItemAsync(key: string): Promise<string | null>;
|
|
17
|
+
/**
|
|
18
|
+
* Sets the value for the given key. A function computes the new value from the previous one.
|
|
19
|
+
*/
|
|
20
|
+
setItemAsync(key: string, value: string | ISQLiteStorageSetItemUpdateFunction): Promise<void>;
|
|
21
|
+
/** Removes the value for the given key. Returns whether a row was actually removed. */
|
|
22
|
+
removeItemAsync(key: string): Promise<boolean>;
|
|
23
|
+
/** Retrieves every key stored. */
|
|
24
|
+
getAllKeysAsync(): Promise<string[]>;
|
|
25
|
+
/** Clears every key-value pair. Returns whether anything was actually cleared. */
|
|
26
|
+
clearAsync(): Promise<boolean>;
|
|
27
|
+
/** Closes the database connection. */
|
|
28
|
+
closeAsync(): Promise<void>;
|
|
29
|
+
/** Retrieves the number of key-value pairs stored. */
|
|
30
|
+
getLengthAsync(): Promise<number>;
|
|
31
|
+
/** Retrieves the key at the given index. */
|
|
32
|
+
getKeyByIndexAsync(index: number): Promise<string | null>;
|
|
33
|
+
/**
|
|
34
|
+
* Retrieves the value for the given key.
|
|
35
|
+
* > **Note:** Running heavy tasks with this function can block the JavaScript thread.
|
|
36
|
+
*/
|
|
37
|
+
getItemSync(key: string): string | null;
|
|
38
|
+
/**
|
|
39
|
+
* Sets the value for the given key. A function computes the new value from the previous one.
|
|
40
|
+
* > **Note:** Running heavy tasks with this function can block the JavaScript thread.
|
|
41
|
+
*/
|
|
42
|
+
setItemSync(key: string, value: string | ISQLiteStorageSetItemUpdateFunction): void;
|
|
43
|
+
/**
|
|
44
|
+
* Removes the value for the given key. Returns whether a row was actually removed.
|
|
45
|
+
* > **Note:** Running heavy tasks with this function can block the JavaScript thread.
|
|
46
|
+
*/
|
|
47
|
+
removeItemSync(key: string): boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Retrieves every key stored.
|
|
50
|
+
* > **Note:** Running heavy tasks with this function can block the JavaScript thread.
|
|
51
|
+
*/
|
|
52
|
+
getAllKeysSync(): string[];
|
|
53
|
+
/**
|
|
54
|
+
* Clears every key-value pair. Returns whether anything was actually cleared.
|
|
55
|
+
* > **Note:** Running heavy tasks with this function can block the JavaScript thread.
|
|
56
|
+
*/
|
|
57
|
+
clearSync(): boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Closes the database connection.
|
|
60
|
+
* > **Note:** Running heavy tasks with this function can block the JavaScript thread.
|
|
61
|
+
*/
|
|
62
|
+
closeSync(): void;
|
|
63
|
+
/**
|
|
64
|
+
* Retrieves the number of key-value pairs stored.
|
|
65
|
+
* > **Note:** Running heavy tasks with this function can block the JavaScript thread.
|
|
66
|
+
*/
|
|
67
|
+
getLengthSync(): number;
|
|
68
|
+
/**
|
|
69
|
+
* Retrieves the key at the given index.
|
|
70
|
+
* > **Note:** Running heavy tasks with this function can block the JavaScript thread.
|
|
71
|
+
*/
|
|
72
|
+
getKeyByIndexSync(index: number): string | null;
|
|
73
|
+
/** Alias for `getItemAsync()`. */
|
|
74
|
+
getItem(key: string): Promise<string | null>;
|
|
75
|
+
/** Alias for `setItemAsync()`. */
|
|
76
|
+
setItem(key: string, value: string | ISQLiteStorageSetItemUpdateFunction): Promise<void>;
|
|
77
|
+
/** Alias for `removeItemAsync()`. */
|
|
78
|
+
removeItem(key: string): Promise<void>;
|
|
79
|
+
/** Alias for `getAllKeysAsync()`. */
|
|
80
|
+
getAllKeys(): Promise<string[]>;
|
|
81
|
+
/** Alias for `clearAsync()`. */
|
|
82
|
+
clear(): Promise<void>;
|
|
83
|
+
/** Merges the given value with the existing value for the key — a deep merge for JSON. */
|
|
84
|
+
mergeItem(key: string, value: string): Promise<void>;
|
|
85
|
+
/** Retrieves the values for the given keys. */
|
|
86
|
+
multiGet(keys: string[]): Promise<[string, string | null][]>;
|
|
87
|
+
/** Sets multiple key-value pairs. */
|
|
88
|
+
multiSet(keyValuePairs: [string, string][]): Promise<void>;
|
|
89
|
+
/** Removes the values for the given keys. */
|
|
90
|
+
multiRemove(keys: string[]): Promise<void>;
|
|
91
|
+
/** Merges multiple key-value pairs — a deep merge for JSON existing values. */
|
|
92
|
+
multiMerge(keyValuePairs: [string, string][]): Promise<void>;
|
|
93
|
+
/** Alias for `closeAsync()`. */
|
|
94
|
+
close(): Promise<void>;
|
|
95
|
+
private getDbAsync;
|
|
96
|
+
private getDbSync;
|
|
97
|
+
private maybeMigrateDbAsync;
|
|
98
|
+
private maybeMigrateDbSync;
|
|
99
|
+
private checkValidInput;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* A drop-in replacement for `AsyncStorage` from `@react-native-async-storage/async-storage`.
|
|
103
|
+
*/
|
|
104
|
+
export declare const AsyncStorage: SQLiteStorage;
|
|
105
|
+
export default AsyncStorage;
|
|
106
|
+
/** Alias for `AsyncStorage`, given the storage offers more than asynchronous methods. */
|
|
107
|
+
export declare const Storage: SQLiteStorage;
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
// Ported from expo-sqlite's Storage.ts (.vendors/expo @ origin/sdk-57,
|
|
2
|
+
// packages/expo-sqlite/src/Storage.ts), renamed with this repo's `I`-prefix convention.
|
|
3
|
+
//
|
|
4
|
+
// Published on its own subpath (`@symbiote-native/sqlite/kv-store`) rather than the package
|
|
5
|
+
// root — see the README for why.
|
|
6
|
+
import AwaitLock from 'await-lock';
|
|
7
|
+
import { openDatabaseAsync, openDatabaseSync } from './sqlite-database.js';
|
|
8
|
+
import { normalizeStorageIndex } from './param-utils.js';
|
|
9
|
+
const DATABASE_VERSION = 1;
|
|
10
|
+
const STATEMENT_GET = 'SELECT value FROM storage WHERE key = ?;';
|
|
11
|
+
const STATEMENT_SET = 'INSERT INTO storage (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value;';
|
|
12
|
+
const STATEMENT_REMOVE = 'DELETE FROM storage WHERE key = ?;';
|
|
13
|
+
const STATEMENT_GET_ALL_KEYS = 'SELECT key FROM storage;';
|
|
14
|
+
const STATEMENT_CLEAR = 'DELETE FROM storage;';
|
|
15
|
+
const STATEMENT_LENGTH = 'SELECT COUNT(*) as count FROM storage;';
|
|
16
|
+
const STATEMENT_GET_KEY_BY_INDEX = 'SELECT key FROM storage LIMIT 1 OFFSET ?;';
|
|
17
|
+
const MIGRATION_STATEMENT_0 = 'CREATE TABLE IF NOT EXISTS storage (key TEXT PRIMARY KEY NOT NULL, value TEXT);';
|
|
18
|
+
/**
|
|
19
|
+
* A key-value store backed by SQLite. The constructor's `databaseName` is the database file
|
|
20
|
+
* name used for storage.
|
|
21
|
+
*/
|
|
22
|
+
export class SQLiteStorage {
|
|
23
|
+
databaseName;
|
|
24
|
+
db = null;
|
|
25
|
+
awaitLock = new AwaitLock();
|
|
26
|
+
constructor(databaseName) {
|
|
27
|
+
this.databaseName = databaseName;
|
|
28
|
+
}
|
|
29
|
+
//#region Asynchronous API
|
|
30
|
+
/** Retrieves the value for the given key. */
|
|
31
|
+
async getItemAsync(key) {
|
|
32
|
+
this.checkValidInput(key);
|
|
33
|
+
const db = await this.getDbAsync();
|
|
34
|
+
const result = await db.getFirstAsync(STATEMENT_GET, key);
|
|
35
|
+
return result?.value ?? null;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Sets the value for the given key. A function computes the new value from the previous one.
|
|
39
|
+
*/
|
|
40
|
+
async setItemAsync(key, value) {
|
|
41
|
+
this.checkValidInput(key, value);
|
|
42
|
+
const db = await this.getDbAsync();
|
|
43
|
+
if (typeof value === 'function') {
|
|
44
|
+
await db.withExclusiveTransactionAsync(async (tx) => {
|
|
45
|
+
const prevResult = await tx.getFirstAsync(STATEMENT_GET, key);
|
|
46
|
+
const prevValue = prevResult?.value ?? null;
|
|
47
|
+
const nextValue = value(prevValue);
|
|
48
|
+
this.checkValidInput(key, nextValue);
|
|
49
|
+
await tx.runAsync(STATEMENT_SET, key, nextValue);
|
|
50
|
+
});
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
await db.runAsync(STATEMENT_SET, key, value);
|
|
54
|
+
}
|
|
55
|
+
/** Removes the value for the given key. Returns whether a row was actually removed. */
|
|
56
|
+
async removeItemAsync(key) {
|
|
57
|
+
this.checkValidInput(key);
|
|
58
|
+
const db = await this.getDbAsync();
|
|
59
|
+
const result = await db.runAsync(STATEMENT_REMOVE, key);
|
|
60
|
+
return result.changes > 0;
|
|
61
|
+
}
|
|
62
|
+
/** Retrieves every key stored. */
|
|
63
|
+
async getAllKeysAsync() {
|
|
64
|
+
const db = await this.getDbAsync();
|
|
65
|
+
const result = await db.getAllAsync(STATEMENT_GET_ALL_KEYS);
|
|
66
|
+
return result.map(({ key }) => key);
|
|
67
|
+
}
|
|
68
|
+
/** Clears every key-value pair. Returns whether anything was actually cleared. */
|
|
69
|
+
async clearAsync() {
|
|
70
|
+
const db = await this.getDbAsync();
|
|
71
|
+
const result = await db.runAsync(STATEMENT_CLEAR);
|
|
72
|
+
return result.changes > 0;
|
|
73
|
+
}
|
|
74
|
+
/** Closes the database connection. */
|
|
75
|
+
async closeAsync() {
|
|
76
|
+
await this.awaitLock.acquireAsync();
|
|
77
|
+
try {
|
|
78
|
+
if (this.db) {
|
|
79
|
+
await this.db.closeAsync();
|
|
80
|
+
this.db = null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
this.awaitLock.release();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/** Retrieves the number of key-value pairs stored. */
|
|
88
|
+
async getLengthAsync() {
|
|
89
|
+
const db = await this.getDbAsync();
|
|
90
|
+
const result = await db.getFirstAsync(STATEMENT_LENGTH);
|
|
91
|
+
return result?.count ?? 0;
|
|
92
|
+
}
|
|
93
|
+
/** Retrieves the key at the given index. */
|
|
94
|
+
async getKeyByIndexAsync(index) {
|
|
95
|
+
const db = await this.getDbAsync();
|
|
96
|
+
const offset = normalizeStorageIndex(index);
|
|
97
|
+
if (offset == null) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
const result = await db.getFirstAsync(STATEMENT_GET_KEY_BY_INDEX, offset);
|
|
101
|
+
return result?.key ?? null;
|
|
102
|
+
}
|
|
103
|
+
//#endregion
|
|
104
|
+
//#region Synchronous API
|
|
105
|
+
/**
|
|
106
|
+
* Retrieves the value for the given key.
|
|
107
|
+
* > **Note:** Running heavy tasks with this function can block the JavaScript thread.
|
|
108
|
+
*/
|
|
109
|
+
getItemSync(key) {
|
|
110
|
+
this.checkValidInput(key);
|
|
111
|
+
const db = this.getDbSync();
|
|
112
|
+
const result = db.getFirstSync(STATEMENT_GET, key);
|
|
113
|
+
return result?.value ?? null;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Sets the value for the given key. A function computes the new value from the previous one.
|
|
117
|
+
* > **Note:** Running heavy tasks with this function can block the JavaScript thread.
|
|
118
|
+
*/
|
|
119
|
+
setItemSync(key, value) {
|
|
120
|
+
this.checkValidInput(key, value);
|
|
121
|
+
const db = this.getDbSync();
|
|
122
|
+
if (typeof value === 'function') {
|
|
123
|
+
db.withTransactionSync(() => {
|
|
124
|
+
const prevResult = db.getFirstSync(STATEMENT_GET, key);
|
|
125
|
+
const prevValue = prevResult?.value ?? null;
|
|
126
|
+
const nextValue = value(prevValue);
|
|
127
|
+
this.checkValidInput(key, nextValue);
|
|
128
|
+
db.runSync(STATEMENT_SET, key, nextValue);
|
|
129
|
+
});
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
db.runSync(STATEMENT_SET, key, value);
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Removes the value for the given key. Returns whether a row was actually removed.
|
|
136
|
+
* > **Note:** Running heavy tasks with this function can block the JavaScript thread.
|
|
137
|
+
*/
|
|
138
|
+
removeItemSync(key) {
|
|
139
|
+
this.checkValidInput(key);
|
|
140
|
+
const db = this.getDbSync();
|
|
141
|
+
const result = db.runSync(STATEMENT_REMOVE, key);
|
|
142
|
+
return result.changes > 0;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Retrieves every key stored.
|
|
146
|
+
* > **Note:** Running heavy tasks with this function can block the JavaScript thread.
|
|
147
|
+
*/
|
|
148
|
+
getAllKeysSync() {
|
|
149
|
+
const db = this.getDbSync();
|
|
150
|
+
const result = db.getAllSync(STATEMENT_GET_ALL_KEYS);
|
|
151
|
+
return result.map(({ key }) => key);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Clears every key-value pair. Returns whether anything was actually cleared.
|
|
155
|
+
* > **Note:** Running heavy tasks with this function can block the JavaScript thread.
|
|
156
|
+
*/
|
|
157
|
+
clearSync() {
|
|
158
|
+
const db = this.getDbSync();
|
|
159
|
+
const result = db.runSync(STATEMENT_CLEAR);
|
|
160
|
+
return result.changes > 0;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Closes the database connection.
|
|
164
|
+
* > **Note:** Running heavy tasks with this function can block the JavaScript thread.
|
|
165
|
+
*/
|
|
166
|
+
closeSync() {
|
|
167
|
+
if (this.db) {
|
|
168
|
+
this.db.closeSync();
|
|
169
|
+
this.db = null;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Retrieves the number of key-value pairs stored.
|
|
174
|
+
* > **Note:** Running heavy tasks with this function can block the JavaScript thread.
|
|
175
|
+
*/
|
|
176
|
+
getLengthSync() {
|
|
177
|
+
const db = this.getDbSync();
|
|
178
|
+
const result = db.getFirstSync(STATEMENT_LENGTH);
|
|
179
|
+
return result?.count ?? 0;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Retrieves the key at the given index.
|
|
183
|
+
* > **Note:** Running heavy tasks with this function can block the JavaScript thread.
|
|
184
|
+
*/
|
|
185
|
+
getKeyByIndexSync(index) {
|
|
186
|
+
const db = this.getDbSync();
|
|
187
|
+
const offset = normalizeStorageIndex(index);
|
|
188
|
+
if (offset == null) {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
const result = db.getFirstSync(STATEMENT_GET_KEY_BY_INDEX, offset);
|
|
192
|
+
return result?.key ?? null;
|
|
193
|
+
}
|
|
194
|
+
//#endregion
|
|
195
|
+
//#region react-native-async-storage compatible API
|
|
196
|
+
/** Alias for `getItemAsync()`. */
|
|
197
|
+
async getItem(key) {
|
|
198
|
+
return this.getItemAsync(key);
|
|
199
|
+
}
|
|
200
|
+
/** Alias for `setItemAsync()`. */
|
|
201
|
+
async setItem(key, value) {
|
|
202
|
+
await this.setItemAsync(key, value);
|
|
203
|
+
}
|
|
204
|
+
/** Alias for `removeItemAsync()`. */
|
|
205
|
+
async removeItem(key) {
|
|
206
|
+
await this.removeItemAsync(key);
|
|
207
|
+
}
|
|
208
|
+
/** Alias for `getAllKeysAsync()`. */
|
|
209
|
+
async getAllKeys() {
|
|
210
|
+
return this.getAllKeysAsync();
|
|
211
|
+
}
|
|
212
|
+
/** Alias for `clearAsync()`. */
|
|
213
|
+
async clear() {
|
|
214
|
+
await this.clearAsync();
|
|
215
|
+
}
|
|
216
|
+
/** Merges the given value with the existing value for the key — a deep merge for JSON. */
|
|
217
|
+
async mergeItem(key, value) {
|
|
218
|
+
this.checkValidInput(key, value);
|
|
219
|
+
await this.setItemAsync(key, prevValue => {
|
|
220
|
+
if (prevValue == null) {
|
|
221
|
+
return value;
|
|
222
|
+
}
|
|
223
|
+
const prevJSON = JSON.parse(prevValue);
|
|
224
|
+
const newJSON = JSON.parse(value);
|
|
225
|
+
const mergedJSON = mergeDeep(prevJSON, newJSON);
|
|
226
|
+
return JSON.stringify(mergedJSON);
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
/** Retrieves the values for the given keys. */
|
|
230
|
+
async multiGet(keys) {
|
|
231
|
+
return Promise.all(keys.map(async (key) => {
|
|
232
|
+
this.checkValidInput(key);
|
|
233
|
+
return [key, await this.getItemAsync(key)];
|
|
234
|
+
}));
|
|
235
|
+
}
|
|
236
|
+
/** Sets multiple key-value pairs. */
|
|
237
|
+
async multiSet(keyValuePairs) {
|
|
238
|
+
const db = await this.getDbAsync();
|
|
239
|
+
await db.withExclusiveTransactionAsync(async (tx) => {
|
|
240
|
+
for (const [key, value] of keyValuePairs) {
|
|
241
|
+
this.checkValidInput(key, value);
|
|
242
|
+
await tx.runAsync(STATEMENT_SET, key, value);
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
/** Removes the values for the given keys. */
|
|
247
|
+
async multiRemove(keys) {
|
|
248
|
+
const db = await this.getDbAsync();
|
|
249
|
+
await db.withExclusiveTransactionAsync(async (tx) => {
|
|
250
|
+
for (const key of keys) {
|
|
251
|
+
this.checkValidInput(key);
|
|
252
|
+
await tx.runAsync(STATEMENT_REMOVE, key);
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
/** Merges multiple key-value pairs — a deep merge for JSON existing values. */
|
|
257
|
+
async multiMerge(keyValuePairs) {
|
|
258
|
+
const db = await this.getDbAsync();
|
|
259
|
+
await db.withExclusiveTransactionAsync(async (tx) => {
|
|
260
|
+
for (const [key, value] of keyValuePairs) {
|
|
261
|
+
this.checkValidInput(key, value);
|
|
262
|
+
const prevValue = await tx.getFirstAsync(STATEMENT_GET, key);
|
|
263
|
+
if (prevValue == null) {
|
|
264
|
+
await tx.runAsync(STATEMENT_SET, key, value);
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
const prevJSON = JSON.parse(prevValue.value);
|
|
268
|
+
const newJSON = JSON.parse(value);
|
|
269
|
+
const mergedJSON = mergeDeep(prevJSON, newJSON);
|
|
270
|
+
await tx.runAsync(STATEMENT_SET, key, JSON.stringify(mergedJSON));
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
/** Alias for `closeAsync()`. */
|
|
275
|
+
async close() {
|
|
276
|
+
await this.closeAsync();
|
|
277
|
+
}
|
|
278
|
+
//#endregion
|
|
279
|
+
//#region Internals
|
|
280
|
+
async getDbAsync() {
|
|
281
|
+
await this.awaitLock.acquireAsync();
|
|
282
|
+
try {
|
|
283
|
+
if (!this.db) {
|
|
284
|
+
const db = await openDatabaseAsync(this.databaseName);
|
|
285
|
+
await this.maybeMigrateDbAsync(db);
|
|
286
|
+
this.db = db;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
finally {
|
|
290
|
+
this.awaitLock.release();
|
|
291
|
+
}
|
|
292
|
+
return this.db;
|
|
293
|
+
}
|
|
294
|
+
getDbSync() {
|
|
295
|
+
// Unlike getDbAsync(), this cannot take `awaitLock` — it is promise-based and this method is
|
|
296
|
+
// synchronous. The migration is idempotent instead, so the next open repairs a database a
|
|
297
|
+
// race left without a `storage` table.
|
|
298
|
+
if (!this.db) {
|
|
299
|
+
const db = openDatabaseSync(this.databaseName);
|
|
300
|
+
this.maybeMigrateDbSync(db);
|
|
301
|
+
this.db = db;
|
|
302
|
+
}
|
|
303
|
+
return this.db;
|
|
304
|
+
}
|
|
305
|
+
maybeMigrateDbAsync(db) {
|
|
306
|
+
return db.withTransactionAsync(async () => {
|
|
307
|
+
const result = await db.getFirstAsync('PRAGMA user_version');
|
|
308
|
+
const currentDbVersion = result?.user_version ?? 0;
|
|
309
|
+
// Baseline schema, deliberately outside the version ladder below — `CREATE TABLE IF NOT
|
|
310
|
+
// EXISTS` is a no-op on a healthy database and also repairs one a raced first-run
|
|
311
|
+
// migration left at `user_version >= 1` with no `storage` table. A new column has to be
|
|
312
|
+
// added here too, so fresh and repaired databases both get it.
|
|
313
|
+
await db.execAsync(MIGRATION_STATEMENT_0);
|
|
314
|
+
// Version ladder: add each new migration below, gated on `currentDbVersion`. Since the
|
|
315
|
+
// baseline above already carries every column, a ladder entry adding one has to tolerate
|
|
316
|
+
// it already being there — gate it on `PRAGMA table_info(storage)`, or a fresh database
|
|
317
|
+
// fails the migration with `duplicate column name`.
|
|
318
|
+
if (currentDbVersion >= DATABASE_VERSION) {
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
await db.execAsync(`PRAGMA user_version = ${DATABASE_VERSION}`);
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
maybeMigrateDbSync(db) {
|
|
325
|
+
db.withTransactionSync(() => {
|
|
326
|
+
const result = db.getFirstSync('PRAGMA user_version');
|
|
327
|
+
const currentDbVersion = result?.user_version ?? 0;
|
|
328
|
+
// Keep in sync with maybeMigrateDbAsync(), which documents the two steps below.
|
|
329
|
+
db.execSync(MIGRATION_STATEMENT_0);
|
|
330
|
+
if (currentDbVersion >= DATABASE_VERSION) {
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
db.execSync(`PRAGMA user_version = ${DATABASE_VERSION}`);
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
checkValidInput(...input) {
|
|
337
|
+
const [key, value] = input;
|
|
338
|
+
if (typeof key !== 'string') {
|
|
339
|
+
throw new Error(`[SQLiteStorage] Using ${typeof key} type for key is not supported. Use string instead. Key passed: ${String(key)}`);
|
|
340
|
+
}
|
|
341
|
+
if (input.length > 1 &&
|
|
342
|
+
typeof value !== 'string' &&
|
|
343
|
+
typeof value !== 'function') {
|
|
344
|
+
throw new Error(`[SQLiteStorage] Using ${typeof value} type for value is not supported. Use string instead. Key passed: ${key}. Value passed : ${String(value)}`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
/** Recursively merges two JSON-decoded values, concatenating arrays and merging objects. */
|
|
349
|
+
function mergeDeep(target, source) {
|
|
350
|
+
if (typeof target !== 'object' || target === null || Array.isArray(target)) {
|
|
351
|
+
return source;
|
|
352
|
+
}
|
|
353
|
+
if (typeof source !== 'object' || source === null || Array.isArray(source)) {
|
|
354
|
+
return target;
|
|
355
|
+
}
|
|
356
|
+
// I/O edge: both are JSON.parse() output narrowed only to "non-null, non-array object" above —
|
|
357
|
+
// JSON itself carries no further static shape to check against.
|
|
358
|
+
const targetRecord = target;
|
|
359
|
+
const sourceRecord = source;
|
|
360
|
+
const output = { ...targetRecord };
|
|
361
|
+
for (const key of Object.keys(sourceRecord)) {
|
|
362
|
+
const sourceValue = sourceRecord[key];
|
|
363
|
+
if (Array.isArray(sourceValue)) {
|
|
364
|
+
const existing = output[key];
|
|
365
|
+
output[key] = (Array.isArray(existing) ? existing : []).concat(sourceValue);
|
|
366
|
+
}
|
|
367
|
+
else if (typeof sourceValue === 'object' && sourceValue !== null) {
|
|
368
|
+
output[key] = mergeDeep(targetRecord[key], sourceValue);
|
|
369
|
+
}
|
|
370
|
+
else {
|
|
371
|
+
output[key] = sourceValue;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return output;
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* A drop-in replacement for `AsyncStorage` from `@react-native-async-storage/async-storage`.
|
|
378
|
+
*/
|
|
379
|
+
export const AsyncStorage = new SQLiteStorage('ExpoSQLiteStorage');
|
|
380
|
+
export default AsyncStorage;
|
|
381
|
+
/** Alias for `AsyncStorage`, given the storage offers more than asynchronous methods. */
|
|
382
|
+
export const Storage = AsyncStorage;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { SQLiteDatabase } from './sqlite-database';
|
|
2
|
+
import type { ISQLiteOpenOptions } from './native-database';
|
|
3
|
+
/** The event payload for `addDatabaseChangeListener`. */
|
|
4
|
+
export type IDatabaseChangeEvent = {
|
|
5
|
+
/** The database name — `main` by default, or another name set via `ATTACH DATABASE`. */
|
|
6
|
+
databaseName: string;
|
|
7
|
+
/** The absolute file path to the database. */
|
|
8
|
+
databaseFilePath: string;
|
|
9
|
+
/** The table name. */
|
|
10
|
+
tableName: string;
|
|
11
|
+
/** The changed row ID. */
|
|
12
|
+
rowId: number;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Called once, right after the native handle opens and before `openDatabaseAsync`/
|
|
16
|
+
* `openDatabaseSync` returns the database — e.g. to run `execAsync`/`execSync` migrations.
|
|
17
|
+
*
|
|
18
|
+
* **Deviation from upstream:** expo-sqlite only exposes this as a `SQLiteProviderProps.onInit`
|
|
19
|
+
* field, called by its React `<SQLiteProvider>` after opening, before rendering children. This
|
|
20
|
+
* package has no Provider yet (per-adapter Provider/hook wrappers are a later, separate pass —
|
|
21
|
+
* see the README), so `onInit` is exposed directly on `openDatabaseAsync`/`openDatabaseSync`
|
|
22
|
+
* instead, giving any adapter — or plain module-scope code with no view tree at all — the same
|
|
23
|
+
* "run this once right after open" hook without needing a Provider to exist first. A future
|
|
24
|
+
* Provider can still accept its own `onInit` prop and either forward it here or call the
|
|
25
|
+
* callback itself; that choice is left to that later pass.
|
|
26
|
+
*/
|
|
27
|
+
export type IOnInitCallback = (db: SQLiteDatabase) => Promise<void> | void;
|
|
28
|
+
export type IOpenDatabaseOptions = ISQLiteOpenOptions & {
|
|
29
|
+
onInit?: IOnInitCallback;
|
|
30
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type ReactElement, type ReactNode } from 'react';
|
|
2
|
+
import { type IOnInitCallback, type ISQLiteOpenOptions, type SQLiteDatabase } from '../core';
|
|
3
|
+
export type ISQLiteProviderProps = {
|
|
4
|
+
/** The name of the database file to open. */
|
|
5
|
+
databaseName: string;
|
|
6
|
+
/** The directory where the database file is located. @default defaultDatabaseDirectory */
|
|
7
|
+
directory?: string;
|
|
8
|
+
/** Open options. */
|
|
9
|
+
options?: ISQLiteOpenOptions;
|
|
10
|
+
/** The children to render. */
|
|
11
|
+
children: ReactNode;
|
|
12
|
+
/** Run once, right after the native handle opens and before children render. */
|
|
13
|
+
onInit?: IOnInitCallback;
|
|
14
|
+
/** Handle errors from SQLiteProvider. @default rethrow the error */
|
|
15
|
+
onError?: (error: Error) => void;
|
|
16
|
+
/** Enable React.Suspense integration. @default false */
|
|
17
|
+
useSuspense?: boolean;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Context.Provider component that provides a SQLite database to all children. All descendants
|
|
21
|
+
* of this component can access the database via `useSQLiteContext`.
|
|
22
|
+
*/
|
|
23
|
+
export declare const SQLiteProvider: import("react").MemoExoticComponent<({ children, onError, useSuspense, ...props }: ISQLiteProviderProps) => ReactElement | null>;
|
|
24
|
+
/** A hook for accessing the SQLite database provided by an ancestor `<SQLiteProvider>`. */
|
|
25
|
+
export declare function useSQLiteContext(): SQLiteDatabase;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
// React <SQLiteProvider> + useSQLiteContext(), ported from expo-sqlite's hooks.tsx (sdk-57,
|
|
3
|
+
// .vendors/expo) onto this package's own openDatabaseAsync/SQLiteDatabase. Deliberately excludes
|
|
4
|
+
// `assetSource` (bundling a database from a require()'d asset needs expo-asset, out of scope for
|
|
5
|
+
// this pass — see the README). React 19's built-in `use()` replaces upstream's private
|
|
6
|
+
// Suspense-promise polyfill (~40 LOC of `ReactUsePromise` plumbing, unnecessary now that the
|
|
7
|
+
// peer range is react >=19.0.0). Unlike upstream, `onInit` is NOT invoked here — this package's
|
|
8
|
+
// own `openDatabaseAsync` already runs it internally before resolving (see
|
|
9
|
+
// `IOnInitCallback`'s doc comment in `core/types.ts`), so the provider only has to forward it.
|
|
10
|
+
import { createContext, memo, use, useContext, useEffect, useRef, useState, } from 'react';
|
|
11
|
+
import { openDatabaseAsync, } from '../core/index.js';
|
|
12
|
+
const SQLiteContext = createContext(null);
|
|
13
|
+
function isRecord(value) {
|
|
14
|
+
return typeof value === 'object' && value !== null;
|
|
15
|
+
}
|
|
16
|
+
/** Structural equality, recursing into plain objects — used only to keep the memo comparator
|
|
17
|
+
* below from reopening a database when a parent re-passes a deep-equal but new `options` object. */
|
|
18
|
+
function deepEqual(a, b) {
|
|
19
|
+
if (a === b)
|
|
20
|
+
return true;
|
|
21
|
+
if (!isRecord(a) || !isRecord(b))
|
|
22
|
+
return false;
|
|
23
|
+
const aKeys = Object.keys(a);
|
|
24
|
+
const bKeys = Object.keys(b);
|
|
25
|
+
return (aKeys.length === bKeys.length &&
|
|
26
|
+
aKeys.every(key => deepEqual(a[key], b[key])));
|
|
27
|
+
}
|
|
28
|
+
function propsAreEqual(prev, next) {
|
|
29
|
+
return (prev.databaseName === next.databaseName &&
|
|
30
|
+
deepEqual(prev.options, next.options) &&
|
|
31
|
+
prev.directory === next.directory &&
|
|
32
|
+
prev.onInit === next.onInit &&
|
|
33
|
+
prev.onError === next.onError &&
|
|
34
|
+
prev.useSuspense === next.useSuspense);
|
|
35
|
+
}
|
|
36
|
+
function toError(caught) {
|
|
37
|
+
return caught instanceof Error ? caught : new Error(String(caught));
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Context.Provider component that provides a SQLite database to all children. All descendants
|
|
41
|
+
* of this component can access the database via `useSQLiteContext`.
|
|
42
|
+
*/
|
|
43
|
+
export const SQLiteProvider = memo(function SQLiteProvider({ children, onError, useSuspense = false, ...props }) {
|
|
44
|
+
if (onError != null && useSuspense) {
|
|
45
|
+
throw new Error('Cannot use `onError` with `useSuspense`, use error boundaries instead.');
|
|
46
|
+
}
|
|
47
|
+
if (useSuspense) {
|
|
48
|
+
return (_jsx(SQLiteProviderSuspense, { ...props, children: children }));
|
|
49
|
+
}
|
|
50
|
+
return (_jsx(SQLiteProviderNonSuspense, { ...props, onError: onError, children: children }));
|
|
51
|
+
}, propsAreEqual);
|
|
52
|
+
/** A hook for accessing the SQLite database provided by an ancestor `<SQLiteProvider>`. */
|
|
53
|
+
export function useSQLiteContext() {
|
|
54
|
+
const context = useContext(SQLiteContext);
|
|
55
|
+
if (context == null) {
|
|
56
|
+
throw new Error('useSQLiteContext must be used within a <SQLiteProvider>');
|
|
57
|
+
}
|
|
58
|
+
return context;
|
|
59
|
+
}
|
|
60
|
+
//#region Internals
|
|
61
|
+
function SQLiteProviderNonSuspense({ databaseName, directory, options, children, onInit, onError, }) {
|
|
62
|
+
const databaseRef = useRef(null);
|
|
63
|
+
const [loading, setLoading] = useState(true);
|
|
64
|
+
const [error, setError] = useState(null);
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
async function setup() {
|
|
67
|
+
try {
|
|
68
|
+
const db = await openDatabaseAsync(databaseName, { ...options, onInit }, directory);
|
|
69
|
+
databaseRef.current = db;
|
|
70
|
+
setLoading(false);
|
|
71
|
+
}
|
|
72
|
+
catch (caught) {
|
|
73
|
+
setError(toError(caught));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async function teardown(db) {
|
|
77
|
+
try {
|
|
78
|
+
await db?.closeAsync();
|
|
79
|
+
}
|
|
80
|
+
catch (caught) {
|
|
81
|
+
setError(toError(caught));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
setup();
|
|
85
|
+
return () => {
|
|
86
|
+
const db = databaseRef.current;
|
|
87
|
+
teardown(db);
|
|
88
|
+
databaseRef.current = null;
|
|
89
|
+
setLoading(true);
|
|
90
|
+
};
|
|
91
|
+
}, [databaseName, directory, options, onInit]);
|
|
92
|
+
if (error != null) {
|
|
93
|
+
const handler = onError ??
|
|
94
|
+
((caught) => {
|
|
95
|
+
throw caught;
|
|
96
|
+
});
|
|
97
|
+
handler(error);
|
|
98
|
+
}
|
|
99
|
+
if (loading || databaseRef.current == null) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
return (_jsx(SQLiteContext.Provider, { value: databaseRef.current, children: children }));
|
|
103
|
+
}
|
|
104
|
+
// A single module-scope cache entry, mirroring upstream: only ONE suspended database is tracked
|
|
105
|
+
// across the whole app at a time, and a second concurrent <SQLiteProvider useSuspense> for a
|
|
106
|
+
// different database closes the first before opening the next. Faithful to upstream's own
|
|
107
|
+
// `databaseInstance` shape, not a per-provider cache.
|
|
108
|
+
let databaseCache = null;
|
|
109
|
+
function getDatabaseAsync(databaseName, directory, options, onInit) {
|
|
110
|
+
if (databaseCache != null &&
|
|
111
|
+
databaseCache.databaseName === databaseName &&
|
|
112
|
+
databaseCache.directory === directory &&
|
|
113
|
+
databaseCache.options === options &&
|
|
114
|
+
databaseCache.onInit === onInit) {
|
|
115
|
+
return databaseCache.promise;
|
|
116
|
+
}
|
|
117
|
+
const previous = databaseCache;
|
|
118
|
+
const promise = previous != null
|
|
119
|
+
? previous.promise
|
|
120
|
+
.then(db => db.closeAsync())
|
|
121
|
+
.then(() => openDatabaseAsync(databaseName, { ...options, onInit }, directory))
|
|
122
|
+
: openDatabaseAsync(databaseName, { ...options, onInit }, directory);
|
|
123
|
+
databaseCache = { databaseName, directory, options, onInit, promise };
|
|
124
|
+
return promise;
|
|
125
|
+
}
|
|
126
|
+
function SQLiteProviderSuspense({ databaseName, directory, options, children, onInit, }) {
|
|
127
|
+
const database = use(getDatabaseAsync(databaseName, directory, options, onInit));
|
|
128
|
+
return (_jsx(SQLiteContext.Provider, { value: database, children: children }));
|
|
129
|
+
}
|