react-native-nitro-storage 0.10.3 → 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 +85 -14
- package/README.md +148 -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 +48 -33
- 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 +22 -4
- 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 +94 -17
- package/cpp/core/SqliteDiskStore.hpp +10 -0
- package/docs/api-reference.md +73 -28
- package/docs/benchmarks.md +41 -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 +149 -57
- 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 +61 -22
- 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 +62 -23
- 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 +84 -33
- 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 = {
|
|
@@ -342,6 +343,20 @@ export function createStorageCore(
|
|
|
342
343
|
let eventObserverRedactSecureValues = true;
|
|
343
344
|
const storageEvents = new StorageEventRegistry();
|
|
344
345
|
const metrics = createMetricsRegistry();
|
|
346
|
+
|
|
347
|
+
function setOwnDataProperty(
|
|
348
|
+
target: object,
|
|
349
|
+
key: string,
|
|
350
|
+
value: unknown,
|
|
351
|
+
): void {
|
|
352
|
+
Object.defineProperty(target, key, {
|
|
353
|
+
configurable: true,
|
|
354
|
+
enumerable: true,
|
|
355
|
+
value,
|
|
356
|
+
writable: true,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
|
|
345
360
|
const durability = createDurabilityCoordinator({
|
|
346
361
|
backend: {
|
|
347
362
|
setBatch: (keys, values, scope) => {
|
|
@@ -493,6 +508,26 @@ export function createStorageCore(
|
|
|
493
508
|
return scope !== StorageScope.Secure || !eventObserverRedactSecureValues;
|
|
494
509
|
}
|
|
495
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
|
+
|
|
496
531
|
function eventForGlobalObserver(
|
|
497
532
|
event: StorageChangeEvent,
|
|
498
533
|
): StorageChangeEvent {
|
|
@@ -693,6 +728,7 @@ export function createStorageCore(
|
|
|
693
728
|
|
|
694
729
|
function setRawValue(key: string, value: string, scope: StorageScope): void {
|
|
695
730
|
assertValidScope(scope);
|
|
731
|
+
assertValidStorageKey(key);
|
|
696
732
|
const storedValue = escapeCollidingRawValue(value);
|
|
697
733
|
const oldValue =
|
|
698
734
|
scope === StorageScope.Memory ? getEventRawValue(scope, key) : undefined;
|
|
@@ -731,9 +767,10 @@ export function createStorageCore(
|
|
|
731
767
|
|
|
732
768
|
function removeRawValue(key: string, scope: StorageScope): void {
|
|
733
769
|
assertValidScope(scope);
|
|
734
|
-
|
|
770
|
+
assertValidStorageKey(key);
|
|
771
|
+
const oldValue = readPreviousEventValue(scope, key);
|
|
735
772
|
if (scope === StorageScope.Memory) {
|
|
736
|
-
|
|
773
|
+
deleteMemoryKey(key);
|
|
737
774
|
notifyKeyListeners(memoryListeners, key);
|
|
738
775
|
emitKeyChange(scope, key, oldValue, undefined, "remove", "memory");
|
|
739
776
|
return;
|
|
@@ -837,7 +874,7 @@ export function createStorageCore(
|
|
|
837
874
|
const previousValues = shouldReadPreviousEventValues(scope)
|
|
838
875
|
? removeKeys.map((key) => getEventRawValue(scope, key))
|
|
839
876
|
: [];
|
|
840
|
-
removeKeys.forEach(
|
|
877
|
+
removeKeys.forEach(deleteMemoryKey);
|
|
841
878
|
removeKeys.forEach((key) => {
|
|
842
879
|
notifyKeyListeners(memoryListeners, key);
|
|
843
880
|
});
|
|
@@ -1290,9 +1327,11 @@ export function createStorageCore(
|
|
|
1290
1327
|
return measureOperation("storage:getKeysByPrefix", scope, () => {
|
|
1291
1328
|
assertValidScope(scope);
|
|
1292
1329
|
if (scope === StorageScope.Memory) {
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1330
|
+
const keys: string[] = [];
|
|
1331
|
+
for (const key of memoryStore.keys()) {
|
|
1332
|
+
if (key.startsWith(prefix)) keys.push(key);
|
|
1333
|
+
}
|
|
1334
|
+
return keys;
|
|
1296
1335
|
}
|
|
1297
1336
|
if (scope === StorageScope.Disk) {
|
|
1298
1337
|
flushDiskWrites();
|
|
@@ -1318,7 +1357,7 @@ export function createStorageCore(
|
|
|
1318
1357
|
keys.forEach((key) => {
|
|
1319
1358
|
const value = memoryStore.get(key);
|
|
1320
1359
|
if (typeof value === "string") {
|
|
1321
|
-
result
|
|
1360
|
+
setOwnDataProperty(result, key, unescapeCollidingRawValue(value));
|
|
1322
1361
|
}
|
|
1323
1362
|
});
|
|
1324
1363
|
return result;
|
|
@@ -1334,7 +1373,7 @@ export function createStorageCore(
|
|
|
1334
1373
|
keys.forEach((key, idx) => {
|
|
1335
1374
|
const value = values[idx];
|
|
1336
1375
|
if (value !== undefined) {
|
|
1337
|
-
result
|
|
1376
|
+
setOwnDataProperty(result, key, unescapeCollidingRawValue(value));
|
|
1338
1377
|
}
|
|
1339
1378
|
});
|
|
1340
1379
|
return result;
|
|
@@ -1345,11 +1384,11 @@ export function createStorageCore(
|
|
|
1345
1384
|
assertValidScope(scope);
|
|
1346
1385
|
const result: Record<string, string> = {};
|
|
1347
1386
|
if (scope === StorageScope.Memory) {
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
}
|
|
1387
|
+
memoryStore.forEach((value, key) => {
|
|
1388
|
+
if (typeof value === "string") {
|
|
1389
|
+
setOwnDataProperty(result, key, unescapeCollidingRawValue(value));
|
|
1390
|
+
}
|
|
1391
|
+
});
|
|
1353
1392
|
return result;
|
|
1354
1393
|
}
|
|
1355
1394
|
if (scope === StorageScope.Disk) {
|
|
@@ -1363,7 +1402,9 @@ export function createStorageCore(
|
|
|
1363
1402
|
const values = adapter.backend.getBatch(keys, scope);
|
|
1364
1403
|
keys.forEach((key, idx) => {
|
|
1365
1404
|
const val = values[idx];
|
|
1366
|
-
if (val !== undefined)
|
|
1405
|
+
if (val !== undefined) {
|
|
1406
|
+
setOwnDataProperty(result, key, unescapeCollidingRawValue(val));
|
|
1407
|
+
}
|
|
1367
1408
|
});
|
|
1368
1409
|
return result;
|
|
1369
1410
|
});
|
|
@@ -1503,14 +1544,18 @@ export function createStorageCore(
|
|
|
1503
1544
|
scope,
|
|
1504
1545
|
() => {
|
|
1505
1546
|
assertValidScope(scope);
|
|
1547
|
+
keys.forEach(assertValidStorageKey);
|
|
1506
1548
|
if (keys.length === 0) return;
|
|
1507
1549
|
const values = keys.map((k) => data[k] as string);
|
|
1508
1550
|
const storedValues = values.map(escapeCollidingRawValue);
|
|
1551
|
+
const readPrevious =
|
|
1552
|
+
scope === StorageScope.Memory ||
|
|
1553
|
+
shouldReadPreviousEventValues(scope);
|
|
1509
1554
|
const changes = keys.map((key, index) =>
|
|
1510
1555
|
createKeyChange(
|
|
1511
1556
|
scope,
|
|
1512
1557
|
key,
|
|
1513
|
-
getEventRawValue(scope, key),
|
|
1558
|
+
readPrevious ? getEventRawValue(scope, key) : undefined,
|
|
1514
1559
|
values[index],
|
|
1515
1560
|
"import",
|
|
1516
1561
|
scope === StorageScope.Memory ? "memory" : adapter.changeSource,
|
|
@@ -1553,6 +1598,7 @@ export function createStorageCore(
|
|
|
1553
1598
|
function createStorageItem<T = undefined>(
|
|
1554
1599
|
config: StorageItemConfig<T>,
|
|
1555
1600
|
): StorageItem<T> {
|
|
1601
|
+
assertValidStorageKey(config.key);
|
|
1556
1602
|
const storageKey = prefixKey(config.namespace, config.key);
|
|
1557
1603
|
const serialize = config.serialize ?? defaultSerialize;
|
|
1558
1604
|
const deserialize = config.deserialize ?? defaultDeserialize;
|
|
@@ -1732,7 +1778,7 @@ export function createStorageCore(
|
|
|
1732
1778
|
const raw = readBackendRaw(() =>
|
|
1733
1779
|
adapter.backend.getSecureBiometric(storageKey),
|
|
1734
1780
|
);
|
|
1735
|
-
if (readCache) {
|
|
1781
|
+
if (readCache || fallbackToCacheOnReadError) {
|
|
1736
1782
|
cacheRawValue(
|
|
1737
1783
|
resolveNonMemoryScope(),
|
|
1738
1784
|
storageKey,
|
|
@@ -1746,7 +1792,9 @@ export function createStorageCore(
|
|
|
1746
1792
|
const raw = readBackendRaw(() =>
|
|
1747
1793
|
adapter.backend.get(storageKey, config.scope),
|
|
1748
1794
|
);
|
|
1749
|
-
|
|
1795
|
+
if (readCache || fallbackToCacheOnReadError) {
|
|
1796
|
+
cacheRawValue(resolveNonMemoryScope(), storageKey, raw);
|
|
1797
|
+
}
|
|
1750
1798
|
return raw;
|
|
1751
1799
|
};
|
|
1752
1800
|
|
|
@@ -1757,7 +1805,10 @@ export function createStorageCore(
|
|
|
1757
1805
|
return read();
|
|
1758
1806
|
} catch (error) {
|
|
1759
1807
|
onReadError?.(error);
|
|
1760
|
-
if (
|
|
1808
|
+
if (
|
|
1809
|
+
fallbackToCacheOnReadError &&
|
|
1810
|
+
isStorageError(error, "keychain_locked")
|
|
1811
|
+
) {
|
|
1761
1812
|
const scope = resolveNonMemoryScope();
|
|
1762
1813
|
const cached = readCachedRawValue(
|
|
1763
1814
|
scope,
|
|
@@ -2407,13 +2458,11 @@ export function createStorageCore(
|
|
|
2407
2458
|
const removeStoredRaw = (
|
|
2408
2459
|
operation: StorageChangeOperation = "remove",
|
|
2409
2460
|
): void => {
|
|
2410
|
-
const oldValue =
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
)
|
|
2416
|
-
: getEventRawValue(config.scope, storageKey);
|
|
2461
|
+
const oldValue = readPreviousEventValue(
|
|
2462
|
+
config.scope,
|
|
2463
|
+
storageKey,
|
|
2464
|
+
isBiometric ? "biometric" : "plain",
|
|
2465
|
+
);
|
|
2417
2466
|
if (isBiometric) {
|
|
2418
2467
|
invalidateRawCache(StorageScope.Secure, storageKey);
|
|
2419
2468
|
scheduleRenameSourceCleanup();
|
|
@@ -2494,9 +2543,7 @@ export function createStorageCore(
|
|
|
2494
2543
|
} else {
|
|
2495
2544
|
memoryExpirationDeadlines.delete(storageKey);
|
|
2496
2545
|
}
|
|
2497
|
-
|
|
2498
|
-
typeof value === "string" ? escapeCollidingRawValue(value) : value;
|
|
2499
|
-
memoryStore.set(storageKey, storedValue);
|
|
2546
|
+
memoryStore.set(storageKey, toMemoryStoredValue(value));
|
|
2500
2547
|
notifyKeyListeners(memoryListeners, storageKey);
|
|
2501
2548
|
emitKeyChange(
|
|
2502
2549
|
config.scope,
|
|
@@ -3000,7 +3047,7 @@ export function createStorageCore(
|
|
|
3000
3047
|
|
|
3001
3048
|
// Atomic write: update all values in memoryStore, invalidate caches, then batch-notify
|
|
3002
3049
|
items.forEach(({ item, value }) => {
|
|
3003
|
-
memoryStore.set(item.key, value);
|
|
3050
|
+
memoryStore.set(item.key, toMemoryStoredValue(value));
|
|
3004
3051
|
asInternal(
|
|
3005
3052
|
item as StorageItem<unknown>,
|
|
3006
3053
|
)._invalidateParsedCacheOnly();
|
|
@@ -3455,9 +3502,13 @@ export function createStorageCore(
|
|
|
3455
3502
|
const rollbackSource =
|
|
3456
3503
|
scope === StorageScope.Memory ? "memory" : adapter.changeSource;
|
|
3457
3504
|
const rollbackErrors: { label: string; error: unknown }[] = [];
|
|
3505
|
+
const readRollbackEvents = shouldReadPreviousEventValues(scope);
|
|
3458
3506
|
const readRollbackEventValue = (
|
|
3459
3507
|
entry: TransactionRollbackEntry,
|
|
3460
3508
|
): string | undefined => {
|
|
3509
|
+
if (!readRollbackEvents) {
|
|
3510
|
+
return undefined;
|
|
3511
|
+
}
|
|
3461
3512
|
try {
|
|
3462
3513
|
return getEventRawValueForRepresentation(
|
|
3463
3514
|
scope,
|
|
@@ -3746,7 +3797,7 @@ export function createStorageCore(
|
|
|
3746
3797
|
const initial: Record<string, true> = {};
|
|
3747
3798
|
if (defaultValue) {
|
|
3748
3799
|
for (const id of defaultValue) {
|
|
3749
|
-
initial
|
|
3800
|
+
setOwnDataProperty(initial, id, true);
|
|
3750
3801
|
}
|
|
3751
3802
|
}
|
|
3752
3803
|
|
|
@@ -3763,7 +3814,7 @@ export function createStorageCore(
|
|
|
3763
3814
|
return;
|
|
3764
3815
|
}
|
|
3765
3816
|
const next = { ...current };
|
|
3766
|
-
next
|
|
3817
|
+
setOwnDataProperty(next, id, true);
|
|
3767
3818
|
item.set(next);
|
|
3768
3819
|
};
|
|
3769
3820
|
|
|
@@ -3789,7 +3840,7 @@ export function createStorageCore(
|
|
|
3789
3840
|
const getTyped = (): Partial<Record<TMember, true>> => {
|
|
3790
3841
|
const typed: Partial<Record<TMember, true>> = {};
|
|
3791
3842
|
for (const id of Object.keys(item.get())) {
|
|
3792
|
-
typed
|
|
3843
|
+
setOwnDataProperty(typed, id, true);
|
|
3793
3844
|
}
|
|
3794
3845
|
return typed;
|
|
3795
3846
|
};
|
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(
|