react-native-nitro-storage 0.8.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 (78) hide show
  1. package/CHANGELOG.md +107 -25
  2. package/README.md +140 -27
  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 +109 -293
  6. package/cpp/bindings/HybridStorage.hpp +17 -10
  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 +859 -145
  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 +860 -146
  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 +12 -3
  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/nitrogen/generated/shared/c++/HybridStorageSpec.hpp +1 -1
  68. package/package.json +3 -3
  69. package/react-native-nitro-storage.podspec +3 -1
  70. package/src/capabilities.ts +3 -1
  71. package/src/core/durability.ts +139 -39
  72. package/src/index.ts +20 -10
  73. package/src/index.web.ts +328 -166
  74. package/src/internal.ts +0 -14
  75. package/src/shared.ts +85 -0
  76. package/src/storage-core.ts +1165 -199
  77. package/src/storage-runtime.ts +6 -0
  78. package/src/testing.ts +8 -1
@@ -3,7 +3,7 @@
3
3
  import { createDurabilityCoordinator } from "./core/durability";
4
4
  import { createMetricsRegistry } from "./core/metrics";
5
5
  import { MIGRATION_VERSION_KEY, isStoredEnvelope, assertBatchScope, assertValidScope, toVersionToken, prefixKey, isNamespaced, escapeCollidingRawValue, unescapeCollidingRawValue } from "./internal";
6
- import { assertAccessControlLevel, assertBiometricLevel, canUseRawBatchPath, canUseSecureRawBatchPath, createKeyChange, defaultDeserialize, defaultSerialize, isKeychainLockedError, isUpdater, notifyAllListeners, notifyKeyListeners, redactSecureKeyChange, typedKeys } from "./shared";
6
+ import { assertAccessControlLevel, assertBiometricLevel, canUseRawBatchPath, canUseSecureRawBatchPath, createStorageCompositeError, createKeyChange, defaultDeserialize, defaultSerialize, isKeychainLockedError, isUpdater, normalizeStorageError, notifyAllListeners, notifyKeyListeners, redactSecureKeyChange, typedKeys } from "./shared";
7
7
  import { StorageEventRegistry } from "./storage-events";
