@chipmobilesdk/rn-local-db 0.1.2 → 0.2.1

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 (84) hide show
  1. package/CHANGELOG.md +122 -52
  2. package/README.md +663 -661
  3. package/package.json +59 -62
  4. package/skills/sdk-rn-local-db/SKILL.md +0 -1
  5. package/src/adapter/budgets.ts +67 -67
  6. package/src/adapter/collection.ts +303 -298
  7. package/src/adapter/deletion.ts +252 -238
  8. package/src/adapter/index.ts +64 -68
  9. package/src/adapter/lifecycle.ts +517 -444
  10. package/src/adapter/transaction.ts +71 -65
  11. package/src/artifacts/availability.ts +67 -69
  12. package/src/artifacts/cascade.ts +114 -78
  13. package/src/artifacts/paths.ts +29 -29
  14. package/src/artifacts/reconcile.ts +63 -63
  15. package/src/artifacts/store.ts +334 -323
  16. package/src/contract/adapter.ts +76 -78
  17. package/src/contract/attachment.ts +75 -72
  18. package/src/contract/backup.ts +28 -28
  19. package/src/contract/config.ts +54 -54
  20. package/src/contract/encryption.ts +34 -34
  21. package/src/contract/errors.ts +76 -76
  22. package/src/contract/migration.ts +42 -40
  23. package/src/contract/query.ts +81 -81
  24. package/src/contract/record.ts +46 -42
  25. package/src/contract/schema.ts +36 -36
  26. package/src/contract/scope.ts +36 -36
  27. package/src/contract/transaction.ts +52 -46
  28. package/src/crypto/chunked-cipher.ts +153 -144
  29. package/src/crypto/derive.ts +64 -63
  30. package/src/crypto/key-provider.ts +78 -78
  31. package/src/crypto/provider.ts +81 -81
  32. package/src/diagnostics/dev-log.ts +112 -108
  33. package/src/diagnostics/errors.ts +62 -66
  34. package/src/diagnostics/faults.ts +58 -64
  35. package/src/diagnostics/map-native.ts +108 -106
  36. package/src/diagnostics/redact.ts +137 -141
  37. package/src/index.ts +80 -110
  38. package/src/migration/executor.ts +132 -136
  39. package/src/migration/ledger.ts +95 -97
  40. package/src/migration/validate-chain.ts +65 -65
  41. package/src/platform/backup-android-verify.ts +88 -88
  42. package/src/platform/backup-ios.ts +48 -48
  43. package/src/platform/bytes.ts +144 -0
  44. package/src/platform/fs.ts +89 -89
  45. package/src/platform/location.ts +135 -135
  46. package/src/query/budgets.ts +54 -57
  47. package/src/query/compare.ts +91 -91
  48. package/src/query/cursor.ts +100 -99
  49. package/src/query/filter.ts +82 -82
  50. package/src/query/paginate.ts +51 -51
  51. package/src/query/sort.ts +55 -56
  52. package/src/query/validate.ts +171 -171
  53. package/src/schema/declare.ts +42 -45
  54. package/src/schema/validate-config.ts +158 -158
  55. package/src/scope/hash.ts +131 -134
  56. package/src/scope/normalize.ts +83 -90
  57. package/src/scope/validate.ts +83 -83
  58. package/src/sqlite/adapter.ts +26 -26
  59. package/src/sqlite/batch.ts +242 -232
  60. package/src/sqlite/catalog.ts +105 -108
  61. package/src/sqlite/connection.ts +227 -205
  62. package/src/sqlite/index-coverage.ts +54 -53
  63. package/src/sqlite/native.ts +83 -83
  64. package/src/sqlite/row-codec.ts +217 -201
  65. package/src/sqlite/sql-builder.ts +244 -244
  66. package/src/sqlite/transaction-queue.ts +87 -0
  67. package/src/sqlite/transactions.ts +33 -33
  68. package/src/sqlite/values.ts +24 -24
  69. package/src/testing/conformance/attachments.ts +138 -136
  70. package/src/testing/conformance/encryption.ts +150 -150
  71. package/src/testing/conformance/index.ts +161 -161
  72. package/src/testing/conformance/lifecycle.ts +99 -99
  73. package/src/testing/conformance/migrations.ts +177 -177
  74. package/src/testing/conformance/queries.ts +204 -167
  75. package/src/testing/conformance/records.ts +169 -169
  76. package/src/testing/conformance/scopes.ts +86 -88
  77. package/src/testing/conformance/transactions.ts +149 -149
  78. package/src/testing/faults.ts +21 -21
  79. package/src/testing/index.ts +17 -21
  80. package/src/testing/memory-adapter/collection.ts +214 -219
  81. package/src/testing/memory-adapter/index.ts +510 -388
  82. package/src/testing/memory-adapter/migrations.ts +117 -119
  83. package/src/testing/memory-adapter/reset.ts +50 -50
  84. package/src/testing/memory-adapter/store.ts +128 -134
