react-native-nitro-storage 0.9.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/CHANGELOG.md +101 -29
  2. package/README.md +83 -26
  3. package/android/build.gradle +3 -1
  4. package/android/src/main/cpp/AndroidStorageAdapterCpp.cpp +5 -2
  5. package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +240 -34
  6. package/cpp/bindings/HybridStorage.cpp +106 -283
  7. package/cpp/bindings/HybridStorage.hpp +16 -9
  8. package/docs/api-reference.md +75 -19
  9. package/docs/benchmarks.md +6 -1
  10. package/docs/keychain-lifecycle-testing.md +36 -0
  11. package/docs/recipes.md +1 -2
  12. package/docs/secure-storage.md +45 -9
  13. package/ios/IOSStorageAdapterCpp.mm +391 -175
  14. package/lib/commonjs/capabilities.js +3 -1
  15. package/lib/commonjs/capabilities.js.map +1 -1
  16. package/lib/commonjs/core/durability.js +111 -45
  17. package/lib/commonjs/core/durability.js.map +1 -1
  18. package/lib/commonjs/index.js +15 -5
  19. package/lib/commonjs/index.js.map +1 -1
  20. package/lib/commonjs/index.web.js +219 -97
  21. package/lib/commonjs/index.web.js.map +1 -1
  22. package/lib/commonjs/internal.js +3 -11
  23. package/lib/commonjs/internal.js.map +1 -1
  24. package/lib/commonjs/shared.js +62 -2
  25. package/lib/commonjs/shared.js.map +1 -1
  26. package/lib/commonjs/storage-core.js +883 -163
  27. package/lib/commonjs/storage-core.js.map +1 -1
  28. package/lib/commonjs/storage-runtime.js +5 -1
  29. package/lib/commonjs/storage-runtime.js.map +1 -1
  30. package/lib/commonjs/testing.js +13 -0
  31. package/lib/commonjs/testing.js.map +1 -1
  32. package/lib/module/capabilities.js +2 -1
  33. package/lib/module/capabilities.js.map +1 -1
  34. package/lib/module/core/durability.js +111 -45
  35. package/lib/module/core/durability.js.map +1 -1
  36. package/lib/module/index.js +12 -8
  37. package/lib/module/index.js.map +1 -1
  38. package/lib/module/index.web.js +215 -99
  39. package/lib/module/index.web.js.map +1 -1
  40. package/lib/module/internal.js +2 -9
  41. package/lib/module/internal.js.map +1 -1
  42. package/lib/module/shared.js +60 -2
  43. package/lib/module/shared.js.map +1 -1
  44. package/lib/module/storage-core.js +884 -164
  45. package/lib/module/storage-core.js.map +1 -1
  46. package/lib/module/storage-runtime.js +4 -1
  47. package/lib/module/storage-runtime.js.map +1 -1
  48. package/lib/module/testing.js +8 -1
  49. package/lib/module/testing.js.map +1 -1
  50. package/lib/typescript/capabilities.d.ts +2 -1
  51. package/lib/typescript/capabilities.d.ts.map +1 -1
  52. package/lib/typescript/core/durability.d.ts +8 -4
  53. package/lib/typescript/core/durability.d.ts.map +1 -1
  54. package/lib/typescript/index.d.ts +2 -2
  55. package/lib/typescript/index.d.ts.map +1 -1
  56. package/lib/typescript/index.web.d.ts +3 -29
  57. package/lib/typescript/index.web.d.ts.map +1 -1
  58. package/lib/typescript/internal.d.ts +0 -2
  59. package/lib/typescript/internal.d.ts.map +1 -1
  60. package/lib/typescript/shared.d.ts +19 -0
  61. package/lib/typescript/shared.d.ts.map +1 -1
  62. package/lib/typescript/storage-core.d.ts +15 -6
  63. package/lib/typescript/storage-core.d.ts.map +1 -1
  64. package/lib/typescript/storage-runtime.d.ts +2 -1
  65. package/lib/typescript/storage-runtime.d.ts.map +1 -1
  66. package/lib/typescript/testing.d.ts +1 -1
  67. package/lib/typescript/testing.d.ts.map +1 -1
  68. package/package.json +2 -2
  69. package/src/capabilities.ts +3 -1
  70. package/src/core/durability.ts +140 -43
  71. package/src/index.ts +20 -10
  72. package/src/index.web.ts +328 -166
  73. package/src/internal.ts +0 -14
  74. package/src/shared.ts +85 -0
  75. package/src/storage-core.ts +1198 -232
  76. package/src/storage-runtime.ts +6 -0
  77. package/src/testing.ts +8 -1