8
8
  import { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
9
9
  const EMPTY_KEYS = Object.freeze([]);
@@ -15,6 +15,8 @@ export function createStorageCore(buildAdapter) {
15
15
  const itemGroups = new Map();
16
16
  const registeredKeyCounts = new Map();
17
17
  const memoryStore = new Map();
18
+ const memoryExpirationDeadlines = new Map();
19
+ const memoryItemsByKey = new Map();
18
20
  const memoryListeners = new Map();
19
21
  const scopedListeners = {
20
22
  [StorageScope.Disk]: new Map(),
@@ -49,15 +51,33 @@ export function createStorageCore(buildAdapter) {
49
51
  function getScopeRawCache(scope) {
50
52
  return scopedRawCache[scope];
51
53
  }
52
- function cacheRawValue(scope, key, value) {
53
- getScopeRawCache(scope).set(key, value);
54
+ function getCachedRawValueEntry(scope, key, create = false) {
55
+ const scopeCache = getScopeRawCache(scope);
56
+ const existing = scopeCache.get(key);
57
+ if (existing || !create) {
58
+ return existing;
59
+ }
60
+ const entry = new Map();
61
+ scopeCache.set(key, entry);
62
+ return entry;
63
+ }
64
+ function cacheRawValue(scope, key, value, representation = "plain") {
65
+ getCachedRawValueEntry(scope, key, true)?.set(representation, value);
66
+ }
67
+ function readCachedRawValue(scope, key, representation = "plain") {
68
+ return getCachedRawValueEntry(scope, key)?.get(representation);
54
69
  }
55
- function readCachedRawValue(scope, key) {
56
- return getScopeRawCache(scope).get(key);
70
+ function invalidateRawCache(scope, key) {
71
+ getScopeRawCache(scope).delete(key);
57
72
  }
58
73
  function clearScopeRawCache(scope) {
59
74
  getScopeRawCache(scope).clear();
60
75
  }
76
+ function invalidateMemoryItemCaches(key) {
77
+ memoryItemsByKey.get(key)?.forEach(item => {
78
+ item._invalidateParsedCacheOnly();
79
+ });
80
+ }
61
81
  function addKeyListener(registry, key, listener) {
62
82
  let listeners = registry.get(key);
63
83
  if (!listeners) {
@@ -83,6 +103,13 @@ export function createStorageCore(buildAdapter) {
83
103
  }
84
104
  return getRawValue(key, scope);
85
105
  }
106
+ function getEventRawValueForRepresentation(scope, key, representation) {
107
+ if (representation === "plain") {
108
+ return getEventRawValue(scope, key);
109
+ }
110
+ const raw = adapter.backend.getSecureBiometric(key);
111
+ return raw === undefined ? undefined : unescapeCollidingRawValue(raw);
112
+ }
86
113
  function shouldReadPreviousEventValues(scope) {
87
114
  if (storageEvents.hasListeners(scope)) {
88
115
  return true;
@@ -155,11 +182,14 @@ export function createStorageCore(buildAdapter) {
155
182
  function flushSecureWrites() {
156
183
  durability.flushSecureWrites();
157
184
  }
185
+ function runSecurePromotion(key, promotion) {
186
+ return durability.runSecurePromotion(key, promotion);
187
+ }
158
188
  function scheduleDiskWrite(key, value) {
159
- durability.scheduleDiskWrite(key, value);
189
+ return durability.scheduleDiskWrite(key, value);
160
190
  }
161
191
  function scheduleSecureWrite(key, value, accessControl) {
162
- durability.scheduleSecureWrite(key, value, accessControl);
192
+ return durability.scheduleSecureWrite(key, value, accessControl);
163
193
  }
164
194
  function setDiskWritesAsyncMode(enabled) {
165
195
  durability.setDiskWritesAsync(enabled);
@@ -180,6 +210,12 @@ export function createStorageCore(buildAdapter) {
180
210
  }
181
211
  return adapter.backend.get(key, scope);
182
212
  }
213
+ function getStoredRawValueForRepresentation(key, scope, representation) {
214
+ if (representation === "plain") {
215
+ return getStoredRawValue(key, scope);
216
+ }
217
+ return adapter.backend.getSecureBiometric(key);
218
+ }
183
219
  function getRawValue(key, scope) {
184
220
  assertValidScope(scope);
185
221
  if (scope === StorageScope.Memory) {
@@ -218,6 +254,7 @@ export function createStorageCore(buildAdapter) {
218
254
  clearPendingDiskWrite(key);
219
255
  }
220
256
  if (scope === StorageScope.Secure) {
257
+ invalidateRawCache(scope, key);
221
258
  flushSecureWrites();
222
259
  clearPendingSecureWrite(key);
223
260
  if (adapter.applyAccessControlOnSecureRawWrite) {
@@ -248,6 +285,7 @@ export function createStorageCore(buildAdapter) {
248
285
  clearPendingDiskWrite(key);
249
286
  }
250
287
  if (scope === StorageScope.Secure) {
288
+ invalidateRawCache(scope, key);
251
289
  flushSecureWrites();
252
290
  clearPendingSecureWrite(key);
253
291
  }
@@ -267,6 +305,7 @@ export function createStorageCore(buildAdapter) {
267
305
  getScopedListeners,
268
306
  cacheRawValue,
269
307
  readCachedRawValue,
308
+ invalidateRawCache,
270
309
  clearScopeRawCache,
271
310
  clearPendingDiskWrite,
272
311
  clearPendingSecureWrite,
@@ -320,6 +359,11 @@ export function createStorageCore(buildAdapter) {
320
359
  return;
321
360
  }
322
361
  const previousValues = shouldReadPreviousEventValues(scope) ? adapter.backend.getBatch(removeKeys, scope).map(value => value === undefined ? undefined : unescapeCollidingRawValue(value)) : [];
362
+ if (scope === StorageScope.Secure) {
363
+ removeKeys.forEach(key => {
364
+ invalidateRawCache(scope, key);
365
+ });
366
+ }
323
367
  adapter.backend.removeBatch(removeKeys, scope);
324
368
  removeKeys.forEach(key => {
325
369
  cacheRawValue(scope, key, undefined);
@@ -387,6 +431,7 @@ export function createStorageCore(buildAdapter) {
387
431
  const previousValues = shouldReadPreviousEventValues(scope) ? storage.getAll(scope) : {};
388
432
  if (scope === StorageScope.Memory) {
389
433
  memoryStore.clear();
434
+ memoryExpirationDeadlines.clear();
390
435
  notifyAllListeners(memoryListeners);
391
436
  emitBatchChange(scope, "clear", "memory", Object.keys(previousValues).map(key => createKeyChange(scope, key, previousValues[key], undefined, "clear", "memory")));
392
437
  return;
@@ -490,6 +535,7 @@ export function createStorageCore(buildAdapter) {
490
535
  }
491
536
  affectedKeys.forEach(key => {
492
537
  memoryStore.delete(key);
538
+ memoryExpirationDeadlines.delete(key);
493
539
  });
494
540
  affectedKeys.forEach(key => {
495
541
  notifyKeyListeners(memoryListeners, key);
@@ -520,7 +566,21 @@ export function createStorageCore(buildAdapter) {
520
566
  },
521
567
  clearBiometric: () => {
522
568
  measureOperation("storage:clearBiometric", StorageScope.Secure, () => {
523
- adapter.backend.clearSecureBiometric();
569
+ flushSecureWrites();
570
+ const readEventValues = shouldReadPreviousEventValues(StorageScope.Secure);
571
+ const shouldEmitChanges = storageEvents.hasListeners(StorageScope.Secure) || eventObserver !== undefined;
572
+ const biometricKeys = shouldEmitChanges ? adapter.backend.getAllKeys(StorageScope.Secure).filter(key => adapter.backend.hasSecureBiometric(key)) : [];
573
+ const previousValues = readEventValues ? biometricKeys.map(key => {
574
+ const raw = adapter.backend.getSecureBiometric(key);
575
+ return raw === undefined ? undefined : unescapeCollidingRawValue(raw);
576
+ }) : [];
577
+ clearScopeRawCache(StorageScope.Secure);
578
+ try {
579
+ adapter.backend.clearSecureBiometric();
580
+ } finally {
581
+ clearScopeRawCache(StorageScope.Secure);
582
+ }
583
+ emitBatchChange(StorageScope.Secure, "clear", adapter.changeSource, biometricKeys.map((key, index) => createKeyChange(StorageScope.Secure, key, readEventValues ? previousValues[index] : undefined, undefined, "clear", adapter.changeSource)));
524
584
  });
525
585
  },
526
586
  has: (key, scope) => {
@@ -745,6 +805,9 @@ export function createStorageCore(buildAdapter) {
745
805
  if (scope === StorageScope.Secure) {
746
806
  flushSecureWrites();
747
807
  adapter.backend.setSecureAccessControl(secureDefaultAccessControl);
808
+ keys.forEach(key => {
809
+ invalidateRawCache(scope, key);
810
+ });
748
811
  }
749
812
  if (scope === StorageScope.Disk) {
750
813
  flushDiskWrites();
@@ -770,8 +833,9 @@ export function createStorageCore(buildAdapter) {
770
833
  const expiration = config.expiration;
771
834
  const onExpired = config.onExpired;
772
835
  const expirationTtlMs = expiration?.ttlMs;
773
- const memoryExpiration = expiration && isMemory ? new Map() : null;
836
+ const memoryExpiration = expiration && isMemory ? memoryExpirationDeadlines : null;
774
837
  const readCache = !isMemory && config.readCache === true;
838
+ const rawCacheRepresentation = isBiometric ? "biometric" : "plain";
775
839
  const coalesceDiskWrites = config.scope === StorageScope.Disk && config.coalesceDiskWrites === true;
776
840
  const coalesceSecureWrites = config.scope === StorageScope.Secure && config.coalesceSecureWrites === true && !isBiometric;
777
841
  const defaultValue = config.defaultValue;
@@ -841,28 +905,40 @@ export function createStorageCore(buildAdapter) {
841
905
  const memoryStored = memoryStore.get(storageKey);
842
906
  return typeof memoryStored === "string" ? unescapeCollidingRawValue(memoryStored) : memoryStored;
843
907
  }
908
+ if (nonMemoryScope === StorageScope.Disk) {
909
+ if (durability.hasPendingDiskWrite(storageKey)) {
910
+ return durability.readPendingDiskWrite(storageKey);
911
+ }
912
+ }
913
+ if (nonMemoryScope === StorageScope.Secure && !isBiometric) {
914
+ if (durability.hasPendingSecureWrite(storageKey)) {
915
+ return durability.readPendingSecureWrite(storageKey);
916
+ }
917
+ }
844
918
  migrateRenamesIfNeeded();
845
919
  if (nonMemoryScope === StorageScope.Disk) {
846
- const pending = durability.readPendingDiskWrite(storageKey);
847
- if (pending !== undefined) {
848
- return pending;
920
+ if (durability.hasPendingDiskWrite(storageKey)) {
921
+ return durability.readPendingDiskWrite(storageKey);
849
922
  }
850
923
  }
851
924
  if (nonMemoryScope === StorageScope.Secure && !isBiometric) {
852
- const pending = durability.readPendingSecureWrite(storageKey);
853
- if (pending !== undefined) {
854
- return pending;
925
+ if (durability.hasPendingSecureWrite(storageKey)) {
926
+ return durability.readPendingSecureWrite(storageKey);
855
927
  }
856
928
  }
857
929
  if (readCache) {
858
- const cache = getScopeRawCache(resolveNonMemoryScope());
859
- const cached = cache.get(storageKey);
860
- if (cached !== undefined || cache.has(storageKey)) {
861
- return cached;
930
+ const scope = resolveNonMemoryScope();
931
+ const cachedEntry = getCachedRawValueEntry(scope, storageKey);
932
+ if (cachedEntry?.has(rawCacheRepresentation)) {
933
+ return cachedEntry.get(rawCacheRepresentation);
862
934
  }
863
935
  }
864
936
  if (isBiometric) {
865
- return readBackendRaw(() => adapter.backend.getSecureBiometric(storageKey));
937
+ const raw = readBackendRaw(() => adapter.backend.getSecureBiometric(storageKey));
938
+ if (readCache) {
939
+ cacheRawValue(resolveNonMemoryScope(), storageKey, raw, rawCacheRepresentation);
940
+ }
941
+ return raw;
866
942
  }
867
943
  const raw = readBackendRaw(() => adapter.backend.get(storageKey, config.scope));
868
944
  cacheRawValue(resolveNonMemoryScope(), storageKey, raw);
@@ -874,7 +950,8 @@ export function createStorageCore(buildAdapter) {
874
950
  } catch (error) {
875
951
  onReadError?.(error);
876
952
  if (fallbackToCacheOnReadError) {
877
- const cached = getScopeRawCache(resolveNonMemoryScope()).get(storageKey);
953
+ const scope = resolveNonMemoryScope();
954
+ const cached = readCachedRawValue(scope, storageKey, rawCacheRepresentation);
878
955
  if (cached !== undefined) {
879
956
  return cached;
880
957
  }
@@ -883,26 +960,375 @@ export function createStorageCore(buildAdapter) {
883
960
  throw error;
884
961
  }
885
962
  };
886
- const writeStoredRaw = rawValue => {
963
+ function getRenameSourceKeys() {
964
+ return renameFromKeys.filter(key => key !== storageKey);
965
+ }
966
+ function getRenameSourceRaw(key) {
967
+ return isBiometric ? adapter.backend.getSecureBiometric(key) : adapter.backend.get(key, config.scope);
968
+ }
969
+ function invalidateRenameSourceCache(key) {
970
+ if (nonMemoryScope !== null) {
971
+ invalidateRawCache(nonMemoryScope, key);
972
+ }
973
+ }
974
+ function removeRenameSource(key) {
975
+ invalidateRenameSourceCache(key);
976
+ if (isBiometric) {
977
+ adapter.backend.deleteSecureBiometric(key);
978
+ invalidateRenameSourceCache(key);
979
+ return;
980
+ }
981
+ adapter.backend.remove(key, config.scope);
982
+ invalidateRenameSourceCache(key);
983
+ }
984
+ function restoreRenameSource(snapshot) {
985
+ invalidateRenameSourceCache(snapshot.key);
986
+ if (nonMemoryScope === StorageScope.Secure) {
987
+ if (snapshot.biometricValue !== undefined) {
988
+ adapter.backend.setSecureBiometricWithLevel(snapshot.key, snapshot.biometricValue, resolvedBiometricLevel === BiometricLevel.None ? BiometricLevel.BiometryOnly : resolvedBiometricLevel);
989
+ } else {
990
+ adapter.backend.deleteSecureBiometric(snapshot.key);
991
+ }
992
+ if (snapshot.plainValue !== undefined) {
993
+ if (adapter.applyAccessControlOnSecureRawWrite) {
994
+ adapter.backend.setSecureAccessControl(secureAccessControl ?? secureDefaultAccessControl);
995
+ }
996
+ adapter.backend.set(snapshot.key, snapshot.plainValue, config.scope);
997
+ } else if (snapshot.biometricValue === undefined) {
998
+ adapter.backend.remove(snapshot.key, config.scope);
999
+ }
1000
+ invalidateRenameSourceCache(snapshot.key);
1001
+ return;
1002
+ }
1003
+ if (snapshot.plainValue !== undefined) {
1004
+ adapter.backend.set(snapshot.key, snapshot.plainValue, config.scope);
1005
+ } else {
1006
+ adapter.backend.remove(snapshot.key, config.scope);
1007
+ }
1008
+ invalidateRenameSourceCache(snapshot.key);
1009
+ }
1010
+ function readRenameSnapshots() {
1011
+ return getRenameSourceKeys().flatMap(key => {
1012
+ const plainValue = nonMemoryScope === StorageScope.Secure ? adapter.backend.get(key, config.scope) : getRenameSourceRaw(key);
1013
+ const biometricValue = nonMemoryScope === StorageScope.Secure ? adapter.backend.getSecureBiometric(key) : undefined;
1014
+ if (plainValue === undefined && biometricValue === undefined) {
1015
+ return [];
1016
+ }
1017
+ return [{
1018
+ key,
1019
+ ...(plainValue === undefined ? {} : {
1020
+ plainValue
1021
+ }),
1022
+ ...(biometricValue === undefined ? {} : {
1023
+ biometricValue
1024
+ })
1025
+ }];
1026
+ });
1027
+ }
1028
+ function selectedRenameValue(snapshot) {
1029
+ return isBiometric ? snapshot.biometricValue : snapshot.plainValue;
1030
+ }
1031
+ function hasPendingCurrentWrite() {
1032
+ if (nonMemoryScope === StorageScope.Disk) {
1033
+ return durability.hasPendingDiskWrite(storageKey);
1034
+ }
1035
+ if (nonMemoryScope === StorageScope.Secure && !isBiometric) {
1036
+ return durability.hasPendingSecureWrite(storageKey);
1037
+ }
1038
+ return false;
1039
+ }
1040
+ function hasPendingRenameSourceWrite() {
1041
+ if (isBiometric || nonMemoryScope === null) {
1042
+ return false;
1043
+ }
1044
+ return getRenameSourceKeys().some(key => nonMemoryScope === StorageScope.Disk ? durability.hasPendingDiskWrite(key) : durability.hasPendingSecureWrite(key));
1045
+ }
1046
+ function flushPendingRenameSourceWrites() {
1047
+ if (!hasPendingRenameSourceWrite()) {
1048
+ return;
1049
+ }
1050
+ if (nonMemoryScope === StorageScope.Disk) {
1051
+ flushDiskWrites();
1052
+ return;
1053
+ }
1054
+ flushSecureWrites();
1055
+ }
1056
+ function clearPendingCurrentWriteIf(write) {
1057
+ if (write === undefined) {
1058
+ return;
1059
+ }
1060
+ if (nonMemoryScope === StorageScope.Disk && "generation" in write) {
1061
+ durability.clearPendingDiskWriteIf(write);
1062
+ return;
1063
+ }
1064
+ if (nonMemoryScope === StorageScope.Secure && !isBiometric) {
1065
+ durability.clearPendingSecureWriteIf(write);
1066
+ }
1067
+ }
1068
+ function flushPendingCurrentWrite() {
1069
+ if (nonMemoryScope === StorageScope.Disk) {
1070
+ flushDiskWrites();
1071
+ return;
1072
+ }
1073
+ if (nonMemoryScope === StorageScope.Secure && !isBiometric) {
1074
+ flushSecureWrites();
1075
+ }
1076
+ }
1077
+ function removeCurrentBackendValue() {
1078
+ invalidateRawCache(resolveNonMemoryScope(), storageKey);
1079
+ if (isBiometric) {
1080
+ adapter.backend.deleteSecureBiometric(storageKey);
1081
+ return;
1082
+ }
1083
+ adapter.backend.remove(storageKey, config.scope);
1084
+ }
1085
+ function scheduleRenameSourceCleanup() {
1086
+ const sourceKeys = getRenameSourceKeys();
1087
+ sourceKeys.forEach(invalidateRenameSourceCache);
1088
+ if (isBiometric) {
1089
+ sourceKeys.forEach(removeRenameSource);
1090
+ return;
1091
+ }
1092
+ if (nonMemoryScope === StorageScope.Disk && (coalesceDiskWrites || isDiskWritesAsync())) {
1093
+ sourceKeys.forEach(key => {
1094
+ scheduleDiskWrite(key, undefined);
1095
+ });
1096
+ return;
1097
+ }
1098
+ if (nonMemoryScope === StorageScope.Secure && coalesceSecureWrites) {
1099
+ sourceKeys.forEach(key => {
1100
+ scheduleSecureWrite(key, undefined, secureAccessControl ?? secureDefaultAccessControl);
1101
+ });
1102
+ return;
1103
+ }
1104
+ sourceKeys.forEach(removeRenameSource);
1105
+ }
1106
+ let atomicMutationDepth = 0;
1107
+ function getItemStateKeys() {
1108
+ return Array.from(new Set([storageKey, ...getRenameSourceKeys()]));
1109
+ }
1110
+ function captureItemState() {
1111
+ const records = getItemStateKeys().map(key => {
1112
+ let pending;
1113
+ if (nonMemoryScope === StorageScope.Disk) {
1114
+ if (durability.hasPendingDiskWrite(key)) {
1115
+ pending = {
1116
+ value: durability.readPendingDiskWrite(key)
1117
+ };
1118
+ }
1119
+ } else if (nonMemoryScope === StorageScope.Secure && !isBiometric && durability.hasPendingSecureWrite(key)) {
1120
+ const accessControl = durability.readPendingSecureAccessControl(key);
1121
+ pending = {
1122
+ value: durability.readPendingSecureWrite(key),
1123
+ ...(accessControl === undefined ? {} : {
1124
+ accessControl
1125
+ })
1126
+ };
1127
+ }
1128
+ return {
1129
+ key,
1130
+ plainValue: nonMemoryScope === null ? undefined : adapter.backend.get(key, config.scope),
1131
+ biometricValue: nonMemoryScope === StorageScope.Secure ? adapter.backend.getSecureBiometric(key) : undefined,
1132
+ ...(pending === undefined ? {} : {
1133
+ pending
1134
+ })
1135
+ };
1136
+ });
1137
+ return {
1138
+ records,
1139
+ renamesMigrated
1140
+ };
1141
+ }
1142
+ function clearPendingItemWrite(key) {
1143
+ if (nonMemoryScope === StorageScope.Disk) {
1144
+ durability.clearPendingDiskWrite(key);
1145
+ } else if (nonMemoryScope === StorageScope.Secure && !isBiometric) {
1146
+ durability.clearPendingSecureWrite(key);
1147
+ }
1148
+ }
1149
+ function restoreItemState(snapshot) {
1150
+ const rollbackErrors = [];
1151
+ const recordsByKey = new Map(snapshot.records.map(record => [record.key, record]));
1152
+ snapshot.records.forEach(({
1153
+ key
1154
+ }) => {
1155
+ clearPendingItemWrite(key);
1156
+ if (nonMemoryScope !== null) {
1157
+ invalidateRawCache(nonMemoryScope, key);
1158
+ }
1159
+ });
1160
+ const biometricRecords = snapshot.records.filter(({
1161
+ biometricValue
1162
+ }) => biometricValue !== undefined);
1163
+ if (nonMemoryScope === StorageScope.Secure) {
1164
+ biometricRecords.forEach(({
1165
+ key,
1166
+ biometricValue
1167
+ }) => {
1168
+ try {
1169
+ adapter.backend.setSecureBiometricWithLevel(key, biometricValue, resolvedBiometricLevel === BiometricLevel.None ? BiometricLevel.BiometryOnly : resolvedBiometricLevel);
1170
+ cacheRawValue(StorageScope.Secure, key, biometricValue, "biometric");
1171
+ } catch (error) {
1172
+ rollbackErrors.push({
1173
+ label: "rollback biometric",
1174
+ error
1175
+ });
1176
+ }
1177
+ });
1178
+ }
1179
+ const plainSets = new Map();
1180
+ const plainRemoves = [];
1181
+ snapshot.records.forEach(({
1182
+ key,
1183
+ plainValue,
1184
+ biometricValue
1185
+ }) => {
1186
+ if (plainValue === undefined) {
1187
+ if (biometricValue === undefined) {
1188
+ plainRemoves.push(key);
1189
+ }
1190
+ return;
1191
+ }
1192
+ const accessControl = secureAccessControl ?? secureDefaultAccessControl;
1193
+ const group = plainSets.get(accessControl) ?? {
1194
+ keys: [],
1195
+ values: []
1196
+ };
1197
+ group.keys.push(key);
1198
+ group.values.push(plainValue);
1199
+ plainSets.set(accessControl, group);
1200
+ });
1201
+ plainSets.forEach((group, accessControl) => {
1202
+ try {
1203
+ if (nonMemoryScope === StorageScope.Secure) {
1204
+ adapter.backend.setSecureAccessControl(accessControl);
1205
+ }
1206
+ adapter.backend.setBatch(group.keys, group.values, config.scope);
1207
+ group.keys.forEach((key, index) => {
1208
+ cacheRawValue(resolveNonMemoryScope(), key, group.values[index], "plain");
1209
+ });
1210
+ } catch (error) {
1211
+ rollbackErrors.push({
1212
+ label: "rollback plain set",
1213
+ error
1214
+ });
1215
+ }
1216
+ });
1217
+ if (plainRemoves.length > 0) {
1218
+ try {
1219
+ adapter.backend.removeBatch(plainRemoves, config.scope);
1220
+ plainRemoves.forEach(key => {
1221
+ cacheRawValue(resolveNonMemoryScope(), key, undefined, "plain");
1222
+ });
1223
+ } catch (error) {
1224
+ rollbackErrors.push({
1225
+ label: "rollback plain remove",
1226
+ error
1227
+ });
1228
+ }
1229
+ }
1230
+ if (nonMemoryScope === StorageScope.Secure) {
1231
+ snapshot.records.forEach(({
1232
+ key,
1233
+ biometricValue
1234
+ }) => {
1235
+ if (biometricValue !== undefined) {
1236
+ return;
1237
+ }
1238
+ try {
1239
+ adapter.backend.deleteSecureBiometric(key);
1240
+ cacheRawValue(StorageScope.Secure, key, undefined, "biometric");
1241
+ } catch (error) {
1242
+ rollbackErrors.push({
1243
+ label: "rollback biometric remove",
1244
+ error
1245
+ });
1246
+ }
1247
+ });
1248
+ }
1249
+ snapshot.records.forEach(({
1250
+ key,
1251
+ pending
1252
+ }) => {
1253
+ if (pending === undefined || nonMemoryScope === null) {
1254
+ return;
1255
+ }
1256
+ if (nonMemoryScope === StorageScope.Disk) {
1257
+ scheduleDiskWrite(key, pending.value);
1258
+ } else if (!isBiometric) {
1259
+ scheduleSecureWrite(key, pending.value, pending.accessControl ?? secureAccessControl ?? secureDefaultAccessControl);
1260
+ }
1261
+ });
1262
+ recordsByKey.forEach(({
1263
+ key
1264
+ }) => {
1265
+ if (nonMemoryScope !== null) {
1266
+ invalidateRawCache(nonMemoryScope, key);
1267
+ }
1268
+ });
1269
+ renamesMigrated = snapshot.renamesMigrated;
1270
+ invalidateParsedCache();
1271
+ return rollbackErrors;
1272
+ }
1273
+ function runAtomicItemMutation(mutation) {
1274
+ if (isMemory || renameFromKeys.length === 0 || atomicMutationDepth > 0) {
1275
+ mutation();
1276
+ return;
1277
+ }
1278
+ const snapshot = captureItemState();
1279
+ atomicMutationDepth += 1;
1280
+ try {
1281
+ mutation();
1282
+ } catch (primaryError) {
1283
+ const rollbackErrors = restoreItemState(snapshot);
1284
+ if (rollbackErrors.length > 0) {
1285
+ throw createStorageCompositeError("item mutation rollback", primaryError, rollbackErrors);
1286
+ }
1287
+ throw primaryError;
1288
+ } finally {
1289
+ atomicMutationDepth -= 1;
1290
+ }
1291
+ }
1292
+ const writeStoredRaw = (rawValue, options = {}) => {
1293
+ const cleanupRenameSources = options.cleanupRenameSources !== false;
887
1294
  const oldValue = undefined;
888
1295
  if (isBiometric) {
889
- adapter.backend.setSecureBiometricWithLevel(storageKey, rawValue, resolvedBiometricLevel);
1296
+ invalidateRawCache(StorageScope.Secure, storageKey);
1297
+ runSecurePromotion(storageKey, () => {
1298
+ try {
1299
+ adapter.backend.setSecureBiometricWithLevel(storageKey, rawValue, resolvedBiometricLevel);
1300
+ } catch (error) {
1301
+ throw normalizeStorageError(error);
1302
+ }
1303
+ });
1304
+ if (cleanupRenameSources) {
1305
+ scheduleRenameSourceCleanup();
1306
+ }
890
1307
  emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", adapter.changeSource);
891
- return;
1308
+ return undefined;
1309
+ }
1310
+ if (nonMemoryScope === StorageScope.Secure) {
1311
+ invalidateRawCache(StorageScope.Secure, storageKey);
892
1312
  }
893
1313
  cacheRawValue(resolveNonMemoryScope(), storageKey, rawValue);
894
1314
  if (nonMemoryScope === StorageScope.Disk) {
895
1315
  if (coalesceDiskWrites || isDiskWritesAsync()) {
896
- scheduleDiskWrite(storageKey, rawValue);
1316
+ const pendingWrite = scheduleDiskWrite(storageKey, rawValue);
1317
+ if (cleanupRenameSources) {
1318
+ scheduleRenameSourceCleanup();
1319
+ }
897
1320
  emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", adapter.changeSource);
898
- return;
1321
+ return pendingWrite;
899
1322
  }
900
1323
  clearPendingDiskWrite(storageKey);
901
1324
  }
902
1325
  if (coalesceSecureWrites) {
903
- scheduleSecureWrite(storageKey, rawValue, secureAccessControl ?? secureDefaultAccessControl);
1326
+ const pendingWrite = scheduleSecureWrite(storageKey, rawValue, secureAccessControl ?? secureDefaultAccessControl);
1327
+ if (cleanupRenameSources) {
1328
+ scheduleRenameSourceCleanup();
1329
+ }
904
1330
  emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", adapter.changeSource);
905
- return;
1331
+ return pendingWrite;
906
1332
  }
907
1333
  if (nonMemoryScope === StorageScope.Secure) {
908
1334
  clearPendingSecureWrite(storageKey);
@@ -911,41 +1337,102 @@ export function createStorageCore(buildAdapter) {
911
1337
  }
912
1338
  }
913
1339
  adapter.backend.set(storageKey, rawValue, config.scope);
1340
+ if (cleanupRenameSources) {
1341
+ scheduleRenameSourceCleanup();
1342
+ }
914
1343
  emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", adapter.changeSource);
1344
+ return undefined;
915
1345
  };
916
1346
  const migrateRenamesIfNeeded = () => {
917
1347
  if (renamesMigrated) {
918
1348
  return;
919
1349
  }
920
1350
  try {
1351
+ flushPendingRenameSourceWrites();
921
1352
  const hasCurrent = isBiometric ? adapter.backend.hasSecureBiometric(storageKey) : adapter.backend.has(storageKey, config.scope);
1353
+ const snapshots = readRenameSnapshots();
922
1354
  if (hasCurrent) {
923
- for (const legacyKey of renameFromKeys) {
924
- if (isBiometric) {
925
- if (adapter.backend.hasSecureBiometric(legacyKey)) {
926
- adapter.backend.deleteSecureBiometric(legacyKey);
1355
+ try {
1356
+ snapshots.forEach(({
1357
+ key
1358
+ }) => {
1359
+ removeRenameSource(key);
1360
+ });
1361
+ } catch (primaryError) {
1362
+ const rollbackErrors = [];
1363
+ snapshots.forEach(snapshot => {
1364
+ try {
1365
+ restoreRenameSource(snapshot);
1366
+ } catch (error) {
1367
+ rollbackErrors.push({
1368
+ label: "rollback rename source",
1369
+ error
1370
+ });
927
1371
  }
928
- } else if (adapter.backend.has(legacyKey, config.scope)) {
929
- adapter.backend.remove(legacyKey, config.scope);
1372
+ });
1373
+ if (rollbackErrors.length > 0) {
1374
+ throw createStorageCompositeError("rename cleanup", primaryError, rollbackErrors);
930
1375
  }
1376
+ throw primaryError;
931
1377
  }
932
1378
  renamesMigrated = true;
933
1379
  return;
934
1380
  }
935
- for (const legacyKey of renameFromKeys) {
936
- const legacyRaw = isBiometric ? adapter.backend.getSecureBiometric(legacyKey) : adapter.backend.get(legacyKey, config.scope);
937
- if (legacyRaw === undefined) {
938
- continue;
1381
+ const snapshot = snapshots.find(candidate => selectedRenameValue(candidate) !== undefined);
1382
+ if (snapshot === undefined) {
1383
+ renamesMigrated = true;
1384
+ return;
1385
+ }
1386
+ let writeAttempted = false;
1387
+ let pendingCurrentWrite;
1388
+ try {
1389
+ writeAttempted = true;
1390
+ pendingCurrentWrite = writeStoredRaw(selectedRenameValue(snapshot), {
1391
+ cleanupRenameSources: false
1392
+ });
1393
+ if (hasPendingCurrentWrite()) {
1394
+ flushPendingCurrentWrite();
939
1395
  }
940
- writeStoredRaw(legacyRaw);
941
- if (isBiometric) {
942
- adapter.backend.deleteSecureBiometric(legacyKey);
943
- } else {
944
- adapter.backend.remove(legacyKey, config.scope);
1396
+ snapshots.forEach(({
1397
+ key
1398
+ }) => {
1399
+ removeRenameSource(key);
1400
+ });
1401
+ renamesMigrated = true;
1402
+ } catch (primaryError) {
1403
+ const rollbackErrors = [];
1404
+ clearPendingCurrentWriteIf(pendingCurrentWrite);
1405
+ if (writeAttempted) {
1406
+ try {
1407
+ removeCurrentBackendValue();
1408
+ } catch (error) {
1409
+ rollbackErrors.push({
1410
+ label: "rollback current value",
1411
+ error
1412
+ });
1413
+ }
945
1414
  }
946
- break;
1415
+ snapshots.forEach(renameSnapshot => {
1416
+ try {
1417
+ restoreRenameSource(renameSnapshot);
1418
+ } catch (error) {
1419
+ rollbackErrors.push({
1420
+ label: "rollback rename source",
1421
+ error
1422
+ });
1423
+ }
1424
+ });
1425
+ invalidateRawCache(resolveNonMemoryScope(), storageKey);
1426
+ snapshots.forEach(({
1427
+ key
1428
+ }) => {
1429
+ invalidateRenameSourceCache(key);
1430
+ });
1431
+ if (rollbackErrors.length > 0) {
1432
+ throw createStorageCompositeError("rename migration", primaryError, rollbackErrors);
1433
+ }
1434
+ throw primaryError;
947
1435
  }
948
- renamesMigrated = true;
949
1436
  } catch (error) {
950
1437
  if (isKeychainLockedError(error)) {
951
1438
  onReadError?.(error);
@@ -955,16 +1442,22 @@ export function createStorageCore(buildAdapter) {
955
1442
  }
956
1443
  };
957
1444
  const removeStoredRaw = (operation = "remove") => {
958
- const oldValue = getEventRawValue(config.scope, storageKey);
1445
+ const oldValue = isBiometric ? getEventRawValueForRepresentation(config.scope, storageKey, "biometric") : getEventRawValue(config.scope, storageKey);
959
1446
  if (isBiometric) {
1447
+ invalidateRawCache(StorageScope.Secure, storageKey);
1448
+ scheduleRenameSourceCleanup();
960
1449
  adapter.backend.deleteSecureBiometric(storageKey);
961
1450
  emitKeyChange(config.scope, storageKey, oldValue, undefined, operation, adapter.changeSource);
962
1451
  return;
963
1452
  }
1453
+ if (nonMemoryScope === StorageScope.Secure) {
1454
+ invalidateRawCache(StorageScope.Secure, storageKey);
1455
+ }
964
1456
  cacheRawValue(resolveNonMemoryScope(), storageKey, undefined);
965
1457
  if (nonMemoryScope === StorageScope.Disk) {
966
1458
  if (coalesceDiskWrites || isDiskWritesAsync()) {
967
1459
  scheduleDiskWrite(storageKey, undefined);
1460
+ scheduleRenameSourceCleanup();
968
1461
  emitKeyChange(config.scope, storageKey, oldValue, undefined, operation, adapter.changeSource);
969
1462
  return;
970
1463
  }
@@ -972,12 +1465,14 @@ export function createStorageCore(buildAdapter) {
972
1465
  }
973
1466
  if (coalesceSecureWrites) {
974
1467
  scheduleSecureWrite(storageKey, undefined, secureAccessControl ?? secureDefaultAccessControl);
1468
+ scheduleRenameSourceCleanup();
975
1469
  emitKeyChange(config.scope, storageKey, oldValue, undefined, operation, adapter.changeSource);
976
1470
  return;
977
1471
  }
978
1472
  if (nonMemoryScope === StorageScope.Secure) {
979
1473
  clearPendingSecureWrite(storageKey);
980
1474
  }
1475
+ scheduleRenameSourceCleanup();
981
1476
  adapter.backend.remove(storageKey, config.scope);
982
1477
  emitKeyChange(config.scope, storageKey, oldValue, undefined, operation, adapter.changeSource);
983
1478
  };
@@ -986,6 +1481,8 @@ export function createStorageCore(buildAdapter) {
986
1481
  const oldValue = getEventRawValue(config.scope, storageKey);
987
1482
  if (memoryExpiration) {
988
1483
  memoryExpiration.set(storageKey, Date.now() + (expirationTtlMs ?? 0));
1484
+ } else {
1485
+ memoryExpirationDeadlines.delete(storageKey);
989
1486
  }
990
1487
  const storedValue = typeof value === "string" ? escapeCollidingRawValue(value) : value;
991
1488
  memoryStore.set(storageKey, storedValue);
@@ -1034,7 +1531,9 @@ export function createStorageCore(buildAdapter) {
1034
1531
  if (lastExpiresAt > Date.now()) {
1035
1532
  return lastValue;
1036
1533
  }
1037
- removeStoredRaw("expire");
1534
+ runAtomicItemMutation(() => {
1535
+ removeStoredRaw("expire");
1536
+ });
1038
1537
  invalidateParsedCache();
1039
1538
  onExpired?.(storageKey);
1040
1539
  lastValue = ensureValidatedValue(defaultValue, false);
@@ -1072,7 +1571,9 @@ export function createStorageCore(buildAdapter) {
1072
1571
  if (isStoredEnvelope(parsed)) {
1073
1572
  envelopeExpiresAt = parsed.expiresAt;
1074
1573
  if (parsed.expiresAt <= Date.now()) {
1075
- removeStoredRaw("expire");
1574
+ runAtomicItemMutation(() => {
1575
+ removeStoredRaw("expire");
1576
+ });
1076
1577
  invalidateParsedCache();
1077
1578
  onExpired?.(storageKey);
1078
1579
  lastValue = ensureValidatedValue(defaultValue, false);
@@ -1109,36 +1610,45 @@ export function createStorageCore(buildAdapter) {
1109
1610
  }));
1110
1611
  const set = valueOrFn => {
1111
1612
  measureOperation("item:set", config.scope, () => {
1112
- const newValue = isUpdater(valueOrFn) ? valueOrFn(getInternal()) : valueOrFn;
1113
- if (validate && !validate(newValue)) {
1114
- throw new Error(`Validation failed for key "${storageKey}" in scope "${StorageScope[config.scope]}".`);
1115
- }
1116
- invalidateParsedCache();
1117
- writeValueWithoutValidation(newValue);
1613
+ runAtomicItemMutation(() => {
1614
+ const newValue = isUpdater(valueOrFn) ? valueOrFn(getInternal()) : valueOrFn;
1615
+ if (validate && !validate(newValue)) {
1616
+ throw new Error(`Validation failed for key "${storageKey}" in scope "${StorageScope[config.scope]}".`);
1617
+ }
1618
+ invalidateParsedCache();
1619
+ writeValueWithoutValidation(newValue);
1620
+ });
1118
1621
  });
1119
1622
  };
1120
1623
  const setIfVersion = (version, valueOrFn) => measureOperation("item:setIfVersion", config.scope, () => {
1121
- const currentVersion = getCurrentVersion();
1122
- if (currentVersion !== version) {
1123
- return false;
1124
- }
1125
- set(valueOrFn);
1126
- return true;
1624
+ let didSet = false;
1625
+ runAtomicItemMutation(() => {
1626
+ const currentVersion = getCurrentVersion();
1627
+ if (currentVersion !== version) {
1628
+ return;
1629
+ }
1630
+ set(valueOrFn);
1631
+ didSet = true;
1632
+ });
1633
+ return didSet;
1127
1634
  });
1128
1635
  const deleteItem = () => {
1129
1636
  measureOperation("item:delete", config.scope, () => {
1130
- invalidateParsedCache();
1131
- if (isMemory) {
1132
- const oldValue = getEventRawValue(config.scope, storageKey);
1133
- if (memoryExpiration) {
1134
- memoryExpiration.delete(storageKey);
1637
+ runAtomicItemMutation(() => {
1638
+ invalidateParsedCache();
1639
+ if (isMemory) {
1640
+ const oldValue = getEventRawValue(config.scope, storageKey);
1641
+ if (memoryExpiration) {
1642
+ memoryExpiration.delete(storageKey);
1643
+ }
1644
+ memoryExpirationDeadlines.delete(storageKey);
1645
+ memoryStore.delete(storageKey);
1646
+ notifyKeyListeners(memoryListeners, storageKey);
1647
+ emitKeyChange(config.scope, storageKey, oldValue, undefined, "remove", "memory");
1648
+ return;
1135
1649
  }
1136
- memoryStore.delete(storageKey);
1137
- notifyKeyListeners(memoryListeners, storageKey);
1138
- emitKeyChange(config.scope, storageKey, oldValue, undefined, "remove", "memory");
1139
- return;
1140
- }
1141
- removeStoredRaw();
1650
+ removeStoredRaw();
1651
+ });
1142
1652
  });
1143
1653
  };
1144
1654
  const merge = partial => {
@@ -1231,14 +1741,18 @@ export function createStorageCore(buildAdapter) {
1231
1741
  invalidateParsedCache();
1232
1742
  },
1233
1743
  _deleteMemoryEntry: () => {
1234
- if (memoryExpiration) {
1235
- memoryExpiration.delete(storageKey);
1236
- }
1744
+ memoryExpirationDeadlines.delete(storageKey);
1237
1745
  memoryStore.delete(storageKey);
1238
1746
  invalidateParsedCache();
1239
1747
  },
1240
1748
  _hasValidation: validate !== undefined,
1241
1749
  _hasExpiration: expiration !== undefined,
1750
+ _hasRenameFrom: renameFromKeys.length > 0,
1751
+ _renameFromKeys: renameFromKeys,
1752
+ _getRenameMigrationState: () => renamesMigrated,
1753
+ _setRenameMigrationState: migrated => {
1754
+ renamesMigrated = migrated;
1755
+ },
1242
1756
  _readCacheEnabled: readCache,
1243
1757
  _isBiometric: isBiometric,
1244
1758
  _biometricLevel: resolvedBiometricLevel,
@@ -1260,6 +1774,14 @@ export function createStorageCore(buildAdapter) {
1260
1774
  }
1261
1775
  groupSet.add(storageItem);
1262
1776
  }
1777
+ if (isMemory) {
1778
+ let items = memoryItemsByKey.get(storageKey);
1779
+ if (!items) {
1780
+ items = new Set();
1781
+ memoryItemsByKey.set(storageKey, items);
1782
+ }
1783
+ items.add(storageItem);
1784
+ }
1263
1785
  const registryKey = `${config.scope}:${storageKey}`;
1264
1786
  registeredKeyCounts.set(registryKey, (registeredKeyCounts.get(registryKey) ?? 0) + 1);
1265
1787
  return storageItem;
@@ -1279,24 +1801,21 @@ export function createStorageCore(buildAdapter) {
1279
1801
  const keyIndexes = [];
1280
1802
  items.forEach((item, index) => {
1281
1803
  if (scope === StorageScope.Disk) {
1282
- const pending = durability.readPendingDiskWrite(item.key);
1283
- if (pending !== undefined) {
1284
- rawValues[index] = pending;
1804
+ if (durability.hasPendingDiskWrite(item.key)) {
1805
+ rawValues[index] = durability.readPendingDiskWrite(item.key);
1285
1806
  return;
1286
1807
  }
1287
1808
  }
1288
1809
  if (scope === StorageScope.Secure) {
1289
- const pending = durability.readPendingSecureWrite(item.key);
1290
- if (pending !== undefined) {
1291
- rawValues[index] = pending;
1810
+ if (durability.hasPendingSecureWrite(item.key)) {
1811
+ rawValues[index] = durability.readPendingSecureWrite(item.key);
1292
1812
  return;
1293
1813
  }
1294
1814
  }
1295
1815
  if (item._readCacheEnabled === true) {
1296
- const cache = getScopeRawCache(scope);
1297
- const cached = cache.get(item.key);
1298
- if (cached !== undefined || cache.has(item.key)) {
1299
- rawValues[index] = cached;
1816
+ const cachedEntry = getCachedRawValueEntry(scope, item.key);
1817
+ if (cachedEntry?.has("plain")) {
1818
+ rawValues[index] = cachedEntry.get("plain");
1300
1819
  return;
1301
1820
  }
1302
1821
  }
@@ -1391,6 +1910,10 @@ export function createStorageCore(buildAdapter) {
1391
1910
  const keys = secureEntries.map(({
1392
1911
  item
1393
1912
  }) => item.key);
1913
+ keys.forEach(key => {
1914
+ invalidateRawCache(scope, key);
1915
+ });
1916
+ const serializedValues = [];
1394
1917
  const oldValues = shouldReadPreviousEventValues(scope) ? adapter.backend.getBatch(keys, scope).map(value => value === undefined ? undefined : unescapeCollidingRawValue(value)) : [];
1395
1918
  const groupedByAccessControl = new Map();
1396
1919
  secureEntries.forEach(({
@@ -1398,6 +1921,8 @@ export function createStorageCore(buildAdapter) {
1398
1921
  value,
1399
1922
  internal
1400
1923
  }) => {
1924
+ const serialized = item.serialize(value);
1925
+ serializedValues.push(serialized);
1401
1926
  const accessControl = internal._secureAccessControl ?? secureDefaultAccessControl;
1402
1927
  const existingGroup = groupedByAccessControl.get(accessControl);
1403
1928
  const group = existingGroup ?? {
@@ -1405,7 +1930,7 @@ export function createStorageCore(buildAdapter) {
1405
1930
  values: []
1406
1931
  };
1407
1932
  group.keys.push(item.key);
1408
- group.values.push(item.serialize(value));
1933
+ group.values.push(serialized);
1409
1934
  if (!existingGroup) {
1410
1935
  groupedByAccessControl.set(accessControl, group);
1411
1936
  }
@@ -1417,10 +1942,8 @@ export function createStorageCore(buildAdapter) {
1417
1942
  cacheRawValue(scope, key, group.values[index]);
1418
1943
  });
1419
1944
  });
1420
- emitBatchChange(scope, "setBatch", adapter.changeSource, secureEntries.map(({
1421
- item,
1422
- value
1423
- }, index) => createKeyChange(scope, item.key, oldValues[index], item.serialize(value), "setBatch", adapter.changeSource)));
1945
+ const willEmitChanges = storageEvents.hasListeners(scope) || eventObserver !== undefined;
1946
+ emitBatchChange(scope, "setBatch", adapter.changeSource, willEmitChanges ? keys.map((key, index) => createKeyChange(scope, key, oldValues[index], serializedValues[index], "setBatch", adapter.changeSource)) : []);
1424
1947
  return;
1425
1948
  }
1426
1949
  flushDiskWrites();
@@ -1460,6 +1983,12 @@ export function createStorageCore(buildAdapter) {
1460
1983
  emitBatchChange(scope, "removeBatch", "memory", changes);
1461
1984
  return;
1462
1985
  }
1986
+ if (items.some(item => asInternal(item)._hasRenameFrom)) {
1987
+ items.forEach(item => {
1988
+ asInternal(item).delete();
1989
+ });
1990
+ return;
1991
+ }
1463
1992
  const keys = items.map(item => item.key);
1464
1993
  if (scope === StorageScope.Disk) {
1465
1994
  flushDiskWrites();
@@ -1467,7 +1996,25 @@ export function createStorageCore(buildAdapter) {
1467
1996
  if (scope === StorageScope.Secure) {
1468
1997
  flushSecureWrites();
1469
1998
  }
1470
- const oldValues = shouldReadPreviousEventValues(scope) ? adapter.backend.getBatch(keys, scope).map(value => value === undefined ? undefined : unescapeCollidingRawValue(value)) : [];
1999
+ const oldValues = shouldReadPreviousEventValues(scope) ? (() => {
2000
+ const plainItems = items.filter(item => asInternal(item)._isBiometric !== true);
2001
+ const plainValues = plainItems.length === 0 ? [] : adapter.backend.getBatch(plainItems.map(item => item.key), scope).map(value => value === undefined ? undefined : unescapeCollidingRawValue(value));
2002
+ let plainIndex = 0;
2003
+ return items.map(item => {
2004
+ const internal = asInternal(item);
2005
+ if (internal._isBiometric === true) {
2006
+ return getEventRawValueForRepresentation(scope, item.key, "biometric");
2007
+ }
2008
+ const value = plainValues[plainIndex];
2009
+ plainIndex += 1;
2010
+ return value;
2011
+ });
2012
+ })() : [];
2013
+ if (scope === StorageScope.Secure) {
2014
+ keys.forEach(key => {
2015
+ invalidateRawCache(scope, key);
2016
+ });
2017
+ }
1471
2018
  adapter.backend.removeBatch(keys, scope);
1472
2019
  keys.forEach(key => {
1473
2020
  cacheRawValue(scope, key, undefined);
@@ -1515,33 +2062,74 @@ export function createStorageCore(buildAdapter) {
1515
2062
  }
1516
2063
  const NOT_SET = Symbol();
1517
2064
  const rollback = new Map();
1518
- const rememberRollback = (key, item) => {
1519
- if (rollback.has(key)) {
1520
- return;
1521
- }
1522
- if (scope === StorageScope.Memory) {
1523
- rollback.set(key, {
1524
- kind: "memory",
1525
- value: memoryStore.has(key) ? memoryStore.get(key) : NOT_SET
1526
- });
1527
- } else {
1528
- const internal = item ? item : undefined;
1529
- if (scope === StorageScope.Secure && internal?._isBiometric === true) {
1530
- rollback.set(key, {
1531
- kind: "biometric",
1532
- value: adapter.backend.getSecureBiometric(key),
1533
- level: internal._biometricLevel
2065
+ const itemRenameStates = new Map();
2066
+ const rememberRollback = (key, item, includeOtherSecureRepresentation = false) => {
2067
+ const internal = item ? item : undefined;
2068
+ const representation = scope === StorageScope.Secure && internal?._isBiometric === true ? "biometric" : "plain";
2069
+ const rememberRepresentation = representationToRemember => {
2070
+ const identity = JSON.stringify([scope, key, representationToRemember]);
2071
+ if (rollback.has(identity)) {
2072
+ return;
2073
+ }
2074
+ if (scope === StorageScope.Memory) {
2075
+ const expiresAt = memoryExpirationDeadlines.get(key);
2076
+ rollback.set(identity, {
2077
+ key,
2078
+ representation: representationToRemember,
2079
+ record: {
2080
+ kind: "memory",
2081
+ value: memoryStore.has(key) ? memoryStore.get(key) : NOT_SET,
2082
+ ...(expiresAt === undefined ? {} : {
2083
+ expiresAt
2084
+ })
2085
+ }
2086
+ });
2087
+ return;
2088
+ }
2089
+ if (representationToRemember === "biometric") {
2090
+ rollback.set(identity, {
2091
+ key,
2092
+ representation: representationToRemember,
2093
+ record: {
2094
+ kind: "biometric",
2095
+ value: getStoredRawValueForRepresentation(key, scope, representationToRemember),
2096
+ level: internal?._biometricLevel !== undefined && internal._biometricLevel !== BiometricLevel.None ? internal._biometricLevel : BiometricLevel.BiometryOnly
2097
+ }
1534
2098
  });
1535
2099
  return;
1536
2100
  }
1537
- rollback.set(key, {
1538
- kind: "raw",
1539
- value: getStoredRawValue(key, scope),
1540
- ...(scope === StorageScope.Secure && internal?._secureAccessControl !== undefined ? {
1541
- accessControl: internal._secureAccessControl
1542
- } : {})
2101
+ rollback.set(identity, {
2102
+ key,
2103
+ representation: representationToRemember,
2104
+ record: {
2105
+ kind: "raw",
2106
+ value: getStoredRawValueForRepresentation(key, scope, representationToRemember),
2107
+ ...(scope === StorageScope.Secure && internal?._secureAccessControl !== undefined ? {
2108
+ accessControl: internal._secureAccessControl
2109
+ } : {})
2110
+ }
1543
2111
  });
2112
+ };
2113
+ if (representation === "biometric" || includeOtherSecureRepresentation) {
2114
+ rememberRepresentation("plain");
1544
2115
  }
2116
+ rememberRepresentation(representation);
2117
+ if (includeOtherSecureRepresentation) {
2118
+ rememberRepresentation("biometric");
2119
+ }
2120
+ };
2121
+ const rememberItemRollback = item => {
2122
+ const internal = item;
2123
+ const includeOtherSecureRepresentation = scope === StorageScope.Secure && (internal._isBiometric === true || internal._hasRenameFrom === true);
2124
+ rememberRollback(item.key, item, includeOtherSecureRepresentation);
2125
+ if (internal._getRenameMigrationState) {
2126
+ if (!itemRenameStates.has(internal)) {
2127
+ itemRenameStates.set(internal, internal._getRenameMigrationState());
2128
+ }
2129
+ }
2130
+ (internal._renameFromKeys ?? EMPTY_KEYS).forEach(aliasKey => {
2131
+ rememberRollback(aliasKey, item, true);
2132
+ });
1545
2133
  };
1546
2134
  const tx = {
1547
2135
  scope,
@@ -1551,49 +2139,85 @@ export function createStorageCore(buildAdapter) {
1551
2139
  setRawValue(key, value, scope);
1552
2140
  },
1553
2141
  removeRaw: key => {
1554
- rememberRollback(key);
2142
+ rememberRollback(key, undefined, scope === StorageScope.Secure);
1555
2143
  removeRawValue(key, scope);
1556
2144
  },
1557
2145
  getItem: item => {
1558
2146
  assertBatchScope([item], scope);
2147
+ rememberItemRollback(item);
1559
2148
  return item.get();
1560
2149
  },
1561
2150
  setItem: (item, value) => {
1562
2151
  assertBatchScope([item], scope);
1563
- rememberRollback(item.key, item);
2152
+ rememberItemRollback(item);
1564
2153
  item.set(value);
1565
2154
  },
1566
2155
  removeItem: item => {
1567
2156
  assertBatchScope([item], scope);
1568
- rememberRollback(item.key, item);
2157
+ rememberItemRollback(item);
1569
2158
  item.delete();
1570
2159
  }
1571
2160
  };
1572
2161
  try {
1573
2162
  return transaction(tx);
1574
2163
  } catch (error) {
1575
- const rollbackEntries = Array.from(rollback.entries()).reverse();
2164
+ itemRenameStates.forEach((migrated, item) => {
2165
+ item._setRenameMigrationState(migrated);
2166
+ });
2167
+ const rollbackEntries = Array.from(rollback.values()).reverse();
1576
2168
  const rollbackSource = scope === StorageScope.Memory ? "memory" : adapter.changeSource;
1577
- const preRollbackValues = rollbackEntries.length > 0 ? rollbackEntries.map(([key]) => getEventRawValue(scope, key)) : [];
2169
+ const rollbackErrors = [];
2170
+ const readRollbackEventValue = entry => {
2171
+ try {
2172
+ return getEventRawValueForRepresentation(scope, entry.key, entry.representation);
2173
+ } catch (readError) {
2174
+ rollbackErrors.push({
2175
+ label: `rollback read ${entry.representation}:${entry.key}`,
2176
+ error: readError
2177
+ });
2178
+ return undefined;
2179
+ }
2180
+ };
2181
+ const preRollbackValues = rollbackEntries.map(readRollbackEventValue);
1578
2182
  if (scope === StorageScope.Memory) {
1579
- rollbackEntries.forEach(([key, record]) => {
1580
- if (record.value === NOT_SET) {
1581
- memoryStore.delete(key);
1582
- } else {
1583
- memoryStore.set(key, record.value);
2183
+ rollbackEntries.forEach(entry => {
2184
+ try {
2185
+ const record = entry.record;
2186
+ if (record.kind !== "memory") {
2187
+ return;
2188
+ }
2189
+ if (record.value === NOT_SET) {
2190
+ memoryStore.delete(entry.key);
2191
+ } else {
2192
+ memoryStore.set(entry.key, record.value);
2193
+ }
2194
+ if (record.expiresAt === undefined) {
2195
+ memoryExpirationDeadlines.delete(entry.key);
2196
+ } else {
2197
+ memoryExpirationDeadlines.set(entry.key, record.expiresAt);
2198
+ }
2199
+ invalidateMemoryItemCaches(entry.key);
2200
+ notifyKeyListeners(memoryListeners, entry.key);
2201
+ } catch (rollbackError) {
2202
+ rollbackErrors.push({
2203
+ label: `rollback ${entry.representation}:${entry.key}`,
2204
+ error: rollbackError
2205
+ });
1584
2206
  }
1585
- notifyKeyListeners(memoryListeners, key);
1586
2207
  });
1587
2208
  } else {
1588
2209
  const groupedKeysToSet = new Map();
1589
2210
  const keysToRemove = [];
1590
- rollbackEntries.forEach(([key, record]) => {
2211
+ const biometricEntries = [];
2212
+ const biometricValues = new Map();
2213
+ rollbackEntries.forEach(entry => {
2214
+ const {
2215
+ key,
2216
+ record
2217
+ } = entry;
1591
2218
  if (record.kind === "biometric") {
1592
- if (record.value === undefined) {
1593
- adapter.backend.deleteSecureBiometric(key);
1594
- } else {
1595
- adapter.backend.setSecureBiometricWithLevel(key, record.value, record.level);
1596
- }
2219
+ biometricEntries.push(entry);
2220
+ biometricValues.set(key, record.value);
1597
2221
  return;
1598
2222
  }
1599
2223
  if (record.kind !== "raw") {
@@ -1616,29 +2240,108 @@ export function createStorageCore(buildAdapter) {
1616
2240
  }
1617
2241
  });
1618
2242
  if (scope === StorageScope.Disk) {
1619
- flushDiskWrites();
2243
+ try {
2244
+ flushDiskWrites();
2245
+ } catch (flushError) {
2246
+ rollbackErrors.push({
2247
+ label: "rollback flush disk",
2248
+ error: flushError
2249
+ });
2250
+ }
2251
+ }
2252
+ if (scope === StorageScope.Secure) {
2253
+ try {
2254
+ flushSecureWrites();
2255
+ } catch (flushError) {
2256
+ rollbackErrors.push({
2257
+ label: "rollback flush secure",
2258
+ error: flushError
2259
+ });
2260
+ }
1620
2261
  }
1621
2262
  if (scope === StorageScope.Secure) {
1622
- flushSecureWrites();
2263
+ rollbackEntries.forEach(entry => {
2264
+ invalidateRawCache(scope, entry.key);
2265
+ });
1623
2266
  }
2267
+ biometricEntries.forEach(entry => {
2268
+ const {
2269
+ key,
2270
+ record
2271
+ } = entry;
2272
+ if (record.kind !== "biometric" || record.value === undefined) {
2273
+ return;
2274
+ }
2275
+ try {
2276
+ adapter.backend.setSecureBiometricWithLevel(key, record.value, record.level);
2277
+ cacheRawValue(StorageScope.Secure, key, record.value, "biometric");
2278
+ } catch (rollbackError) {
2279
+ rollbackErrors.push({
2280
+ label: "rollback biometric",
2281
+ error: rollbackError
2282
+ });
2283
+ }
2284
+ });
1624
2285
  groupedKeysToSet.forEach((group, accessControl) => {
1625
- if (scope === StorageScope.Secure) {
1626
- adapter.backend.setSecureAccessControl(accessControl);
2286
+ try {
2287
+ if (scope === StorageScope.Secure) {
2288
+ adapter.backend.setSecureAccessControl(accessControl);
2289
+ }
2290
+ adapter.backend.setBatch(group.keys, group.values, scope);
2291
+ group.keys.forEach((key, index) => {
2292
+ cacheRawValue(scope, key, group.values[index], "plain");
2293
+ });
2294
+ } catch (rollbackError) {
2295
+ rollbackErrors.push({
2296
+ label: "rollback plain setBatch",
2297
+ error: rollbackError
2298
+ });
1627
2299
  }
1628
- adapter.backend.setBatch(group.keys, group.values, scope);
1629
- group.keys.forEach((key, index) => {
1630
- cacheRawValue(scope, key, group.values[index]);
1631
- });
1632
2300
  });
1633
2301
  if (keysToRemove.length > 0) {
1634
- adapter.backend.removeBatch(keysToRemove, scope);
1635
- keysToRemove.forEach(key => {
1636
- cacheRawValue(scope, key, undefined);
1637
- });
2302
+ const keysToRemoveWithoutBiometric = keysToRemove.filter(key => biometricValues.get(key) === undefined);
2303
+ try {
2304
+ if (keysToRemoveWithoutBiometric.length > 0) {
2305
+ adapter.backend.removeBatch(keysToRemoveWithoutBiometric, scope);
2306
+ }
2307
+ keysToRemove.forEach(key => {
2308
+ cacheRawValue(scope, key, undefined, "plain");
2309
+ });
2310
+ } catch (rollbackError) {
2311
+ rollbackErrors.push({
2312
+ label: "rollback plain removeBatch",
2313
+ error: rollbackError
2314
+ });
2315
+ }
1638
2316
  }
2317
+ biometricEntries.forEach(entry => {
2318
+ const {
2319
+ key,
2320
+ record
2321
+ } = entry;
2322
+ if (record.kind !== "biometric") {
2323
+ return;
2324
+ }
2325
+ if (record.value !== undefined) {
2326
+ return;
2327
+ }
2328
+ invalidateRawCache(StorageScope.Secure, key);
2329
+ try {
2330
+ adapter.backend.deleteSecureBiometric(key);
2331
+ cacheRawValue(StorageScope.Secure, key, record.value, "biometric");
2332
+ } catch (rollbackError) {
2333
+ rollbackErrors.push({
2334
+ label: "rollback biometric",
2335
+ error: rollbackError
2336
+ });
2337
+ }
2338
+ });
1639
2339
  }
1640
2340
  if (rollbackEntries.length > 0) {
1641
- emitBatchChange(scope, "rollback", rollbackSource, rollbackEntries.map(([key], index) => createKeyChange(scope, key, preRollbackValues[index], getEventRawValue(scope, key), "rollback", rollbackSource)));
2341
+ emitBatchChange(scope, "rollback", rollbackSource, rollbackEntries.map((entry, index) => createKeyChange(scope, entry.key, preRollbackValues[index], readRollbackEventValue(entry), "rollback", rollbackSource)));
2342
+ }
2343
+ if (rollbackErrors.length > 0) {
2344
+ throw createStorageCompositeError("transaction rollback", error, rollbackErrors);
1642
2345
  }
1643
2346
  throw error;
1644
2347
  }
@@ -1717,12 +2420,15 @@ export function createStorageCore(buildAdapter) {
1717
2420
  });
1718
2421
  const has = id => item.get()[id] === true;
1719
2422
  const add = id => {
1720
- if (item.get()[id] === true) {
2423
+ const current = item.get();
2424
+ if (current[id] === true) {
1721
2425
  return;
1722
2426
  }
1723
- item.merge({
1724
- [id]: true
1725
- });
2427
+ const next = {
2428
+ ...current
2429
+ };
2430
+ next[id] = true;
2431
+ item.set(next);
1726
2432
  };
1727
2433
  const deleteId = id => {
1728
2434
  const current = item.get();
@@ -1743,8 +2449,16 @@ export function createStorageCore(buildAdapter) {
1743
2449
  add(id);
1744
2450
  return true;
1745
2451
  };
2452
+ const getTyped = () => {
2453
+ const typed = {};
2454
+ for (const id of Object.keys(item.get())) {
2455
+ typed[id] = true;
2456
+ }
2457
+ return typed;
2458
+ };
1746
2459
  return {
1747
2460
  get: item.get,
2461
+ getTyped,
1748
2462
  has,
1749
2463
  add,
1750
2464
  delete: deleteId,