react-native-nitro-storage 0.9.0 → 0.10.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 (76) hide show
  1. package/CHANGELOG.md +89 -29
  2. package/README.md +81 -24
  3. package/android/src/main/cpp/AndroidStorageAdapterCpp.cpp +5 -2
  4. package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +240 -34
  5. package/cpp/bindings/HybridStorage.cpp +106 -283
  6. package/cpp/bindings/HybridStorage.hpp +16 -9
  7. package/docs/api-reference.md +75 -19
  8. package/docs/benchmarks.md +6 -1
  9. package/docs/keychain-lifecycle-testing.md +36 -0
  10. package/docs/recipes.md +1 -2
  11. package/docs/secure-storage.md +45 -9
  12. package/ios/IOSStorageAdapterCpp.mm +391 -175
  13. package/lib/commonjs/capabilities.js +3 -1
  14. package/lib/commonjs/capabilities.js.map +1 -1
  15. package/lib/commonjs/core/durability.js +110 -43
  16. package/lib/commonjs/core/durability.js.map +1 -1
  17. package/lib/commonjs/index.js +15 -5
  18. package/lib/commonjs/index.js.map +1 -1
  19. package/lib/commonjs/index.web.js +219 -97
  20. package/lib/commonjs/index.web.js.map +1 -1
  21. package/lib/commonjs/internal.js +3 -11
  22. package/lib/commonjs/internal.js.map +1 -1
  23. package/lib/commonjs/shared.js +62 -2
  24. package/lib/commonjs/shared.js.map +1 -1
  25. package/lib/commonjs/storage-core.js +852 -141
  26. package/lib/commonjs/storage-core.js.map +1 -1
  27. package/lib/commonjs/storage-runtime.js +5 -1
  28. package/lib/commonjs/storage-runtime.js.map +1 -1
  29. package/lib/commonjs/testing.js +13 -0
  30. package/lib/commonjs/testing.js.map +1 -1
  31. package/lib/module/capabilities.js +2 -1
  32. package/lib/module/capabilities.js.map +1 -1
  33. package/lib/module/core/durability.js +110 -43
  34. package/lib/module/core/durability.js.map +1 -1
  35. package/lib/module/index.js +12 -8
  36. package/lib/module/index.js.map +1 -1
  37. package/lib/module/index.web.js +215 -99
  38. package/lib/module/index.web.js.map +1 -1
  39. package/lib/module/internal.js +2 -9
  40. package/lib/module/internal.js.map +1 -1
  41. package/lib/module/shared.js +60 -2
  42. package/lib/module/shared.js.map +1 -1
  43. package/lib/module/storage-core.js +853 -142
  44. package/lib/module/storage-core.js.map +1 -1
  45. package/lib/module/storage-runtime.js +4 -1
  46. package/lib/module/storage-runtime.js.map +1 -1
  47. package/lib/module/testing.js +8 -1
  48. package/lib/module/testing.js.map +1 -1
  49. package/lib/typescript/capabilities.d.ts +2 -1
  50. package/lib/typescript/capabilities.d.ts.map +1 -1
  51. package/lib/typescript/core/durability.d.ts +7 -2
  52. package/lib/typescript/core/durability.d.ts.map +1 -1
  53. package/lib/typescript/index.d.ts +2 -2
  54. package/lib/typescript/index.d.ts.map +1 -1
  55. package/lib/typescript/index.web.d.ts +3 -29
  56. package/lib/typescript/index.web.d.ts.map +1 -1
  57. package/lib/typescript/internal.d.ts +0 -2
  58. package/lib/typescript/internal.d.ts.map +1 -1
  59. package/lib/typescript/shared.d.ts +19 -0
  60. package/lib/typescript/shared.d.ts.map +1 -1
  61. package/lib/typescript/storage-core.d.ts +15 -6
  62. package/lib/typescript/storage-core.d.ts.map +1 -1
  63. package/lib/typescript/storage-runtime.d.ts +2 -1
  64. package/lib/typescript/storage-runtime.d.ts.map +1 -1
  65. package/lib/typescript/testing.d.ts +1 -1
  66. package/lib/typescript/testing.d.ts.map +1 -1
  67. package/package.json +1 -1
  68. package/src/capabilities.ts +3 -1
  69. package/src/core/durability.ts +139 -39
  70. package/src/index.ts +20 -10
  71. package/src/index.web.ts +328 -166
  72. package/src/internal.ts +0 -14
  73. package/src/shared.ts +85 -0
  74. package/src/storage-core.ts +1165 -202
  75. package/src/storage-runtime.ts +6 -0
  76. 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;
@@ -525,16 +586,23 @@ export function createStorageCore(
525
586
  durability.flushSecureWrites();
526
587
  }
527
588
 