@@ -17,11 +17,13 @@ import {
17
17
  assertBiometricLevel,
18
18
  canUseRawBatchPath,
19
19
  canUseSecureRawBatchPath,
20
+ createStorageCompositeError,
20
21
  createKeyChange,
21
22
  defaultDeserialize,
22
23
  defaultSerialize,
23
24
  isKeychainLockedError,
24
25
  isUpdater,
26
+ normalizeStorageError,
25
27
  notifyAllListeners,
26
28
  notifyKeyListeners,
27
29
  redactSecureKeyChange,
@@ -30,6 +32,8 @@ import {
30
32
  type KeyListenerRegistry,
31
33
  type Migration,
32
34
  type NonMemoryScope,
35
+ type PendingDiskWrite,
36
+ type PendingSecureWrite,
33
37
  type RollbackRecord,
34
38
  type SecureAuthStorageConfig,
35
39
  type StorageEventObserverOptions,
@@ -124,6 +128,10 @@ type StorageItemInternal<T> = StorageItem<T> & {
124
128
  _deleteMemoryEntry: () => void;
125
129
  _hasValidation: boolean;
126
130
  _hasExpiration: boolean;
131
+ _hasRenameFrom: boolean;
132
+ _renameFromKeys: readonly string[];
133
+ _getRenameMigrationState: () => boolean;
134
+ _setRenameMigrationState: (migrated: boolean) => void;
127
135
  _readCacheEnabled: boolean;
128
136
  _isBiometric: boolean;
129
137
  _biometricLevel: BiometricLevel;
@@ -138,6 +146,7 @@ export type BatchReadItem<T> = Pick<
138
146
  > & {
139
147
  _hasValidation?: boolean;
140
148
  _hasExpiration?: boolean;
149
+ _hasRenameFrom?: boolean;
141
150
  _readCacheEnabled?: boolean;
142
151
  _isBiometric?: boolean;
143
152
  _defaultValue?: unknown;
@@ -146,7 +155,10 @@ export type BatchReadItem<T> = Pick<
146
155
  export type BatchRemoveItem = Pick<
147
156
  StorageItem<unknown>,
148
157
  "key" | "scope" | "delete"
149
- >;
158
+ > & {
159
+ _hasRenameFrom?: boolean;
160
+ _isBiometric?: boolean;
161
+ };
150
162
  export type BatchValues<TItems extends readonly BatchReadItem<unknown>[]> = {
151
163
  [Index in keyof TItems]: TItems[Index] extends BatchReadItem<infer Value>
152
164
  ? Value
@@ -178,14 +190,17 @@ export type StorageClearOptions = {
178
190
  };
179
191
 
180
192
  export type SetItemConfig<TMember extends string = string> = Omit<
181
- StorageItemConfig<Partial<Record<TMember, true>>>,
193
+ StorageItemConfig<Record<string, true>>,
182
194
  "defaultValue" | "serialize" | "deserialize"
183
195
  > & {
184
196
  defaultValue?: readonly TMember[];
185
197
  };
186
198
 
187
199
  export type SetStorageItem<TMember extends string = string> = {
188
- get: () => Partial<Record<TMember, true>>;
200
+ /** Compatibility shape retained from the original set-item API. */
201
+ get: () => Record<string, true>;
202
+ /** Precise membership shape for new code. */
203
+ getTyped: () => Partial<Record<TMember, true>>;
189
204
  has: (id: TMember) => boolean;
190
205
  add: (id: TMember) => void;
191
206
  delete: (id: TMember) => void;
@@ -197,7 +212,7 @@ export type SetStorageItem<TMember extends string = string> = {
197
212
  subscribe: (callback: () => void) => () => void;
198
213
  scope: StorageScope;
199
214
  key: string;
200
- item: StorageItem<Partial<Record<TMember, true>>>;
215
+ item: StorageItem<Record<string, true>>;
201
216
  };
202
217
 
203
218
  export type StorageCoreBackend = {
@@ -225,6 +240,8 @@ export type StorageCoreBackend = {
225
240
  clearSecureBiometric(): void;
226
241
  };
227
242
 
243
+ export type StorageRawCacheRepresentation = "plain" | "biometric";
244
+
228
245
  export type StorageCoreAdapter = {
229
246
  backend: StorageCoreBackend;
230
247
  changeSource: StorageChangeSource;
@@ -249,8 +266,14 @@ export type StorageCoreInternals = {
249
266
  scope: NonMemoryScope,
250
267
  key: string,
251
268
  value: string | undefined,
269
+ representation?: StorageRawCacheRepresentation,
252
270
  ): void;
253
- readCachedRawValue(scope: NonMemoryScope, key: string): string | undefined;
271
+ readCachedRawValue(
272
+ scope: NonMemoryScope,
273
+ key: string,
274
+ representation?: StorageRawCacheRepresentation,
275
+ ): string | undefined;
276
+ invalidateRawCache(scope: NonMemoryScope, key: string): void;
254
277
  clearScopeRawCache(scope: NonMemoryScope): void;
255
278
  clearPendingDiskWrite(key: string): void;
256
279
  clearPendingSecureWrite(key: string): void;
@@ -297,15 +320,15 @@ export function createStorageCore(
297
320
  const itemGroups = new Map<string, Set<StorageItemInternal<unknown>>>();
298
321
  const registeredKeyCounts = new Map<string, number>();
299
322
  const memoryStore = new Map<string, unknown>();
323
+ const memoryExpirationDeadlines = new Map<string, number>();
324
+ const memoryItemsByKey = new Map<string, Set<StorageItemInternal<unknown>>>();
300
325
  const memoryListeners: KeyListenerRegistry = new Map();
301
326
  const scopedListeners: Record<NonMemoryScope, KeyListenerRegistry> = {
302
327
  [StorageScope.Disk]: new Map(),
303
328
  [StorageScope.Secure]: new Map(),
304
329
  };
305
- const scopedRawCache: Record<
306
- NonMemoryScope,
307
- Map<string, string | undefined>
308
- > = {
330
+ type RawCacheEntry = Map<StorageRawCacheRepresentation, string | undefined>;
331
+ const scopedRawCache: Record<NonMemoryScope, Map<string, RawCacheEntry>> = {
309
332
  [StorageScope.Disk]: new Map(),
310
333
  [StorageScope.Secure]: new Map(),
311
334
  };
@@ -333,31 +356,57 @@ export function createStorageCore(
333
356
  return scopedListeners[scope];
334
357
  }
335
358
 
336
- function getScopeRawCache(
337
- scope: NonMemoryScope,
338
- ): Map<string, string | undefined> {
359
+ function getScopeRawCache(scope: NonMemoryScope): Map<string, RawCacheEntry> {
339
360
  return scopedRawCache[scope];
340
361
  }
341
362
 
363
+ function getCachedRawValueEntry(
364
+ scope: NonMemoryScope,
365
+ key: string,
366
+ create = false,
367
+ ): RawCacheEntry | undefined {
368
+ const scopeCache = getScopeRawCache(scope);
369
+ const existing = scopeCache.get(key);
370
+ if (existing || !create) {
371
+ return existing;
372
+ }
373
+
374
+ const entry: RawCacheEntry = new Map();
375
+ scopeCache.set(key, entry);
376
+ return entry;
377
+ }
378
+
342
379
  function cacheRawValue(
343
380
  scope: NonMemoryScope,
344
381
  key: string,
345
382
  value: string | undefined,
383
+ representation: StorageRawCacheRepresentation = "plain",
346
384
  ): void {
347
- getScopeRawCache(scope).set(key, value);
385
+ getCachedRawValueEntry(scope, key, true)?.set(representation, value);
348
386
  }
349
387
 
350
388
  function readCachedRawValue(
351
389
  scope: NonMemoryScope,
352
390
  key: string,
391
+ representation: StorageRawCacheRepresentation = "plain",
353
392
  ): string | undefined {
354
- return getScopeRawCache(scope).get(key);
393
+ return getCachedRawValueEntry(scope, key)?.get(representation);
394
+ }
395
+
396
+ function invalidateRawCache(scope: NonMemoryScope, key: string): void {
397
+ getScopeRawCache(scope).delete(key);
355
398
  }
356
399
 
357
400
  function clearScopeRawCache(scope: NonMemoryScope): void {
358
401
  getScopeRawCache(scope).clear();
359
402
  }
360
403
 
404
+ function invalidateMemoryItemCaches(key: string): void {
405
+ memoryItemsByKey.get(key)?.forEach((item) => {
406
+ item._invalidateParsedCacheOnly();
407
+ });
408
+ }
409
+
361
410
  function addKeyListener(
362
411
  registry: KeyListenerRegistry,
363
412
  key: string,
@@ -396,6 +445,18 @@ export function createStorageCore(
396
445
  return getRawValue(key, scope);
397
446
  }
398
447
 
448
+ function getEventRawValueForRepresentation(
449
+ scope: StorageScope,
450
+ key: string,
451
+ representation: StorageRawCacheRepresentation,
452
+ ): string | undefined {
453
+ if (representation === "plain") {
454
+ return getEventRawValue(scope, key);
455
+ }
456
+ const raw = adapter.backend.getSecureBiometric(key);
457
+ return raw === undefined ? undefined : unescapeCollidingRawValue(raw);
458
+ }
459
+
399
460
  function shouldReadPreviousEventValues(scope: StorageScope): boolean {
400
461
  if (storageEvents.hasListeners(scope)) {
401
462
  return true;
@@ -493,22 +554,6 @@ export function createStorageCore(
493
554
  metrics.record(operation, scope, durationMs, keysCount);
494
555
  }
495
556
 
496
- function readPendingSecureWrite(key: string): string | undefined {
497
- return durability.readPendingSecureWrite(key);
498
- }
499
-
500
- function readPendingDiskWrite(key: string): string | undefined {
501
- return durability.readPendingDiskWrite(key);
502
- }
503
-
504
- function hasPendingDiskWrite(key: string): boolean {
505
- return durability.hasPendingDiskWrite(key);
506
- }
507
-
508
- function hasPendingSecureWrite(key: string): boolean {
509
- return durability.hasPendingSecureWrite(key);
510
- }
511
-
512
557
  function clearPendingDiskWrite(key: string): void {
513
558
  durability.clearPendingDiskWrite(key);
514
559
  }
@@ -525,16 +570,23 @@ export function createStorageCore(
525
570
  durability.flushSecureWrites();
526
571
  }
527
572
 
528
- function scheduleDiskWrite(key: string, value: string | undefined): void {
529
- durability.scheduleDiskWrite(key, value);
573
+ function runSecurePromotion<T>(key: string, promotion: () => T): T {
574
+ return durability.runSecurePromotion(key, promotion);
575
+ }
576
+
577
+ function scheduleDiskWrite(
578
+ key: string,
579
+ value: string | undefined,
580
+ ): PendingDiskWrite {
581
+ return durability.scheduleDiskWrite(key, value);
530
582
  }
531
583
 
532
584
  function scheduleSecureWrite(
533
585
  key: string,
534
586
  value: string | undefined,
535
587
  accessControl?: AccessControl,
536
- ): void {
537
- durability.scheduleSecureWrite(key, value, accessControl);
588
+ ): PendingSecureWrite {
589
+ return durability.scheduleSecureWrite(key, value, accessControl);
538
590
  }
539
591
 
540
592
  function setDiskWritesAsyncMode(enabled: boolean): void {
@@ -554,17 +606,34 @@ export function createStorageCore(
554
606
  return typeof value === "string" ? value : undefined;
555
607
  }
556
608
 
557
- if (scope === StorageScope.Disk && hasPendingDiskWrite(key)) {
558
- return readPendingDiskWrite(key);
609
+ if (scope === StorageScope.Disk) {
610
+ const pending = durability.getPendingDiskWrite(key);
611
+ if (pending !== undefined) {
612
+ return pending.value;
613
+ }
559
614
  }
560
615
 
561
- if (scope === StorageScope.Secure && hasPendingSecureWrite(key)) {
562
- return readPendingSecureWrite(key);
616
+ if (scope === StorageScope.Secure) {
617
+ const pending = durability.getPendingSecureWrite(key);
618
+ if (pending !== undefined) {
619
+ return pending.value;
620
+ }
563
621
  }
564
622
 
565
623
  return adapter.backend.get(key, scope);
566
624
  }
567
625
 
626
+ function getStoredRawValueForRepresentation(
627
+ key: string,
628
+ scope: StorageScope,
629
+ representation: StorageRawCacheRepresentation,
630
+ ): string | undefined {
631
+ if (representation === "plain") {
632
+ return getStoredRawValue(key, scope);
633
+ }
634
+ return adapter.backend.getSecureBiometric(key);
635
+ }
636
+
568
637
  function getRawValue(key: string, scope: StorageScope): string | undefined {
569
638
  assertValidScope(scope);
570
639
  if (scope === StorageScope.Memory) {
@@ -574,18 +643,22 @@ export function createStorageCore(
574
643
  : undefined;
575
644
  }
576
645
 
577
- if (scope === StorageScope.Disk && hasPendingDiskWrite(key)) {
578
- const pending = readPendingDiskWrite(key);
579
- return pending === undefined
580
- ? undefined
581
- : unescapeCollidingRawValue(pending);
646
+ if (scope === StorageScope.Disk) {
647
+ const pending = durability.getPendingDiskWrite(key);
648
+ if (pending !== undefined) {
649
+ return pending.value === undefined
650
+ ? undefined
651
+ : unescapeCollidingRawValue(pending.value);
652
+ }
582
653
  }
583
654
 
584
- if (scope === StorageScope.Secure && hasPendingSecureWrite(key)) {
585
- const pending = readPendingSecureWrite(key);
586
- return pending === undefined
587
- ? undefined
588
- : unescapeCollidingRawValue(pending);
655
+ if (scope === StorageScope.Secure) {
656
+ const pending = durability.getPendingSecureWrite(key);
657
+ if (pending !== undefined) {
658
+ return pending.value === undefined
659
+ ? undefined
660
+ : unescapeCollidingRawValue(pending.value);
661
+ }
589
662
  }
590
663
 
591
664
  const raw = adapter.backend.get(key, scope);
@@ -617,6 +690,7 @@ export function createStorageCore(
617
690
  }
618
691
 
619
692
  if (scope === StorageScope.Secure) {
693
+ invalidateRawCache(scope, key);
620
694
  flushSecureWrites();
621
695
  clearPendingSecureWrite(key);
622
696
  if (adapter.applyAccessControlOnSecureRawWrite) {
@@ -659,6 +733,7 @@ export function createStorageCore(
659
733
  }
660
734
 
661
735
  if (scope === StorageScope.Secure) {
736
+ invalidateRawCache(scope, key);
662
737
  flushSecureWrites();
663
738
  clearPendingSecureWrite(key);
664
739
  }
@@ -689,6 +764,7 @@ export function createStorageCore(
689
764
  getScopedListeners,
690
765
  cacheRawValue,
691
766
  readCachedRawValue,
767
+ invalidateRawCache,
692
768
  clearScopeRawCache,
693
769
  clearPendingDiskWrite,
694
770
  clearPendingSecureWrite,
@@ -779,6 +855,11 @@ export function createStorageCore(
779
855
  : unescapeCollidingRawValue(value),
780
856
  )
781
857
  : [];
858
+ if (scope === StorageScope.Secure) {
859
+ removeKeys.forEach((key) => {
860
+ invalidateRawCache(scope, key);
861
+ });
862
+ }
782
863
  adapter.backend.removeBatch(removeKeys, scope);
783
864
  removeKeys.forEach((key) => {
784
865
  cacheRawValue(scope, key, undefined);
@@ -885,6 +966,7 @@ export function createStorageCore(
885
966
  : {};
886
967
  if (scope === StorageScope.Memory) {
887
968
  memoryStore.clear();
969
+ memoryExpirationDeadlines.clear();
888
970
  notifyAllListeners(memoryListeners);
889
971
  emitBatchChange(
890
972
  scope,
@@ -1044,6 +1126,7 @@ export function createStorageCore(
1044
1126
 
1045
1127
  affectedKeys.forEach((key) => {
1046
1128
  memoryStore.delete(key);
1129
+ memoryExpirationDeadlines.delete(key);
1047
1130
  });
1048
1131
  affectedKeys.forEach((key) => {
1049
1132
  notifyKeyListeners(memoryListeners, key);
@@ -1103,7 +1186,48 @@ export function createStorageCore(
1103
1186
  },
1104
1187
  clearBiometric: () => {
1105
1188
  measureOperation("storage:clearBiometric", StorageScope.Secure, () => {
1106
- adapter.backend.clearSecureBiometric();
1189
+ flushSecureWrites();
1190
+ const readEventValues = shouldReadPreviousEventValues(
1191
+ StorageScope.Secure,
1192
+ );
1193
+ const shouldEmitChanges =
1194
+ storageEvents.hasListeners(StorageScope.Secure) ||
1195
+ eventObserver !== undefined;
1196
+ const biometricKeys = shouldEmitChanges
1197
+ ? adapter.backend
1198
+ .getAllKeys(StorageScope.Secure)
1199
+ .filter((key) => adapter.backend.hasSecureBiometric(key))
1200
+ : [];
1201
+ const previousValues = readEventValues
1202
+ ? biometricKeys.map((key) => {
1203
+ const raw = adapter.backend.getSecureBiometric(key);
1204
+ return raw === undefined
1205
+ ? undefined
1206
+ : unescapeCollidingRawValue(raw);
1207
+ })
1208
+ : [];
1209
+
1210
+ clearScopeRawCache(StorageScope.Secure);
1211
+ try {
1212
+ adapter.backend.clearSecureBiometric();
1213
+ } finally {
1214
+ clearScopeRawCache(StorageScope.Secure);
1215
+ }
1216
+ emitBatchChange(
1217
+ StorageScope.Secure,
1218
+ "clear",
1219
+ adapter.changeSource,
1220
+ biometricKeys.map((key, index) =>
1221
+ createKeyChange(
1222
+ StorageScope.Secure,
1223
+ key,
1224
+ readEventValues ? previousValues[index] : undefined,
1225
+ undefined,
1226
+ "clear",
1227
+ adapter.changeSource,
1228
+ ),
1229
+ ),
1230
+ );
1107
1231
  });
1108
1232
  },
1109
1233
  has: (key: string, scope: StorageScope): boolean => {
@@ -1376,6 +1500,9 @@ export function createStorageCore(
1376
1500
  if (scope === StorageScope.Secure) {
1377
1501
  flushSecureWrites();
1378
1502
  adapter.backend.setSecureAccessControl(secureDefaultAccessControl);
1503
+ keys.forEach((key) => {
1504
+ invalidateRawCache(scope, key);
1505
+ });
1379
1506
  }
1380
1507
  if (scope === StorageScope.Disk) {
1381
1508
  flushDiskWrites();
@@ -1414,8 +1541,11 @@ export function createStorageCore(
1414
1541
  const onExpired = config.onExpired;
1415
1542
  const expirationTtlMs = expiration?.ttlMs;
1416
1543
  const memoryExpiration =
1417
- expiration && isMemory ? new Map<string, number>() : null;
1544
+ expiration && isMemory ? memoryExpirationDeadlines : null;
1418
1545
  const readCache = !isMemory && config.readCache === true;
1546
+ const rawCacheRepresentation: StorageRawCacheRepresentation = isBiometric
1547
+ ? "biometric"
1548
+ : "plain";
1419
1549
  const coalesceDiskWrites =
1420
1550
  config.scope === StorageScope.Disk && config.coalesceDiskWrites === true;
1421
1551
  const coalesceSecureWrites =
@@ -1527,34 +1657,57 @@ export function createStorageCore(
1527
1657
  : memoryStored;
1528
1658
  }
1529
1659
 
1660
+ if (nonMemoryScope === StorageScope.Disk) {
1661
+ const pending = durability.getPendingDiskWrite(storageKey);
1662
+ if (pending !== undefined) {
1663
+ return pending.value;
1664
+ }
1665
+ }
1666
+
1667
+ if (nonMemoryScope === StorageScope.Secure && !isBiometric) {
1668
+ const pending = durability.getPendingSecureWrite(storageKey);
1669
+ if (pending !== undefined) {
1670
+ return pending.value;
1671
+ }
1672
+ }
1673
+
1530
1674
  migrateRenamesIfNeeded();
1531
1675
 
1532
1676
  if (nonMemoryScope === StorageScope.Disk) {
1533
- const pending = durability.readPendingDiskWrite(storageKey);
1677
+ const pending = durability.getPendingDiskWrite(storageKey);
1534
1678
  if (pending !== undefined) {
1535
- return pending;
1679
+ return pending.value;
1536
1680
  }
1537
1681
  }
1538
1682
 
1539
1683
  if (nonMemoryScope === StorageScope.Secure && !isBiometric) {
1540
- const pending = durability.readPendingSecureWrite(storageKey);
1684
+ const pending = durability.getPendingSecureWrite(storageKey);
1541
1685
  if (pending !== undefined) {
1542
- return pending;
1686
+ return pending.value;
1543
1687
  }
1544
1688
  }
1545
1689
 
1546
1690
  if (readCache) {
1547
- const cache = getScopeRawCache(resolveNonMemoryScope());
1548
- const cached = cache.get(storageKey);
1549
- if (cached !== undefined || cache.has(storageKey)) {
1550
- return cached;
1691
+ const scope = resolveNonMemoryScope();
1692
+ const cachedEntry = getCachedRawValueEntry(scope, storageKey);
1693
+ if (cachedEntry?.has(rawCacheRepresentation)) {
1694
+ return cachedEntry.get(rawCacheRepresentation);
1551
1695
  }
1552
1696
  }
1553
1697
 
1554
1698
  if (isBiometric) {
1555
- return readBackendRaw(() =>
1699
+ const raw = readBackendRaw(() =>
1556
1700
  adapter.backend.getSecureBiometric(storageKey),
1557
1701
  );
1702
+ if (readCache) {
1703
+ cacheRawValue(
1704
+ resolveNonMemoryScope(),
1705
+ storageKey,
1706
+ raw,
1707
+ rawCacheRepresentation,
1708
+ );
1709
+ }
1710
+ return raw;
1558
1711
  }
1559
1712
 
1560
1713
  const raw = readBackendRaw(() =>
@@ -1572,8 +1725,11 @@ export function createStorageCore(
1572
1725
  } catch (error) {
1573
1726
  onReadError?.(error);
1574
1727
  if (fallbackToCacheOnReadError) {
1575
- const cached = getScopeRawCache(resolveNonMemoryScope()).get(
1728
+ const scope = resolveNonMemoryScope();
1729
+ const cached = readCachedRawValue(
1730
+ scope,
1576
1731
  storageKey,
1732
+ rawCacheRepresentation,
1577
1733
  );
1578
1734
  if (cached !== undefined) {
1579
1735
  return cached;
@@ -1584,14 +1740,447 @@ export function createStorageCore(
1584
1740
  }
1585
1741
  };
1586
1742
 
1587
- const writeStoredRaw = (rawValue: string): void => {
1743
+ type StoredRawWriteOptions = {
1744
+ cleanupRenameSources?: boolean;
1745
+ };
1746
+ type RenameSnapshot = {
1747
+ key: string;
1748
+ plainValue?: string;
1749
+ biometricValue?: string;
1750
+ };
1751
+
1752
+ function getRenameSourceKeys(): string[] {
1753
+ return renameFromKeys.filter((key) => key !== storageKey);
1754
+ }
1755
+
1756
+ function getRenameSourceRaw(key: string): string | undefined {
1757
+ return isBiometric
1758
+ ? adapter.backend.getSecureBiometric(key)
1759
+ : adapter.backend.get(key, config.scope);
1760
+ }
1761
+
1762
+ function invalidateRenameSourceCache(key: string): void {
1763
+ if (nonMemoryScope !== null) {
1764
+ invalidateRawCache(nonMemoryScope, key);
1765
+ }
1766
+ }
1767
+
1768
+ function removeRenameSource(key: string): void {
1769
+ invalidateRenameSourceCache(key);
1770
+ if (isBiometric) {
1771
+ adapter.backend.deleteSecureBiometric(key);
1772
+ invalidateRenameSourceCache(key);
1773
+ return;
1774
+ }
1775
+ adapter.backend.remove(key, config.scope);
1776
+ invalidateRenameSourceCache(key);
1777
+ }
1778
+
1779
+ function restoreRenameSource(snapshot: RenameSnapshot): void {
1780
+ invalidateRenameSourceCache(snapshot.key);
1781
+ if (nonMemoryScope === StorageScope.Secure) {
1782
+ if (snapshot.biometricValue !== undefined) {
1783
+ adapter.backend.setSecureBiometricWithLevel(
1784
+ snapshot.key,
1785
+ snapshot.biometricValue,
1786
+ resolvedBiometricLevel === BiometricLevel.None
1787
+ ? BiometricLevel.BiometryOnly
1788
+ : resolvedBiometricLevel,
1789
+ );
1790
+ } else {
1791
+ adapter.backend.deleteSecureBiometric(snapshot.key);
1792
+ }
1793
+ if (snapshot.plainValue !== undefined) {
1794
+ if (adapter.applyAccessControlOnSecureRawWrite) {
1795
+ adapter.backend.setSecureAccessControl(
1796
+ secureAccessControl ?? secureDefaultAccessControl,
1797
+ );
1798
+ }
1799
+ adapter.backend.set(snapshot.key, snapshot.plainValue, config.scope);
1800
+ } else if (snapshot.biometricValue === undefined) {
1801
+ adapter.backend.remove(snapshot.key, config.scope);
1802
+ }
1803
+ invalidateRenameSourceCache(snapshot.key);
1804
+ return;
1805
+ }
1806
+ if (snapshot.plainValue !== undefined) {
1807
+ adapter.backend.set(snapshot.key, snapshot.plainValue, config.scope);
1808
+ } else {
1809
+ adapter.backend.remove(snapshot.key, config.scope);
1810
+ }
1811
+ invalidateRenameSourceCache(snapshot.key);
1812
+ }
1813
+
1814
+ function readRenameSnapshots(): RenameSnapshot[] {
1815
+ return getRenameSourceKeys().flatMap((key) => {
1816
+ const plainValue =
1817
+ nonMemoryScope === StorageScope.Secure
1818
+ ? adapter.backend.get(key, config.scope)
1819
+ : getRenameSourceRaw(key);
1820
+ const biometricValue =
1821
+ nonMemoryScope === StorageScope.Secure
1822
+ ? adapter.backend.getSecureBiometric(key)
1823
+ : undefined;
1824
+ if (plainValue === undefined && biometricValue === undefined) {
1825
+ return [];
1826
+ }
1827
+ return [
1828
+ {
1829
+ key,
1830
+ ...(plainValue === undefined ? {} : { plainValue }),
1831
+ ...(biometricValue === undefined ? {} : { biometricValue }),
1832
+ },
1833
+ ];
1834
+ });
1835
+ }
1836
+
1837
+ function selectedRenameValue(snapshot: RenameSnapshot): string | undefined {
1838
+ return isBiometric ? snapshot.biometricValue : snapshot.plainValue;
1839
+ }
1840
+
1841
+ function hasPendingCurrentWrite(): boolean {
1842
+ if (nonMemoryScope === StorageScope.Disk) {
1843
+ return durability.hasPendingDiskWrite(storageKey);
1844
+ }
1845
+ if (nonMemoryScope === StorageScope.Secure && !isBiometric) {
1846
+ return durability.hasPendingSecureWrite(storageKey);
1847
+ }
1848
+ return false;
1849
+ }
1850
+
1851
+ function hasPendingRenameSourceWrite(): boolean {
1852
+ if (isBiometric || nonMemoryScope === null) {
1853
+ return false;
1854
+ }
1855
+ return getRenameSourceKeys().some((key) =>
1856
+ nonMemoryScope === StorageScope.Disk
1857
+ ? durability.hasPendingDiskWrite(key)
1858
+ : durability.hasPendingSecureWrite(key),
1859
+ );
1860
+ }
1861
+
1862
+ function flushPendingRenameSourceWrites(): void {
1863
+ if (!hasPendingRenameSourceWrite()) {
1864
+ return;
1865
+ }
1866
+ if (nonMemoryScope === StorageScope.Disk) {
1867
+ flushDiskWrites();
1868
+ return;
1869
+ }
1870
+ flushSecureWrites();
1871
+ }
1872
+
1873
+ function clearPendingCurrentWriteIf(
1874
+ write: PendingDiskWrite | PendingSecureWrite | undefined,
1875
+ ): void {
1876
+ if (write === undefined) {
1877
+ return;
1878
+ }
1879
+ if (nonMemoryScope === StorageScope.Disk && "generation" in write) {
1880
+ durability.clearPendingDiskWriteIf(write);
1881
+ return;
1882
+ }
1883
+ if (nonMemoryScope === StorageScope.Secure && !isBiometric) {
1884
+ durability.clearPendingSecureWriteIf(write);
1885
+ }
1886
+ }
1887
+
1888
+ function flushPendingCurrentWrite(): void {
1889
+ if (nonMemoryScope === StorageScope.Disk) {
1890
+ flushDiskWrites();
1891
+ return;
1892
+ }
1893
+ if (nonMemoryScope === StorageScope.Secure && !isBiometric) {
1894
+ flushSecureWrites();
1895
+ }
1896
+ }
1897
+
1898
+ function removeCurrentBackendValue(): void {
1899
+ invalidateRawCache(resolveNonMemoryScope(), storageKey);
1900
+ if (isBiometric) {
1901
+ adapter.backend.deleteSecureBiometric(storageKey);
1902
+ return;
1903
+ }
1904
+ adapter.backend.remove(storageKey, config.scope);
1905
+ }
1906
+
1907
+ function scheduleRenameSourceCleanup(): void {
1908
+ const sourceKeys = getRenameSourceKeys();
1909
+ sourceKeys.forEach(invalidateRenameSourceCache);
1910
+
1911
+ if (isBiometric) {
1912
+ sourceKeys.forEach(removeRenameSource);
1913
+ return;
1914
+ }
1915
+
1916
+ if (
1917
+ nonMemoryScope === StorageScope.Disk &&
1918
+ (coalesceDiskWrites || isDiskWritesAsync())
1919
+ ) {
1920
+ sourceKeys.forEach((key) => {
1921
+ scheduleDiskWrite(key, undefined);
1922
+ });
1923
+ return;
1924
+ }
1925
+
1926
+ if (nonMemoryScope === StorageScope.Secure && coalesceSecureWrites) {
1927
+ sourceKeys.forEach((key) => {
1928
+ scheduleSecureWrite(
1929
+ key,
1930
+ undefined,
1931
+ secureAccessControl ?? secureDefaultAccessControl,
1932
+ );
1933
+ });
1934
+ return;
1935
+ }
1936
+
1937
+ sourceKeys.forEach(removeRenameSource);
1938
+ }
1939
+
1940
+ type ItemPendingSnapshot = {
1941
+ value: string | undefined;
1942
+ accessControl?: AccessControl;
1943
+ };
1944
+ type ItemStateRecord = {
1945
+ key: string;
1946
+ plainValue: string | undefined;
1947
+ biometricValue: string | undefined;
1948
+ pending?: ItemPendingSnapshot;
1949
+ };
1950
+ type ItemStateSnapshot = {
1951
+ records: readonly ItemStateRecord[];
1952
+ renamesMigrated: boolean;
1953
+ };
1954
+
1955
+ let atomicMutationDepth = 0;
1956
+
1957
+ function getItemStateKeys(): string[] {
1958
+ return Array.from(new Set([storageKey, ...getRenameSourceKeys()]));
1959
+ }
1960
+
1961
+ function captureItemState(): ItemStateSnapshot {
1962
+ const records = getItemStateKeys().map((key) => {
1963
+ let pending: ItemPendingSnapshot | undefined;
1964
+ if (nonMemoryScope === StorageScope.Disk) {
1965
+ const pendingWrite = durability.getPendingDiskWrite(key);
1966
+ if (pendingWrite !== undefined) {
1967
+ pending = {
1968
+ value: pendingWrite.value,
1969
+ };
1970
+ }
1971
+ } else if (nonMemoryScope === StorageScope.Secure && !isBiometric) {
1972
+ const pendingWrite = durability.getPendingSecureWrite(key);
1973
+ if (pendingWrite !== undefined) {
1974
+ pending = {
1975
+ value: pendingWrite.value,
1976
+ ...(pendingWrite.accessControl === undefined
1977
+ ? {}
1978
+ : { accessControl: pendingWrite.accessControl }),
1979
+ };
1980
+ }
1981
+ }
1982
+
1983
+ return {
1984
+ key,
1985
+ plainValue:
1986
+ nonMemoryScope === null
1987
+ ? undefined
1988
+ : adapter.backend.get(key, config.scope),
1989
+ biometricValue:
1990
+ nonMemoryScope === StorageScope.Secure
1991
+ ? adapter.backend.getSecureBiometric(key)
1992
+ : undefined,
1993
+ ...(pending === undefined ? {} : { pending }),
1994
+ };
1995
+ });
1996
+ return { records, renamesMigrated };
1997
+ }
1998
+
1999
+ function clearPendingItemWrite(key: string): void {
2000
+ if (nonMemoryScope === StorageScope.Disk) {
2001
+ durability.clearPendingDiskWrite(key);
2002
+ } else if (nonMemoryScope === StorageScope.Secure && !isBiometric) {
2003
+ durability.clearPendingSecureWrite(key);
2004
+ }
2005
+ }
2006
+
2007
+ function restoreItemState(snapshot: ItemStateSnapshot): {
2008
+ label: string;
2009
+ error: unknown;
2010
+ }[] {
2011
+ const rollbackErrors: { label: string; error: unknown }[] = [];
2012
+ const recordsByKey = new Map(
2013
+ snapshot.records.map((record) => [record.key, record]),
2014
+ );
2015
+ snapshot.records.forEach(({ key }) => {
2016
+ clearPendingItemWrite(key);
2017
+ if (nonMemoryScope !== null) {
2018
+ invalidateRawCache(nonMemoryScope, key);
2019
+ }
2020
+ });
2021
+
2022
+ const biometricRecords = snapshot.records.filter(
2023
+ ({ biometricValue }) => biometricValue !== undefined,
2024
+ );
2025
+ if (nonMemoryScope === StorageScope.Secure) {
2026
+ biometricRecords.forEach(({ key, biometricValue }) => {
2027
+ try {
2028
+ adapter.backend.setSecureBiometricWithLevel(
2029
+ key,
2030
+ biometricValue as string,
2031
+ resolvedBiometricLevel === BiometricLevel.None
2032
+ ? BiometricLevel.BiometryOnly
2033
+ : resolvedBiometricLevel,
2034
+ );
2035
+ cacheRawValue(
2036
+ StorageScope.Secure,
2037
+ key,
2038
+ biometricValue,
2039
+ "biometric",
2040
+ );
2041
+ } catch (error) {
2042
+ rollbackErrors.push({ label: "rollback biometric", error });
2043
+ }
2044
+ });
2045
+ }
2046
+
2047
+ const plainSets = new Map<
2048
+ AccessControl,
2049
+ { keys: string[]; values: string[] }
2050
+ >();
2051
+ const plainRemoves: string[] = [];
2052
+ snapshot.records.forEach(({ key, plainValue, biometricValue }) => {
2053
+ if (plainValue === undefined) {
2054
+ if (biometricValue === undefined) {
2055
+ plainRemoves.push(key);
2056
+ }
2057
+ return;
2058
+ }
2059
+ const accessControl = secureAccessControl ?? secureDefaultAccessControl;
2060
+ const group = plainSets.get(accessControl) ?? { keys: [], values: [] };
2061
+ group.keys.push(key);
2062
+ group.values.push(plainValue);
2063
+ plainSets.set(accessControl, group);
2064
+ });
2065
+
2066
+ plainSets.forEach((group, accessControl) => {
2067
+ try {
2068
+ if (nonMemoryScope === StorageScope.Secure) {
2069
+ adapter.backend.setSecureAccessControl(accessControl);
2070
+ }
2071
+ adapter.backend.setBatch(group.keys, group.values, config.scope);
2072
+ group.keys.forEach((key, index) => {
2073
+ cacheRawValue(
2074
+ resolveNonMemoryScope(),
2075
+ key,
2076
+ group.values[index],
2077
+ "plain",
2078
+ );
2079
+ });
2080
+ } catch (error) {
2081
+ rollbackErrors.push({ label: "rollback plain set", error });
2082
+ }
2083
+ });
2084
+
2085
+ if (plainRemoves.length > 0) {
2086
+ try {
2087
+ adapter.backend.removeBatch(plainRemoves, config.scope);
2088
+ plainRemoves.forEach((key) => {
2089
+ cacheRawValue(resolveNonMemoryScope(), key, undefined, "plain");
2090
+ });
2091
+ } catch (error) {
2092
+ rollbackErrors.push({ label: "rollback plain remove", error });
2093
+ }
2094
+ }
2095
+
2096
+ if (nonMemoryScope === StorageScope.Secure) {
2097
+ snapshot.records.forEach(({ key, biometricValue }) => {
2098
+ if (biometricValue !== undefined) {
2099
+ return;
2100
+ }
2101
+ try {
2102
+ adapter.backend.deleteSecureBiometric(key);
2103
+ cacheRawValue(StorageScope.Secure, key, undefined, "biometric");
2104
+ } catch (error) {
2105
+ rollbackErrors.push({ label: "rollback biometric remove", error });
2106
+ }
2107
+ });
2108
+ }
2109
+
2110
+ snapshot.records.forEach(({ key, pending }) => {
2111
+ if (pending === undefined || nonMemoryScope === null) {
2112
+ return;
2113
+ }
2114
+ if (nonMemoryScope === StorageScope.Disk) {
2115
+ scheduleDiskWrite(key, pending.value);
2116
+ } else if (!isBiometric) {
2117
+ scheduleSecureWrite(
2118
+ key,
2119
+ pending.value,
2120
+ pending.accessControl ??
2121
+ secureAccessControl ??
2122
+ secureDefaultAccessControl,
2123
+ );
2124
+ }
2125
+ });
2126
+
2127
+ recordsByKey.forEach(({ key }) => {
2128
+ if (nonMemoryScope !== null) {
2129
+ invalidateRawCache(nonMemoryScope, key);
2130
+ }
2131
+ });
2132
+ renamesMigrated = snapshot.renamesMigrated;
2133
+ invalidateParsedCache();
2134
+ return rollbackErrors;
2135
+ }
2136
+
2137
+ function runAtomicItemMutation(mutation: () => void): void {
2138
+ if (isMemory || renameFromKeys.length === 0 || atomicMutationDepth > 0) {
2139
+ mutation();
2140
+ return;
2141
+ }
2142
+
2143
+ const snapshot = captureItemState();
2144
+ atomicMutationDepth += 1;
2145
+ try {
2146
+ mutation();
2147
+ } catch (primaryError) {
2148
+ const rollbackErrors = restoreItemState(snapshot);
2149
+ if (rollbackErrors.length > 0) {
2150
+ throw createStorageCompositeError(
2151
+ "item mutation rollback",
2152
+ primaryError,
2153
+ rollbackErrors,
2154
+ );
2155
+ }
2156
+ throw primaryError;
2157
+ } finally {
2158
+ atomicMutationDepth -= 1;
2159
+ }
2160
+ }
2161
+
2162
+ const writeStoredRaw = (
2163
+ rawValue: string,
2164
+ options: StoredRawWriteOptions = {},
2165
+ ): PendingDiskWrite | PendingSecureWrite | undefined => {
2166
+ const cleanupRenameSources = options.cleanupRenameSources !== false;
1588
2167
  const oldValue = undefined;
1589
2168
  if (isBiometric) {
1590
- adapter.backend.setSecureBiometricWithLevel(
1591
- storageKey,
1592
- rawValue,
1593
- resolvedBiometricLevel,
1594
- );
2169
+ invalidateRawCache(StorageScope.Secure, storageKey);
2170
+ runSecurePromotion(storageKey, () => {
2171
+ try {
2172
+ adapter.backend.setSecureBiometricWithLevel(
2173
+ storageKey,
2174
+ rawValue,
2175
+ resolvedBiometricLevel,
2176
+ );
2177
+ } catch (error) {
2178
+ throw normalizeStorageError(error);
2179
+ }
2180
+ });
2181
+ if (cleanupRenameSources) {
2182
+ scheduleRenameSourceCleanup();
2183
+ }
1595
2184
  emitKeyChange(
1596
2185
  config.scope,
1597
2186
  storageKey,
@@ -1600,14 +2189,20 @@ export function createStorageCore(
1600
2189
  "set",
1601
2190
  adapter.changeSource,
1602
2191
  );
1603
- return;
2192
+ return undefined;
1604
2193
  }
1605
2194
 
2195
+ if (nonMemoryScope === StorageScope.Secure) {
2196
+ invalidateRawCache(StorageScope.Secure, storageKey);
2197
+ }
1606
2198
  cacheRawValue(resolveNonMemoryScope(), storageKey, rawValue);
1607
2199
 
1608
2200
  if (nonMemoryScope === StorageScope.Disk) {
1609
2201
  if (coalesceDiskWrites || isDiskWritesAsync()) {
1610
- scheduleDiskWrite(storageKey, rawValue);
2202
+ const pendingWrite = scheduleDiskWrite(storageKey, rawValue);
2203
+ if (cleanupRenameSources) {
2204
+ scheduleRenameSourceCleanup();
2205
+ }
1611
2206
  emitKeyChange(
1612
2207
  config.scope,
1613
2208
  storageKey,
@@ -1616,18 +2211,21 @@ export function createStorageCore(
1616
2211
  "set",
1617
2212
  adapter.changeSource,
1618
2213
  );
1619
- return;
2214
+ return pendingWrite;
1620
2215
  }
1621
2216
 
1622
2217
  clearPendingDiskWrite(storageKey);
1623
2218
  }
1624
2219
 
1625
2220
  if (coalesceSecureWrites) {
1626
- scheduleSecureWrite(
2221
+ const pendingWrite = scheduleSecureWrite(
1627
2222
  storageKey,
1628
2223
  rawValue,
1629
2224
  secureAccessControl ?? secureDefaultAccessControl,
1630
2225
  );
2226
+ if (cleanupRenameSources) {
2227
+ scheduleRenameSourceCleanup();
2228
+ }
1631
2229
  emitKeyChange(
1632
2230
  config.scope,
1633
2231
  storageKey,
@@ -1636,7 +2234,7 @@ export function createStorageCore(
1636
2234
  "set",
1637
2235
  adapter.changeSource,
1638
2236
  );
1639
- return;
2237
+ return pendingWrite;
1640
2238
  }
1641
2239
 
1642
2240
  if (nonMemoryScope === StorageScope.Secure) {
@@ -1649,6 +2247,9 @@ export function createStorageCore(
1649
2247
  }
1650
2248
 
1651
2249
  adapter.backend.set(storageKey, rawValue, config.scope);
2250
+ if (cleanupRenameSources) {
2251
+ scheduleRenameSourceCleanup();
2252
+ }
1652
2253
  emitKeyChange(
1653
2254
  config.scope,
1654
2255
  storageKey,
@@ -1657,6 +2258,7 @@ export function createStorageCore(
1657
2258
  "set",
1658
2259
  adapter.changeSource,
1659
2260
  );
2261
+ return undefined;
1660
2262
  };
1661
2263
 
1662
2264
  const migrateRenamesIfNeeded = (): void => {
@@ -1665,42 +2267,101 @@ export function createStorageCore(
1665
2267
  }
1666
2268
 
1667
2269
  try {
2270
+ flushPendingRenameSourceWrites();
1668
2271
  const hasCurrent = isBiometric
1669
2272
  ? adapter.backend.hasSecureBiometric(storageKey)
1670
2273
  : adapter.backend.has(storageKey, config.scope);
2274
+ const snapshots = readRenameSnapshots();
1671
2275
 
1672
2276
  if (hasCurrent) {
1673
- for (const legacyKey of renameFromKeys) {
1674
- if (isBiometric) {
1675
- if (adapter.backend.hasSecureBiometric(legacyKey)) {
1676
- adapter.backend.deleteSecureBiometric(legacyKey);
2277
+ try {
2278
+ snapshots.forEach(({ key }) => {
2279
+ removeRenameSource(key);
2280
+ });
2281
+ } catch (primaryError) {
2282
+ const rollbackErrors: { label: string; error: unknown }[] = [];
2283
+ snapshots.forEach((snapshot) => {
2284
+ try {
2285
+ restoreRenameSource(snapshot);
2286
+ } catch (error) {
2287
+ rollbackErrors.push({
2288
+ label: "rollback rename source",
2289
+ error,
2290
+ });
1677
2291
  }
1678
- } else if (adapter.backend.has(legacyKey, config.scope)) {
1679
- adapter.backend.remove(legacyKey, config.scope);
2292
+ });
2293
+ if (rollbackErrors.length > 0) {
2294
+ throw createStorageCompositeError(
2295
+ "rename cleanup",
2296
+ primaryError,
2297
+ rollbackErrors,
2298
+ );
1680
2299
  }
2300
+ throw primaryError;
1681
2301
  }
1682
2302
  renamesMigrated = true;
1683
2303
  return;
1684
2304
  }
1685
2305
 
1686
- for (const legacyKey of renameFromKeys) {
1687
- const legacyRaw = isBiometric
1688
- ? adapter.backend.getSecureBiometric(legacyKey)
1689
- : adapter.backend.get(legacyKey, config.scope);
1690
- if (legacyRaw === undefined) {
1691
- continue;
1692
- }
2306
+ const snapshot = snapshots.find(
2307
+ (candidate) => selectedRenameValue(candidate) !== undefined,
2308
+ );
2309
+ if (snapshot === undefined) {
2310
+ renamesMigrated = true;
2311
+ return;
2312
+ }
1693
2313
 
1694
- writeStoredRaw(legacyRaw);
1695
- if (isBiometric) {
1696
- adapter.backend.deleteSecureBiometric(legacyKey);
1697
- } else {
1698
- adapter.backend.remove(legacyKey, config.scope);
2314
+ let writeAttempted = false;
2315
+ let pendingCurrentWrite:
2316
+ PendingDiskWrite | PendingSecureWrite | undefined;
2317
+ try {
2318
+ writeAttempted = true;
2319
+ pendingCurrentWrite = writeStoredRaw(
2320
+ selectedRenameValue(snapshot) as string,
2321
+ {
2322
+ cleanupRenameSources: false,
2323
+ },
2324
+ );
2325
+ if (hasPendingCurrentWrite()) {
2326
+ flushPendingCurrentWrite();
2327
+ }
2328
+ snapshots.forEach(({ key }) => {
2329
+ removeRenameSource(key);
2330
+ });
2331
+ renamesMigrated = true;
2332
+ } catch (primaryError) {
2333
+ const rollbackErrors: { label: string; error: unknown }[] = [];
2334
+ clearPendingCurrentWriteIf(pendingCurrentWrite);
2335
+ if (writeAttempted) {
2336
+ try {
2337
+ removeCurrentBackendValue();
2338
+ } catch (error) {
2339
+ rollbackErrors.push({ label: "rollback current value", error });
2340
+ }
1699
2341
  }
1700
- break;
2342
+ snapshots.forEach((renameSnapshot) => {
2343
+ try {
2344
+ restoreRenameSource(renameSnapshot);
2345
+ } catch (error) {
2346
+ rollbackErrors.push({
2347
+ label: "rollback rename source",
2348
+ error,
2349
+ });
2350
+ }
2351
+ });
2352
+ invalidateRawCache(resolveNonMemoryScope(), storageKey);
2353
+ snapshots.forEach(({ key }) => {
2354
+ invalidateRenameSourceCache(key);
2355
+ });
2356
+ if (rollbackErrors.length > 0) {
2357
+ throw createStorageCompositeError(
2358
+ "rename migration",
2359
+ primaryError,
2360
+ rollbackErrors,
2361
+ );
2362
+ }
2363
+ throw primaryError;
1701
2364
  }
1702
-
1703
- renamesMigrated = true;
1704
2365
  } catch (error) {
1705
2366
  if (isKeychainLockedError(error)) {
1706
2367
  onReadError?.(error);
@@ -1713,8 +2374,16 @@ export function createStorageCore(
1713
2374
  const removeStoredRaw = (
1714
2375
  operation: StorageChangeOperation = "remove",
1715
2376
  ): void => {
1716
- const oldValue = getEventRawValue(config.scope, storageKey);
2377
+ const oldValue = isBiometric
2378
+ ? getEventRawValueForRepresentation(
2379
+ config.scope,
2380
+ storageKey,
2381
+ "biometric",
2382
+ )
2383
+ : getEventRawValue(config.scope, storageKey);
1717
2384
  if (isBiometric) {
2385
+ invalidateRawCache(StorageScope.Secure, storageKey);
2386
+ scheduleRenameSourceCleanup();
1718
2387
  adapter.backend.deleteSecureBiometric(storageKey);
1719
2388
  emitKeyChange(
1720
2389
  config.scope,
@@ -1727,11 +2396,15 @@ export function createStorageCore(
1727
2396
  return;
1728
2397
  }
1729
2398
 
2399
+ if (nonMemoryScope === StorageScope.Secure) {
2400
+ invalidateRawCache(StorageScope.Secure, storageKey);
2401
+ }
1730
2402
  cacheRawValue(resolveNonMemoryScope(), storageKey, undefined);
1731
2403
 
1732
2404
  if (nonMemoryScope === StorageScope.Disk) {
1733
2405
  if (coalesceDiskWrites || isDiskWritesAsync()) {
1734
2406
  scheduleDiskWrite(storageKey, undefined);
2407
+ scheduleRenameSourceCleanup();
1735
2408
  emitKeyChange(
1736
2409
  config.scope,
1737
2410
  storageKey,
@@ -1752,6 +2425,7 @@ export function createStorageCore(
1752
2425
  undefined,
1753
2426
  secureAccessControl ?? secureDefaultAccessControl,
1754
2427
  );
2428
+ scheduleRenameSourceCleanup();
1755
2429
  emitKeyChange(
1756
2430
  config.scope,
1757
2431
  storageKey,
@@ -1767,6 +2441,7 @@ export function createStorageCore(
1767
2441
  clearPendingSecureWrite(storageKey);
1768
2442
  }
1769
2443
 
2444
+ scheduleRenameSourceCleanup();
1770
2445
  adapter.backend.remove(storageKey, config.scope);
1771
2446
  emitKeyChange(
1772
2447
  config.scope,
@@ -1783,6 +2458,8 @@ export function createStorageCore(
1783
2458
  const oldValue = getEventRawValue(config.scope, storageKey);
1784
2459
  if (memoryExpiration) {
1785
2460
  memoryExpiration.set(storageKey, Date.now() + (expirationTtlMs ?? 0));
2461
+ } else {
2462
+ memoryExpirationDeadlines.delete(storageKey);
1786
2463
  }
1787
2464
  const storedValue =
1788
2465
  typeof value === "string" ? escapeCollidingRawValue(value) : value;
@@ -1852,7 +2529,9 @@ export function createStorageCore(
1852
2529
  return lastValue as T;
1853
2530
  }
1854
2531
 
1855
- removeStoredRaw("expire");
2532
+ runAtomicItemMutation(() => {
2533
+ removeStoredRaw("expire");
2534
+ });
1856
2535
  invalidateParsedCache();
1857
2536
  onExpired?.(storageKey);
1858
2537
  lastValue = ensureValidatedValue(defaultValue, false);
@@ -1896,7 +2575,9 @@ export function createStorageCore(
1896
2575
  if (isStoredEnvelope(parsed)) {
1897
2576
  envelopeExpiresAt = parsed.expiresAt;
1898
2577
  if (parsed.expiresAt <= Date.now()) {
1899
- removeStoredRaw("expire");
2578
+ runAtomicItemMutation(() => {
2579
+ removeStoredRaw("expire");
2580
+ });
1900
2581
  invalidateParsedCache();
1901
2582
  onExpired?.(storageKey);
1902
2583
  lastValue = ensureValidatedValue(defaultValue, false);
@@ -1943,18 +2624,20 @@ export function createStorageCore(
1943
2624
 
1944
2625
  const set = (valueOrFn: T | ((prev: T) => T)): void => {
1945
2626
  measureOperation("item:set", config.scope, () => {
1946
- const newValue = isUpdater(valueOrFn)
1947
- ? valueOrFn(getInternal())
1948
- : valueOrFn;
1949
-
1950
- if (validate && !validate(newValue)) {
1951
- throw new Error(
1952
- `Validation failed for key "${storageKey}" in scope "${StorageScope[config.scope]}".`,
1953
- );
1954
- }
2627
+ runAtomicItemMutation(() => {
2628
+ const newValue = isUpdater(valueOrFn)
2629
+ ? valueOrFn(getInternal())
2630
+ : valueOrFn;
2631
+
2632
+ if (validate && !validate(newValue)) {
2633
+ throw new Error(
2634
+ `Validation failed for key "${storageKey}" in scope "${StorageScope[config.scope]}".`,
2635
+ );
2636
+ }
1955
2637
 
1956
- invalidateParsedCache();
1957
- writeValueWithoutValidation(newValue);
2638
+ invalidateParsedCache();
2639
+ writeValueWithoutValidation(newValue);
2640
+ });
1958
2641
  });
1959
2642
  };
1960
2643
 
@@ -1963,37 +2646,44 @@ export function createStorageCore(
1963
2646
  valueOrFn: T | ((prev: T) => T),
1964
2647
  ): boolean =>
1965
2648
  measureOperation("item:setIfVersion", config.scope, () => {
1966
- const currentVersion = getCurrentVersion();
1967
- if (currentVersion !== version) {
1968
- return false;
1969
- }
1970
- set(valueOrFn);
1971
- return true;
2649
+ let didSet = false;
2650
+ runAtomicItemMutation(() => {
2651
+ const currentVersion = getCurrentVersion();
2652
+ if (currentVersion !== version) {
2653
+ return;
2654
+ }
2655
+ set(valueOrFn);
2656
+ didSet = true;
2657
+ });
2658
+ return didSet;
1972
2659
  });
1973
2660
 
1974
2661
  const deleteItem = (): void => {
1975
2662
  measureOperation("item:delete", config.scope, () => {
1976
- invalidateParsedCache();
2663
+ runAtomicItemMutation(() => {
2664
+ invalidateParsedCache();
1977
2665
 
1978
- if (isMemory) {
1979
- const oldValue = getEventRawValue(config.scope, storageKey);
1980
- if (memoryExpiration) {
1981
- memoryExpiration.delete(storageKey);
2666
+ if (isMemory) {
2667
+ const oldValue = getEventRawValue(config.scope, storageKey);
2668
+ if (memoryExpiration) {
2669
+ memoryExpiration.delete(storageKey);
2670
+ }
2671
+ memoryExpirationDeadlines.delete(storageKey);
2672
+ memoryStore.delete(storageKey);
2673
+ notifyKeyListeners(memoryListeners, storageKey);
2674
+ emitKeyChange(
2675
+ config.scope,
2676
+ storageKey,
2677
+ oldValue,
2678
+ undefined,
2679
+ "remove",
2680
+ "memory",
2681
+ );
2682
+ return;
1982
2683
  }
1983
- memoryStore.delete(storageKey);
1984
- notifyKeyListeners(memoryListeners, storageKey);
1985
- emitKeyChange(
1986
- config.scope,
1987
- storageKey,
1988
- oldValue,
1989
- undefined,
1990
- "remove",
1991
- "memory",
1992
- );
1993
- return;
1994
- }
1995
2684
 
1996
- removeStoredRaw();
2685
+ removeStoredRaw();
2686
+ });
1997
2687
  });
1998
2688
  };
1999
2689
 
@@ -2023,13 +2713,15 @@ export function createStorageCore(
2023
2713
  if (isMemory) return memoryStore.has(storageKey);
2024
2714
  if (isBiometric) return adapter.backend.hasSecureBiometric(storageKey);
2025
2715
  if (nonMemoryScope === StorageScope.Disk) {
2026
- if (durability.hasPendingDiskWrite(storageKey)) {
2027
- return durability.readPendingDiskWrite(storageKey) !== undefined;
2716
+ const pending = durability.getPendingDiskWrite(storageKey);
2717
+ if (pending !== undefined) {
2718
+ return pending.value !== undefined;
2028
2719
  }
2029
2720
  }
2030
2721
  if (nonMemoryScope === StorageScope.Secure) {
2031
- if (durability.hasPendingSecureWrite(storageKey)) {
2032
- return durability.readPendingSecureWrite(storageKey) !== undefined;
2722
+ const pending = durability.getPendingSecureWrite(storageKey);
2723
+ if (pending !== undefined) {
2724
+ return pending.value !== undefined;
2033
2725
  }
2034
2726
  }
2035
2727
  return adapter.backend.has(storageKey, config.scope);
@@ -2098,14 +2790,18 @@ export function createStorageCore(
2098
2790
  invalidateParsedCache();
2099
2791
  },
2100
2792
  _deleteMemoryEntry: () => {
2101
- if (memoryExpiration) {
2102
- memoryExpiration.delete(storageKey);
2103
- }
2793
+ memoryExpirationDeadlines.delete(storageKey);
2104
2794
  memoryStore.delete(storageKey);
2105
2795
  invalidateParsedCache();
2106
2796
  },
2107
2797
  _hasValidation: validate !== undefined,
2108
2798
  _hasExpiration: expiration !== undefined,
2799
+ _hasRenameFrom: renameFromKeys.length > 0,
2800
+ _renameFromKeys: renameFromKeys,
2801
+ _getRenameMigrationState: () => renamesMigrated,
2802
+ _setRenameMigrationState: (migrated) => {
2803
+ renamesMigrated = migrated;
2804
+ },
2109
2805
  _readCacheEnabled: readCache,
2110
2806
  _isBiometric: isBiometric,
2111
2807
  _biometricLevel: resolvedBiometricLevel,
@@ -2127,6 +2823,15 @@ export function createStorageCore(
2127
2823
  groupSet.add(storageItem as StorageItemInternal<unknown>);
2128
2824
  }
2129
2825
 
2826
+ if (isMemory) {
2827
+ let items = memoryItemsByKey.get(storageKey);
2828
+ if (!items) {
2829
+ items = new Set();
2830
+ memoryItemsByKey.set(storageKey, items);
2831
+ }
2832
+ items.add(storageItem as StorageItemInternal<unknown>);
2833
+ }
2834
+
2130
2835
  const registryKey = `${config.scope}:${storageKey}`;
2131
2836
  registeredKeyCounts.set(
2132
2837
  registryKey,
@@ -2165,26 +2870,25 @@ export function createStorageCore(
2165
2870
 
2166
2871
  items.forEach((item, index) => {
2167
2872
  if (scope === StorageScope.Disk) {
2168
- const pending = durability.readPendingDiskWrite(item.key);
2873
+ const pending = durability.getPendingDiskWrite(item.key);
2169
2874
  if (pending !== undefined) {
2170
- rawValues[index] = pending;
2875
+ rawValues[index] = pending.value;
2171
2876
  return;
2172
2877
  }
2173
2878
  }
2174
2879
 
2175
2880
  if (scope === StorageScope.Secure) {
2176
- const pending = durability.readPendingSecureWrite(item.key);
2881
+ const pending = durability.getPendingSecureWrite(item.key);
2177
2882
  if (pending !== undefined) {
2178
- rawValues[index] = pending;
2883
+ rawValues[index] = pending.value;
2179
2884
  return;
2180
2885
  }
2181
2886
  }
2182
2887
 
2183
2888
  if (item._readCacheEnabled === true) {
2184
- const cache = getScopeRawCache(scope);
2185
- const cached = cache.get(item.key);
2186
- if (cached !== undefined || cache.has(item.key)) {
2187
- rawValues[index] = cached;
2889
+ const cachedEntry = getCachedRawValueEntry(scope, item.key);
2890
+ if (cachedEntry?.has("plain")) {
2891
+ rawValues[index] = cachedEntry.get("plain");
2188
2892
  return;
2189
2893
  }
2190
2894
  }
@@ -2293,6 +2997,10 @@ export function createStorageCore(
2293
2997
 
2294
2998
  flushSecureWrites();
2295
2999
  const keys = secureEntries.map(({ item }) => item.key);
3000
+ keys.forEach((key) => {
3001
+ invalidateRawCache(scope, key);
3002
+ });
3003
+ const serializedValues: string[] = [];
2296
3004
  const oldValues = shouldReadPreviousEventValues(scope)
2297
3005
  ? adapter.backend
2298
3006
  .getBatch(keys, scope)
@@ -2308,12 +3016,14 @@ export function createStorageCore(
2308
3016
  >();
2309
3017
 
2310
3018
  secureEntries.forEach(({ item, value, internal }) => {
3019
+ const serialized = item.serialize(value);
3020
+ serializedValues.push(serialized);
2311
3021
  const accessControl =
2312
3022
  internal._secureAccessControl ?? secureDefaultAccessControl;
2313
3023
  const existingGroup = groupedByAccessControl.get(accessControl);
2314
3024
  const group = existingGroup ?? { keys: [], values: [] };
2315
3025
  group.keys.push(item.key);
2316
- group.values.push(item.serialize(value));
3026
+ group.values.push(serialized);
2317
3027
  if (!existingGroup) {
2318
3028
  groupedByAccessControl.set(accessControl, group);
2319
3029
  }
@@ -2326,20 +3036,24 @@ export function createStorageCore(
2326
3036
  cacheRawValue(scope, key, group.values[index]);
2327
3037
  });
2328
3038
  });
3039
+ const willEmitChanges =
3040
+ storageEvents.hasListeners(scope) || eventObserver !== undefined;
2329
3041
  emitBatchChange(
2330
3042
  scope,
2331
3043
  "setBatch",
2332
3044
  adapter.changeSource,
2333
- secureEntries.map(({ item, value }, index) =>
2334
- createKeyChange(
2335
- scope,
2336
- item.key,
2337
- oldValues[index],
2338
- item.serialize(value),
2339
- "setBatch",
2340
- adapter.changeSource,
2341
- ),
2342
- ),
3045
+ willEmitChanges
3046
+ ? keys.map((key, index) =>
3047
+ createKeyChange(
3048
+ scope,
3049
+ key,
3050
+ oldValues[index],
3051
+ serializedValues[index],
3052
+ "setBatch",
3053
+ adapter.changeSource,
3054
+ ),
3055
+ )
3056
+ : [],
2343
3057
  );
2344
3058
  return;
2345
3059
  }
@@ -2423,6 +3137,17 @@ export function createStorageCore(
2423
3137
  return;
2424
3138
  }
2425
3139
 
3140
+ if (
3141
+ items.some(
3142
+ (item) => asInternal(item as StorageItem<unknown>)._hasRenameFrom,
3143
+ )
3144
+ ) {
3145
+ items.forEach((item) => {
3146
+ asInternal(item as StorageItem<unknown>).delete();
3147
+ });
3148
+ return;
3149
+ }
3150
+
2426
3151
  const keys = items.map((item) => item.key);
2427
3152
  if (scope === StorageScope.Disk) {
2428
3153
  flushDiskWrites();
@@ -2431,14 +3156,46 @@ export function createStorageCore(
2431
3156
  flushSecureWrites();
2432
3157
  }
2433
3158
  const oldValues = shouldReadPreviousEventValues(scope)
2434
- ? adapter.backend
2435
- .getBatch(keys, scope)
2436
- .map((value) =>
2437
- value === undefined
2438
- ? undefined
2439
- : unescapeCollidingRawValue(value),
2440
- )
3159
+ ? (() => {
3160
+ const plainItems = items.filter(
3161
+ (item) =>
3162
+ asInternal(item as StorageItem<unknown>)._isBiometric !==
3163
+ true,
3164
+ );
3165
+ const plainValues =
3166
+ plainItems.length === 0
3167
+ ? []
3168
+ : adapter.backend
3169
+ .getBatch(
3170
+ plainItems.map((item) => item.key),
3171
+ scope,
3172
+ )
3173
+ .map((value) =>
3174
+ value === undefined
3175
+ ? undefined
3176
+ : unescapeCollidingRawValue(value),
3177
+ );
3178
+ let plainIndex = 0;
3179
+ return items.map((item) => {
3180
+ const internal = asInternal(item as StorageItem<unknown>);
3181
+ if (internal._isBiometric === true) {
3182
+ return getEventRawValueForRepresentation(
3183
+ scope,
3184
+ item.key,
3185
+ "biometric",
3186
+ );
3187
+ }
3188
+ const value = plainValues[plainIndex];
3189
+ plainIndex += 1;
3190
+ return value;
3191
+ });
3192
+ })()
2441
3193
  : [];
3194
+ if (scope === StorageScope.Secure) {
3195
+ keys.forEach((key) => {
3196
+ invalidateRawCache(scope, key);
3197
+ });
3198
+ }
2442
3199
  adapter.backend.removeBatch(keys, scope);
2443
3200
  keys.forEach((key) => {
2444
3201
  cacheRawValue(scope, key, undefined);
@@ -2515,46 +3272,118 @@ export function createStorageCore(
2515
3272
  }
2516
3273
 
2517
3274
  const NOT_SET = Symbol();
2518
- const rollback = new Map<string, RollbackRecord>();
3275
+ type TransactionRollbackEntry = {
3276
+ key: string;
3277
+ representation: StorageRawCacheRepresentation;
3278
+ record: RollbackRecord;
3279
+ };
3280
+ const rollback = new Map<string, TransactionRollbackEntry>();
3281
+ const itemRenameStates = new Map<StorageItemInternal<unknown>, boolean>();
2519
3282
 
2520
3283
  const rememberRollback = (
2521
3284
  key: string,
2522
3285
  item?: Pick<StorageItem<unknown>, "key" | "scope">,
3286
+ includeOtherSecureRepresentation = false,
2523
3287
  ) => {
2524
- if (rollback.has(key)) {
2525
- return;
2526
- }
2527
- if (scope === StorageScope.Memory) {
2528
- rollback.set(key, {
2529
- kind: "memory",
2530
- value: memoryStore.has(key) ? memoryStore.get(key) : NOT_SET,
2531
- });
2532
- } else {
2533
- const internal = item
2534
- ? (item as StorageItemInternal<unknown>)
2535
- : undefined;
2536
- if (
2537
- scope === StorageScope.Secure &&
2538
- internal?._isBiometric === true
2539
- ) {
2540
- rollback.set(key, {
2541
- kind: "biometric",
2542
- value: adapter.backend.getSecureBiometric(key),
2543
- level: internal._biometricLevel,
3288
+ const internal = item
3289
+ ? (item as StorageItemInternal<unknown>)
3290
+ : undefined;
3291
+ const representation: StorageRawCacheRepresentation =
3292
+ scope === StorageScope.Secure && internal?._isBiometric === true
3293
+ ? "biometric"
3294
+ : "plain";
3295
+ const rememberRepresentation = (
3296
+ representationToRemember: StorageRawCacheRepresentation,
3297
+ ) => {
3298
+ const identity = JSON.stringify([
3299
+ scope,
3300
+ key,
3301
+ representationToRemember,
3302
+ ]);
3303
+ if (rollback.has(identity)) {
3304
+ return;
3305
+ }
3306
+ if (scope === StorageScope.Memory) {
3307
+ const expiresAt = memoryExpirationDeadlines.get(key);
3308
+ rollback.set(identity, {
3309
+ key,
3310
+ representation: representationToRemember,
3311
+ record: {
3312
+ kind: "memory",
3313
+ value: memoryStore.has(key) ? memoryStore.get(key) : NOT_SET,
3314
+ ...(expiresAt === undefined ? {} : { expiresAt }),
3315
+ },
2544
3316
  });
2545
3317
  return;
2546
3318
  }
2547
- rollback.set(key, {
2548
- kind: "raw",
2549
- value: getStoredRawValue(key, scope),
2550
- ...(scope === StorageScope.Secure &&
2551
- internal?._secureAccessControl !== undefined
2552
- ? { accessControl: internal._secureAccessControl }
2553
- : {}),
3319
+ if (representationToRemember === "biometric") {
3320
+ rollback.set(identity, {
3321
+ key,
3322
+ representation: representationToRemember,
3323
+ record: {
3324
+ kind: "biometric",
3325
+ value: getStoredRawValueForRepresentation(
3326
+ key,
3327
+ scope,
3328
+ representationToRemember,
3329
+ ),
3330
+ level:
3331
+ internal?._biometricLevel !== undefined &&
3332
+ internal._biometricLevel !== BiometricLevel.None
3333
+ ? internal._biometricLevel
3334
+ : BiometricLevel.BiometryOnly,
3335
+ },
3336
+ });
3337
+ return;
3338
+ }
3339
+ rollback.set(identity, {
3340
+ key,
3341
+ representation: representationToRemember,
3342
+ record: {
3343
+ kind: "raw",
3344
+ value: getStoredRawValueForRepresentation(
3345
+ key,
3346
+ scope,
3347
+ representationToRemember,
3348
+ ),
3349
+ ...(scope === StorageScope.Secure &&
3350
+ internal?._secureAccessControl !== undefined
3351
+ ? { accessControl: internal._secureAccessControl }
3352
+ : {}),
3353
+ },
2554
3354
  });
3355
+ };
3356
+
3357
+ if (
3358
+ representation === "biometric" ||
3359
+ includeOtherSecureRepresentation
3360
+ ) {
3361
+ rememberRepresentation("plain");
3362
+ }
3363
+ rememberRepresentation(representation);
3364
+ if (includeOtherSecureRepresentation) {
3365
+ rememberRepresentation("biometric");
2555
3366
  }
2556
3367
  };
2557
3368
 
3369
+ const rememberItemRollback = (
3370
+ item: Pick<StorageItem<unknown>, "key" | "scope">,
3371
+ ): void => {
3372
+ const internal = item as StorageItemInternal<unknown>;
3373
+ const includeOtherSecureRepresentation =
3374
+ scope === StorageScope.Secure &&
3375
+ (internal._isBiometric === true || internal._hasRenameFrom === true);
3376
+ rememberRollback(item.key, item, includeOtherSecureRepresentation);
3377
+ if (internal._getRenameMigrationState) {
3378
+ if (!itemRenameStates.has(internal)) {
3379
+ itemRenameStates.set(internal, internal._getRenameMigrationState());
3380
+ }
3381
+ }
3382
+ (internal._renameFromKeys ?? EMPTY_KEYS).forEach((aliasKey) => {
3383
+ rememberRollback(aliasKey, item, true);
3384
+ });
3385
+ };
3386
+
2558
3387
  const tx: TransactionContext = {
2559
3388
  scope,
2560
3389
  getRaw: (key) => getRawValue(key, scope),
@@ -2563,21 +3392,22 @@ export function createStorageCore(
2563
3392
  setRawValue(key, value, scope);
2564
3393
  },
2565
3394
  removeRaw: (key) => {
2566
- rememberRollback(key);
3395
+ rememberRollback(key, undefined, scope === StorageScope.Secure);
2567
3396
  removeRawValue(key, scope);
2568
3397
  },
2569
3398
  getItem: (item) => {
2570
3399
  assertBatchScope([item], scope);
3400
+ rememberItemRollback(item);
2571
3401
  return item.get();
2572
3402
  },
2573
3403
  setItem: (item, value) => {
2574
3404
  assertBatchScope([item], scope);
2575
- rememberRollback(item.key, item);
3405
+ rememberItemRollback(item);
2576
3406
  item.set(value);
2577
3407
  },
2578
3408
  removeItem: (item) => {
2579
3409
  assertBatchScope([item], scope);
2580
- rememberRollback(item.key, item);
3410
+ rememberItemRollback(item);
2581
3411
  item.delete();
2582
3412
  },
2583
3413
  };
@@ -2585,21 +3415,56 @@ export function createStorageCore(
2585
3415
  try {
2586
3416
  return transaction(tx);
2587
3417
  } catch (error) {
2588
- const rollbackEntries = Array.from(rollback.entries()).reverse();
3418
+ itemRenameStates.forEach((migrated, item) => {
3419
+ item._setRenameMigrationState(migrated);
3420
+ });
3421
+ const rollbackEntries = Array.from(rollback.values()).reverse();
2589
3422
  const rollbackSource =
2590
3423
  scope === StorageScope.Memory ? "memory" : adapter.changeSource;
2591
- const preRollbackValues =
2592
- rollbackEntries.length > 0
2593
- ? rollbackEntries.map(([key]) => getEventRawValue(scope, key))
2594
- : [];
3424
+ const rollbackErrors: { label: string; error: unknown }[] = [];
3425
+ const readRollbackEventValue = (
3426
+ entry: TransactionRollbackEntry,
3427
+ ): string | undefined => {
3428
+ try {
3429
+ return getEventRawValueForRepresentation(
3430
+ scope,
3431
+ entry.key,
3432
+ entry.representation,
3433
+ );
3434
+ } catch (readError) {
3435
+ rollbackErrors.push({
3436
+ label: `rollback read ${entry.representation}:${entry.key}`,
3437
+ error: readError,
3438
+ });
3439
+ return undefined;
3440
+ }
3441
+ };
3442
+ const preRollbackValues = rollbackEntries.map(readRollbackEventValue);
2595
3443
  if (scope === StorageScope.Memory) {
2596
- rollbackEntries.forEach(([key, record]) => {
2597
- if (record.value === NOT_SET) {
2598
- memoryStore.delete(key);
2599
- } else {
2600
- memoryStore.set(key, record.value);
3444
+ rollbackEntries.forEach((entry) => {
3445
+ try {
3446
+ const record = entry.record;
3447
+ if (record.kind !== "memory") {
3448
+ return;
3449
+ }
3450
+ if (record.value === NOT_SET) {
3451
+ memoryStore.delete(entry.key);
3452
+ } else {
3453
+ memoryStore.set(entry.key, record.value);
3454
+ }
3455
+ if (record.expiresAt === undefined) {
3456
+ memoryExpirationDeadlines.delete(entry.key);
3457
+ } else {
3458
+ memoryExpirationDeadlines.set(entry.key, record.expiresAt);
3459
+ }
3460
+ invalidateMemoryItemCaches(entry.key);
3461
+ notifyKeyListeners(memoryListeners, entry.key);
3462
+ } catch (rollbackError) {
3463
+ rollbackErrors.push({
3464
+ label: `rollback ${entry.representation}:${entry.key}`,
3465
+ error: rollbackError,
3466
+ });
2601
3467
  }
2602
- notifyKeyListeners(memoryListeners, key);
2603
3468
  });
2604
3469
  } else {
2605
3470
  const groupedKeysToSet = new Map<
@@ -2607,18 +3472,14 @@ export function createStorageCore(
2607
3472
  { keys: string[]; values: string[] }
2608
3473
  >();
2609
3474
  const keysToRemove: string[] = [];
3475
+ const biometricEntries: TransactionRollbackEntry[] = [];
3476
+ const biometricValues = new Map<string, string | undefined>();
2610
3477
 
2611
- rollbackEntries.forEach(([key, record]) => {
3478
+ rollbackEntries.forEach((entry) => {
3479
+ const { key, record } = entry;
2612
3480
  if (record.kind === "biometric") {
2613
- if (record.value === undefined) {
2614
- adapter.backend.deleteSecureBiometric(key);
2615
- } else {
2616
- adapter.backend.setSecureBiometricWithLevel(
2617
- key,
2618
- record.value,
2619
- record.level,
2620
- );
2621
- }
3481
+ biometricEntries.push(entry);
3482
+ biometricValues.set(key, record.value);
2622
3483
  return;
2623
3484
  }
2624
3485
  if (record.kind !== "raw") {
@@ -2640,26 +3501,115 @@ export function createStorageCore(
2640
3501
  });
2641
3502
 
2642
3503
  if (scope === StorageScope.Disk) {
2643
- flushDiskWrites();
3504
+ try {
3505
+ flushDiskWrites();
3506
+ } catch (flushError) {
3507
+ rollbackErrors.push({
3508
+ label: "rollback flush disk",
3509
+ error: flushError,
3510
+ });
3511
+ }
2644
3512
  }
2645
3513
  if (scope === StorageScope.Secure) {
2646
- flushSecureWrites();
3514
+ try {
3515
+ flushSecureWrites();
3516
+ } catch (flushError) {
3517
+ rollbackErrors.push({
3518
+ label: "rollback flush secure",
3519
+ error: flushError,
3520
+ });
3521
+ }
2647
3522
  }
3523
+ if (scope === StorageScope.Secure) {
3524
+ rollbackEntries.forEach((entry) => {
3525
+ invalidateRawCache(scope, entry.key);
3526
+ });
3527
+ }
3528
+ biometricEntries.forEach((entry) => {
3529
+ const { key, record } = entry;
3530
+ if (record.kind !== "biometric" || record.value === undefined) {
3531
+ return;
3532
+ }
3533
+ try {
3534
+ adapter.backend.setSecureBiometricWithLevel(
3535
+ key,
3536
+ record.value,
3537
+ record.level,
3538
+ );
3539
+ cacheRawValue(
3540
+ StorageScope.Secure,
3541
+ key,
3542
+ record.value,
3543
+ "biometric",
3544
+ );
3545
+ } catch (rollbackError) {
3546
+ rollbackErrors.push({
3547
+ label: "rollback biometric",
3548
+ error: rollbackError,
3549
+ });
3550
+ }
3551
+ });
2648
3552
  groupedKeysToSet.forEach((group, accessControl) => {
2649
- if (scope === StorageScope.Secure) {
2650
- adapter.backend.setSecureAccessControl(accessControl);
3553
+ try {
3554
+ if (scope === StorageScope.Secure) {
3555
+ adapter.backend.setSecureAccessControl(accessControl);
3556
+ }
3557
+ adapter.backend.setBatch(group.keys, group.values, scope);
3558
+ group.keys.forEach((key, index) => {
3559
+ cacheRawValue(scope, key, group.values[index], "plain");
3560
+ });
3561
+ } catch (rollbackError) {
3562
+ rollbackErrors.push({
3563
+ label: "rollback plain setBatch",
3564
+ error: rollbackError,
3565
+ });
2651
3566
  }
2652
- adapter.backend.setBatch(group.keys, group.values, scope);
2653
- group.keys.forEach((key, index) => {
2654
- cacheRawValue(scope, key, group.values[index]);
2655
- });
2656
3567
  });
2657
3568
  if (keysToRemove.length > 0) {
2658
- adapter.backend.removeBatch(keysToRemove, scope);
2659
- keysToRemove.forEach((key) => {
2660
- cacheRawValue(scope, key, undefined);
2661
- });
3569
+ const keysToRemoveWithoutBiometric = keysToRemove.filter(
3570
+ (key) => biometricValues.get(key) === undefined,
3571
+ );
3572
+ try {
3573
+ if (keysToRemoveWithoutBiometric.length > 0) {
3574
+ adapter.backend.removeBatch(
3575
+ keysToRemoveWithoutBiometric,
3576
+ scope,
3577
+ );
3578
+ }
3579
+ keysToRemove.forEach((key) => {
3580
+ cacheRawValue(scope, key, undefined, "plain");
3581
+ });
3582
+ } catch (rollbackError) {
3583
+ rollbackErrors.push({
3584
+ label: "rollback plain removeBatch",
3585
+ error: rollbackError,
3586
+ });
3587
+ }
2662
3588
  }
3589
+ biometricEntries.forEach((entry) => {
3590
+ const { key, record } = entry;
3591
+ if (record.kind !== "biometric") {
3592
+ return;
3593
+ }
3594
+ if (record.value !== undefined) {
3595
+ return;
3596
+ }
3597
+ invalidateRawCache(StorageScope.Secure, key);
3598
+ try {
3599
+ adapter.backend.deleteSecureBiometric(key);
3600
+ cacheRawValue(
3601
+ StorageScope.Secure,
3602
+ key,
3603
+ record.value,
3604
+ "biometric",
3605
+ );
3606
+ } catch (rollbackError) {
3607
+ rollbackErrors.push({
3608
+ label: "rollback biometric",
3609
+ error: rollbackError,
3610
+ });
3611
+ }
3612
+ });
2663
3613
  }
2664
3614
 
2665
3615
  if (rollbackEntries.length > 0) {
@@ -2667,18 +3617,25 @@ export function createStorageCore(
2667
3617
  scope,
2668
3618
  "rollback",
2669
3619
  rollbackSource,
2670
- rollbackEntries.map(([key], index) =>
3620
+ rollbackEntries.map((entry, index) =>
2671
3621
  createKeyChange(
2672
3622
  scope,
2673
- key,
3623
+ entry.key,
2674
3624
  preRollbackValues[index],
2675
- getEventRawValue(scope, key),
3625
+ readRollbackEventValue(entry),
2676
3626
  "rollback",
2677
3627
  rollbackSource,
2678
3628
  ),
2679
3629
  ),
2680
3630
  );
2681
3631
  }
3632
+ if (rollbackErrors.length > 0) {
3633
+ throw createStorageCompositeError(
3634
+ "transaction rollback",
3635
+ error,
3636
+ rollbackErrors,
3637
+ );
3638
+ }
2682
3639
  throw error;
2683
3640
  }
2684
3641
  });
@@ -2753,14 +3710,14 @@ export function createStorageCore(
2753
3710
  config: SetItemConfig<TMember>,
2754
3711
  ): SetStorageItem<TMember> {
2755
3712
  const { defaultValue, ...rest } = config;
2756
- const initial: Partial<Record<TMember, true>> = {};
3713
+ const initial: Record<string, true> = {};
2757
3714
  if (defaultValue) {
2758
3715
  for (const id of defaultValue) {
2759
3716
  initial[id] = true;
2760
3717
  }
2761
3718
  }
2762
3719
 
2763
- const item = createStorageItem<Partial<Record<TMember, true>>>({
3720
+ const item = createStorageItem<Record<string, true>>({
2764
3721
  ...rest,
2765
3722
  defaultValue: initial,
2766
3723
  });
@@ -2796,8 +3753,17 @@ export function createStorageCore(
2796
3753
  return true;
2797
3754
  };
2798
3755
 
3756
+ const getTyped = (): Partial<Record<TMember, true>> => {
3757
+ const typed: Partial<Record<TMember, true>> = {};
3758
+ for (const id of Object.keys(item.get())) {
3759
+ typed[id as TMember] = true;
3760
+ }
3761
+ return typed;
3762
+ };
3763
+
2799
3764
  return {
2800
3765
  get: item.get,
3766
+ getTyped,
2801
3767
  has,
2802
3768
  add,
2803
3769
  delete: deleteId,