react-native-nitro-storage 0.10.4 → 0.11.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/CHANGELOG.md +71 -14
- package/README.md +127 -41
- package/SECURITY.md +16 -6
- package/android/build.gradle +0 -1
- package/android/consumer-rules.pro +0 -3
- package/android/src/main/cpp/AndroidStorageAdapterCpp.cpp +1 -4
- package/android/src/main/cpp/AndroidStorageAdapterCpp.hpp +3 -3
- package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +73 -23
- package/android/src/main/java/com/nitrostorage/DiskSqliteStore.kt +15 -0
- package/app.plugin.js +51 -51
- package/cpp/bindings/HybridStorage.cpp +25 -6
- package/cpp/core/NativeStorageAdapter.hpp +14 -1
- package/cpp/core/SqliteDiskStore.cpp +84 -11
- package/cpp/core/SqliteDiskStore.hpp +10 -0
- package/docs/api-reference.md +73 -28
- package/docs/benchmarks.md +4 -12
- package/docs/mmkv-migration.md +3 -1
- package/docs/native-libraries.md +10 -4
- package/docs/secure-storage.md +26 -12
- package/docs/web-backends.md +6 -2
- package/indexeddb-backend/package.json +6 -0
- package/ios/IOSStorageAdapterCpp.hpp +4 -0
- package/ios/IOSStorageAdapterCpp.mm +96 -18
- package/lib/commonjs/capabilities.js +3 -8
- package/lib/commonjs/capabilities.js.map +1 -1
- package/lib/commonjs/index.js +23 -8
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +61 -19
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/indexeddb-backend.js +21 -37
- package/lib/commonjs/indexeddb-backend.js.map +1 -1
- package/lib/commonjs/internal.js +6 -0
- package/lib/commonjs/internal.js.map +1 -1
- package/lib/commonjs/storage-core.js +35 -11
- package/lib/commonjs/storage-core.js.map +1 -1
- package/lib/commonjs/storage-runtime.js +1 -1
- package/lib/commonjs/storage-runtime.js.map +1 -1
- package/lib/commonjs/testing.js +68 -23
- package/lib/commonjs/testing.js.map +1 -1
- package/lib/commonjs/web-backend-contract.js +6 -2
- package/lib/commonjs/web-backend-contract.js.map +1 -1
- package/lib/module/capabilities.js +3 -8
- package/lib/module/capabilities.js.map +1 -1
- package/lib/module/index.js +14 -5
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +61 -12
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/indexeddb-backend.js +21 -37
- package/lib/module/indexeddb-backend.js.map +1 -1
- package/lib/module/internal.js +5 -0
- package/lib/module/internal.js.map +1 -1
- package/lib/module/storage-core.js +36 -12
- package/lib/module/storage-core.js.map +1 -1
- package/lib/module/storage-runtime.js +1 -1
- package/lib/module/storage-runtime.js.map +1 -1
- package/lib/module/testing.js +59 -16
- package/lib/module/testing.js.map +1 -1
- package/lib/module/web-backend-contract.js +5 -2
- package/lib/module/web-backend-contract.js.map +1 -1
- package/lib/typescript/capabilities.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +8 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/index.web.d.ts +7 -1
- package/lib/typescript/index.web.d.ts.map +1 -1
- package/lib/typescript/indexeddb-backend.d.ts.map +1 -1
- package/lib/typescript/internal.d.ts +1 -0
- package/lib/typescript/internal.d.ts.map +1 -1
- package/lib/typescript/storage-core.d.ts +1 -1
- package/lib/typescript/storage-core.d.ts.map +1 -1
- package/lib/typescript/storage-runtime.d.ts +1 -1
- package/lib/typescript/storage-runtime.d.ts.map +1 -1
- package/lib/typescript/testing.d.ts +15 -2
- package/lib/typescript/testing.d.ts.map +1 -1
- package/lib/typescript/web-backend-contract.d.ts +1 -0
- package/lib/typescript/web-backend-contract.d.ts.map +1 -1
- package/package.json +14 -5
- package/react-native-nitro-storage.podspec +4 -2
- package/src/capabilities.ts +4 -9
- package/src/index.ts +19 -5
- package/src/index.web.ts +86 -19
- package/src/indexeddb-backend.ts +20 -42
- package/src/internal.ts +8 -0
- package/src/storage-core.ts +52 -19
- package/src/storage-runtime.ts +3 -1
- package/src/testing.ts +91 -18
- package/src/web-backend-contract.ts +6 -2
- package/testing/package.json +8 -0
package/src/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
DEFAULT_SECURE_WRITES_ASYNC,
|
|
5
5
|
resolveNativeWriteBuffering,
|
|
6
6
|
} from "./capabilities";
|
|
7
|
+
import { createIndexedDBBackend as createIndexedDBBackendFromSubpath } from "./indexeddb-backend";
|
|
7
8
|
import { unescapeCollidingRawValue } from "./internal";
|
|
8
9
|
import {
|
|
9
10
|
assertAccessControlLevel,
|
|
@@ -86,6 +87,11 @@ export type {
|
|
|
86
87
|
StorageKeyRef,
|
|
87
88
|
TransactionContext,
|
|
88
89
|
} from "./storage-core";
|
|
90
|
+
export {
|
|
91
|
+
describeWebBackendCapabilities,
|
|
92
|
+
isIndexedDBWebBackend,
|
|
93
|
+
type WebBackendCapabilities,
|
|
94
|
+
} from "./web-backend-contract";
|
|
89
95
|
export type { PlatformScope, PlatformStorage } from "./storage-platform";
|
|
90
96
|
|
|
91
97
|
let _storageModule: Storage | null = null;
|
|
@@ -200,6 +206,10 @@ function buildNativeAdapter(
|
|
|
200
206
|
return;
|
|
201
207
|
}
|
|
202
208
|
|
|
209
|
+
if (consumeSuppressedNativeEvent(scope, key)) {
|
|
210
|
+
notifyKeyListeners(internals.getScopedListeners(scope), key);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
203
213
|
const oldValue =
|
|
204
214
|
scope === StorageScope.Secure
|
|
205
215
|
? undefined
|
|
@@ -210,9 +220,6 @@ function buildNativeAdapter(
|
|
|
210
220
|
internals.cacheRawValue(scope, key, value);
|
|
211
221
|
}
|
|
212
222
|
notifyKeyListeners(internals.getScopedListeners(scope), key);
|
|
213
|
-
if (consumeSuppressedNativeEvent(scope, key)) {
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
223
|
internals.emitKeyChange(
|
|
217
224
|
scope,
|
|
218
225
|
key,
|
|
@@ -315,7 +322,7 @@ export const storage = {
|
|
|
315
322
|
getCapabilities: (): StorageCapabilities => ({
|
|
316
323
|
platform: "native",
|
|
317
324
|
backend: {
|
|
318
|
-
disk: "
|
|
325
|
+
disk: "sqlite",
|
|
319
326
|
secure: nativeSecureBackend,
|
|
320
327
|
},
|
|
321
328
|
writeBuffering: resolveNativeWriteBuffering(
|
|
@@ -392,4 +399,11 @@ export {
|
|
|
392
399
|
useStorageSelector,
|
|
393
400
|
useStorageValue,
|
|
394
401
|
} from "./storage-hooks";
|
|
395
|
-
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* @deprecated Import `createIndexedDBBackend` from
|
|
405
|
+
* `react-native-nitro-storage/indexeddb-backend` instead. The root export
|
|
406
|
+
* will be removed in a future minor release.
|
|
407
|
+
*/
|
|
408
|
+
export const createIndexedDBBackend: typeof createIndexedDBBackendFromSubpath =
|
|
409
|
+
createIndexedDBBackendFromSubpath;
|
package/src/index.web.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { resolveWebWriteBuffering } from "./capabilities";
|
|
2
|
+
import { createIndexedDBBackend as createIndexedDBBackendFromSubpath } from "./indexeddb-backend";
|
|
2
3
|
import { unescapeCollidingRawValue } from "./internal";
|
|
3
4
|
import {
|
|
4
5
|
assertAccessControlLevel,
|
|
@@ -111,20 +112,26 @@ function getInternals(): StorageCoreInternals {
|
|
|
111
112
|
return internals;
|
|
112
113
|
}
|
|
113
114
|
|
|
115
|
+
const DEFAULT_DISK_BACKEND_NAME = "localStorage:disk";
|
|
116
|
+
const DEFAULT_SECURE_BACKEND_NAME = "localStorage:secure";
|
|
117
|
+
|
|
118
|
+
function isSecureWebStorageKey(key: string): boolean {
|
|
119
|
+
return (
|
|
120
|
+
key.startsWith(SECURE_WEB_PREFIX) || key.startsWith(BIOMETRIC_WEB_PREFIX)
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
114
124
|
function createDefaultDiskBackend(): WebDiskStorageBackend {
|
|
115
125
|
return createLocalStorageWebBackend({
|
|
116
|
-
name:
|
|
117
|
-
includeKey: (key) =>
|
|
118
|
-
!key.startsWith(SECURE_WEB_PREFIX) &&
|
|
119
|
-
!key.startsWith(BIOMETRIC_WEB_PREFIX),
|
|
126
|
+
name: DEFAULT_DISK_BACKEND_NAME,
|
|
127
|
+
includeKey: (key) => !isSecureWebStorageKey(key),
|
|
120
128
|
});
|
|
121
129
|
}
|
|
122
130
|
|
|
123
131
|
function createDefaultSecureBackend(): WebSecureStorageBackend {
|
|
124
132
|
return createLocalStorageWebBackend({
|
|
125
|
-
name:
|
|
126
|
-
includeKey:
|
|
127
|
-
key.startsWith(SECURE_WEB_PREFIX) || key.startsWith(BIOMETRIC_WEB_PREFIX),
|
|
133
|
+
name: DEFAULT_SECURE_BACKEND_NAME,
|
|
134
|
+
includeKey: isSecureWebStorageKey,
|
|
128
135
|
});
|
|
129
136
|
}
|
|
130
137
|
|
|
@@ -227,7 +234,9 @@ function replaceWebScopeKeyIndex(
|
|
|
227
234
|
keyIndex.clear();
|
|
228
235
|
for (const key of keys) {
|
|
229
236
|
if (scope === StorageScope.Disk) {
|
|
230
|
-
|
|
237
|
+
if (!isSecureWebStorageKey(key)) {
|
|
238
|
+
keyIndex.add(key);
|
|
239
|
+
}
|
|
231
240
|
continue;
|
|
232
241
|
}
|
|
233
242
|
|
|
@@ -432,23 +441,41 @@ function applyExternalChangeEvent(
|
|
|
432
441
|
);
|
|
433
442
|
}
|
|
434
443
|
|
|
444
|
+
function usesDefaultLocalStorageBackend(scope: NonMemoryScope): boolean {
|
|
445
|
+
return (
|
|
446
|
+
getWebBackend(scope)?.name ===
|
|
447
|
+
(scope === StorageScope.Disk
|
|
448
|
+
? DEFAULT_DISK_BACKEND_NAME
|
|
449
|
+
: DEFAULT_SECURE_BACKEND_NAME)
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
|
|
435
453
|
function handleWebStorageEvent(event: StorageEvent): void {
|
|
454
|
+
if (
|
|
455
|
+
typeof globalThis.localStorage === "undefined" ||
|
|
456
|
+
event.storageArea !== globalThis.localStorage
|
|
457
|
+
) {
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
|
|
436
461
|
const key = event.key;
|
|
437
462
|
if (key === null) {
|
|
438
|
-
|
|
439
|
-
|
|
463
|
+
if (usesDefaultLocalStorageBackend(StorageScope.Disk)) {
|
|
464
|
+
applyExternalChangeEvent(StorageScope.Disk, null, null);
|
|
465
|
+
}
|
|
466
|
+
if (usesDefaultLocalStorageBackend(StorageScope.Secure)) {
|
|
467
|
+
applyExternalChangeEvent(StorageScope.Secure, null, null);
|
|
468
|
+
}
|
|
440
469
|
return;
|
|
441
470
|
}
|
|
442
471
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
) {
|
|
447
|
-
applyExternalChangeEvent(StorageScope.Secure, key, event.newValue);
|
|
472
|
+
const scope = isSecureWebStorageKey(key)
|
|
473
|
+
? StorageScope.Secure
|
|
474
|
+
: StorageScope.Disk;
|
|
475
|
+
if (!usesDefaultLocalStorageBackend(scope)) {
|
|
448
476
|
return;
|
|
449
477
|
}
|
|
450
|
-
|
|
451
|
-
applyExternalChangeEvent(StorageScope.Disk, key, event.newValue);
|
|
478
|
+
applyExternalChangeEvent(scope, key, event.newValue);
|
|
452
479
|
}
|
|
453
480
|
|
|
454
481
|
function subscribeToBackendChanges(scope: NonMemoryScope): void {
|
|
@@ -462,6 +489,22 @@ function subscribeToBackendChanges(scope: NonMemoryScope): void {
|
|
|
462
489
|
}
|
|
463
490
|
|
|
464
491
|
const unsubscribe = backend.subscribe((event: WebStorageChangeEvent) => {
|
|
492
|
+
if (webDiskStorageBackend !== webSecureStorageBackend) {
|
|
493
|
+
applyExternalChangeEvent(scope, event.key, event.newValue);
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
if (event.key === null) {
|
|
497
|
+
getInternals().clearScopeRawCache(scope);
|
|
498
|
+
reconcileWebScopeKeyIndex(scope);
|
|
499
|
+
notifyAllListeners(getInternals().getScopedListeners(scope));
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
const eventScope = isSecureWebStorageKey(event.key)
|
|
503
|
+
? StorageScope.Secure
|
|
504
|
+
: StorageScope.Disk;
|
|
505
|
+
if (eventScope !== scope) {
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
465
508
|
applyExternalChangeEvent(scope, event.key, event.newValue);
|
|
466
509
|
});
|
|
467
510
|
externalSyncUnsubscribers.set(scope, unsubscribe);
|
|
@@ -581,7 +624,24 @@ const WebStorage: Storage = {
|
|
|
581
624
|
}
|
|
582
625
|
try {
|
|
583
626
|
withWebBackendOperation(scope, "clear", (backend) => {
|
|
584
|
-
|
|
627
|
+
if (webDiskStorageBackend !== webSecureStorageBackend) {
|
|
628
|
+
backend.clear();
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
const ownedKeys = backend
|
|
632
|
+
.getAllKeys()
|
|
633
|
+
.filter((key) =>
|
|
634
|
+
scope === StorageScope.Secure
|
|
635
|
+
? isSecureWebStorageKey(key)
|
|
636
|
+
: !isSecureWebStorageKey(key),
|
|
637
|
+
);
|
|
638
|
+
if (backend.removeMany) {
|
|
639
|
+
backend.removeMany(ownedKeys);
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
ownedKeys.forEach((key) => {
|
|
643
|
+
backend.removeItem(key);
|
|
644
|
+
});
|
|
585
645
|
});
|
|
586
646
|
} catch (error) {
|
|
587
647
|
throwAfterWebMutationFailure(scope, "clear", error);
|
|
@@ -1135,4 +1195,11 @@ export {
|
|
|
1135
1195
|
useStorageSelector,
|
|
1136
1196
|
useStorageValue,
|
|
1137
1197
|
} from "./storage-hooks";
|
|
1138
|
-
|
|
1198
|
+
|
|
1199
|
+
/**
|
|
1200
|
+
* @deprecated Import `createIndexedDBBackend` from
|
|
1201
|
+
* `react-native-nitro-storage/indexeddb-backend` instead. The root export
|
|
1202
|
+
* will be removed in a future minor release.
|
|
1203
|
+
*/
|
|
1204
|
+
export const createIndexedDBBackend: typeof createIndexedDBBackendFromSubpath =
|
|
1205
|
+
createIndexedDBBackendFromSubpath;
|
package/src/indexeddb-backend.ts
CHANGED
|
@@ -111,42 +111,24 @@ export async function createIndexedDBBackend(
|
|
|
111
111
|
options.onError?.(normalized);
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
void Promise.all(Array.from(pendingWrites)).catch(() => {
|
|
119
|
-
// Errors are surfaced through flush() and onError; the page may be
|
|
120
|
-
// hidden, so the flush result is intentionally not awaited here.
|
|
121
|
-
});
|
|
122
|
-
}
|
|
114
|
+
let hydrated = false;
|
|
115
|
+
const queuedChannelEvents: WebStorageChangeEvent[] = [];
|
|
123
116
|
|
|
124
|
-
function
|
|
125
|
-
if (
|
|
126
|
-
|
|
117
|
+
function applyChannelEvent(data: WebStorageChangeEvent): void {
|
|
118
|
+
if (data.key === null) {
|
|
119
|
+
cache.clear();
|
|
120
|
+
} else if (data.newValue === null) {
|
|
121
|
+
cache.delete(data.key);
|
|
122
|
+
} else {
|
|
123
|
+
cache.set(data.key, data.newValue);
|
|
127
124
|
}
|
|
128
125
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
if (
|
|
134
|
-
typeof document !== "undefined" &&
|
|
135
|
-
document.visibilityState === "hidden"
|
|
136
|
-
) {
|
|
137
|
-
handleLifecycleFlush();
|
|
138
|
-
}
|
|
139
|
-
};
|
|
140
|
-
globalThis.addEventListener("pagehide", onPageHide);
|
|
141
|
-
globalThis.addEventListener("visibilitychange", onVisibilityChange);
|
|
142
|
-
return () => {
|
|
143
|
-
globalThis.removeEventListener("pagehide", onPageHide);
|
|
144
|
-
globalThis.removeEventListener("visibilitychange", onVisibilityChange);
|
|
145
|
-
};
|
|
126
|
+
emitExternal({
|
|
127
|
+
key: data.key,
|
|
128
|
+
newValue: data.newValue,
|
|
129
|
+
});
|
|
146
130
|
}
|
|
147
131
|
|
|
148
|
-
const removeLifecycleFlush = installLifecycleFlush();
|
|
149
|
-
|
|
150
132
|
channel?.addEventListener("message", (event: MessageEvent) => {
|
|
151
133
|
if (closed) {
|
|
152
134
|
return;
|
|
@@ -158,18 +140,12 @@ export async function createIndexedDBBackend(
|
|
|
158
140
|
return;
|
|
159
141
|
}
|
|
160
142
|
|
|
161
|
-
if (
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
cache.delete(data.key);
|
|
165
|
-
} else {
|
|
166
|
-
cache.set(data.key, data.newValue);
|
|
143
|
+
if (!hydrated) {
|
|
144
|
+
queuedChannelEvents.push({ key: data.key, newValue: data.newValue });
|
|
145
|
+
return;
|
|
167
146
|
}
|
|
168
147
|
|
|
169
|
-
|
|
170
|
-
key: data.key,
|
|
171
|
-
newValue: data.newValue,
|
|
172
|
-
});
|
|
148
|
+
applyChannelEvent(data);
|
|
173
149
|
});
|
|
174
150
|
|
|
175
151
|
function publish(event: WebStorageChangeEvent): void {
|
|
@@ -204,6 +180,9 @@ export async function createIndexedDBBackend(
|
|
|
204
180
|
};
|
|
205
181
|
});
|
|
206
182
|
|
|
183
|
+
hydrated = true;
|
|
184
|
+
queuedChannelEvents.splice(0).forEach(applyChannelEvent);
|
|
185
|
+
|
|
207
186
|
function trackWrite(tx: IDBTransaction, keys: string[] | null): void {
|
|
208
187
|
const pending = new Promise<void>((resolve) => {
|
|
209
188
|
tx.oncomplete = () => {
|
|
@@ -381,7 +360,6 @@ export async function createIndexedDBBackend(
|
|
|
381
360
|
return;
|
|
382
361
|
}
|
|
383
362
|
closed = true;
|
|
384
|
-
removeLifecycleFlush();
|
|
385
363
|
subscribers.clear();
|
|
386
364
|
channel?.close();
|
|
387
365
|
db.close();
|
package/src/internal.ts
CHANGED
|
@@ -72,6 +72,14 @@ export function assertValidScope(scope: StorageScope): void {
|
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
export function assertValidStorageKey(key: string): void {
|
|
76
|
+
if (typeof key !== "string" || key.length === 0) {
|
|
77
|
+
throw new Error(
|
|
78
|
+
"[nitro-error:invalid_key] NitroStorage: storage keys must be non-empty strings.",
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
75
83
|
export type ScopedBatchItem = {
|
|
76
84
|
key: string;
|
|
77
85
|
scope: StorageScope;
|
package/src/storage-core.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
assertBatchScope,
|
|
8
8
|
assertValidScope,
|
|
9
9
|
toVersionToken,
|
|
10
|
+
assertValidStorageKey,
|
|
10
11
|
prefixKey,
|
|
11
12
|
isNamespaced,
|
|
12
13
|
escapeCollidingRawValue,
|
|
@@ -57,7 +58,7 @@ import {
|
|
|
57
58
|
type StorageKeyChangeEvent,
|
|
58
59
|
} from "./storage-events";
|
|
59
60
|
import type { StorageSetter } from "./storage-hooks";
|
|
60
|
-
import type
|
|
61
|
+
import { isStorageError, type SecureStorageMetadata } from "./storage-runtime";
|
|
61
62
|
import { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
|
|
62
63
|
|
|
63
64
|
export type TransactionContext = {
|
|
@@ -507,6 +508,26 @@ export function createStorageCore(
|
|
|
507
508
|
return scope !== StorageScope.Secure || !eventObserverRedactSecureValues;
|
|
508
509
|
}
|
|
509
510
|
|
|
511
|
+
function readPreviousEventValue(
|
|
512
|
+
scope: StorageScope,
|
|
513
|
+
key: string,
|
|
514
|
+
representation: StorageRawCacheRepresentation = "plain",
|
|
515
|
+
): string | undefined {
|
|
516
|
+
if (!shouldReadPreviousEventValues(scope)) {
|
|
517
|
+
return undefined;
|
|
518
|
+
}
|
|
519
|
+
return getEventRawValueForRepresentation(scope, key, representation);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function toMemoryStoredValue(value: unknown): unknown {
|
|
523
|
+
return typeof value === "string" ? escapeCollidingRawValue(value) : value;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function deleteMemoryKey(key: string): void {
|
|
527
|
+
memoryStore.delete(key);
|
|
528
|
+
memoryExpirationDeadlines.delete(key);
|
|
529
|
+
}
|
|
530
|
+
|
|
510
531
|
function eventForGlobalObserver(
|
|
511
532
|
event: StorageChangeEvent,
|
|
512
533
|
): StorageChangeEvent {
|
|
@@ -707,6 +728,7 @@ export function createStorageCore(
|
|
|
707
728
|
|
|
708
729
|
function setRawValue(key: string, value: string, scope: StorageScope): void {
|
|
709
730
|
assertValidScope(scope);
|
|
731
|
+
assertValidStorageKey(key);
|
|
710
732
|
const storedValue = escapeCollidingRawValue(value);
|
|
711
733
|
const oldValue =
|
|
712
734
|
scope === StorageScope.Memory ? getEventRawValue(scope, key) : undefined;
|
|
@@ -745,9 +767,10 @@ export function createStorageCore(
|
|
|
745
767
|
|
|
746
768
|
function removeRawValue(key: string, scope: StorageScope): void {
|
|
747
769
|
assertValidScope(scope);
|
|
748
|
-
|
|
770
|
+
assertValidStorageKey(key);
|
|
771
|
+
const oldValue = readPreviousEventValue(scope, key);
|
|
749
772
|
if (scope === StorageScope.Memory) {
|
|
750
|
-
|
|
773
|
+
deleteMemoryKey(key);
|
|
751
774
|
notifyKeyListeners(memoryListeners, key);
|
|
752
775
|
emitKeyChange(scope, key, oldValue, undefined, "remove", "memory");
|
|
753
776
|
return;
|
|
@@ -851,7 +874,7 @@ export function createStorageCore(
|
|
|
851
874
|
const previousValues = shouldReadPreviousEventValues(scope)
|
|
852
875
|
? removeKeys.map((key) => getEventRawValue(scope, key))
|
|
853
876
|
: [];
|
|
854
|
-
removeKeys.forEach(
|
|
877
|
+
removeKeys.forEach(deleteMemoryKey);
|
|
855
878
|
removeKeys.forEach((key) => {
|
|
856
879
|
notifyKeyListeners(memoryListeners, key);
|
|
857
880
|
});
|
|
@@ -1521,14 +1544,18 @@ export function createStorageCore(
|
|
|
1521
1544
|
scope,
|
|
1522
1545
|
() => {
|
|
1523
1546
|
assertValidScope(scope);
|
|
1547
|
+
keys.forEach(assertValidStorageKey);
|
|
1524
1548
|
if (keys.length === 0) return;
|
|
1525
1549
|
const values = keys.map((k) => data[k] as string);
|
|
1526
1550
|
const storedValues = values.map(escapeCollidingRawValue);
|
|
1551
|
+
const readPrevious =
|
|
1552
|
+
scope === StorageScope.Memory ||
|
|
1553
|
+
shouldReadPreviousEventValues(scope);
|
|
1527
1554
|
const changes = keys.map((key, index) =>
|
|
1528
1555
|
createKeyChange(
|
|
1529
1556
|
scope,
|
|
1530
1557
|
key,
|
|
1531
|
-
getEventRawValue(scope, key),
|
|
1558
|
+
readPrevious ? getEventRawValue(scope, key) : undefined,
|
|
1532
1559
|
values[index],
|
|
1533
1560
|
"import",
|
|
1534
1561
|
scope === StorageScope.Memory ? "memory" : adapter.changeSource,
|
|
@@ -1571,6 +1598,7 @@ export function createStorageCore(
|
|
|
1571
1598
|
function createStorageItem<T = undefined>(
|
|
1572
1599
|
config: StorageItemConfig<T>,
|
|
1573
1600
|
): StorageItem<T> {
|
|
1601
|
+
assertValidStorageKey(config.key);
|
|
1574
1602
|
const storageKey = prefixKey(config.namespace, config.key);
|
|
1575
1603
|
const serialize = config.serialize ?? defaultSerialize;
|
|
1576
1604
|
const deserialize = config.deserialize ?? defaultDeserialize;
|
|
@@ -1750,7 +1778,7 @@ export function createStorageCore(
|
|
|
1750
1778
|
const raw = readBackendRaw(() =>
|
|
1751
1779
|
adapter.backend.getSecureBiometric(storageKey),
|
|
1752
1780
|
);
|
|
1753
|
-
if (readCache) {
|
|
1781
|
+
if (readCache || fallbackToCacheOnReadError) {
|
|
1754
1782
|
cacheRawValue(
|
|
1755
1783
|
resolveNonMemoryScope(),
|
|
1756
1784
|
storageKey,
|
|
@@ -1764,7 +1792,9 @@ export function createStorageCore(
|
|
|
1764
1792
|
const raw = readBackendRaw(() =>
|
|
1765
1793
|
adapter.backend.get(storageKey, config.scope),
|
|
1766
1794
|
);
|
|
1767
|
-
|
|
1795
|
+
if (readCache || fallbackToCacheOnReadError) {
|
|
1796
|
+
cacheRawValue(resolveNonMemoryScope(), storageKey, raw);
|
|
1797
|
+
}
|
|
1768
1798
|
return raw;
|
|
1769
1799
|
};
|
|
1770
1800
|
|
|
@@ -1775,7 +1805,10 @@ export function createStorageCore(
|
|
|
1775
1805
|
return read();
|
|
1776
1806
|
} catch (error) {
|
|
1777
1807
|
onReadError?.(error);
|
|
1778
|
-
if (
|
|
1808
|
+
if (
|
|
1809
|
+
fallbackToCacheOnReadError &&
|
|
1810
|
+
isStorageError(error, "keychain_locked")
|
|
1811
|
+
) {
|
|
1779
1812
|
const scope = resolveNonMemoryScope();
|
|
1780
1813
|
const cached = readCachedRawValue(
|
|
1781
1814
|
scope,
|
|
@@ -2425,13 +2458,11 @@ export function createStorageCore(
|
|
|
2425
2458
|
const removeStoredRaw = (
|
|
2426
2459
|
operation: StorageChangeOperation = "remove",
|
|
2427
2460
|
): void => {
|
|
2428
|
-
const oldValue =
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
)
|
|
2434
|
-
: getEventRawValue(config.scope, storageKey);
|
|
2461
|
+
const oldValue = readPreviousEventValue(
|
|
2462
|
+
config.scope,
|
|
2463
|
+
storageKey,
|
|
2464
|
+
isBiometric ? "biometric" : "plain",
|
|
2465
|
+
);
|
|
2435
2466
|
if (isBiometric) {
|
|
2436
2467
|
invalidateRawCache(StorageScope.Secure, storageKey);
|
|
2437
2468
|
scheduleRenameSourceCleanup();
|
|
@@ -2512,9 +2543,7 @@ export function createStorageCore(
|
|
|
2512
2543
|
} else {
|
|
2513
2544
|
memoryExpirationDeadlines.delete(storageKey);
|
|
2514
2545
|
}
|
|
2515
|
-
|
|
2516
|
-
typeof value === "string" ? escapeCollidingRawValue(value) : value;
|
|
2517
|
-
memoryStore.set(storageKey, storedValue);
|
|
2546
|
+
memoryStore.set(storageKey, toMemoryStoredValue(value));
|
|
2518
2547
|
notifyKeyListeners(memoryListeners, storageKey);
|
|
2519
2548
|
emitKeyChange(
|
|
2520
2549
|
config.scope,
|
|
@@ -3018,7 +3047,7 @@ export function createStorageCore(
|
|
|
3018
3047
|
|
|
3019
3048
|
// Atomic write: update all values in memoryStore, invalidate caches, then batch-notify
|
|
3020
3049
|
items.forEach(({ item, value }) => {
|
|
3021
|
-
memoryStore.set(item.key, value);
|
|
3050
|
+
memoryStore.set(item.key, toMemoryStoredValue(value));
|
|
3022
3051
|
asInternal(
|
|
3023
3052
|
item as StorageItem<unknown>,
|
|
3024
3053
|
)._invalidateParsedCacheOnly();
|
|
@@ -3473,9 +3502,13 @@ export function createStorageCore(
|
|
|
3473
3502
|
const rollbackSource =
|
|
3474
3503
|
scope === StorageScope.Memory ? "memory" : adapter.changeSource;
|
|
3475
3504
|
const rollbackErrors: { label: string; error: unknown }[] = [];
|
|
3505
|
+
const readRollbackEvents = shouldReadPreviousEventValues(scope);
|
|
3476
3506
|
const readRollbackEventValue = (
|
|
3477
3507
|
entry: TransactionRollbackEntry,
|
|
3478
3508
|
): string | undefined => {
|
|
3509
|
+
if (!readRollbackEvents) {
|
|
3510
|
+
return undefined;
|
|
3511
|
+
}
|
|
3479
3512
|
try {
|
|
3480
3513
|
return getEventRawValueForRepresentation(
|
|
3481
3514
|
scope,
|
package/src/storage-runtime.ts
CHANGED
|
@@ -5,7 +5,8 @@ export type StorageErrorCode =
|
|
|
5
5
|
| "storage_corruption"
|
|
6
6
|
| "storage_compensation_failed"
|
|
7
7
|
| "biometric_unavailable"
|
|
8
|
-
| "unsupported"
|
|
8
|
+
| "unsupported"
|
|
9
|
+
| "invalid_key";
|
|
9
10
|
|
|
10
11
|
export type StorageCapabilities = {
|
|
11
12
|
platform: "native" | "web";
|
|
@@ -65,6 +66,7 @@ const STORAGE_ERROR_CODES = new Set<StorageErrorCode>([
|
|
|
65
66
|
"storage_compensation_failed",
|
|
66
67
|
"biometric_unavailable",
|
|
67
68
|
"unsupported",
|
|
69
|
+
"invalid_key",
|
|
68
70
|
]);
|
|
69
71
|
|
|
70
72
|
export function getStorageErrorCode(
|