528
- function scheduleDiskWrite(key: string, value: string | undefined): void {
529
- durability.scheduleDiskWrite(key, value);
589
+ function runSecurePromotion<T>(key: string, promotion: () => T): T {
590
+ return durability.runSecurePromotion(key, promotion);
591
+ }
592
+
593
+ function scheduleDiskWrite(
594
+ key: string,
595
+ value: string | undefined,
596
+ ): PendingDiskWrite {
597
+ return durability.scheduleDiskWrite(key, value);
530
598
  }
531
599
 
532
600
  function scheduleSecureWrite(
533
601
  key: string,
534
602
  value: string | undefined,
535
603
  accessControl?: AccessControl,
536
- ): void {
537
- durability.scheduleSecureWrite(key, value, accessControl);
604
+ ): PendingSecureWrite {
605
+ return durability.scheduleSecureWrite(key, value, accessControl);
538
606
  }
539
607
 
540
608
  function setDiskWritesAsyncMode(enabled: boolean): void {
@@ -565,6 +633,17 @@ export function createStorageCore(
565
633
  return adapter.backend.get(key, scope);
566
634
  }
567
635
 
636
+ function getStoredRawValueForRepresentation(
637
+ key: string,
638
+ scope: StorageScope,
639
+ representation: StorageRawCacheRepresentation,
640
+ ): string | undefined {
641
+ if (representation === "plain") {
642
+ return getStoredRawValue(key, scope);
643
+ }
644
+ return adapter.backend.getSecureBiometric(key);
645
+ }
646
+
568
647
  function getRawValue(key: string, scope: StorageScope): string | undefined {
569
648
  assertValidScope(scope);
570
649
  if (scope === StorageScope.Memory) {
@@ -617,6 +696,7 @@ export function createStorageCore(
617
696
  }
618
697
 
619
698
  if (scope === StorageScope.Secure) {
699
+ invalidateRawCache(scope, key);
620
700
  flushSecureWrites();
621
701
  clearPendingSecureWrite(key);
622
702
  if (adapter.applyAccessControlOnSecureRawWrite) {
@@ -659,6 +739,7 @@ export function createStorageCore(
659
739
  }
660
740
 
661
741
  if (scope === StorageScope.Secure) {
742
+ invalidateRawCache(scope, key);
662
743
  flushSecureWrites();
663
744
  clearPendingSecureWrite(key);
664
745
  }
@@ -689,6 +770,7 @@ export function createStorageCore(
689
770
  getScopedListeners,
690
771
  cacheRawValue,
691
772
  readCachedRawValue,
773
+ invalidateRawCache,
692
774
  clearScopeRawCache,
693
775
  clearPendingDiskWrite,
694
776
  clearPendingSecureWrite,
@@ -779,6 +861,11 @@ export function createStorageCore(
779
861
  : unescapeCollidingRawValue(value),
780
862
  )
781
863
  : [];
864
+ if (scope === StorageScope.Secure) {
865
+ removeKeys.forEach((key) => {
866
+ invalidateRawCache(scope, key);
867
+ });
868
+ }
782
869
  adapter.backend.removeBatch(removeKeys, scope);
783
870
  removeKeys.forEach((key) => {
784
871
  cacheRawValue(scope, key, undefined);
@@ -885,6 +972,7 @@ export function createStorageCore(
885
972
  : {};
886
973
  if (scope === StorageScope.Memory) {
887
974
  memoryStore.clear();
975
+ memoryExpirationDeadlines.clear();
888
976
  notifyAllListeners(memoryListeners);
889
977
  emitBatchChange(
890
978
  scope,
@@ -1044,6 +1132,7 @@ export function createStorageCore(
1044
1132
 
1045
1133
  affectedKeys.forEach((key) => {
1046
1134
  memoryStore.delete(key);
1135
+ memoryExpirationDeadlines.delete(key);
1047
1136
  });
1048
1137
  affectedKeys.forEach((key) => {
1049
1138
  notifyKeyListeners(memoryListeners, key);
@@ -1103,7 +1192,48 @@ export function createStorageCore(
1103
1192
  },
1104
1193
  clearBiometric: () => {
1105
1194
  measureOperation("storage:clearBiometric", StorageScope.Secure, () => {
1106
- adapter.backend.clearSecureBiometric();
1195
+ flushSecureWrites();
1196
+ const readEventValues = shouldReadPreviousEventValues(
1197
+ StorageScope.Secure,
1198
+ );
1199
+ const shouldEmitChanges =
1200
+ storageEvents.hasListeners(StorageScope.Secure) ||
1201
+ eventObserver !== undefined;
1202
+ const biometricKeys = shouldEmitChanges
1203
+ ? adapter.backend
1204
+ .getAllKeys(StorageScope.Secure)
1205
+ .filter((key) => adapter.backend.hasSecureBiometric(key))
1206
+ : [];
1207
+ const previousValues = readEventValues
1208
+ ? biometricKeys.map((key) => {
1209
+ const raw = adapter.backend.getSecureBiometric(key);
1210
+ return raw === undefined
1211
+ ? undefined
1212
+ : unescapeCollidingRawValue(raw);
1213
+ })
1214
+ : [];
1215
+
1216
+ clearScopeRawCache(StorageScope.Secure);
1217
+ try {
1218
+ adapter.backend.clearSecureBiometric();
1219
+ } finally {
1220
+ clearScopeRawCache(StorageScope.Secure);
1221
+ }
1222
+ emitBatchChange(
1223
+ StorageScope.Secure,
1224
+ "clear",
1225
+ adapter.changeSource,
1226
+ biometricKeys.map((key, index) =>
1227
+ createKeyChange(
1228
+ StorageScope.Secure,
1229
+ key,
1230
+ readEventValues ? previousValues[index] : undefined,
1231
+ undefined,
1232
+ "clear",
1233
+ adapter.changeSource,
1234
+ ),
1235
+ ),
1236
+ );
1107
1237
  });
1108
1238
  },
1109
1239
  has: (key: string, scope: StorageScope): boolean => {
@@ -1376,6 +1506,9 @@ export function createStorageCore(
1376
1506
  if (scope === StorageScope.Secure) {
1377
1507
  flushSecureWrites();
1378
1508
  adapter.backend.setSecureAccessControl(secureDefaultAccessControl);
1509
+ keys.forEach((key) => {
1510
+ invalidateRawCache(scope, key);
1511
+ });
1379
1512
  }
1380
1513
  if (scope === StorageScope.Disk) {
1381
1514
  flushDiskWrites();
@@ -1414,8 +1547,11 @@ export function createStorageCore(
1414
1547
  const onExpired = config.onExpired;
1415
1548
  const expirationTtlMs = expiration?.ttlMs;
1416
1549
  const memoryExpiration =
1417
- expiration && isMemory ? new Map<string, number>() : null;
1550
+ expiration && isMemory ? memoryExpirationDeadlines : null;
1418
1551
  const readCache = !isMemory && config.readCache === true;
1552
+ const rawCacheRepresentation: StorageRawCacheRepresentation = isBiometric
1553
+ ? "biometric"
1554
+ : "plain";
1419
1555
  const coalesceDiskWrites =
1420
1556
  config.scope === StorageScope.Disk && config.coalesceDiskWrites === true;
1421
1557
  const coalesceSecureWrites =
@@ -1527,34 +1663,53 @@ export function createStorageCore(
1527
1663
  : memoryStored;
1528
1664
  }
1529
1665
 
1666
+ if (nonMemoryScope === StorageScope.Disk) {
1667
+ if (durability.hasPendingDiskWrite(storageKey)) {
1668
+ return durability.readPendingDiskWrite(storageKey);
1669
+ }
1670
+ }
1671
+
1672
+ if (nonMemoryScope === StorageScope.Secure && !isBiometric) {
1673
+ if (durability.hasPendingSecureWrite(storageKey)) {
1674
+ return durability.readPendingSecureWrite(storageKey);
1675
+ }
1676
+ }
1677
+
1530
1678
  migrateRenamesIfNeeded();
1531
1679
 
1532
1680
  if (nonMemoryScope === StorageScope.Disk) {
1533
- const pending = durability.readPendingDiskWrite(storageKey);
1534
- if (pending !== undefined) {
1535
- return pending;
1681
+ if (durability.hasPendingDiskWrite(storageKey)) {
1682
+ return durability.readPendingDiskWrite(storageKey);
1536
1683
  }
1537
1684
  }
1538
1685
 
1539
1686
  if (nonMemoryScope === StorageScope.Secure && !isBiometric) {
1540
- const pending = durability.readPendingSecureWrite(storageKey);
1541
- if (pending !== undefined) {
1542
- return pending;
1687
+ if (durability.hasPendingSecureWrite(storageKey)) {
1688
+ return durability.readPendingSecureWrite(storageKey);
1543
1689
  }
1544
1690
  }
1545
1691
 
1546
1692
  if (readCache) {
1547
- const cache = getScopeRawCache(resolveNonMemoryScope());
1548
- const cached = cache.get(storageKey);
1549
- if (cached !== undefined || cache.has(storageKey)) {
1550
- return cached;
1693
+ const scope = resolveNonMemoryScope();
1694
+ const cachedEntry = getCachedRawValueEntry(scope, storageKey);
1695
+ if (cachedEntry?.has(rawCacheRepresentation)) {
1696
+ return cachedEntry.get(rawCacheRepresentation);
1551
1697
  }
1552
1698
  }
1553
1699
 
1554
1700
  if (isBiometric) {
1555
- return readBackendRaw(() =>
1701
+ const raw = readBackendRaw(() =>
1556
1702
  adapter.backend.getSecureBiometric(storageKey),
1557
1703
  );
1704
+ if (readCache) {
1705
+ cacheRawValue(
1706
+ resolveNonMemoryScope(),
1707
+ storageKey,
1708
+ raw,
1709
+ rawCacheRepresentation,
1710
+ );
1711
+ }
1712
+ return raw;
1558
1713
  }
1559
1714
 
1560
1715
  const raw = readBackendRaw(() =>
@@ -1572,8 +1727,11 @@ export function createStorageCore(
1572
1727
  } catch (error) {
1573
1728
  onReadError?.(error);
1574
1729
  if (fallbackToCacheOnReadError) {
1575
- const cached = getScopeRawCache(resolveNonMemoryScope()).get(
1730
+ const scope = resolveNonMemoryScope();
1731
+ const cached = readCachedRawValue(
1732
+ scope,
1576
1733
  storageKey,
1734
+ rawCacheRepresentation,
1577
1735
  );
1578
1736
  if (cached !== undefined) {
1579
1737
  return cached;
@@ -1584,14 +1742,446 @@ export function createStorageCore(
1584
1742
  }
1585
1743
  };
1586
1744
 
1587
- const writeStoredRaw = (rawValue: string): void => {
1745
+ type StoredRawWriteOptions = {
1746
+ cleanupRenameSources?: boolean;
1747
+ };
1748
+ type RenameSnapshot = {
1749
+ key: string;
1750
+ plainValue?: string;
1751
+ biometricValue?: string;
1752
+ };
1753
+
1754
+ function getRenameSourceKeys(): string[] {
1755
+ return renameFromKeys.filter((key) => key !== storageKey);
1756
+ }
1757
+
1758
+ function getRenameSourceRaw(key: string): string | undefined {
1759
+ return isBiometric
1760
+ ? adapter.backend.getSecureBiometric(key)
1761
+ : adapter.backend.get(key, config.scope);
1762
+ }
1763
+
1764
+ function invalidateRenameSourceCache(key: string): void {
1765
+ if (nonMemoryScope !== null) {
1766
+ invalidateRawCache(nonMemoryScope, key);
1767
+ }
1768
+ }
1769
+
1770
+ function removeRenameSource(key: string): void {
1771
+ invalidateRenameSourceCache(key);
1772
+ if (isBiometric) {
1773
+ adapter.backend.deleteSecureBiometric(key);
1774
+ invalidateRenameSourceCache(key);
1775
+ return;
1776
+ }
1777
+ adapter.backend.remove(key, config.scope);
1778
+ invalidateRenameSourceCache(key);
1779
+ }
1780
+
1781
+ function restoreRenameSource(snapshot: RenameSnapshot): void {
1782
+ invalidateRenameSourceCache(snapshot.key);
1783
+ if (nonMemoryScope === StorageScope.Secure) {
1784
+ if (snapshot.biometricValue !== undefined) {
1785
+ adapter.backend.setSecureBiometricWithLevel(
1786
+ snapshot.key,
1787
+ snapshot.biometricValue,
1788
+ resolvedBiometricLevel === BiometricLevel.None
1789
+ ? BiometricLevel.BiometryOnly
1790
+ : resolvedBiometricLevel,
1791
+ );
1792
+ } else {
1793
+ adapter.backend.deleteSecureBiometric(snapshot.key);
1794
+ }
1795
+ if (snapshot.plainValue !== undefined) {
1796
+ if (adapter.applyAccessControlOnSecureRawWrite) {
1797
+ adapter.backend.setSecureAccessControl(
1798
+ secureAccessControl ?? secureDefaultAccessControl,
1799
+ );
1800
+ }
1801
+ adapter.backend.set(snapshot.key, snapshot.plainValue, config.scope);
1802
+ } else if (snapshot.biometricValue === undefined) {
1803
+ adapter.backend.remove(snapshot.key, config.scope);
1804
+ }
1805
+ invalidateRenameSourceCache(snapshot.key);
1806
+ return;
1807
+ }
1808
+ if (snapshot.plainValue !== undefined) {
1809
+ adapter.backend.set(snapshot.key, snapshot.plainValue, config.scope);
1810
+ } else {
1811
+ adapter.backend.remove(snapshot.key, config.scope);
1812
+ }
1813
+ invalidateRenameSourceCache(snapshot.key);
1814
+ }
1815
+
1816
+ function readRenameSnapshots(): RenameSnapshot[] {
1817
+ return getRenameSourceKeys().flatMap((key) => {
1818
+ const plainValue =
1819
+ nonMemoryScope === StorageScope.Secure
1820
+ ? adapter.backend.get(key, config.scope)
1821
+ : getRenameSourceRaw(key);
1822
+ const biometricValue =
1823
+ nonMemoryScope === StorageScope.Secure
1824
+ ? adapter.backend.getSecureBiometric(key)
1825
+ : undefined;
1826
+ if (plainValue === undefined && biometricValue === undefined) {
1827
+ return [];
1828
+ }
1829
+ return [
1830
+ {
1831
+ key,
1832
+ ...(plainValue === undefined ? {} : { plainValue }),
1833
+ ...(biometricValue === undefined ? {} : { biometricValue }),
1834
+ },
1835
+ ];
1836
+ });
1837
+ }
1838
+
1839
+ function selectedRenameValue(snapshot: RenameSnapshot): string | undefined {
1840
+ return isBiometric ? snapshot.biometricValue : snapshot.plainValue;
1841
+ }
1842
+
1843
+ function hasPendingCurrentWrite(): boolean {
1844
+ if (nonMemoryScope === StorageScope.Disk) {
1845
+ return durability.hasPendingDiskWrite(storageKey);
1846
+ }
1847
+ if (nonMemoryScope === StorageScope.Secure && !isBiometric) {
1848
+ return durability.hasPendingSecureWrite(storageKey);
1849
+ }
1850
+ return false;
1851
+ }
1852
+
1853
+ function hasPendingRenameSourceWrite(): boolean {
1854
+ if (isBiometric || nonMemoryScope === null) {
1855
+ return false;
1856
+ }
1857
+ return getRenameSourceKeys().some((key) =>
1858
+ nonMemoryScope === StorageScope.Disk
1859
+ ? durability.hasPendingDiskWrite(key)
1860
+ : durability.hasPendingSecureWrite(key),
1861
+ );
1862
+ }
1863
+
1864
+ function flushPendingRenameSourceWrites(): void {
1865
+ if (!hasPendingRenameSourceWrite()) {
1866
+ return;
1867
+ }
1868
+ if (nonMemoryScope === StorageScope.Disk) {
1869
+ flushDiskWrites();
1870
+ return;
1871
+ }
1872
+ flushSecureWrites();
1873
+ }
1874
+
1875
+ function clearPendingCurrentWriteIf(
1876
+ write: PendingDiskWrite | PendingSecureWrite | undefined,
1877
+ ): void {
1878
+ if (write === undefined) {
1879
+ return;
1880
+ }
1881
+ if (nonMemoryScope === StorageScope.Disk && "generation" in write) {
1882
+ durability.clearPendingDiskWriteIf(write);
1883
+ return;
1884
+ }
1885
+ if (nonMemoryScope === StorageScope.Secure && !isBiometric) {
1886
+ durability.clearPendingSecureWriteIf(write);
1887
+ }
1888
+ }
1889
+
1890
+ function flushPendingCurrentWrite(): void {
1891
+ if (nonMemoryScope === StorageScope.Disk) {
1892
+ flushDiskWrites();
1893
+ return;
1894
+ }
1895
+ if (nonMemoryScope === StorageScope.Secure && !isBiometric) {
1896
+ flushSecureWrites();
1897
+ }
1898
+ }
1899
+
1900
+ function removeCurrentBackendValue(): void {
1901
+ invalidateRawCache(resolveNonMemoryScope(), storageKey);
1902
+ if (isBiometric) {
1903
+ adapter.backend.deleteSecureBiometric(storageKey);
1904
+ return;
1905
+ }
1906
+ adapter.backend.remove(storageKey, config.scope);
1907
+ }
1908
+
1909
+ function scheduleRenameSourceCleanup(): void {
1910
+ const sourceKeys = getRenameSourceKeys();
1911
+ sourceKeys.forEach(invalidateRenameSourceCache);
1912
+
1913
+ if (isBiometric) {
1914
+ sourceKeys.forEach(removeRenameSource);
1915
+ return;
1916
+ }
1917
+
1918
+ if (
1919
+ nonMemoryScope === StorageScope.Disk &&
1920
+ (coalesceDiskWrites || isDiskWritesAsync())
1921
+ ) {
1922
+ sourceKeys.forEach((key) => {
1923
+ scheduleDiskWrite(key, undefined);
1924
+ });
1925
+ return;
1926
+ }
1927
+
1928
+ if (nonMemoryScope === StorageScope.Secure && coalesceSecureWrites) {
1929
+ sourceKeys.forEach((key) => {
1930
+ scheduleSecureWrite(
1931
+ key,
1932
+ undefined,
1933
+ secureAccessControl ?? secureDefaultAccessControl,
1934
+ );
1935
+ });
1936
+ return;
1937
+ }
1938
+
1939
+ sourceKeys.forEach(removeRenameSource);
1940
+ }
1941
+
1942
+ type ItemPendingSnapshot = {
1943
+ value: string | undefined;
1944
+ accessControl?: AccessControl;
1945
+ };
1946
+ type ItemStateRecord = {
1947
+ key: string;
1948
+ plainValue: string | undefined;
1949
+ biometricValue: string | undefined;
1950
+ pending?: ItemPendingSnapshot;
1951
+ };
1952
+ type ItemStateSnapshot = {
1953
+ records: readonly ItemStateRecord[];
1954
+ renamesMigrated: boolean;
1955
+ };
1956
+
1957
+ let atomicMutationDepth = 0;
1958
+
1959
+ function getItemStateKeys(): string[] {
1960
+ return Array.from(new Set([storageKey, ...getRenameSourceKeys()]));
1961
+ }
1962
+
1963
+ function captureItemState(): ItemStateSnapshot {
1964
+ const records = getItemStateKeys().map((key) => {
1965
+ let pending: ItemPendingSnapshot | undefined;
1966
+ if (nonMemoryScope === StorageScope.Disk) {
1967
+ if (durability.hasPendingDiskWrite(key)) {
1968
+ pending = {
1969
+ value: durability.readPendingDiskWrite(key),
1970
+ };
1971
+ }
1972
+ } else if (
1973
+ nonMemoryScope === StorageScope.Secure &&
1974
+ !isBiometric &&
1975
+ durability.hasPendingSecureWrite(key)
1976
+ ) {
1977
+ const accessControl = durability.readPendingSecureAccessControl(key);
1978
+ pending = {
1979
+ value: durability.readPendingSecureWrite(key),
1980
+ ...(accessControl === undefined ? {} : { accessControl }),
1981
+ };
1982
+ }
1983
+
1984
+ return {
1985
+ key,
1986
+ plainValue:
1987
+ nonMemoryScope === null
1988
+ ? undefined
1989
+ : adapter.backend.get(key, config.scope),
1990
+ biometricValue:
1991
+ nonMemoryScope === StorageScope.Secure
1992
+ ? adapter.backend.getSecureBiometric(key)
1993
+ : undefined,
1994
+ ...(pending === undefined ? {} : { pending }),
1995
+ };
1996
+ });
1997
+ return { records, renamesMigrated };
1998
+ }
1999
+
2000
+ function clearPendingItemWrite(key: string): void {
2001
+ if (nonMemoryScope === StorageScope.Disk) {
2002
+ durability.clearPendingDiskWrite(key);
2003
+ } else if (nonMemoryScope === StorageScope.Secure && !isBiometric) {
2004
+ durability.clearPendingSecureWrite(key);
2005
+ }
2006
+ }
2007
+
2008
+ function restoreItemState(snapshot: ItemStateSnapshot): {
2009
+ label: string;
2010
+ error: unknown;
2011
+ }[] {
2012
+ const rollbackErrors: { label: string; error: unknown }[] = [];
2013
+ const recordsByKey = new Map(
2014
+ snapshot.records.map((record) => [record.key, record]),
2015
+ );
2016
+ snapshot.records.forEach(({ key }) => {
2017
+ clearPendingItemWrite(key);
2018
+ if (nonMemoryScope !== null) {
2019
+ invalidateRawCache(nonMemoryScope, key);
2020
+ }
2021
+ });
2022
+
2023
+ const biometricRecords = snapshot.records.filter(
2024
+ ({ biometricValue }) => biometricValue !== undefined,
2025
+ );
2026
+ if (nonMemoryScope === StorageScope.Secure) {
2027
+ biometricRecords.forEach(({ key, biometricValue }) => {
2028
+ try {
2029
+ adapter.backend.setSecureBiometricWithLevel(
2030
+ key,
2031
+ biometricValue as string,
2032
+ resolvedBiometricLevel === BiometricLevel.None
2033
+ ? BiometricLevel.BiometryOnly
2034
+ : resolvedBiometricLevel,
2035
+ );
2036
+ cacheRawValue(
2037
+ StorageScope.Secure,
2038
+ key,
2039
+ biometricValue,
2040
+ "biometric",
2041
+ );
2042
+ } catch (error) {
2043
+ rollbackErrors.push({ label: "rollback biometric", error });
2044
+ }
2045
+ });
2046
+ }
2047
+
2048
+ const plainSets = new Map<
2049
+ AccessControl,
2050
+ { keys: string[]; values: string[] }
2051
+ >();
2052
+ const plainRemoves: string[] = [];
2053
+ snapshot.records.forEach(({ key, plainValue, biometricValue }) => {
2054
+ if (plainValue === undefined) {
2055
+ if (biometricValue === undefined) {
2056
+ plainRemoves.push(key);
2057
+ }
2058
+ return;
2059
+ }
2060
+ const accessControl = secureAccessControl ?? secureDefaultAccessControl;
2061
+ const group = plainSets.get(accessControl) ?? { keys: [], values: [] };
2062
+ group.keys.push(key);
2063
+ group.values.push(plainValue);
2064
+ plainSets.set(accessControl, group);
2065
+ });
2066
+
2067
+ plainSets.forEach((group, accessControl) => {
2068
+ try {
2069
+ if (nonMemoryScope === StorageScope.Secure) {
2070
+ adapter.backend.setSecureAccessControl(accessControl);
2071
+ }
2072
+ adapter.backend.setBatch(group.keys, group.values, config.scope);
2073
+ group.keys.forEach((key, index) => {
2074
+ cacheRawValue(
2075
+ resolveNonMemoryScope(),
2076
+ key,
2077
+ group.values[index],
2078
+ "plain",
2079
+ );
2080
+ });
2081
+ } catch (error) {
2082
+ rollbackErrors.push({ label: "rollback plain set", error });
2083
+ }
2084
+ });
2085
+
2086
+ if (plainRemoves.length > 0) {
2087
+ try {
2088
+ adapter.backend.removeBatch(plainRemoves, config.scope);
2089
+ plainRemoves.forEach((key) => {
2090
+ cacheRawValue(resolveNonMemoryScope(), key, undefined, "plain");
2091
+ });
2092
+ } catch (error) {
2093
+ rollbackErrors.push({ label: "rollback plain remove", error });
2094
+ }
2095
+ }
2096
+
2097
+ if (nonMemoryScope === StorageScope.Secure) {
2098
+ snapshot.records.forEach(({ key, biometricValue }) => {
2099
+ if (biometricValue !== undefined) {
2100
+ return;
2101
+ }
2102
+ try {
2103
+ adapter.backend.deleteSecureBiometric(key);
2104
+ cacheRawValue(StorageScope.Secure, key, undefined, "biometric");
2105
+ } catch (error) {
2106
+ rollbackErrors.push({ label: "rollback biometric remove", error });
2107
+ }
2108
+ });
2109
+ }
2110
+
2111
+ snapshot.records.forEach(({ key, pending }) => {
2112
+ if (pending === undefined || nonMemoryScope === null) {
2113
+ return;
2114
+ }
2115
+ if (nonMemoryScope === StorageScope.Disk) {
2116
+ scheduleDiskWrite(key, pending.value);
2117
+ } else if (!isBiometric) {
2118
+ scheduleSecureWrite(
2119
+ key,
2120
+ pending.value,
2121
+ pending.accessControl ??
2122
+ secureAccessControl ??
2123
+ secureDefaultAccessControl,
2124
+ );
2125
+ }
2126
+ });
2127
+
2128
+ recordsByKey.forEach(({ key }) => {
2129
+ if (nonMemoryScope !== null) {
2130
+ invalidateRawCache(nonMemoryScope, key);
2131
+ }
2132
+ });
2133
+ renamesMigrated = snapshot.renamesMigrated;
2134
+ invalidateParsedCache();
2135
+ return rollbackErrors;
2136
+ }
2137
+
2138
+ function runAtomicItemMutation(mutation: () => void): void {
2139
+ if (isMemory || renameFromKeys.length === 0 || atomicMutationDepth > 0) {
2140
+ mutation();
2141
+ return;
2142
+ }
2143
+
2144
+ const snapshot = captureItemState();
2145
+ atomicMutationDepth += 1;
2146
+ try {
2147
+ mutation();
2148
+ } catch (primaryError) {
2149
+ const rollbackErrors = restoreItemState(snapshot);
2150
+ if (rollbackErrors.length > 0) {
2151
+ throw createStorageCompositeError(
2152
+ "item mutation rollback",
2153
+ primaryError,
2154
+ rollbackErrors,
2155
+ );
2156
+ }
2157
+ throw primaryError;
2158
+ } finally {
2159
+ atomicMutationDepth -= 1;
2160
+ }
2161
+ }
2162
+
2163
+ const writeStoredRaw = (
2164
+ rawValue: string,
2165
+ options: StoredRawWriteOptions = {},
2166
+ ): PendingDiskWrite | PendingSecureWrite | undefined => {
2167
+ const cleanupRenameSources = options.cleanupRenameSources !== false;
1588
2168
  const oldValue = undefined;
1589
2169
  if (isBiometric) {
1590
- adapter.backend.setSecureBiometricWithLevel(
1591
- storageKey,
1592
- rawValue,
1593
- resolvedBiometricLevel,
1594
- );
2170
+ invalidateRawCache(StorageScope.Secure, storageKey);
2171
+ runSecurePromotion(storageKey, () => {
2172
+ try {
2173
+ adapter.backend.setSecureBiometricWithLevel(
2174
+ storageKey,
2175
+ rawValue,
2176
+ resolvedBiometricLevel,
2177
+ );
2178
+ } catch (error) {
2179
+ throw normalizeStorageError(error);
2180
+ }
2181
+ });
2182
+ if (cleanupRenameSources) {
2183
+ scheduleRenameSourceCleanup();
2184
+ }
1595
2185
  emitKeyChange(
1596
2186
  config.scope,
1597
2187
  storageKey,
@@ -1600,14 +2190,20 @@ export function createStorageCore(
1600
2190
  "set",
1601
2191
  adapter.changeSource,
1602
2192
  );
1603
- return;
2193
+ return undefined;
1604
2194
  }
1605
2195
 
2196
+ if (nonMemoryScope === StorageScope.Secure) {
2197
+ invalidateRawCache(StorageScope.Secure, storageKey);
2198
+ }
1606
2199
  cacheRawValue(resolveNonMemoryScope(), storageKey, rawValue);
1607
2200
 
1608
2201
  if (nonMemoryScope === StorageScope.Disk) {
1609
2202
  if (coalesceDiskWrites || isDiskWritesAsync()) {
1610
- scheduleDiskWrite(storageKey, rawValue);
2203
+ const pendingWrite = scheduleDiskWrite(storageKey, rawValue);
2204
+ if (cleanupRenameSources) {
2205
+ scheduleRenameSourceCleanup();
2206
+ }
1611
2207
  emitKeyChange(
1612
2208
  config.scope,
1613
2209
  storageKey,
@@ -1616,18 +2212,21 @@ export function createStorageCore(
1616
2212
  "set",
1617
2213
  adapter.changeSource,
1618
2214
  );
1619
- return;
2215
+ return pendingWrite;
1620
2216
  }
1621
2217
 
1622
2218
  clearPendingDiskWrite(storageKey);
1623
2219
  }
1624
2220
 
1625
2221
  if (coalesceSecureWrites) {
1626
- scheduleSecureWrite(
2222
+ const pendingWrite = scheduleSecureWrite(
1627
2223
  storageKey,
1628
2224
  rawValue,
1629
2225
  secureAccessControl ?? secureDefaultAccessControl,
1630
2226
  );
2227
+ if (cleanupRenameSources) {
2228
+ scheduleRenameSourceCleanup();
2229
+ }
1631
2230
  emitKeyChange(
1632
2231
  config.scope,
1633
2232
  storageKey,
@@ -1636,7 +2235,7 @@ export function createStorageCore(
1636
2235
  "set",
1637
2236
  adapter.changeSource,
1638
2237
  );
1639
- return;
2238
+ return pendingWrite;
1640
2239
  }
1641
2240
 
1642
2241
  if (nonMemoryScope === StorageScope.Secure) {
@@ -1649,6 +2248,9 @@ export function createStorageCore(
1649
2248
  }
1650
2249
 
1651
2250
  adapter.backend.set(storageKey, rawValue, config.scope);
2251
+ if (cleanupRenameSources) {
2252
+ scheduleRenameSourceCleanup();
2253
+ }
1652
2254
  emitKeyChange(
1653
2255
  config.scope,
1654
2256
  storageKey,
@@ -1657,6 +2259,7 @@ export function createStorageCore(
1657
2259
  "set",
1658
2260
  adapter.changeSource,
1659
2261
  );
2262
+ return undefined;
1660
2263
  };
1661
2264
 
1662
2265
  const migrateRenamesIfNeeded = (): void => {
@@ -1665,42 +2268,101 @@ export function createStorageCore(
1665
2268
  }
1666
2269
 
1667
2270
  try {
2271
+ flushPendingRenameSourceWrites();
1668
2272
  const hasCurrent = isBiometric
1669
2273
  ? adapter.backend.hasSecureBiometric(storageKey)
1670
2274
  : adapter.backend.has(storageKey, config.scope);
2275
+ const snapshots = readRenameSnapshots();
1671
2276
 
1672
2277
  if (hasCurrent) {
1673
- for (const legacyKey of renameFromKeys) {
1674
- if (isBiometric) {
1675
- if (adapter.backend.hasSecureBiometric(legacyKey)) {
1676
- adapter.backend.deleteSecureBiometric(legacyKey);
2278
+ try {
2279
+ snapshots.forEach(({ key }) => {
2280
+ removeRenameSource(key);
2281
+ });
2282
+ } catch (primaryError) {
2283
+ const rollbackErrors: { label: string; error: unknown }[] = [];
2284
+ snapshots.forEach((snapshot) => {
2285
+ try {
2286
+ restoreRenameSource(snapshot);
2287
+ } catch (error) {
2288
+ rollbackErrors.push({
2289
+ label: "rollback rename source",
2290
+ error,
2291
+ });
1677
2292
  }
1678
- } else if (adapter.backend.has(legacyKey, config.scope)) {
1679
- adapter.backend.remove(legacyKey, config.scope);
2293
+ });
2294
+ if (rollbackErrors.length > 0) {
2295
+ throw createStorageCompositeError(
2296
+ "rename cleanup",
2297
+ primaryError,
2298
+ rollbackErrors,
2299
+ );
1680
2300
  }
2301
+ throw primaryError;
1681
2302
  }
1682
2303
  renamesMigrated = true;
1683
2304
  return;
1684
2305
  }
1685
2306
 
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
- }
2307
+ const snapshot = snapshots.find(
2308
+ (candidate) => selectedRenameValue(candidate) !== undefined,
2309
+ );
2310
+ if (snapshot === undefined) {
2311
+ renamesMigrated = true;
2312
+ return;
2313
+ }
1693
2314
 
1694
- writeStoredRaw(legacyRaw);
1695
- if (isBiometric) {
1696
- adapter.backend.deleteSecureBiometric(legacyKey);
1697
- } else {
1698
- adapter.backend.remove(legacyKey, config.scope);
2315
+ let writeAttempted = false;
2316
+ let pendingCurrentWrite:
2317
+ PendingDiskWrite | PendingSecureWrite | undefined;
2318
+ try {
2319
+ writeAttempted = true;
2320
+ pendingCurrentWrite = writeStoredRaw(
2321
+ selectedRenameValue(snapshot) as string,
2322
+ {
2323
+ cleanupRenameSources: false,
2324
+ },
2325
+ );
2326
+ if (hasPendingCurrentWrite()) {
2327
+ flushPendingCurrentWrite();
2328
+ }
2329
+ snapshots.forEach(({ key }) => {
2330
+ removeRenameSource(key);
2331
+ });
2332
+ renamesMigrated = true;
2333
+ } catch (primaryError) {
2334
+ const rollbackErrors: { label: string; error: unknown }[] = [];
2335
+ clearPendingCurrentWriteIf(pendingCurrentWrite);
2336
+ if (writeAttempted) {
2337
+ try {
2338
+ removeCurrentBackendValue();
2339
+ } catch (error) {
2340
+ rollbackErrors.push({ label: "rollback current value", error });
2341
+ }
2342
+ }
2343
+ snapshots.forEach((renameSnapshot) => {
2344
+ try {
2345
+ restoreRenameSource(renameSnapshot);
2346
+ } catch (error) {
2347
+ rollbackErrors.push({
2348
+ label: "rollback rename source",
2349
+ error,
2350
+ });
2351
+ }
2352
+ });
2353
+ invalidateRawCache(resolveNonMemoryScope(), storageKey);
2354
+ snapshots.forEach(({ key }) => {
2355
+ invalidateRenameSourceCache(key);
2356
+ });
2357
+ if (rollbackErrors.length > 0) {
2358
+ throw createStorageCompositeError(
2359
+ "rename migration",
2360
+ primaryError,
2361
+ rollbackErrors,
2362
+ );
1699
2363
  }
1700
- break;
2364
+ throw primaryError;
1701
2365
  }
1702
-
1703
- renamesMigrated = true;
1704
2366
  } catch (error) {
1705
2367
  if (isKeychainLockedError(error)) {
1706
2368
  onReadError?.(error);
@@ -1713,8 +2375,16 @@ export function createStorageCore(
1713
2375
  const removeStoredRaw = (
1714
2376
  operation: StorageChangeOperation = "remove",
1715
2377
  ): void => {
1716
- const oldValue = getEventRawValue(config.scope, storageKey);
2378
+ const oldValue = isBiometric
2379
+ ? getEventRawValueForRepresentation(
2380
+ config.scope,
2381
+ storageKey,
2382
+ "biometric",
2383
+ )
2384
+ : getEventRawValue(config.scope, storageKey);
1717
2385
  if (isBiometric) {
2386
+ invalidateRawCache(StorageScope.Secure, storageKey);
2387
+ scheduleRenameSourceCleanup();
1718
2388
  adapter.backend.deleteSecureBiometric(storageKey);
1719
2389
  emitKeyChange(
1720
2390
  config.scope,
@@ -1727,11 +2397,15 @@ export function createStorageCore(
1727
2397
  return;
1728
2398
  }
1729
2399
 
2400
+ if (nonMemoryScope === StorageScope.Secure) {
2401
+ invalidateRawCache(StorageScope.Secure, storageKey);
2402
+ }
1730
2403
  cacheRawValue(resolveNonMemoryScope(), storageKey, undefined);
1731
2404
 
1732
2405
  if (nonMemoryScope === StorageScope.Disk) {
1733
2406
  if (coalesceDiskWrites || isDiskWritesAsync()) {
1734
2407
  scheduleDiskWrite(storageKey, undefined);
2408
+ scheduleRenameSourceCleanup();
1735
2409
  emitKeyChange(
1736
2410
  config.scope,
1737
2411
  storageKey,
@@ -1752,6 +2426,7 @@ export function createStorageCore(
1752
2426
  undefined,
1753
2427
  secureAccessControl ?? secureDefaultAccessControl,
1754
2428
  );
2429
+ scheduleRenameSourceCleanup();
1755
2430
  emitKeyChange(
1756
2431
  config.scope,
1757
2432
  storageKey,
@@ -1767,6 +2442,7 @@ export function createStorageCore(
1767
2442
  clearPendingSecureWrite(storageKey);
1768
2443
  }
1769
2444
 
2445
+ scheduleRenameSourceCleanup();
1770
2446
  adapter.backend.remove(storageKey, config.scope);
1771
2447
  emitKeyChange(
1772
2448
  config.scope,
@@ -1783,6 +2459,8 @@ export function createStorageCore(
1783
2459
  const oldValue = getEventRawValue(config.scope, storageKey);
1784
2460
  if (memoryExpiration) {
1785
2461
  memoryExpiration.set(storageKey, Date.now() + (expirationTtlMs ?? 0));
2462
+ } else {
2463
+ memoryExpirationDeadlines.delete(storageKey);
1786
2464
  }
1787
2465
  const storedValue =
1788
2466
  typeof value === "string" ? escapeCollidingRawValue(value) : value;
@@ -1852,7 +2530,9 @@ export function createStorageCore(
1852
2530
  return lastValue as T;
1853
2531
  }
1854
2532
 
1855
- removeStoredRaw("expire");
2533
+ runAtomicItemMutation(() => {
2534
+ removeStoredRaw("expire");
2535
+ });
1856
2536
  invalidateParsedCache();
1857
2537
  onExpired?.(storageKey);
1858
2538
  lastValue = ensureValidatedValue(defaultValue, false);
@@ -1896,7 +2576,9 @@ export function createStorageCore(
1896
2576
  if (isStoredEnvelope(parsed)) {
1897
2577
  envelopeExpiresAt = parsed.expiresAt;
1898
2578
  if (parsed.expiresAt <= Date.now()) {
1899
- removeStoredRaw("expire");
2579
+ runAtomicItemMutation(() => {
2580
+ removeStoredRaw("expire");
2581
+ });
1900
2582
  invalidateParsedCache();
1901
2583
  onExpired?.(storageKey);
1902
2584
  lastValue = ensureValidatedValue(defaultValue, false);
@@ -1943,18 +2625,20 @@ export function createStorageCore(
1943
2625
 
1944
2626
  const set = (valueOrFn: T | ((prev: T) => T)): void => {
1945
2627
  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
- }
2628
+ runAtomicItemMutation(() => {
2629
+ const newValue = isUpdater(valueOrFn)
2630
+ ? valueOrFn(getInternal())
2631
+ : valueOrFn;
2632
+
2633
+ if (validate && !validate(newValue)) {
2634
+ throw new Error(
2635
+ `Validation failed for key "${storageKey}" in scope "${StorageScope[config.scope]}".`,
2636
+ );
2637
+ }
1955
2638
 
1956
- invalidateParsedCache();
1957
- writeValueWithoutValidation(newValue);
2639
+ invalidateParsedCache();
2640
+ writeValueWithoutValidation(newValue);
2641
+ });
1958
2642
  });
1959
2643
  };
1960
2644
 
@@ -1963,37 +2647,44 @@ export function createStorageCore(
1963
2647
  valueOrFn: T | ((prev: T) => T),
1964
2648
  ): boolean =>
1965
2649
  measureOperation("item:setIfVersion", config.scope, () => {
1966
- const currentVersion = getCurrentVersion();
1967
- if (currentVersion !== version) {
1968
- return false;
1969
- }
1970
- set(valueOrFn);
1971
- return true;
2650
+ let didSet = false;
2651
+ runAtomicItemMutation(() => {
2652
+ const currentVersion = getCurrentVersion();
2653
+ if (currentVersion !== version) {
2654
+ return;
2655
+ }
2656
+ set(valueOrFn);
2657
+ didSet = true;
2658
+ });
2659
+ return didSet;
1972
2660
  });
1973
2661
 
1974
2662
  const deleteItem = (): void => {
1975
2663
  measureOperation("item:delete", config.scope, () => {
1976
- invalidateParsedCache();
2664
+ runAtomicItemMutation(() => {
2665
+ invalidateParsedCache();
1977
2666
 
1978
- if (isMemory) {
1979
- const oldValue = getEventRawValue(config.scope, storageKey);
1980
- if (memoryExpiration) {
1981
- memoryExpiration.delete(storageKey);
2667
+ if (isMemory) {
2668
+ const oldValue = getEventRawValue(config.scope, storageKey);
2669
+ if (memoryExpiration) {
2670
+ memoryExpiration.delete(storageKey);
2671
+ }
2672
+ memoryExpirationDeadlines.delete(storageKey);
2673
+ memoryStore.delete(storageKey);
2674
+ notifyKeyListeners(memoryListeners, storageKey);
2675
+ emitKeyChange(
2676
+ config.scope,
2677
+ storageKey,
2678
+ oldValue,
2679
+ undefined,
2680
+ "remove",
2681
+ "memory",
2682
+ );
2683
+ return;
1982
2684
  }
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
2685
 
1996
- removeStoredRaw();
2686
+ removeStoredRaw();
2687
+ });
1997
2688
  });
1998
2689
  };
1999
2690
 
@@ -2098,14 +2789,18 @@ export function createStorageCore(
2098
2789
  invalidateParsedCache();
2099
2790
  },
2100
2791
  _deleteMemoryEntry: () => {
2101
- if (memoryExpiration) {
2102
- memoryExpiration.delete(storageKey);
2103
- }
2792
+ memoryExpirationDeadlines.delete(storageKey);
2104
2793
  memoryStore.delete(storageKey);
2105
2794
  invalidateParsedCache();
2106
2795
  },
2107
2796
  _hasValidation: validate !== undefined,
2108
2797
  _hasExpiration: expiration !== undefined,
2798
+ _hasRenameFrom: renameFromKeys.length > 0,
2799
+ _renameFromKeys: renameFromKeys,
2800
+ _getRenameMigrationState: () => renamesMigrated,
2801
+ _setRenameMigrationState: (migrated) => {
2802
+ renamesMigrated = migrated;
2803
+ },
2109
2804
  _readCacheEnabled: readCache,
2110
2805
  _isBiometric: isBiometric,
2111
2806
  _biometricLevel: resolvedBiometricLevel,
@@ -2127,6 +2822,15 @@ export function createStorageCore(
2127
2822
  groupSet.add(storageItem as StorageItemInternal<unknown>);
2128
2823
  }
2129
2824
 
2825
+ if (isMemory) {
2826
+ let items = memoryItemsByKey.get(storageKey);
2827
+ if (!items) {
2828
+ items = new Set();
2829
+ memoryItemsByKey.set(storageKey, items);
2830
+ }
2831
+ items.add(storageItem as StorageItemInternal<unknown>);
2832
+ }
2833
+
2130
2834
  const registryKey = `${config.scope}:${storageKey}`;
2131
2835
  registeredKeyCounts.set(
2132
2836
  registryKey,
@@ -2165,26 +2869,23 @@ export function createStorageCore(
2165
2869
 
2166
2870
  items.forEach((item, index) => {
2167
2871
  if (scope === StorageScope.Disk) {
2168
- const pending = durability.readPendingDiskWrite(item.key);
2169
- if (pending !== undefined) {
2170
- rawValues[index] = pending;
2872
+ if (durability.hasPendingDiskWrite(item.key)) {
2873
+ rawValues[index] = durability.readPendingDiskWrite(item.key);
2171
2874
  return;
2172
2875
  }
2173
2876
  }
2174
2877
 
2175
2878
  if (scope === StorageScope.Secure) {
2176
- const pending = durability.readPendingSecureWrite(item.key);
2177
- if (pending !== undefined) {
2178
- rawValues[index] = pending;
2879
+ if (durability.hasPendingSecureWrite(item.key)) {
2880
+ rawValues[index] = durability.readPendingSecureWrite(item.key);
2179
2881
  return;
2180
2882
  }
2181
2883
  }
2182
2884
 
2183
2885
  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;
2886
+ const cachedEntry = getCachedRawValueEntry(scope, item.key);
2887
+ if (cachedEntry?.has("plain")) {
2888
+ rawValues[index] = cachedEntry.get("plain");
2188
2889
  return;
2189
2890
  }
2190
2891
  }
@@ -2293,6 +2994,10 @@ export function createStorageCore(
2293
2994
 
2294
2995
  flushSecureWrites();
2295
2996
  const keys = secureEntries.map(({ item }) => item.key);
2997
+ keys.forEach((key) => {
2998
+ invalidateRawCache(scope, key);
2999
+ });
3000
+ const serializedValues: string[] = [];
2296
3001
  const oldValues = shouldReadPreviousEventValues(scope)
2297
3002
  ? adapter.backend
2298
3003
  .getBatch(keys, scope)
@@ -2308,12 +3013,14 @@ export function createStorageCore(
2308
3013
  >();
2309
3014
 
2310
3015
  secureEntries.forEach(({ item, value, internal }) => {
3016
+ const serialized = item.serialize(value);
3017
+ serializedValues.push(serialized);
2311
3018
  const accessControl =
2312
3019
  internal._secureAccessControl ?? secureDefaultAccessControl;
2313
3020
  const existingGroup = groupedByAccessControl.get(accessControl);
2314
3021
  const group = existingGroup ?? { keys: [], values: [] };
2315
3022
  group.keys.push(item.key);
2316
- group.values.push(item.serialize(value));
3023
+ group.values.push(serialized);
2317
3024
  if (!existingGroup) {
2318
3025
  groupedByAccessControl.set(accessControl, group);
2319
3026
  }
@@ -2326,20 +3033,24 @@ export function createStorageCore(
2326
3033
  cacheRawValue(scope, key, group.values[index]);
2327
3034
  });
2328
3035
  });
3036
+ const willEmitChanges =
3037
+ storageEvents.hasListeners(scope) || eventObserver !== undefined;
2329
3038
  emitBatchChange(
2330
3039
  scope,
2331
3040
  "setBatch",
2332
3041
  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
- ),
3042
+ willEmitChanges
3043
+ ? keys.map((key, index) =>
3044
+ createKeyChange(
3045
+ scope,
3046
+ key,
3047
+ oldValues[index],
3048
+ serializedValues[index],
3049
+ "setBatch",
3050
+ adapter.changeSource,
3051
+ ),
3052
+ )
3053
+ : [],
2343
3054
  );
2344
3055
  return;
2345
3056
  }
@@ -2423,6 +3134,17 @@ export function createStorageCore(
2423
3134
  return;
2424
3135
  }
2425
3136
 
3137
+ if (
3138
+ items.some(
3139
+ (item) => asInternal(item as StorageItem<unknown>)._hasRenameFrom,
3140
+ )
3141
+ ) {
3142
+ items.forEach((item) => {
3143
+ asInternal(item as StorageItem<unknown>).delete();
3144
+ });
3145
+ return;
3146
+ }
3147
+
2426
3148
  const keys = items.map((item) => item.key);
2427
3149
  if (scope === StorageScope.Disk) {
2428
3150
  flushDiskWrites();
@@ -2431,14 +3153,46 @@ export function createStorageCore(
2431
3153
  flushSecureWrites();
2432
3154
  }
2433
3155
  const oldValues = shouldReadPreviousEventValues(scope)
2434
- ? adapter.backend
2435
- .getBatch(keys, scope)
2436
- .map((value) =>
2437
- value === undefined
2438
- ? undefined
2439
- : unescapeCollidingRawValue(value),
2440
- )
3156
+ ? (() => {
3157
+ const plainItems = items.filter(
3158
+ (item) =>
3159
+ asInternal(item as StorageItem<unknown>)._isBiometric !==
3160
+ true,
3161
+ );
3162
+ const plainValues =
3163
+ plainItems.length === 0
3164
+ ? []
3165
+ : adapter.backend
3166
+ .getBatch(
3167
+ plainItems.map((item) => item.key),
3168
+ scope,
3169
+ )
3170
+ .map((value) =>
3171
+ value === undefined
3172
+ ? undefined
3173
+ : unescapeCollidingRawValue(value),
3174
+ );
3175
+ let plainIndex = 0;
3176
+ return items.map((item) => {
3177
+ const internal = asInternal(item as StorageItem<unknown>);
3178
+ if (internal._isBiometric === true) {
3179
+ return getEventRawValueForRepresentation(
3180
+ scope,
3181
+ item.key,
3182
+ "biometric",
3183
+ );
3184
+ }
3185
+ const value = plainValues[plainIndex];
3186
+ plainIndex += 1;
3187
+ return value;
3188
+ });
3189
+ })()
2441
3190
  : [];
3191
+ if (scope === StorageScope.Secure) {
3192
+ keys.forEach((key) => {
3193
+ invalidateRawCache(scope, key);
3194
+ });
3195
+ }
2442
3196
  adapter.backend.removeBatch(keys, scope);
2443
3197
  keys.forEach((key) => {
2444
3198
  cacheRawValue(scope, key, undefined);
@@ -2515,46 +3269,118 @@ export function createStorageCore(
2515
3269
  }
2516
3270
 
2517
3271
  const NOT_SET = Symbol();
2518
- const rollback = new Map<string, RollbackRecord>();
3272
+ type TransactionRollbackEntry = {
3273
+ key: string;
3274
+ representation: StorageRawCacheRepresentation;
3275
+ record: RollbackRecord;
3276
+ };
3277
+ const rollback = new Map<string, TransactionRollbackEntry>();
3278
+ const itemRenameStates = new Map<StorageItemInternal<unknown>, boolean>();
2519
3279
 
2520
3280
  const rememberRollback = (
2521
3281
  key: string,
2522
3282
  item?: Pick<StorageItem<unknown>, "key" | "scope">,
3283
+ includeOtherSecureRepresentation = false,
2523
3284
  ) => {
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,
3285
+ const internal = item
3286
+ ? (item as StorageItemInternal<unknown>)
3287
+ : undefined;
3288
+ const representation: StorageRawCacheRepresentation =
3289
+ scope === StorageScope.Secure && internal?._isBiometric === true
3290
+ ? "biometric"
3291
+ : "plain";
3292
+ const rememberRepresentation = (
3293
+ representationToRemember: StorageRawCacheRepresentation,
3294
+ ) => {
3295
+ const identity = JSON.stringify([
3296
+ scope,
3297
+ key,
3298
+ representationToRemember,
3299
+ ]);
3300
+ if (rollback.has(identity)) {
3301
+ return;
3302
+ }
3303
+ if (scope === StorageScope.Memory) {
3304
+ const expiresAt = memoryExpirationDeadlines.get(key);
3305
+ rollback.set(identity, {
3306
+ key,
3307
+ representation: representationToRemember,
3308
+ record: {
3309
+ kind: "memory",
3310
+ value: memoryStore.has(key) ? memoryStore.get(key) : NOT_SET,
3311
+ ...(expiresAt === undefined ? {} : { expiresAt }),
3312
+ },
2544
3313
  });
2545
3314
  return;
2546
3315
  }
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
- : {}),
3316
+ if (representationToRemember === "biometric") {
3317
+ rollback.set(identity, {
3318
+ key,
3319
+ representation: representationToRemember,
3320
+ record: {
3321
+ kind: "biometric",
3322
+ value: getStoredRawValueForRepresentation(
3323
+ key,
3324
+ scope,
3325
+ representationToRemember,
3326
+ ),
3327
+ level:
3328
+ internal?._biometricLevel !== undefined &&
3329
+ internal._biometricLevel !== BiometricLevel.None
3330
+ ? internal._biometricLevel
3331
+ : BiometricLevel.BiometryOnly,
3332
+ },
3333
+ });
3334
+ return;
3335
+ }
3336
+ rollback.set(identity, {
3337
+ key,
3338
+ representation: representationToRemember,
3339
+ record: {
3340
+ kind: "raw",
3341
+ value: getStoredRawValueForRepresentation(
3342
+ key,
3343
+ scope,
3344
+ representationToRemember,
3345
+ ),
3346
+ ...(scope === StorageScope.Secure &&
3347
+ internal?._secureAccessControl !== undefined
3348
+ ? { accessControl: internal._secureAccessControl }
3349
+ : {}),
3350
+ },
2554
3351
  });
3352
+ };
3353
+
3354
+ if (
3355
+ representation === "biometric" ||
3356
+ includeOtherSecureRepresentation
3357
+ ) {
3358
+ rememberRepresentation("plain");
3359
+ }
3360
+ rememberRepresentation(representation);
3361
+ if (includeOtherSecureRepresentation) {
3362
+ rememberRepresentation("biometric");
2555
3363
  }
2556
3364
  };
2557
3365
 
3366
+ const rememberItemRollback = (
3367
+ item: Pick<StorageItem<unknown>, "key" | "scope">,
3368
+ ): void => {
3369
+ const internal = item as StorageItemInternal<unknown>;
3370
+ const includeOtherSecureRepresentation =
3371
+ scope === StorageScope.Secure &&
3372
+ (internal._isBiometric === true || internal._hasRenameFrom === true);
3373
+ rememberRollback(item.key, item, includeOtherSecureRepresentation);
3374
+ if (internal._getRenameMigrationState) {
3375
+ if (!itemRenameStates.has(internal)) {
3376
+ itemRenameStates.set(internal, internal._getRenameMigrationState());
3377
+ }
3378
+ }
3379
+ (internal._renameFromKeys ?? EMPTY_KEYS).forEach((aliasKey) => {
3380
+ rememberRollback(aliasKey, item, true);
3381
+ });
3382
+ };
3383
+
2558
3384
  const tx: TransactionContext = {
2559
3385
  scope,
2560
3386
  getRaw: (key) => getRawValue(key, scope),
@@ -2563,21 +3389,22 @@ export function createStorageCore(
2563
3389
  setRawValue(key, value, scope);
2564
3390
  },
2565
3391
  removeRaw: (key) => {
2566
- rememberRollback(key);
3392
+ rememberRollback(key, undefined, scope === StorageScope.Secure);
2567
3393
  removeRawValue(key, scope);
2568
3394
  },
2569
3395
  getItem: (item) => {
2570
3396
  assertBatchScope([item], scope);
3397
+ rememberItemRollback(item);
2571
3398
  return item.get();
2572
3399
  },
2573
3400
  setItem: (item, value) => {
2574
3401
  assertBatchScope([item], scope);
2575
- rememberRollback(item.key, item);
3402
+ rememberItemRollback(item);
2576
3403
  item.set(value);
2577
3404
  },
2578
3405
  removeItem: (item) => {
2579
3406
  assertBatchScope([item], scope);
2580
- rememberRollback(item.key, item);
3407
+ rememberItemRollback(item);
2581
3408
  item.delete();
2582
3409
  },
2583
3410
  };
@@ -2585,21 +3412,56 @@ export function createStorageCore(
2585
3412
  try {
2586
3413
  return transaction(tx);
2587
3414
  } catch (error) {
2588
- const rollbackEntries = Array.from(rollback.entries()).reverse();
3415
+ itemRenameStates.forEach((migrated, item) => {
3416
+ item._setRenameMigrationState(migrated);
3417
+ });
3418
+ const rollbackEntries = Array.from(rollback.values()).reverse();
2589
3419
  const rollbackSource =
2590
3420
  scope === StorageScope.Memory ? "memory" : adapter.changeSource;
2591
- const preRollbackValues =
2592
- rollbackEntries.length > 0
2593
- ? rollbackEntries.map(([key]) => getEventRawValue(scope, key))
2594
- : [];
3421
+ const rollbackErrors: { label: string; error: unknown }[] = [];
3422
+ const readRollbackEventValue = (
3423
+ entry: TransactionRollbackEntry,
3424
+ ): string | undefined => {
3425
+ try {
3426
+ return getEventRawValueForRepresentation(
3427
+ scope,
3428
+ entry.key,
3429
+ entry.representation,
3430
+ );
3431
+ } catch (readError) {
3432
+ rollbackErrors.push({
3433
+ label: `rollback read ${entry.representation}:${entry.key}`,
3434
+ error: readError,
3435
+ });
3436
+ return undefined;
3437
+ }
3438
+ };
3439
+ const preRollbackValues = rollbackEntries.map(readRollbackEventValue);
2595
3440
  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);
3441
+ rollbackEntries.forEach((entry) => {
3442
+ try {
3443
+ const record = entry.record;
3444
+ if (record.kind !== "memory") {
3445
+ return;
3446
+ }
3447
+ if (record.value === NOT_SET) {
3448
+ memoryStore.delete(entry.key);
3449
+ } else {
3450
+ memoryStore.set(entry.key, record.value);
3451
+ }
3452
+ if (record.expiresAt === undefined) {
3453
+ memoryExpirationDeadlines.delete(entry.key);
3454
+ } else {
3455
+ memoryExpirationDeadlines.set(entry.key, record.expiresAt);
3456
+ }
3457
+ invalidateMemoryItemCaches(entry.key);
3458
+ notifyKeyListeners(memoryListeners, entry.key);
3459
+ } catch (rollbackError) {
3460
+ rollbackErrors.push({
3461
+ label: `rollback ${entry.representation}:${entry.key}`,
3462
+ error: rollbackError,
3463
+ });
2601
3464
  }
2602
- notifyKeyListeners(memoryListeners, key);
2603
3465
  });
2604
3466
  } else {
2605
3467
  const groupedKeysToSet = new Map<
@@ -2607,18 +3469,14 @@ export function createStorageCore(
2607
3469
  { keys: string[]; values: string[] }
2608
3470
  >();
2609
3471
  const keysToRemove: string[] = [];
3472
+ const biometricEntries: TransactionRollbackEntry[] = [];
3473
+ const biometricValues = new Map<string, string | undefined>();
2610
3474
 
2611
- rollbackEntries.forEach(([key, record]) => {
3475
+ rollbackEntries.forEach((entry) => {
3476
+ const { key, record } = entry;
2612
3477
  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
- }
3478
+ biometricEntries.push(entry);
3479
+ biometricValues.set(key, record.value);
2622
3480
  return;
2623
3481
  }
2624
3482
  if (record.kind !== "raw") {
@@ -2640,26 +3498,115 @@ export function createStorageCore(
2640
3498
  });
2641
3499
 
2642
3500
  if (scope === StorageScope.Disk) {
2643
- flushDiskWrites();
3501
+ try {
3502
+ flushDiskWrites();
3503
+ } catch (flushError) {
3504
+ rollbackErrors.push({
3505
+ label: "rollback flush disk",
3506
+ error: flushError,
3507
+ });
3508
+ }
2644
3509
  }
2645
3510
  if (scope === StorageScope.Secure) {
2646
- flushSecureWrites();
3511
+ try {
3512
+ flushSecureWrites();
3513
+ } catch (flushError) {
3514
+ rollbackErrors.push({
3515
+ label: "rollback flush secure",
3516
+ error: flushError,
3517
+ });
3518
+ }
3519
+ }
3520
+ if (scope === StorageScope.Secure) {
3521
+ rollbackEntries.forEach((entry) => {
3522
+ invalidateRawCache(scope, entry.key);
3523
+ });
2647
3524
  }
3525
+ biometricEntries.forEach((entry) => {
3526
+ const { key, record } = entry;
3527
+ if (record.kind !== "biometric" || record.value === undefined) {
3528
+ return;
3529
+ }
3530
+ try {
3531
+ adapter.backend.setSecureBiometricWithLevel(
3532
+ key,
3533
+ record.value,
3534
+ record.level,
3535
+ );
3536
+ cacheRawValue(
3537
+ StorageScope.Secure,
3538
+ key,
3539
+ record.value,
3540
+ "biometric",
3541
+ );
3542
+ } catch (rollbackError) {
3543
+ rollbackErrors.push({
3544
+ label: "rollback biometric",
3545
+ error: rollbackError,
3546
+ });
3547
+ }
3548
+ });
2648
3549
  groupedKeysToSet.forEach((group, accessControl) => {
2649
- if (scope === StorageScope.Secure) {
2650
- adapter.backend.setSecureAccessControl(accessControl);
3550
+ try {
3551
+ if (scope === StorageScope.Secure) {
3552
+ adapter.backend.setSecureAccessControl(accessControl);
3553
+ }
3554
+ adapter.backend.setBatch(group.keys, group.values, scope);
3555
+ group.keys.forEach((key, index) => {
3556
+ cacheRawValue(scope, key, group.values[index], "plain");
3557
+ });
3558
+ } catch (rollbackError) {
3559
+ rollbackErrors.push({
3560
+ label: "rollback plain setBatch",
3561
+ error: rollbackError,
3562
+ });
2651
3563
  }
2652
- adapter.backend.setBatch(group.keys, group.values, scope);
2653
- group.keys.forEach((key, index) => {
2654
- cacheRawValue(scope, key, group.values[index]);
2655
- });
2656
3564
  });
2657
3565
  if (keysToRemove.length > 0) {
2658
- adapter.backend.removeBatch(keysToRemove, scope);
2659
- keysToRemove.forEach((key) => {
2660
- cacheRawValue(scope, key, undefined);
2661
- });
3566
+ const keysToRemoveWithoutBiometric = keysToRemove.filter(
3567
+ (key) => biometricValues.get(key) === undefined,
3568
+ );
3569
+ try {
3570
+ if (keysToRemoveWithoutBiometric.length > 0) {
3571
+ adapter.backend.removeBatch(
3572
+ keysToRemoveWithoutBiometric,
3573
+ scope,
3574
+ );
3575
+ }
3576
+ keysToRemove.forEach((key) => {
3577
+ cacheRawValue(scope, key, undefined, "plain");
3578
+ });
3579
+ } catch (rollbackError) {
3580
+ rollbackErrors.push({
3581
+ label: "rollback plain removeBatch",
3582
+ error: rollbackError,
3583
+ });
3584
+ }
2662
3585
  }
3586
+ biometricEntries.forEach((entry) => {
3587
+ const { key, record } = entry;
3588
+ if (record.kind !== "biometric") {
3589
+ return;
3590
+ }
3591
+ if (record.value !== undefined) {
3592
+ return;
3593
+ }
3594
+ invalidateRawCache(StorageScope.Secure, key);
3595
+ try {
3596
+ adapter.backend.deleteSecureBiometric(key);
3597
+ cacheRawValue(
3598
+ StorageScope.Secure,
3599
+ key,
3600
+ record.value,
3601
+ "biometric",
3602
+ );
3603
+ } catch (rollbackError) {
3604
+ rollbackErrors.push({
3605
+ label: "rollback biometric",
3606
+ error: rollbackError,
3607
+ });
3608
+ }
3609
+ });
2663
3610
  }
2664
3611
 
2665
3612
  if (rollbackEntries.length > 0) {
@@ -2667,18 +3614,25 @@ export function createStorageCore(
2667
3614
  scope,
2668
3615
  "rollback",
2669
3616
  rollbackSource,
2670
- rollbackEntries.map(([key], index) =>
3617
+ rollbackEntries.map((entry, index) =>
2671
3618
  createKeyChange(
2672
3619
  scope,
2673
- key,
3620
+ entry.key,
2674
3621
  preRollbackValues[index],
2675
- getEventRawValue(scope, key),
3622
+ readRollbackEventValue(entry),
2676
3623
  "rollback",
2677
3624
  rollbackSource,
2678
3625
  ),
2679
3626
  ),
2680
3627
  );
2681
3628
  }
3629
+ if (rollbackErrors.length > 0) {
3630
+ throw createStorageCompositeError(
3631
+ "transaction rollback",
3632
+ error,
3633
+ rollbackErrors,
3634
+ );
3635
+ }
2682
3636
  throw error;
2683
3637
  }
2684
3638
  });
@@ -2753,14 +3707,14 @@ export function createStorageCore(
2753
3707
  config: SetItemConfig<TMember>,
2754
3708
  ): SetStorageItem<TMember> {
2755
3709
  const { defaultValue, ...rest } = config;
2756
- const initial: Partial<Record<TMember, true>> = {};
3710
+ const initial: Record<string, true> = {};
2757
3711
  if (defaultValue) {
2758
3712
  for (const id of defaultValue) {
2759
3713
  initial[id] = true;
2760
3714
  }
2761
3715
  }
2762
3716
 
2763
- const item = createStorageItem<Partial<Record<TMember, true>>>({
3717
+ const item = createStorageItem<Record<string, true>>({
2764
3718
  ...rest,
2765
3719
  defaultValue: initial,
2766
3720
  });
@@ -2796,8 +3750,17 @@ export function createStorageCore(
2796
3750
  return true;
2797
3751
  };
2798
3752
 
3753
+ const getTyped = (): Partial<Record<TMember, true>> => {
3754
+ const typed: Partial<Record<TMember, true>> = {};
3755
+ for (const id of Object.keys(item.get())) {
3756
+ typed[id as TMember] = true;
3757
+ }
3758
+ return typed;
3759
+ };
3760
+
2799
3761
  return {
2800
3762
  get: item.get,
3763
+ getTyped,
2801
3764
  has,
2802
3765
  add,
2803
3766
  delete: deleteId,