react-native-nitro-storage 0.10.4 → 0.12.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 +86 -14
  2. package/README.md +149 -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 +1 -4
  7. package/android/src/main/cpp/AndroidStorageAdapterCpp.hpp +3 -3
  8. package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +107 -36
  9. package/android/src/main/java/com/nitrostorage/DiskSqliteStore.kt +32 -13
  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 +105 -20
  14. package/cpp/core/SqliteDiskStore.hpp +11 -0
  15. package/docs/api-reference.md +74 -28
  16. package/docs/benchmarks.md +4 -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 +96 -18
  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 +63 -20
  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 +35 -11
  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 +63 -13
  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 +36 -12
  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 +91 -20
  81. package/src/indexeddb-backend.ts +20 -42
  82. package/src/internal.ts +8 -0
  83. package/src/storage-core.ts +52 -19
  84. package/src/storage-runtime.ts +5 -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
 
@@ -158,8 +165,12 @@ function createWebStorageError(
158
165
  const backendName = getBackendName(scope, backend);
159
166
  const message =
160
167
  error instanceof Error ? error.message : String(error ?? "Unknown error");
168
+ const tag =
169
+ error instanceof Error && error.name === "QuotaExceededError"
170
+ ? "[nitro-error:storage_full] "
171
+ : "";
161
172
  const wrapped = new Error(
162
- `NitroStorage(web): ${operation} failed for ${backendName}: ${message}`,
173
+ `${tag}NitroStorage(web): ${operation} failed for ${backendName}: ${message}`,
163
174
  );
164
175
  Object.defineProperty(wrapped, "cause", {
165
176
  configurable: true,
@@ -227,7 +238,9 @@ function replaceWebScopeKeyIndex(
227
238
  keyIndex.clear();
228
239
  for (const key of keys) {
229
240
  if (scope === StorageScope.Disk) {
230
- keyIndex.add(key);
241
+ if (!isSecureWebStorageKey(key)) {
242
+ keyIndex.add(key);
243
+ }
231
244
  continue;
232
245
  }
233
246
 
@@ -432,23 +445,41 @@ function applyExternalChangeEvent(
432
445
  );
433
446
  }
434
447
 
448
+ function usesDefaultLocalStorageBackend(scope: NonMemoryScope): boolean {
449
+ return (
450
+ getWebBackend(scope)?.name ===
451
+ (scope === StorageScope.Disk
452
+ ? DEFAULT_DISK_BACKEND_NAME
453
+ : DEFAULT_SECURE_BACKEND_NAME)
454
+ );
455
+ }
456
+
435
457
  function handleWebStorageEvent(event: StorageEvent): void {
458
+ if (
459
+ typeof globalThis.localStorage === "undefined" ||
460
+ event.storageArea !== globalThis.localStorage
461
+ ) {
462
+ return;
463
+ }
464
+
436
465
  const key = event.key;
437
466
  if (key === null) {
438
- applyExternalChangeEvent(StorageScope.Disk, null, null);
439
- applyExternalChangeEvent(StorageScope.Secure, null, null);
467
+ if (usesDefaultLocalStorageBackend(StorageScope.Disk)) {
468
+ applyExternalChangeEvent(StorageScope.Disk, null, null);
469
+ }
470
+ if (usesDefaultLocalStorageBackend(StorageScope.Secure)) {
471
+ applyExternalChangeEvent(StorageScope.Secure, null, null);
472
+ }
440
473
  return;
441
474
  }
442
475
 
443
- if (
444
- key.startsWith(SECURE_WEB_PREFIX) ||
445
- key.startsWith(BIOMETRIC_WEB_PREFIX)
446
- ) {
447
- applyExternalChangeEvent(StorageScope.Secure, key, event.newValue);
476
+ const scope = isSecureWebStorageKey(key)
477
+ ? StorageScope.Secure
478
+ : StorageScope.Disk;
479
+ if (!usesDefaultLocalStorageBackend(scope)) {
448
480
  return;
449
481
  }
450
-
451
- applyExternalChangeEvent(StorageScope.Disk, key, event.newValue);
482
+ applyExternalChangeEvent(scope, key, event.newValue);
452
483
  }
453
484
 
454
485
  function subscribeToBackendChanges(scope: NonMemoryScope): void {
@@ -462,6 +493,22 @@ function subscribeToBackendChanges(scope: NonMemoryScope): void {
462
493
  }
463
494
 
464
495
  const unsubscribe = backend.subscribe((event: WebStorageChangeEvent) => {
496
+ if (webDiskStorageBackend !== webSecureStorageBackend) {
497
+ applyExternalChangeEvent(scope, event.key, event.newValue);
498
+ return;
499
+ }
500
+ if (event.key === null) {
501
+ getInternals().clearScopeRawCache(scope);
502
+ reconcileWebScopeKeyIndex(scope);
503
+ notifyAllListeners(getInternals().getScopedListeners(scope));
504
+ return;
505
+ }
506
+ const eventScope = isSecureWebStorageKey(event.key)
507
+ ? StorageScope.Secure
508
+ : StorageScope.Disk;
509
+ if (eventScope !== scope) {
510
+ return;
511
+ }
465
512
  applyExternalChangeEvent(scope, event.key, event.newValue);
466
513
  });
467
514
  externalSyncUnsubscribers.set(scope, unsubscribe);
@@ -581,7 +628,24 @@ const WebStorage: Storage = {
581
628
  }
582
629
  try {
583
630
  withWebBackendOperation(scope, "clear", (backend) => {
584
- backend.clear();
631
+ if (webDiskStorageBackend !== webSecureStorageBackend) {
632
+ backend.clear();
633
+ return;
634
+ }
635
+ const ownedKeys = backend
636
+ .getAllKeys()
637
+ .filter((key) =>
638
+ scope === StorageScope.Secure
639
+ ? isSecureWebStorageKey(key)
640
+ : !isSecureWebStorageKey(key),
641
+ );
642
+ if (backend.removeMany) {
643
+ backend.removeMany(ownedKeys);
644
+ return;
645
+ }
646
+ ownedKeys.forEach((key) => {
647
+ backend.removeItem(key);
648
+ });
585
649
  });
586
650
  } catch (error) {
587
651
  throwAfterWebMutationFailure(scope, "clear", error);
@@ -1135,4 +1199,11 @@ export {
1135
1199
  useStorageSelector,
1136
1200
  useStorageValue,
1137
1201
  } from "./storage-hooks";
1138
- export { createIndexedDBBackend } from "./indexeddb-backend";
1202
+
1203
+ /**
1204
+ * @deprecated Import `createIndexedDBBackend` from
1205
+ * `react-native-nitro-storage/indexeddb-backend` instead. The root export
1206
+ * will be removed in a future minor release.
1207
+ */
1208
+ export const createIndexedDBBackend: typeof createIndexedDBBackendFromSubpath =
1209
+ 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 = {
@@ -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
- const oldValue = getEventRawValue(scope, key);
770
+ assertValidStorageKey(key);
771
+ const oldValue = readPreviousEventValue(scope, key);
749
772
  if (scope === StorageScope.Memory) {
750
- memoryStore.delete(key);
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((key) => memoryStore.delete(key));
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
- cacheRawValue(resolveNonMemoryScope(), storageKey, raw);
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 (fallbackToCacheOnReadError) {
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 = isBiometric
2429
- ? getEventRawValueForRepresentation(
2430
- config.scope,
2431
- storageKey,
2432
- "biometric",
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
- const storedValue =
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,
@@ -3,9 +3,11 @@ export type StorageErrorCode =
3
3
  | "authentication_required"
4
4
  | "key_invalidated"
5
5
  | "storage_corruption"
6
+ | "storage_full"
6
7
  | "storage_compensation_failed"
7
8
  | "biometric_unavailable"
8
- | "unsupported";
9
+ | "unsupported"
10
+ | "invalid_key";
9
11
 
10
12
  export type StorageCapabilities = {
11
13
  platform: "native" | "web";
@@ -62,9 +64,11 @@ const STORAGE_ERROR_CODES = new Set<StorageErrorCode>([
62
64
  "authentication_required",
63
65
  "key_invalidated",
64
66
  "storage_corruption",
67
+ "storage_full",
65
68
  "storage_compensation_failed",
66
69
  "biometric_unavailable",
67
70
  "unsupported",
71
+ "invalid_key",
68
72
  ]);
69
73
 
70
74
  export function getStorageErrorCode(