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.
Files changed (87) hide show
  1. package/CHANGELOG.md +85 -14
  2. package/README.md +148 -41
  3. package/SECURITY.md +16 -6
  4. package/android/build.gradle +0 -1
  5. package/android/consumer-rules.pro +0 -3
  6. package/android/src/main/cpp/AndroidStorageAdapterCpp.cpp +48 -33
  7. package/android/src/main/cpp/AndroidStorageAdapterCpp.hpp +3 -3
  8. package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +73 -23
  9. package/android/src/main/java/com/nitrostorage/DiskSqliteStore.kt +22 -4
  10. package/app.plugin.js +51 -51
  11. package/cpp/bindings/HybridStorage.cpp +25 -6
  12. package/cpp/core/NativeStorageAdapter.hpp +14 -1
  13. package/cpp/core/SqliteDiskStore.cpp +94 -17
  14. package/cpp/core/SqliteDiskStore.hpp +10 -0
  15. package/docs/api-reference.md +73 -28
  16. package/docs/benchmarks.md +41 -12
  17. package/docs/mmkv-migration.md +3 -1
  18. package/docs/native-libraries.md +10 -4
  19. package/docs/secure-storage.md +26 -12
  20. package/docs/web-backends.md +6 -2
  21. package/indexeddb-backend/package.json +6 -0
  22. package/ios/IOSStorageAdapterCpp.hpp +4 -0
  23. package/ios/IOSStorageAdapterCpp.mm +149 -57
  24. package/lib/commonjs/capabilities.js +3 -8
  25. package/lib/commonjs/capabilities.js.map +1 -1
  26. package/lib/commonjs/index.js +23 -8
  27. package/lib/commonjs/index.js.map +1 -1
  28. package/lib/commonjs/index.web.js +61 -19
  29. package/lib/commonjs/index.web.js.map +1 -1
  30. package/lib/commonjs/indexeddb-backend.js +21 -37
  31. package/lib/commonjs/indexeddb-backend.js.map +1 -1
  32. package/lib/commonjs/internal.js +6 -0
  33. package/lib/commonjs/internal.js.map +1 -1
  34. package/lib/commonjs/storage-core.js +61 -22
  35. package/lib/commonjs/storage-core.js.map +1 -1
  36. package/lib/commonjs/storage-runtime.js +1 -1
  37. package/lib/commonjs/storage-runtime.js.map +1 -1
  38. package/lib/commonjs/testing.js +68 -23
  39. package/lib/commonjs/testing.js.map +1 -1
  40. package/lib/commonjs/web-backend-contract.js +6 -2
  41. package/lib/commonjs/web-backend-contract.js.map +1 -1
  42. package/lib/module/capabilities.js +3 -8
  43. package/lib/module/capabilities.js.map +1 -1
  44. package/lib/module/index.js +14 -5
  45. package/lib/module/index.js.map +1 -1
  46. package/lib/module/index.web.js +61 -12
  47. package/lib/module/index.web.js.map +1 -1
  48. package/lib/module/indexeddb-backend.js +21 -37
  49. package/lib/module/indexeddb-backend.js.map +1 -1
  50. package/lib/module/internal.js +5 -0
  51. package/lib/module/internal.js.map +1 -1
  52. package/lib/module/storage-core.js +62 -23
  53. package/lib/module/storage-core.js.map +1 -1
  54. package/lib/module/storage-runtime.js +1 -1
  55. package/lib/module/storage-runtime.js.map +1 -1
  56. package/lib/module/testing.js +59 -16
  57. package/lib/module/testing.js.map +1 -1
  58. package/lib/module/web-backend-contract.js +5 -2
  59. package/lib/module/web-backend-contract.js.map +1 -1
  60. package/lib/typescript/capabilities.d.ts.map +1 -1
  61. package/lib/typescript/index.d.ts +8 -1
  62. package/lib/typescript/index.d.ts.map +1 -1
  63. package/lib/typescript/index.web.d.ts +7 -1
  64. package/lib/typescript/index.web.d.ts.map +1 -1
  65. package/lib/typescript/indexeddb-backend.d.ts.map +1 -1
  66. package/lib/typescript/internal.d.ts +1 -0
  67. package/lib/typescript/internal.d.ts.map +1 -1
  68. package/lib/typescript/storage-core.d.ts +1 -1
  69. package/lib/typescript/storage-core.d.ts.map +1 -1
  70. package/lib/typescript/storage-runtime.d.ts +1 -1
  71. package/lib/typescript/storage-runtime.d.ts.map +1 -1
  72. package/lib/typescript/testing.d.ts +15 -2
  73. package/lib/typescript/testing.d.ts.map +1 -1
  74. package/lib/typescript/web-backend-contract.d.ts +1 -0
  75. package/lib/typescript/web-backend-contract.d.ts.map +1 -1
  76. package/package.json +14 -5
  77. package/react-native-nitro-storage.podspec +4 -2
  78. package/src/capabilities.ts +4 -9
  79. package/src/index.ts +19 -5
  80. package/src/index.web.ts +86 -19
  81. package/src/indexeddb-backend.ts +20 -42
  82. package/src/internal.ts +8 -0
  83. package/src/storage-core.ts +84 -33
  84. package/src/storage-runtime.ts +3 -1
  85. package/src/testing.ts +91 -18
  86. package/src/web-backend-contract.ts +6 -2
  87. 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: "platform-preferences",
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
- export { createIndexedDBBackend } from "./indexeddb-backend";
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: "localStorage:disk",
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: "localStorage:secure",
126
- includeKey: (key) =>
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
- keyIndex.add(key);
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
- applyExternalChangeEvent(StorageScope.Disk, null, null);
439
- applyExternalChangeEvent(StorageScope.Secure, null, null);
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
- if (
444
- key.startsWith(SECURE_WEB_PREFIX) ||
445
- key.startsWith(BIOMETRIC_WEB_PREFIX)
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
- backend.clear();
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
- export { createIndexedDBBackend } from "./indexeddb-backend";
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;
@@ -111,42 +111,24 @@ export async function createIndexedDBBackend(
111
111
  options.onError?.(normalized);
112
112
  }
113
113
 
114
- function handleLifecycleFlush(): void {
115
- if (closed || pendingWrites.size === 0) {
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 installLifecycleFlush(): () => void {
125
- if (typeof globalThis === "undefined" || !globalThis.addEventListener) {
126
- return () => {};
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
- const onPageHide = () => {
130
- handleLifecycleFlush();
131
- };
132
- const onVisibilityChange = () => {
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 (data.key === null) {
162
- cache.clear();
163
- } else if (data.newValue === null) {
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
- emitExternal({
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;
@@ -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 { SecureStorageMetadata } from "./storage-runtime";
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
- const oldValue = getEventRawValue(scope, key);
770
+ assertValidStorageKey(key);
771
+ const oldValue = readPreviousEventValue(scope, key);
735
772
  if (scope === StorageScope.Memory) {
736
- memoryStore.delete(key);
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((key) => memoryStore.delete(key));
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
- return Array.from(memoryStore.keys()).filter((key) =>
1294
- key.startsWith(prefix),
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[key] = value;
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[key] = unescapeCollidingRawValue(value);
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
- for (const key of memoryStore.keys()) {
1349
- const value = memoryStore.get(key);
1350
- if (typeof value === "string")
1351
- result[key] = unescapeCollidingRawValue(value);
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) result[key] = unescapeCollidingRawValue(val);
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
- cacheRawValue(resolveNonMemoryScope(), storageKey, raw);
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 (fallbackToCacheOnReadError) {
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 = isBiometric
2411
- ? getEventRawValueForRepresentation(
2412
- config.scope,
2413
- storageKey,
2414
- "biometric",
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
- const storedValue =
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[id] = true;
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[id] = true;
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[id as TMember] = true;
3843
+ setOwnDataProperty(typed, id, true);
3793
3844
  }
3794
3845
  return typed;
3795
3846
  };
@@ -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(