@@ -1,238 +1,252 @@
1
- /**
2
- * Deletion, and the ordering that makes an interrupted one visible.
3
- *
4
- * These are module-level functions rather than handle methods because they
5
- * must work whether or not a handle is open on the target (FR-059) — a
6
- * sign-out that already closed the handle still has to be able to delete, and
7
- * an app recovering from a crash has no handle to call a method on.
8
- *
9
- * The order is: **mark `deleting` first, clear the catalog row last**
10
- * (FR-061, FR-063). A crash at any point in between leaves the flag set, so
11
- * the next open reports a scope mid-deletion rather than presenting it as
12
- * complete. Clearing the row first would be the natural way to write this and
13
- * would produce exactly the failure FR-063 exists to prevent: an
14
- * interruption that looks like a success, leaving a user's data on disk after
15
- * they asked for it to be gone.
16
- *
17
- * Nothing here ever runs on its own. No expiry, no sweep, no deletion on
18
- * close or on app upgrade (FR-058) — the package deletes when asked, where
19
- * asked, and not otherwise.
20
- */
21
-
22
- import type { DeletionOutcome } from '../contract/attachment';
23
- import type { DatabaseTargetConfig, ScopeTargetConfig } from '../contract/config';
24
- import type { ResolvedScope } from '../contract/scope';
25
- import { fileSystem, removeIfPresent } from '../platform/fs';
26
- import { layoutFor, type StorageLayout } from '../platform/location';
27
- import { artifactsDirectory } from '../artifacts/paths';
28
- import { acquireKey } from '../crypto/key-provider';
29
- import { byteOrder } from '../query/compare';
30
- import { resolveScope } from '../scope/validate';
31
- import { clearScope, ensureCatalog, listEntries, markDeleting, readEntry } from '../sqlite/catalog';
32
- import { openConnection, type Connection } from '../sqlite/connection';
33
- import { num, text } from '../sqlite/values';
34
-
35
- /**
36
- * Scopes deleted beneath a handle that is still open.
37
- *
38
- * FR-059a wants the handle to report `SCOPE_DELETED` rather than keep
39
- * answering from a database that no longer exists — and wants the deletion to
40
- * proceed rather than wait for the handle to be closed. A shared set is the
41
- * whole mechanism: the deleter records, the handle checks.
42
- */
43
- const invalidated = new Set<string>();
44
-
45
- function invalidationKey(name: string, scopeKey: string): string {
46
- return `${name}:${scopeKey}`;
47
- }
48
-
49
- export function markScopeInvalidated(name: string, scopeKey: string): void {
50
- invalidated.add(invalidationKey(name, scopeKey));
51
- }
52
-
53
- export function clearScopeInvalidation(name: string, scopeKey: string): void {
54
- invalidated.delete(invalidationKey(name, scopeKey));
55
- }
56
-
57
- export function isScopeInvalidated(name: string, scopeKey: string): boolean {
58
- return invalidated.has(invalidationKey(name, scopeKey));
59
- }
60
-
61
- export function clearAllInvalidations(): void {
62
- invalidated.clear();
63
- }
64
-
65
- /** Deleting a scope that holds nothing is success, not an error (FR-062). */
66
- export function emptyOutcome(scope?: ResolvedScope): DeletionOutcome {
67
- return {
68
- ...(scope ? { scopeKey: scope.key } : {}),
69
- removedRecords: 0,
70
- removedArtifacts: [],
71
- orphanedReferences: [],
72
- failedRemovals: [],
73
- };
74
- }
75
-
76
- /**
77
- * How many records the scope held, best effort.
78
- *
79
- * Best effort because an encrypted scope whose caller supplied no key cannot
80
- * be opened to be counted — and refusing to delete it on that basis would be
81
- * the worst possible answer to a deletion request. The files come off disk
82
- * either way; only the number in the report is unavailable.
83
- */
84
- async function countRecords(
85
- layout: StorageLayout,
86
- scopeKey: string,
87
- encryptionKey: string | undefined,
88
- ): Promise<number> {
89
- let connection: Connection | null = null;
90
- try {
91
- connection = await openConnection({
92
- path: layout.scopeDataPath(scopeKey),
93
- ...(encryptionKey ? { encryptionKey } : {}),
94
- });
95
- const tables = await connection.execute(
96
- "SELECT name FROM sqlite_master WHERE type = 'table' AND name LIKE 'c!_%' ESCAPE '!'",
97
- );
98
- let total = 0;
99
- for (const row of tables.rows) {
100
- const table = text(row.name);
101
- const counted = await connection.execute(`SELECT count(*) AS total FROM "${table}"`);
102
- total += num(counted.rows[0]?.total ?? 0);
103
- }
104
- return total;
105
- } catch {
106
- return 0;
107
- } finally {
108
- await connection?.close();
109
- }
110
- }
111
-
112
- async function artifactIdsOnDisk(layout: StorageLayout, scopeKey: string): Promise<string[]> {
113
- try {
114
- return (await fileSystem().readdir(artifactsDirectory(layout, scopeKey))).sort(byteOrder);
115
- } catch {
116
- return [];
117
- }
118
- }
119
-
120
- /**
121
- * The one implementation behind `deleteScopeData` and `resetScope`.
122
- *
123
- * They do the same thing to disk and mean entirely different things to the
124
- * caller: one is a lifecycle operation an app performs on request, the other
125
- * is an admission that a database is broken and is being discarded to recover
126
- * from it (FR-057a). Keeping them as two exported names with two docstrings
127
- * is what stops the destructive one from being reached by accident — there is
128
- * no path from `openDatabase` to either.
129
- */
130
- async function removeScope(config: ScopeTargetConfig): Promise<DeletionOutcome> {
131
- const scope = resolveScope(config.scope);
132
- const layout = layoutFor(config.name);
133
- const catalog = await openConnection({ path: layout.catalogPath });
134
-
135
- try {
136
- await ensureCatalog(catalog);
137
- const entry = await readEntry(catalog, scope.key);
138
-
139
- // Mark first. Everything after this point is recoverable-in-principle;
140
- // this flag is what makes an interruption visible rather than silent.
141
- if (entry) {
142
- await markDeleting(catalog, scope.key);
143
- }
144
- markScopeInvalidated(config.name, scope.key);
145
-
146
- const removedArtifacts = await artifactIdsOnDisk(layout, scope.key);
147
-
148
- // The key is needed to count what is being removed, not to remove it.
149
- // It is released the moment the count is done, whatever happened.
150
- const keyHandle = config.encryption?.enabled
151
- ? await acquireKey(config.encryption, scope)
152
- : null;
153
- let removedRecords: number;
154
- try {
155
- removedRecords = await countRecords(layout, scope.key, keyHandle?.value());
156
- } finally {
157
- keyHandle?.release();
158
- }
159
-
160
- const failedRemovals: string[] = [];
161
- if (!(await removeIfPresent(layout.scopePath(scope.key))) && entry) {
162
- // The directory was there and would not go. Reporting it is the
163
- // point: an app that told the user "deleted" would be wrong.
164
- const stillThere = await fileSystem()
165
- .exists(layout.scopePath(scope.key))
166
- .catch(() => false);
167
- if (stillThere) {
168
- failedRemovals.push(scope.key);
169
- }
170
- }
171
-
172
- if (failedRemovals.length === 0) {
173
- // Cleared last, on purpose.
174
- await clearScope(catalog, scope.key);
175
- }
176
-
177
- return {
178
- scopeKey: scope.key,
179
- removedRecords,
180
- removedArtifacts,
181
- orphanedReferences: [],
182
- failedRemovals,
183
- };
184
- } finally {
185
- await catalog.close();
186
- }
187
- }
188
-
189
- /**
190
- * Remove one scope's data. Every collection in that scope, and nothing
191
- * outside it (FR-059, SC-012).
192
- */
193
- export function deleteScopeData(config: ScopeTargetConfig): Promise<DeletionOutcome> {
194
- return removeScope(config);
195
- }
196
-
197
- /**
198
- * Discard one scope and leave it ready for a clean first-run migration
199
- * (FR-057a).
200
- *
201
- * Destructive, separately named, and never reachable from `openDatabase`. An
202
- * automatic fallback here would turn a bad migration shipped to production
203
- * into silent data loss for every user who launched the app.
204
- */
205
- export function resetScope(config: ScopeTargetConfig): Promise<DeletionOutcome> {
206
- return removeScope(config);
207
- }
208
-
209
- /** Remove the whole database directory: catalog, every scope, every artifact. */
210
- export async function deleteDatabase(config: DatabaseTargetConfig): Promise<DeletionOutcome> {
211
- const layout = layoutFor(config.name);
212
-
213
- let scopeKeys: string[] = [];
214
- try {
215
- const catalog = await openConnection({ path: layout.catalogPath });
216
- try {
217
- await ensureCatalog(catalog);
218
- scopeKeys = (await listEntries(catalog)).map(entry => entry.scopeKey);
219
- } finally {
220
- await catalog.close();
221
- }
222
- } catch {
223
- // No catalog means nothing was ever registered here. The directory
224
- // removal below is still the right thing to attempt.
225
- }
226
-
227
- for (const scopeKey of scopeKeys) {
228
- markScopeInvalidated(config.name, scopeKey);
229
- }
230
-
231
- const removed = await removeIfPresent(layout.root);
232
- return {
233
- removedRecords: 0,
234
- removedArtifacts: [],
235
- orphanedReferences: [],
236
- failedRemovals: removed || scopeKeys.length === 0 ? [] : scopeKeys,
237
- };
238
- }
1
+ /**
2
+ * Deletion, and the ordering that makes an interrupted one visible.
3
+ *
4
+ * These are module-level functions rather than handle methods because they
5
+ * must work whether or not a handle is open on the target (FR-059) — a
6
+ * sign-out that already closed the handle still has to be able to delete, and
7
+ * an app recovering from a crash has no handle to call a method on.
8
+ *
9
+ * The order is: **mark `deleting` first, clear the catalog row last**
10
+ * (FR-061, FR-063). A crash at any point in between leaves the flag set, so
11
+ * the next open reports a scope mid-deletion rather than presenting it as
12
+ * complete. Clearing the row first would be the natural way to write this and
13
+ * would produce exactly the failure FR-063 exists to prevent: an
14
+ * interruption that looks like a success, leaving a user's data on disk after
15
+ * they asked for it to be gone.
16
+ *
17
+ * Nothing here ever runs on its own. No expiry, no sweep, no deletion on
18
+ * close or on app upgrade (FR-058) — the package deletes when asked, where
19
+ * asked, and not otherwise.
20
+ */
21
+
22
+ import type { DeletionOutcome } from '../contract/attachment';
23
+ import type { DatabaseTargetConfig, ScopeTargetConfig } from '../contract/config';
24
+ import type { ResolvedScope } from '../contract/scope';
25
+ import { fileSystem, removeIfPresent } from '../platform/fs';
26
+ import { layoutFor, type StorageLayout } from '../platform/location';
27
+ import { artifactsDirectory } from '../artifacts/paths';
28
+ import { acquireKey } from '../crypto/key-provider';
29
+ import { byteOrder } from '../query/compare';
30
+ import { resolveScope } from '../scope/validate';
31
+ import { clearScope, ensureCatalog, listEntries, markDeleting, readEntry } from '../sqlite/catalog';
32
+ import { openConnection, type Connection } from '../sqlite/connection';
33
+ import { num, text } from '../sqlite/values';
34
+
35
+ /**
36
+ * Scopes deleted beneath a handle that is still open.
37
+ *
38
+ * FR-059a wants the handle to report `SCOPE_DELETED` rather than keep
39
+ * answering from a database that no longer exists — and wants the deletion to
40
+ * proceed rather than wait for the handle to be closed. A shared set is the
41
+ * whole mechanism: the deleter records, the handle checks.
42
+ */
43
+ const invalidated = new Set<string>();
44
+
45
+ function invalidationKey(name: string, scopeKey: string): string {
46
+ return `${name}:${scopeKey}`;
47
+ }
48
+
49
+ export function markScopeInvalidated(name: string, scopeKey: string): void {
50
+ invalidated.add(invalidationKey(name, scopeKey));
51
+ }
52
+
53
+ export function clearScopeInvalidation(name: string, scopeKey: string): void {
54
+ invalidated.delete(invalidationKey(name, scopeKey));
55
+ }
56
+
57
+ export function isScopeInvalidated(name: string, scopeKey: string): boolean {
58
+ return invalidated.has(invalidationKey(name, scopeKey));
59
+ }
60
+
61
+ export function clearAllInvalidations(): void {
62
+ invalidated.clear();
63
+ }
64
+
65
+ /** Deleting a scope that holds nothing is success, not an error (FR-062). */
66
+ export function emptyOutcome(scope?: ResolvedScope): DeletionOutcome {
67
+ return {
68
+ ...(scope ? { scopeKey: scope.key } : {}),
69
+ removedRecords: 0,
70
+ removedArtifacts: [],
71
+ orphanedReferences: [],
72
+ failedRemovals: [],
73
+ };
74
+ }
75
+
76
+ /**
77
+ * How many records the scope held, best effort.
78
+ *
79
+ * Best effort because an encrypted scope whose caller supplied no key cannot
80
+ * be opened to be counted — and refusing to delete it on that basis would be
81
+ * the worst possible answer to a deletion request. The files come off disk
82
+ * either way; only the number in the report is unavailable.
83
+ */
84
+ async function countRecords(
85
+ layout: StorageLayout,
86
+ scopeKey: string,
87
+ encryptionKey: string | undefined,
88
+ ): Promise<number | undefined> {
89
+ let connection: Connection | null = null;
90
+ try {
91
+ connection = await openConnection({
92
+ path: layout.scopeDataPath(scopeKey),
93
+ ...(encryptionKey ? { encryptionKey } : {}),
94
+ });
95
+ const tables = await connection.execute(
96
+ "SELECT name FROM sqlite_master WHERE type = 'table' AND name LIKE 'c!_%' ESCAPE '!'",
97
+ );
98
+ let total = 0;
99
+ for (const row of tables.rows) {
100
+ const table = text(row.name);
101
+ const counted = await connection.execute(`SELECT count(*) AS total FROM "${table}"`);
102
+ total += num(counted.rows[0]?.total ?? 0);
103
+ }
104
+ return total;
105
+ } catch {
106
+ // Unknown, not zero: a report that said "0 records" about a scope it
107
+ // could not open would understate what was deleted.
108
+ return undefined;
109
+ } finally {
110
+ await connection?.close();
111
+ }
112
+ }
113
+
114
+ async function artifactIdsOnDisk(layout: StorageLayout, scopeKey: string): Promise<string[]> {
115
+ try {
116
+ return (await fileSystem().readdir(artifactsDirectory(layout, scopeKey))).sort(byteOrder);
117
+ } catch {
118
+ return [];
119
+ }
120
+ }
121
+
122
+ /**
123
+ * The one implementation behind `deleteScopeData` and `resetScope`.
124
+ *
125
+ * They do the same thing to disk and mean entirely different things to the
126
+ * caller: one is a lifecycle operation an app performs on request, the other
127
+ * is an admission that a database is broken and is being discarded to recover
128
+ * from it (FR-057a). Keeping them as two exported names with two docstrings
129
+ * is what stops the destructive one from being reached by accident — there is
130
+ * no path from `openDatabase` to either.
131
+ */
132
+ async function removeScope(config: ScopeTargetConfig): Promise<DeletionOutcome> {
133
+ const scope = resolveScope(config.scope);
134
+ const layout = layoutFor(config.name);
135
+ const catalog = await openConnection({ path: layout.catalogPath });
136
+
137
+ try {
138
+ await ensureCatalog(catalog);
139
+ const entry = await readEntry(catalog, scope.key);
140
+
141
+ // Mark first. Everything after this point is recoverable-in-principle;
142
+ // this flag is what makes an interruption visible rather than silent.
143
+ if (entry) {
144
+ await markDeleting(catalog, scope.key);
145
+ }
146
+ markScopeInvalidated(config.name, scope.key);
147
+
148
+ const removedArtifacts = await artifactIdsOnDisk(layout, scope.key);
149
+
150
+ // The key is needed to count what is being removed, not to remove it,
151
+ // so a key provider that fails — or a key already destroyed for this
152
+ // identity, which FR-104 encourages — costs the count, never the
153
+ // deletion. Until 0.2.0 it failed the whole call after the scope was
154
+ // already marked, leaving it stuck in `deleting`.
155
+ let keyHandle: Awaited<ReturnType<typeof acquireKey>> | null = null;
156
+ if (config.encryption?.enabled) {
157
+ try {
158
+ keyHandle = await acquireKey(config.encryption, scope);
159
+ } catch {
160
+ keyHandle = null;
161
+ }
162
+ }
163
+ let counted: number | undefined;
164
+ try {
165
+ counted =
166
+ config.encryption?.enabled && keyHandle === null
167
+ ? undefined
168
+ : await countRecords(layout, scope.key, keyHandle?.value());
169
+ } finally {
170
+ keyHandle?.release();
171
+ }
172
+
173
+ const failedRemovals: string[] = [];
174
+ if (!(await removeIfPresent(layout.scopePath(scope.key))) && entry) {
175
+ // The directory was there and would not go. Reporting it is the
176
+ // point: an app that told the user "deleted" would be wrong.
177
+ const stillThere = await fileSystem()
178
+ .exists(layout.scopePath(scope.key))
179
+ .catch(() => false);
180
+ if (stillThere) {
181
+ failedRemovals.push(scope.key);
182
+ }
183
+ }
184
+
185
+ if (failedRemovals.length === 0) {
186
+ // Cleared last, on purpose.
187
+ await clearScope(catalog, scope.key);
188
+ }
189
+
190
+ return {
191
+ scopeKey: scope.key,
192
+ removedRecords: counted ?? 0,
193
+ ...(counted === undefined ? { recordCountUnavailable: true } : {}),
194
+ removedArtifacts,
195
+ orphanedReferences: [],
196
+ failedRemovals,
197
+ };
198
+ } finally {
199
+ await catalog.close();
200
+ }
201
+ }
202
+
203
+ /**
204
+ * Remove one scope's data. Every collection in that scope, and nothing
205
+ * outside it (FR-059, SC-012).
206
+ */
207
+ export function deleteScopeData(config: ScopeTargetConfig): Promise<DeletionOutcome> {
208
+ return removeScope(config);
209
+ }
210
+
211
+ /**
212
+ * Discard one scope and leave it ready for a clean first-run migration
213
+ * (FR-057a).
214
+ *
215
+ * Destructive, separately named, and never reachable from `openDatabase`. An
216
+ * automatic fallback here would turn a bad migration shipped to production
217
+ * into silent data loss for every user who launched the app.
218
+ */
219
+ export function resetScope(config: ScopeTargetConfig): Promise<DeletionOutcome> {
220
+ return removeScope(config);
221
+ }
222
+
223
+ /** Remove the whole database directory: catalog, every scope, every artifact. */
224
+ export async function deleteDatabase(config: DatabaseTargetConfig): Promise<DeletionOutcome> {
225
+ const layout = layoutFor(config.name);
226
+
227
+ let scopeKeys: string[] = [];
228
+ try {
229
+ const catalog = await openConnection({ path: layout.catalogPath });
230
+ try {
231
+ await ensureCatalog(catalog);
232
+ scopeKeys = (await listEntries(catalog)).map(entry => entry.scopeKey);
233
+ } finally {
234
+ await catalog.close();
235
+ }
236
+ } catch {
237
+ // No catalog means nothing was ever registered here. The directory
238
+ // removal below is still the right thing to attempt.
239
+ }
240
+
241
+ for (const scopeKey of scopeKeys) {
242
+ markScopeInvalidated(config.name, scopeKey);
243
+ }
244
+
245
+ const removed = await removeIfPresent(layout.root);
246
+ return {
247
+ removedRecords: 0,
248
+ removedArtifacts: [],
249
+ orphanedReferences: [],
250
+ failedRemovals: removed || scopeKeys.length === 0 ? [] : scopeKeys,
251
+ };
252
+ }
@@ -1,68 +1,64 @@
1
- /**
2
- * The public lifecycle functions.
3
- *
4
- * Scope deletion and reset are module-level rather than handle methods: they
5
- * must work whether or not a handle is open on the target (FR-059), and
6
- * `resetScope` in particular must never be reachable as a fallback from
7
- * `openDatabase` (FR-057a).
8
- *
9
- * The active adapter is resolved lazily so a consuming app that only ever
10
- * uses the in-memory adapter in tests never loads the native peers.
11
- */
12
-
13
- import type { DatabaseHandle, StorageAdapter } from '../contract/adapter';
14
- import type {
15
- DatabaseConfig,
16
- DatabaseTargetConfig,
17
- ScopeTargetConfig,
18
- } from '../contract/config';
19
- import type { DeletionOutcome } from '../contract/attachment';
20
-
21
- let override: StorageAdapter | null = null;
22
- let cached: StorageAdapter | null = null;
23
-
24
- /**
25
- * Swap the adapter. Intended for tests and for the demo app's in-memory
26
- * mode; production code never calls this.
27
- */
28
- export function setStorageAdapter(adapter: StorageAdapter | null): void {
29
- override = adapter;
30
- }
31
-
32
- function activeAdapter(): StorageAdapter {
33
- if (override) {
34
- return override;
35
- }
36
- if (!cached) {
37
- // Lazy require, matching how rn-auth and rn-camera reach their native
38
- // peers: importing at module scope would pull op-sqlite into every
39
- // consumer, including ones that only run the in-memory adapter.
40
- const { createSqliteAdapter } = require('../sqlite/adapter') as {
41
- createSqliteAdapter: () => StorageAdapter;
42
- };
43
- cached = createSqliteAdapter();
44
- }
45
- return cached;
46
- }
47
-
48
- export function openDatabase(config: DatabaseConfig): Promise<DatabaseHandle> {
49
- return activeAdapter().open(config);
50
- }
51
-
52
- export function deleteScopeData(config: ScopeTargetConfig): Promise<DeletionOutcome> {
53
- return activeAdapter().deleteScopeData(config);
54
- }
55
-
56
- export function deleteDatabase(config: DatabaseTargetConfig): Promise<DeletionOutcome> {
57
- return activeAdapter().deleteDatabase(config);
58
- }
59
-
60
- /**
61
- * Destructive. Discards one scope's data and leaves it ready for a clean
62
- * first-run migration. Named apart from `deleteScopeData` deliberately: one
63
- * is a lifecycle operation, the other is an admission that a database is
64
- * broken.
65
- */
66
- export function resetScope(config: ScopeTargetConfig): Promise<DeletionOutcome> {
67
- return activeAdapter().resetScope(config);
68
- }
1
+ /**
2
+ * The public lifecycle functions.
3
+ *
4
+ * Scope deletion and reset are module-level rather than handle methods: they
5
+ * must work whether or not a handle is open on the target (FR-059), and
6
+ * `resetScope` in particular must never be reachable as a fallback from
7
+ * `openDatabase` (FR-057a).
8
+ *
9
+ * The active adapter is resolved lazily so a consuming app that only ever
10
+ * uses the in-memory adapter in tests never loads the native peers.
11
+ */
12
+
13
+ import type { DatabaseHandle, StorageAdapter } from '../contract/adapter';
14
+ import type { DatabaseConfig, DatabaseTargetConfig, ScopeTargetConfig } from '../contract/config';
15
+ import type { DeletionOutcome } from '../contract/attachment';
16
+
17
+ let override: StorageAdapter | null = null;
18
+ let cached: StorageAdapter | null = null;
19
+
20
+ /**
21
+ * Swap the adapter. Intended for tests and for the demo app's in-memory
22
+ * mode; production code never calls this.
23
+ */
24
+ export function setStorageAdapter(adapter: StorageAdapter | null): void {
25
+ override = adapter;
26
+ }
27
+
28
+ function activeAdapter(): StorageAdapter {
29
+ if (override) {
30
+ return override;
31
+ }
32
+ if (!cached) {
33
+ // Lazy require, matching how rn-auth and rn-camera reach their native
34
+ // peers: importing at module scope would pull op-sqlite into every
35
+ // consumer, including ones that only run the in-memory adapter.
36
+ const { createSqliteAdapter } = require('../sqlite/adapter') as {
37
+ createSqliteAdapter: () => StorageAdapter;
38
+ };
39
+ cached = createSqliteAdapter();
40
+ }
41
+ return cached;
42
+ }
43
+
44
+ export function openDatabase(config: DatabaseConfig): Promise<DatabaseHandle> {
45
+ return activeAdapter().open(config);
46
+ }
47
+
48
+ export function deleteScopeData(config: ScopeTargetConfig): Promise<DeletionOutcome> {
49
+ return activeAdapter().deleteScopeData(config);
50
+ }
51
+
52
+ export function deleteDatabase(config: DatabaseTargetConfig): Promise<DeletionOutcome> {
53
+ return activeAdapter().deleteDatabase(config);
54
+ }
55
+
56
+ /**
57
+ * Destructive. Discards one scope's data and leaves it ready for a clean
58
+ * first-run migration. Named apart from `deleteScopeData` deliberately: one
59
+ * is a lifecycle operation, the other is an admission that a database is
60
+ * broken.
61
+ */
62
+ export function resetScope(config: ScopeTargetConfig): Promise<DeletionOutcome> {
63
+ return activeAdapter().resetScope(config);
64
+ }