react-native-nitro-storage 0.9.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/CHANGELOG.md +101 -29
  2. package/README.md +83 -26
  3. package/android/build.gradle +3 -1
  4. package/android/src/main/cpp/AndroidStorageAdapterCpp.cpp +5 -2
  5. package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +240 -34
  6. package/cpp/bindings/HybridStorage.cpp +106 -283
  7. package/cpp/bindings/HybridStorage.hpp +16 -9
  8. package/docs/api-reference.md +75 -19
  9. package/docs/benchmarks.md +6 -1
  10. package/docs/keychain-lifecycle-testing.md +36 -0
  11. package/docs/recipes.md +1 -2
  12. package/docs/secure-storage.md +45 -9
  13. package/ios/IOSStorageAdapterCpp.mm +391 -175
  14. package/lib/commonjs/capabilities.js +3 -1
  15. package/lib/commonjs/capabilities.js.map +1 -1
  16. package/lib/commonjs/core/durability.js +111 -45
  17. package/lib/commonjs/core/durability.js.map +1 -1
  18. package/lib/commonjs/index.js +15 -5
  19. package/lib/commonjs/index.js.map +1 -1
  20. package/lib/commonjs/index.web.js +219 -97
  21. package/lib/commonjs/index.web.js.map +1 -1
  22. package/lib/commonjs/internal.js +3 -11
  23. package/lib/commonjs/internal.js.map +1 -1
  24. package/lib/commonjs/shared.js +62 -2
  25. package/lib/commonjs/shared.js.map +1 -1
  26. package/lib/commonjs/storage-core.js +883 -163
  27. package/lib/commonjs/storage-core.js.map +1 -1
  28. package/lib/commonjs/storage-runtime.js +5 -1
  29. package/lib/commonjs/storage-runtime.js.map +1 -1
  30. package/lib/commonjs/testing.js +13 -0
  31. package/lib/commonjs/testing.js.map +1 -1
  32. package/lib/module/capabilities.js +2 -1
  33. package/lib/module/capabilities.js.map +1 -1
  34. package/lib/module/core/durability.js +111 -45
  35. package/lib/module/core/durability.js.map +1 -1
  36. package/lib/module/index.js +12 -8
  37. package/lib/module/index.js.map +1 -1
  38. package/lib/module/index.web.js +215 -99
  39. package/lib/module/index.web.js.map +1 -1
  40. package/lib/module/internal.js +2 -9
  41. package/lib/module/internal.js.map +1 -1
  42. package/lib/module/shared.js +60 -2
  43. package/lib/module/shared.js.map +1 -1
  44. package/lib/module/storage-core.js +884 -164
  45. package/lib/module/storage-core.js.map +1 -1
  46. package/lib/module/storage-runtime.js +4 -1
  47. package/lib/module/storage-runtime.js.map +1 -1
  48. package/lib/module/testing.js +8 -1
  49. package/lib/module/testing.js.map +1 -1
  50. package/lib/typescript/capabilities.d.ts +2 -1
  51. package/lib/typescript/capabilities.d.ts.map +1 -1
  52. package/lib/typescript/core/durability.d.ts +8 -4
  53. package/lib/typescript/core/durability.d.ts.map +1 -1
  54. package/lib/typescript/index.d.ts +2 -2
  55. package/lib/typescript/index.d.ts.map +1 -1
  56. package/lib/typescript/index.web.d.ts +3 -29
  57. package/lib/typescript/index.web.d.ts.map +1 -1
  58. package/lib/typescript/internal.d.ts +0 -2
  59. package/lib/typescript/internal.d.ts.map +1 -1
  60. package/lib/typescript/shared.d.ts +19 -0
  61. package/lib/typescript/shared.d.ts.map +1 -1
  62. package/lib/typescript/storage-core.d.ts +15 -6
  63. package/lib/typescript/storage-core.d.ts.map +1 -1
  64. package/lib/typescript/storage-runtime.d.ts +2 -1
  65. package/lib/typescript/storage-runtime.d.ts.map +1 -1
  66. package/lib/typescript/testing.d.ts +1 -1
  67. package/lib/typescript/testing.d.ts.map +1 -1
  68. package/package.json +2 -2
  69. package/src/capabilities.ts +3 -1
  70. package/src/core/durability.ts +140 -43
  71. package/src/index.ts +20 -10
  72. package/src/index.web.ts +328 -166
  73. package/src/internal.ts +0 -14
  74. package/src/shared.ts +85 -0
  75. package/src/storage-core.ts +1198 -232
  76. package/src/storage-runtime.ts +6 -0
  77. package/src/testing.ts +8 -1
@@ -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;
@@ -131,18 +158,6 @@ export function createStorageCore(buildAdapter) {
131
158
  function recordMetric(operation, scope, durationMs, keysCount = 1) {
132
159
  metrics.record(operation, scope, durationMs, keysCount);
133
160
  }
134
- function readPendingSecureWrite(key) {
135
- return durability.readPendingSecureWrite(key);
136
- }
137
- function readPendingDiskWrite(key) {
138
- return durability.readPendingDiskWrite(key);
139
- }
140
- function hasPendingDiskWrite(key) {
141
- return durability.hasPendingDiskWrite(key);
142
- }
143
- function hasPendingSecureWrite(key) {
144
- return durability.hasPendingSecureWrite(key);
145
- }
146
161
  function clearPendingDiskWrite(key) {
147
162
  durability.clearPendingDiskWrite(key);
148
163
  }
@@ -155,11 +170,14 @@ export function createStorageCore(buildAdapter) {
155
170
  function flushSecureWrites() {
156
171
  durability.flushSecureWrites();
157
172
  }
173
+ function runSecurePromotion(key, promotion) {
174
+ return durability.runSecurePromotion(key, promotion);
175
+ }
158
176
  function scheduleDiskWrite(key, value) {
159
- durability.scheduleDiskWrite(key, value);
177
+ return durability.scheduleDiskWrite(key, value);
160
178
  }
161
179
  function scheduleSecureWrite(key, value, accessControl) {
162
- durability.scheduleSecureWrite(key, value, accessControl);
180
+ return durability.scheduleSecureWrite(key, value, accessControl);
163
181
  }
164
182
  function setDiskWritesAsyncMode(enabled) {
165
183
  durability.setDiskWritesAsync(enabled);
@@ -172,27 +190,43 @@ export function createStorageCore(buildAdapter) {
172
190
  const value = memoryStore.get(key);
173
191
  return typeof value === "string" ? value : undefined;
174
192
  }
175
- if (scope === StorageScope.Disk && hasPendingDiskWrite(key)) {
176
- return readPendingDiskWrite(key);
193
+ if (scope === StorageScope.Disk) {
194
+ const pending = durability.getPendingDiskWrite(key);
195
+ if (pending !== undefined) {
196
+ return pending.value;
197
+ }
177
198
  }
178
- if (scope === StorageScope.Secure && hasPendingSecureWrite(key)) {
179
- return readPendingSecureWrite(key);
199
+ if (scope === StorageScope.Secure) {
200
+ const pending = durability.getPendingSecureWrite(key);
201
+ if (pending !== undefined) {
202
+ return pending.value;
203
+ }
180
204
  }
181
205
  return adapter.backend.get(key, scope);
182
206
  }
207
+ function getStoredRawValueForRepresentation(key, scope, representation) {
208
+ if (representation === "plain") {
209
+ return getStoredRawValue(key, scope);
210
+ }
211
+ return adapter.backend.getSecureBiometric(key);
212
+ }
183
213
  function getRawValue(key, scope) {
184
214
  assertValidScope(scope);
185
215
  if (scope === StorageScope.Memory) {
186
216
  const value = memoryStore.get(key);
187
217
  return typeof value === "string" ? unescapeCollidingRawValue(value) : undefined;
188
218
  }
189
- if (scope === StorageScope.Disk && hasPendingDiskWrite(key)) {
190
- const pending = readPendingDiskWrite(key);
191
- return pending === undefined ? undefined : unescapeCollidingRawValue(pending);
219
+ if (scope === StorageScope.Disk) {
220
+ const pending = durability.getPendingDiskWrite(key);
221
+ if (pending !== undefined) {
222
+ return pending.value === undefined ? undefined : unescapeCollidingRawValue(pending.value);
223
+ }
192
224
  }
193
- if (scope === StorageScope.Secure && hasPendingSecureWrite(key)) {
194
- const pending = readPendingSecureWrite(key);
195
- return pending === undefined ? undefined : unescapeCollidingRawValue(pending);
225
+ if (scope === StorageScope.Secure) {
226
+ const pending = durability.getPendingSecureWrite(key);
227
+ if (pending !== undefined) {
228
+ return pending.value === undefined ? undefined : unescapeCollidingRawValue(pending.value);
229
+ }
196
230
  }
197
231
  const raw = adapter.backend.get(key, scope);
198
232
  return raw === undefined ? undefined : unescapeCollidingRawValue(raw);
@@ -218,6 +252,7 @@ export function createStorageCore(buildAdapter) {
218
252
  clearPendingDiskWrite(key);
219
253
  }
220
254
  if (scope === StorageScope.Secure) {
255
+ invalidateRawCache(scope, key);
221
256
  flushSecureWrites();
222
257
  clearPendingSecureWrite(key);
223
258
  if (adapter.applyAccessControlOnSecureRawWrite) {
@@ -248,6 +283,7 @@ export function createStorageCore(buildAdapter) {
248
283
  clearPendingDiskWrite(key);
249
284
  }
250
285
  if (scope === StorageScope.Secure) {
286
+ invalidateRawCache(scope, key);
251
287
  flushSecureWrites();
252
288
  clearPendingSecureWrite(key);
253
289
  }
@@ -267,6 +303,7 @@ export function createStorageCore(buildAdapter) {
267
303
  getScopedListeners,
268
304
  cacheRawValue,
269
305
  readCachedRawValue,
306
+ invalidateRawCache,
270
307
  clearScopeRawCache,
271
308
  clearPendingDiskWrite,
272
309
  clearPendingSecureWrite,
@@ -320,6 +357,11 @@ export function createStorageCore(buildAdapter) {
320
357
  return;
321
358
  }
322
359
  const previousValues = shouldReadPreviousEventValues(scope) ? adapter.backend.getBatch(removeKeys, scope).map(value => value === undefined ? undefined : unescapeCollidingRawValue(value)) : [];
360
+ if (scope === StorageScope.Secure) {
361
+ removeKeys.forEach(key => {
362
+ invalidateRawCache(scope, key);
363
+ });
364
+ }
323
365
  adapter.backend.removeBatch(removeKeys, scope);
324
366
  removeKeys.forEach(key => {
325
367
  cacheRawValue(scope, key, undefined);
@@ -387,6 +429,7 @@ export function createStorageCore(buildAdapter) {
387
429
  const previousValues = shouldReadPreviousEventValues(scope) ? storage.getAll(scope) : {};
388
430
  if (scope === StorageScope.Memory) {
389
431
  memoryStore.clear();
432
+ memoryExpirationDeadlines.clear();
390
433
  notifyAllListeners(memoryListeners);
391
434
  emitBatchChange(scope, "clear", "memory", Object.keys(previousValues).map(key => createKeyChange(scope, key, previousValues[key], undefined, "clear", "memory")));
392
435
  return;
@@ -490,6 +533,7 @@ export function createStorageCore(buildAdapter) {
490
533
  }
491
534
  affectedKeys.forEach(key => {
492
535
  memoryStore.delete(key);
536
+ memoryExpirationDeadlines.delete(key);
493
537
  });
494
538
  affectedKeys.forEach(key => {
495
539
  notifyKeyListeners(memoryListeners, key);
@@ -520,7 +564,21 @@ export function createStorageCore(buildAdapter) {
520
564
  },
521
565
  clearBiometric: () => {
522
566
  measureOperation("storage:clearBiometric", StorageScope.Secure, () => {
523
- adapter.backend.clearSecureBiometric();
567
+ flushSecureWrites();
568
+ const readEventValues = shouldReadPreviousEventValues(StorageScope.Secure);
569
+ const shouldEmitChanges = storageEvents.hasListeners(StorageScope.Secure) || eventObserver !== undefined;
570
+ const biometricKeys = shouldEmitChanges ? adapter.backend.getAllKeys(StorageScope.Secure).filter(key => adapter.backend.hasSecureBiometric(key)) : [];
571
+ const previousValues = readEventValues ? biometricKeys.map(key => {
572
+ const raw = adapter.backend.getSecureBiometric(key);
573
+ return raw === undefined ? undefined : unescapeCollidingRawValue(raw);
574
+ }) : [];
575
+ clearScopeRawCache(StorageScope.Secure);
576
+ try {
577
+ adapter.backend.clearSecureBiometric();
578
+ } finally {
579
+ clearScopeRawCache(StorageScope.Secure);
580
+ }
581
+ emitBatchChange(StorageScope.Secure, "clear", adapter.changeSource, biometricKeys.map((key, index) => createKeyChange(StorageScope.Secure, key, readEventValues ? previousValues[index] : undefined, undefined, "clear", adapter.changeSource)));
524
582
  });
525
583
  },
526
584
  has: (key, scope) => {
@@ -745,6 +803,9 @@ export function createStorageCore(buildAdapter) {
745
803
  if (scope === StorageScope.Secure) {
746
804
  flushSecureWrites();
747
805
  adapter.backend.setSecureAccessControl(secureDefaultAccessControl);
806
+ keys.forEach(key => {
807
+ invalidateRawCache(scope, key);
808
+ });
748
809
  }
749
810
  if (scope === StorageScope.Disk) {
750
811
  flushDiskWrites();
@@ -770,8 +831,9 @@ export function createStorageCore(buildAdapter) {
770
831
  const expiration = config.expiration;
771
832
  const onExpired = config.onExpired;
772
833
  const expirationTtlMs = expiration?.ttlMs;
773
- const memoryExpiration = expiration && isMemory ? new Map() : null;
834
+ const memoryExpiration = expiration && isMemory ? memoryExpirationDeadlines : null;
774
835
  const readCache = !isMemory && config.readCache === true;
836
+ const rawCacheRepresentation = isBiometric ? "biometric" : "plain";
775
837
  const coalesceDiskWrites = config.scope === StorageScope.Disk && config.coalesceDiskWrites === true;
776
838
  const coalesceSecureWrites = config.scope === StorageScope.Secure && config.coalesceSecureWrites === true && !isBiometric;
777
839
  const defaultValue = config.defaultValue;
@@ -841,28 +903,44 @@ export function createStorageCore(buildAdapter) {
841
903
  const memoryStored = memoryStore.get(storageKey);
842
904
  return typeof memoryStored === "string" ? unescapeCollidingRawValue(memoryStored) : memoryStored;
843
905
  }
906
+ if (nonMemoryScope === StorageScope.Disk) {
907
+ const pending = durability.getPendingDiskWrite(storageKey);
908
+ if (pending !== undefined) {
909
+ return pending.value;
910
+ }
911
+ }
912
+ if (nonMemoryScope === StorageScope.Secure && !isBiometric) {
913
+ const pending = durability.getPendingSecureWrite(storageKey);
914
+ if (pending !== undefined) {
915
+ return pending.value;
916
+ }
917
+ }
844
918
  migrateRenamesIfNeeded();
845
919
  if (nonMemoryScope === StorageScope.Disk) {
846
- const pending = durability.readPendingDiskWrite(storageKey);
920
+ const pending = durability.getPendingDiskWrite(storageKey);
847
921
  if (pending !== undefined) {
848
- return pending;
922
+ return pending.value;
849
923
  }
850
924
  }
851
925
  if (nonMemoryScope === StorageScope.Secure && !isBiometric) {
852
- const pending = durability.readPendingSecureWrite(storageKey);
926
+ const pending = durability.getPendingSecureWrite(storageKey);
853
927
  if (pending !== undefined) {
854
- return pending;
928
+ return pending.value;
855
929
  }
856
930
  }
857
931
  if (readCache) {
858
- const cache = getScopeRawCache(resolveNonMemoryScope());
859
- const cached = cache.get(storageKey);
860
- if (cached !== undefined || cache.has(storageKey)) {
861
- return cached;
932
+ const scope = resolveNonMemoryScope();
933
+ const cachedEntry = getCachedRawValueEntry(scope, storageKey);
934
+ if (cachedEntry?.has(rawCacheRepresentation)) {
935
+ return cachedEntry.get(rawCacheRepresentation);
862
936
  }
863
937
  }
864
938
  if (isBiometric) {
865
- return readBackendRaw(() => adapter.backend.getSecureBiometric(storageKey));
939
+ const raw = readBackendRaw(() => adapter.backend.getSecureBiometric(storageKey));
940
+ if (readCache) {
941
+ cacheRawValue(resolveNonMemoryScope(), storageKey, raw, rawCacheRepresentation);
942
+ }
943
+ return raw;
866
944
  }
867
945
  const raw = readBackendRaw(() => adapter.backend.get(storageKey, config.scope));
868
946
  cacheRawValue(resolveNonMemoryScope(), storageKey, raw);
@@ -874,7 +952,8 @@ export function createStorageCore(buildAdapter) {
874
952
  } catch (error) {
875
953
  onReadError?.(error);
876
954
  if (fallbackToCacheOnReadError) {
877
- const cached = getScopeRawCache(resolveNonMemoryScope()).get(storageKey);
955
+ const scope = resolveNonMemoryScope();
956
+ const cached = readCachedRawValue(scope, storageKey, rawCacheRepresentation);
878
957
  if (cached !== undefined) {
879
958
  return cached;
880
959
  }
@@ -883,26 +962,378 @@ export function createStorageCore(buildAdapter) {
883
962
  throw error;
884
963
  }
885
964
  };
886
- const writeStoredRaw = rawValue => {
965
+ function getRenameSourceKeys() {
966
+ return renameFromKeys.filter(key => key !== storageKey);
967
+ }
968
+ function getRenameSourceRaw(key) {
969
+ return isBiometric ? adapter.backend.getSecureBiometric(key) : adapter.backend.get(key, config.scope);
970
+ }
971
+ function invalidateRenameSourceCache(key) {
972
+ if (nonMemoryScope !== null) {
973
+ invalidateRawCache(nonMemoryScope, key);
974
+ }
975
+ }
976
+ function removeRenameSource(key) {
977
+ invalidateRenameSourceCache(key);
978
+ if (isBiometric) {
979
+ adapter.backend.deleteSecureBiometric(key);
980
+ invalidateRenameSourceCache(key);
981
+ return;
982
+ }
983
+ adapter.backend.remove(key, config.scope);
984
+ invalidateRenameSourceCache(key);
985
+ }
986
+ function restoreRenameSource(snapshot) {
987
+ invalidateRenameSourceCache(snapshot.key);
988
+ if (nonMemoryScope === StorageScope.Secure) {
989
+ if (snapshot.biometricValue !== undefined) {
990
+ adapter.backend.setSecureBiometricWithLevel(snapshot.key, snapshot.biometricValue, resolvedBiometricLevel === BiometricLevel.None ? BiometricLevel.BiometryOnly : resolvedBiometricLevel);
991
+ } else {
992
+ adapter.backend.deleteSecureBiometric(snapshot.key);
993
+ }
994
+ if (snapshot.plainValue !== undefined) {
995
+ if (adapter.applyAccessControlOnSecureRawWrite) {
996
+ adapter.backend.setSecureAccessControl(secureAccessControl ?? secureDefaultAccessControl);
997
+ }
998
+ adapter.backend.set(snapshot.key, snapshot.plainValue, config.scope);
999
+ } else if (snapshot.biometricValue === undefined) {
1000
+ adapter.backend.remove(snapshot.key, config.scope);
1001
+ }
1002
+ invalidateRenameSourceCache(snapshot.key);
1003
+ return;
1004
+ }
1005
+ if (snapshot.plainValue !== undefined) {
1006
+ adapter.backend.set(snapshot.key, snapshot.plainValue, config.scope);
1007
+ } else {
1008
+ adapter.backend.remove(snapshot.key, config.scope);
1009
+ }
1010
+ invalidateRenameSourceCache(snapshot.key);
1011
+ }
1012
+ function readRenameSnapshots() {
1013
+ return getRenameSourceKeys().flatMap(key => {
1014
+ const plainValue = nonMemoryScope === StorageScope.Secure ? adapter.backend.get(key, config.scope) : getRenameSourceRaw(key);
1015
+ const biometricValue = nonMemoryScope === StorageScope.Secure ? adapter.backend.getSecureBiometric(key) : undefined;
1016
+ if (plainValue === undefined && biometricValue === undefined) {
1017
+ return [];
1018
+ }
1019
+ return [{
1020
+ key,
1021
+ ...(plainValue === undefined ? {} : {
1022
+ plainValue
1023
+ }),
1024
+ ...(biometricValue === undefined ? {} : {
1025
+ biometricValue
1026
+ })
1027
+ }];
1028
+ });
1029
+ }
1030
+ function selectedRenameValue(snapshot) {
1031
+ return isBiometric ? snapshot.biometricValue : snapshot.plainValue;
1032
+ }
1033
+ function hasPendingCurrentWrite() {
1034
+ if (nonMemoryScope === StorageScope.Disk) {
1035
+ return durability.hasPendingDiskWrite(storageKey);
1036
+ }
1037
+ if (nonMemoryScope === StorageScope.Secure && !isBiometric) {
1038
+ return durability.hasPendingSecureWrite(storageKey);
1039
+ }
1040
+ return false;
1041
+ }
1042
+ function hasPendingRenameSourceWrite() {
1043
+ if (isBiometric || nonMemoryScope === null) {
1044
+ return false;
1045
+ }
1046
+ return getRenameSourceKeys().some(key => nonMemoryScope === StorageScope.Disk ? durability.hasPendingDiskWrite(key) : durability.hasPendingSecureWrite(key));
1047
+ }
1048
+ function flushPendingRenameSourceWrites() {
1049
+ if (!hasPendingRenameSourceWrite()) {
1050
+ return;
1051
+ }
1052
+ if (nonMemoryScope === StorageScope.Disk) {
1053
+ flushDiskWrites();
1054
+ return;
1055
+ }
1056
+ flushSecureWrites();
1057
+ }
1058
+ function clearPendingCurrentWriteIf(write) {
1059
+ if (write === undefined) {
1060
+ return;
1061
+ }
1062
+ if (nonMemoryScope === StorageScope.Disk && "generation" in write) {
1063
+ durability.clearPendingDiskWriteIf(write);
1064
+ return;
1065
+ }
1066
+ if (nonMemoryScope === StorageScope.Secure && !isBiometric) {
1067
+ durability.clearPendingSecureWriteIf(write);
1068
+ }
1069
+ }
1070
+ function flushPendingCurrentWrite() {
1071
+ if (nonMemoryScope === StorageScope.Disk) {
1072
+ flushDiskWrites();
1073
+ return;
1074
+ }
1075
+ if (nonMemoryScope === StorageScope.Secure && !isBiometric) {
1076
+ flushSecureWrites();
1077
+ }
1078
+ }
1079
+ function removeCurrentBackendValue() {
1080
+ invalidateRawCache(resolveNonMemoryScope(), storageKey);
1081
+ if (isBiometric) {
1082
+ adapter.backend.deleteSecureBiometric(storageKey);
1083
+ return;
1084
+ }
1085
+ adapter.backend.remove(storageKey, config.scope);
1086
+ }
1087
+ function scheduleRenameSourceCleanup() {
1088
+ const sourceKeys = getRenameSourceKeys();
1089
+ sourceKeys.forEach(invalidateRenameSourceCache);
1090
+ if (isBiometric) {
1091
+ sourceKeys.forEach(removeRenameSource);
1092
+ return;
1093
+ }
1094
+ if (nonMemoryScope === StorageScope.Disk && (coalesceDiskWrites || isDiskWritesAsync())) {
1095
+ sourceKeys.forEach(key => {
1096
+ scheduleDiskWrite(key, undefined);
1097
+ });
1098
+ return;
1099
+ }
1100
+ if (nonMemoryScope === StorageScope.Secure && coalesceSecureWrites) {
1101
+ sourceKeys.forEach(key => {
1102
+ scheduleSecureWrite(key, undefined, secureAccessControl ?? secureDefaultAccessControl);
1103
+ });
1104
+ return;
1105
+ }
1106
+ sourceKeys.forEach(removeRenameSource);
1107
+ }
1108
+ let atomicMutationDepth = 0;
1109
+ function getItemStateKeys() {
1110
+ return Array.from(new Set([storageKey, ...getRenameSourceKeys()]));
1111
+ }
1112
+ function captureItemState() {
1113
+ const records = getItemStateKeys().map(key => {
1114
+ let pending;
1115
+ if (nonMemoryScope === StorageScope.Disk) {
1116
+ const pendingWrite = durability.getPendingDiskWrite(key);
1117
+ if (pendingWrite !== undefined) {
1118
+ pending = {
1119
+ value: pendingWrite.value
1120
+ };
1121
+ }
1122
+ } else if (nonMemoryScope === StorageScope.Secure && !isBiometric) {
1123
+ const pendingWrite = durability.getPendingSecureWrite(key);
1124
+ if (pendingWrite !== undefined) {
1125
+ pending = {
1126
+ value: pendingWrite.value,
1127
+ ...(pendingWrite.accessControl === undefined ? {} : {
1128
+ accessControl: pendingWrite.accessControl
1129
+ })
1130
+ };
1131
+ }
1132
+ }
1133
+ return {
1134
+ key,
1135
+ plainValue: nonMemoryScope === null ? undefined : adapter.backend.get(key, config.scope),
1136
+ biometricValue: nonMemoryScope === StorageScope.Secure ? adapter.backend.getSecureBiometric(key) : undefined,
1137
+ ...(pending === undefined ? {} : {
1138
+ pending
1139
+ })
1140
+ };
1141
+ });
1142
+ return {
1143
+ records,
1144
+ renamesMigrated
1145
+ };
1146
+ }
1147
+ function clearPendingItemWrite(key) {
1148
+ if (nonMemoryScope === StorageScope.Disk) {
1149
+ durability.clearPendingDiskWrite(key);
1150
+ } else if (nonMemoryScope === StorageScope.Secure && !isBiometric) {
1151
+ durability.clearPendingSecureWrite(key);
1152
+ }
1153
+ }
1154
+ function restoreItemState(snapshot) {
1155
+ const rollbackErrors = [];
1156
+ const recordsByKey = new Map(snapshot.records.map(record => [record.key, record]));
1157
+ snapshot.records.forEach(({
1158
+ key
1159
+ }) => {
1160
+ clearPendingItemWrite(key);
1161
+ if (nonMemoryScope !== null) {
1162
+ invalidateRawCache(nonMemoryScope, key);
1163
+ }
1164
+ });
1165
+ const biometricRecords = snapshot.records.filter(({
1166
+ biometricValue
1167
+ }) => biometricValue !== undefined);
1168
+ if (nonMemoryScope === StorageScope.Secure) {
1169
+ biometricRecords.forEach(({
1170
+ key,
1171
+ biometricValue
1172
+ }) => {
1173
+ try {
1174
+ adapter.backend.setSecureBiometricWithLevel(key, biometricValue, resolvedBiometricLevel === BiometricLevel.None ? BiometricLevel.BiometryOnly : resolvedBiometricLevel);
1175
+ cacheRawValue(StorageScope.Secure, key, biometricValue, "biometric");
1176
+ } catch (error) {
1177
+ rollbackErrors.push({
1178
+ label: "rollback biometric",
1179
+ error
1180
+ });
1181
+ }
1182
+ });
1183
+ }
1184
+ const plainSets = new Map();
1185
+ const plainRemoves = [];
1186
+ snapshot.records.forEach(({
1187
+ key,
1188
+ plainValue,
1189
+ biometricValue
1190
+ }) => {
1191
+ if (plainValue === undefined) {
1192
+ if (biometricValue === undefined) {
1193
+ plainRemoves.push(key);
1194
+ }
1195
+ return;
1196
+ }
1197
+ const accessControl = secureAccessControl ?? secureDefaultAccessControl;
1198
+ const group = plainSets.get(accessControl) ?? {
1199
+ keys: [],
1200
+ values: []
1201
+ };
1202
+ group.keys.push(key);
1203
+ group.values.push(plainValue);
1204
+ plainSets.set(accessControl, group);
1205
+ });
1206
+ plainSets.forEach((group, accessControl) => {
1207
+ try {
1208
+ if (nonMemoryScope === StorageScope.Secure) {
1209
+ adapter.backend.setSecureAccessControl(accessControl);
1210
+ }
1211
+ adapter.backend.setBatch(group.keys, group.values, config.scope);
1212
+ group.keys.forEach((key, index) => {
1213
+ cacheRawValue(resolveNonMemoryScope(), key, group.values[index], "plain");
1214
+ });
1215
+ } catch (error) {
1216
+ rollbackErrors.push({
1217
+ label: "rollback plain set",
1218
+ error
1219
+ });
1220
+ }
1221
+ });
1222
+ if (plainRemoves.length > 0) {
1223
+ try {
1224
+ adapter.backend.removeBatch(plainRemoves, config.scope);
1225
+ plainRemoves.forEach(key => {
1226
+ cacheRawValue(resolveNonMemoryScope(), key, undefined, "plain");
1227
+ });
1228
+ } catch (error) {
1229
+ rollbackErrors.push({
1230
+ label: "rollback plain remove",
1231
+ error
1232
+ });
1233
+ }
1234
+ }
1235
+ if (nonMemoryScope === StorageScope.Secure) {
1236
+ snapshot.records.forEach(({
1237
+ key,
1238
+ biometricValue
1239
+ }) => {
1240
+ if (biometricValue !== undefined) {
1241
+ return;
1242
+ }
1243
+ try {
1244
+ adapter.backend.deleteSecureBiometric(key);
1245
+ cacheRawValue(StorageScope.Secure, key, undefined, "biometric");
1246
+ } catch (error) {
1247
+ rollbackErrors.push({
1248
+ label: "rollback biometric remove",
1249
+ error
1250
+ });
1251
+ }
1252
+ });
1253
+ }
1254
+ snapshot.records.forEach(({
1255
+ key,
1256
+ pending
1257
+ }) => {
1258
+ if (pending === undefined || nonMemoryScope === null) {
1259
+ return;
1260
+ }
1261
+ if (nonMemoryScope === StorageScope.Disk) {
1262
+ scheduleDiskWrite(key, pending.value);
1263
+ } else if (!isBiometric) {
1264
+ scheduleSecureWrite(key, pending.value, pending.accessControl ?? secureAccessControl ?? secureDefaultAccessControl);
1265
+ }
1266
+ });
1267
+ recordsByKey.forEach(({
1268
+ key
1269
+ }) => {
1270
+ if (nonMemoryScope !== null) {
1271
+ invalidateRawCache(nonMemoryScope, key);
1272
+ }
1273
+ });
1274
+ renamesMigrated = snapshot.renamesMigrated;
1275
+ invalidateParsedCache();
1276
+ return rollbackErrors;
1277
+ }
1278
+ function runAtomicItemMutation(mutation) {
1279
+ if (isMemory || renameFromKeys.length === 0 || atomicMutationDepth > 0) {
1280
+ mutation();
1281
+ return;
1282
+ }
1283
+ const snapshot = captureItemState();
1284
+ atomicMutationDepth += 1;
1285
+ try {
1286
+ mutation();
1287
+ } catch (primaryError) {
1288
+ const rollbackErrors = restoreItemState(snapshot);
1289
+ if (rollbackErrors.length > 0) {
1290
+ throw createStorageCompositeError("item mutation rollback", primaryError, rollbackErrors);
1291
+ }
1292
+ throw primaryError;
1293
+ } finally {
1294
+ atomicMutationDepth -= 1;
1295
+ }
1296
+ }
1297
+ const writeStoredRaw = (rawValue, options = {}) => {
1298
+ const cleanupRenameSources = options.cleanupRenameSources !== false;
887
1299
  const oldValue = undefined;
888
1300
  if (isBiometric) {
889
- adapter.backend.setSecureBiometricWithLevel(storageKey, rawValue, resolvedBiometricLevel);
1301
+ invalidateRawCache(StorageScope.Secure, storageKey);
1302
+ runSecurePromotion(storageKey, () => {
1303
+ try {
1304
+ adapter.backend.setSecureBiometricWithLevel(storageKey, rawValue, resolvedBiometricLevel);
1305
+ } catch (error) {
1306
+ throw normalizeStorageError(error);
1307
+ }
1308
+ });
1309
+ if (cleanupRenameSources) {
1310
+ scheduleRenameSourceCleanup();
1311
+ }
890
1312
  emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", adapter.changeSource);
891
- return;
1313
+ return undefined;
1314
+ }
1315
+ if (nonMemoryScope === StorageScope.Secure) {
1316
+ invalidateRawCache(StorageScope.Secure, storageKey);
892
1317
  }
893
1318
  cacheRawValue(resolveNonMemoryScope(), storageKey, rawValue);
894
1319
  if (nonMemoryScope === StorageScope.Disk) {
895
1320
  if (coalesceDiskWrites || isDiskWritesAsync()) {
896
- scheduleDiskWrite(storageKey, rawValue);
1321
+ const pendingWrite = scheduleDiskWrite(storageKey, rawValue);
1322
+ if (cleanupRenameSources) {
1323
+ scheduleRenameSourceCleanup();
1324
+ }
897
1325
  emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", adapter.changeSource);
898
- return;
1326
+ return pendingWrite;
899
1327
  }
900
1328
  clearPendingDiskWrite(storageKey);
901
1329
  }
902
1330
  if (coalesceSecureWrites) {
903
- scheduleSecureWrite(storageKey, rawValue, secureAccessControl ?? secureDefaultAccessControl);
1331
+ const pendingWrite = scheduleSecureWrite(storageKey, rawValue, secureAccessControl ?? secureDefaultAccessControl);
1332
+ if (cleanupRenameSources) {
1333
+ scheduleRenameSourceCleanup();
1334
+ }
904
1335
  emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", adapter.changeSource);
905
- return;
1336
+ return pendingWrite;
906
1337
  }
907
1338
  if (nonMemoryScope === StorageScope.Secure) {
908
1339
  clearPendingSecureWrite(storageKey);
@@ -911,41 +1342,102 @@ export function createStorageCore(buildAdapter) {
911
1342
  }
912
1343
  }
913
1344
  adapter.backend.set(storageKey, rawValue, config.scope);
1345
+ if (cleanupRenameSources) {
1346
+ scheduleRenameSourceCleanup();
1347
+ }
914
1348
  emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", adapter.changeSource);
1349
+ return undefined;
915
1350
  };
916
1351
  const migrateRenamesIfNeeded = () => {
917
1352
  if (renamesMigrated) {
918
1353
  return;
919
1354
  }
920
1355
  try {
1356
+ flushPendingRenameSourceWrites();
921
1357
  const hasCurrent = isBiometric ? adapter.backend.hasSecureBiometric(storageKey) : adapter.backend.has(storageKey, config.scope);
1358
+ const snapshots = readRenameSnapshots();
922
1359
  if (hasCurrent) {
923
- for (const legacyKey of renameFromKeys) {
924
- if (isBiometric) {
925
- if (adapter.backend.hasSecureBiometric(legacyKey)) {
926
- adapter.backend.deleteSecureBiometric(legacyKey);
1360
+ try {
1361
+ snapshots.forEach(({
1362
+ key
1363
+ }) => {
1364
+ removeRenameSource(key);
1365
+ });
1366
+ } catch (primaryError) {
1367
+ const rollbackErrors = [];
1368
+ snapshots.forEach(snapshot => {
1369
+ try {
1370
+ restoreRenameSource(snapshot);
1371
+ } catch (error) {
1372
+ rollbackErrors.push({
1373
+ label: "rollback rename source",
1374
+ error
1375
+ });
927
1376
  }
928
- } else if (adapter.backend.has(legacyKey, config.scope)) {
929
- adapter.backend.remove(legacyKey, config.scope);
1377
+ });
1378
+ if (rollbackErrors.length > 0) {
1379
+ throw createStorageCompositeError("rename cleanup", primaryError, rollbackErrors);
930
1380
  }
1381
+ throw primaryError;
931
1382
  }
932
1383
  renamesMigrated = true;
933
1384
  return;
934
1385
  }
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;
1386
+ const snapshot = snapshots.find(candidate => selectedRenameValue(candidate) !== undefined);
1387
+ if (snapshot === undefined) {
1388
+ renamesMigrated = true;
1389
+ return;
1390
+ }
1391
+ let writeAttempted = false;
1392
+ let pendingCurrentWrite;
1393
+ try {
1394
+ writeAttempted = true;
1395
+ pendingCurrentWrite = writeStoredRaw(selectedRenameValue(snapshot), {
1396
+ cleanupRenameSources: false
1397
+ });
1398
+ if (hasPendingCurrentWrite()) {
1399
+ flushPendingCurrentWrite();
939
1400
  }
940
- writeStoredRaw(legacyRaw);
941
- if (isBiometric) {
942
- adapter.backend.deleteSecureBiometric(legacyKey);
943
- } else {
944
- adapter.backend.remove(legacyKey, config.scope);
1401
+ snapshots.forEach(({
1402
+ key
1403
+ }) => {
1404
+ removeRenameSource(key);
1405
+ });
1406
+ renamesMigrated = true;
1407
+ } catch (primaryError) {
1408
+ const rollbackErrors = [];
1409
+ clearPendingCurrentWriteIf(pendingCurrentWrite);
1410
+ if (writeAttempted) {
1411
+ try {
1412
+ removeCurrentBackendValue();
1413
+ } catch (error) {
1414
+ rollbackErrors.push({
1415
+ label: "rollback current value",
1416
+ error
1417
+ });
1418
+ }
1419
+ }
1420
+ snapshots.forEach(renameSnapshot => {
1421
+ try {
1422
+ restoreRenameSource(renameSnapshot);
1423
+ } catch (error) {
1424
+ rollbackErrors.push({
1425
+ label: "rollback rename source",
1426
+ error
1427
+ });
1428
+ }
1429
+ });
1430
+ invalidateRawCache(resolveNonMemoryScope(), storageKey);
1431
+ snapshots.forEach(({
1432
+ key
1433
+ }) => {
1434
+ invalidateRenameSourceCache(key);
1435
+ });
1436
+ if (rollbackErrors.length > 0) {
1437
+ throw createStorageCompositeError("rename migration", primaryError, rollbackErrors);
945
1438
  }
946
- break;
1439
+ throw primaryError;
947
1440
  }
948
- renamesMigrated = true;
949
1441
  } catch (error) {
950
1442
  if (isKeychainLockedError(error)) {
951
1443
  onReadError?.(error);
@@ -955,16 +1447,22 @@ export function createStorageCore(buildAdapter) {
955
1447
  }
956
1448
  };
957
1449
  const removeStoredRaw = (operation = "remove") => {
958
- const oldValue = getEventRawValue(config.scope, storageKey);
1450
+ const oldValue = isBiometric ? getEventRawValueForRepresentation(config.scope, storageKey, "biometric") : getEventRawValue(config.scope, storageKey);
959
1451
  if (isBiometric) {
1452
+ invalidateRawCache(StorageScope.Secure, storageKey);
1453
+ scheduleRenameSourceCleanup();
960
1454
  adapter.backend.deleteSecureBiometric(storageKey);
961
1455
  emitKeyChange(config.scope, storageKey, oldValue, undefined, operation, adapter.changeSource);
962
1456
  return;
963
1457
  }
1458
+ if (nonMemoryScope === StorageScope.Secure) {
1459
+ invalidateRawCache(StorageScope.Secure, storageKey);
1460
+ }
964
1461
  cacheRawValue(resolveNonMemoryScope(), storageKey, undefined);
965
1462
  if (nonMemoryScope === StorageScope.Disk) {
966
1463
  if (coalesceDiskWrites || isDiskWritesAsync()) {
967
1464
  scheduleDiskWrite(storageKey, undefined);
1465
+ scheduleRenameSourceCleanup();
968
1466
  emitKeyChange(config.scope, storageKey, oldValue, undefined, operation, adapter.changeSource);
969
1467
  return;
970
1468
  }
@@ -972,12 +1470,14 @@ export function createStorageCore(buildAdapter) {
972
1470
  }
973
1471
  if (coalesceSecureWrites) {
974
1472
  scheduleSecureWrite(storageKey, undefined, secureAccessControl ?? secureDefaultAccessControl);
1473
+ scheduleRenameSourceCleanup();
975
1474
  emitKeyChange(config.scope, storageKey, oldValue, undefined, operation, adapter.changeSource);
976
1475
  return;
977
1476
  }
978
1477
  if (nonMemoryScope === StorageScope.Secure) {
979
1478
  clearPendingSecureWrite(storageKey);
980
1479
  }
1480
+ scheduleRenameSourceCleanup();
981
1481
  adapter.backend.remove(storageKey, config.scope);
982
1482
  emitKeyChange(config.scope, storageKey, oldValue, undefined, operation, adapter.changeSource);
983
1483
  };
@@ -986,6 +1486,8 @@ export function createStorageCore(buildAdapter) {
986
1486
  const oldValue = getEventRawValue(config.scope, storageKey);
987
1487
  if (memoryExpiration) {
988
1488
  memoryExpiration.set(storageKey, Date.now() + (expirationTtlMs ?? 0));
1489
+ } else {
1490
+ memoryExpirationDeadlines.delete(storageKey);
989
1491
  }
990
1492
  const storedValue = typeof value === "string" ? escapeCollidingRawValue(value) : value;
991
1493
  memoryStore.set(storageKey, storedValue);
@@ -1034,7 +1536,9 @@ export function createStorageCore(buildAdapter) {
1034
1536
  if (lastExpiresAt > Date.now()) {
1035
1537
  return lastValue;
1036
1538
  }
1037
- removeStoredRaw("expire");
1539
+ runAtomicItemMutation(() => {
1540
+ removeStoredRaw("expire");
1541
+ });
1038
1542
  invalidateParsedCache();
1039
1543
  onExpired?.(storageKey);
1040
1544
  lastValue = ensureValidatedValue(defaultValue, false);
@@ -1072,7 +1576,9 @@ export function createStorageCore(buildAdapter) {
1072
1576
  if (isStoredEnvelope(parsed)) {
1073
1577
  envelopeExpiresAt = parsed.expiresAt;
1074
1578
  if (parsed.expiresAt <= Date.now()) {
1075
- removeStoredRaw("expire");
1579
+ runAtomicItemMutation(() => {
1580
+ removeStoredRaw("expire");
1581
+ });
1076
1582
  invalidateParsedCache();
1077
1583
  onExpired?.(storageKey);
1078
1584
  lastValue = ensureValidatedValue(defaultValue, false);
@@ -1109,36 +1615,45 @@ export function createStorageCore(buildAdapter) {
1109
1615
  }));
1110
1616
  const set = valueOrFn => {
1111
1617
  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);
1618
+ runAtomicItemMutation(() => {
1619
+ const newValue = isUpdater(valueOrFn) ? valueOrFn(getInternal()) : valueOrFn;
1620
+ if (validate && !validate(newValue)) {
1621
+ throw new Error(`Validation failed for key "${storageKey}" in scope "${StorageScope[config.scope]}".`);
1622
+ }
1623
+ invalidateParsedCache();
1624
+ writeValueWithoutValidation(newValue);
1625
+ });
1118
1626
  });
1119
1627
  };
1120
1628
  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;
1629
+ let didSet = false;
1630
+ runAtomicItemMutation(() => {
1631
+ const currentVersion = getCurrentVersion();
1632
+ if (currentVersion !== version) {
1633
+ return;
1634
+ }
1635
+ set(valueOrFn);
1636
+ didSet = true;
1637
+ });
1638
+ return didSet;
1127
1639
  });
1128
1640
  const deleteItem = () => {
1129
1641
  measureOperation("item:delete", config.scope, () => {
1130
- invalidateParsedCache();
1131
- if (isMemory) {
1132
- const oldValue = getEventRawValue(config.scope, storageKey);
1133
- if (memoryExpiration) {
1134
- memoryExpiration.delete(storageKey);
1642
+ runAtomicItemMutation(() => {
1643
+ invalidateParsedCache();
1644
+ if (isMemory) {
1645
+ const oldValue = getEventRawValue(config.scope, storageKey);
1646
+ if (memoryExpiration) {
1647
+ memoryExpiration.delete(storageKey);
1648
+ }
1649
+ memoryExpirationDeadlines.delete(storageKey);
1650
+ memoryStore.delete(storageKey);
1651
+ notifyKeyListeners(memoryListeners, storageKey);
1652
+ emitKeyChange(config.scope, storageKey, oldValue, undefined, "remove", "memory");
1653
+ return;
1135
1654
  }
1136
- memoryStore.delete(storageKey);
1137
- notifyKeyListeners(memoryListeners, storageKey);
1138
- emitKeyChange(config.scope, storageKey, oldValue, undefined, "remove", "memory");
1139
- return;
1140
- }
1141
- removeStoredRaw();
1655
+ removeStoredRaw();
1656
+ });
1142
1657
  });
1143
1658
  };
1144
1659
  const merge = partial => {
@@ -1166,13 +1681,15 @@ export function createStorageCore(buildAdapter) {
1166
1681
  if (isMemory) return memoryStore.has(storageKey);
1167
1682
  if (isBiometric) return adapter.backend.hasSecureBiometric(storageKey);
1168
1683
  if (nonMemoryScope === StorageScope.Disk) {
1169
- if (durability.hasPendingDiskWrite(storageKey)) {
1170
- return durability.readPendingDiskWrite(storageKey) !== undefined;
1684
+ const pending = durability.getPendingDiskWrite(storageKey);
1685
+ if (pending !== undefined) {
1686
+ return pending.value !== undefined;
1171
1687
  }
1172
1688
  }
1173
1689
  if (nonMemoryScope === StorageScope.Secure) {
1174
- if (durability.hasPendingSecureWrite(storageKey)) {
1175
- return durability.readPendingSecureWrite(storageKey) !== undefined;
1690
+ const pending = durability.getPendingSecureWrite(storageKey);
1691
+ if (pending !== undefined) {
1692
+ return pending.value !== undefined;
1176
1693
  }
1177
1694
  }
1178
1695
  return adapter.backend.has(storageKey, config.scope);
@@ -1231,14 +1748,18 @@ export function createStorageCore(buildAdapter) {
1231
1748
  invalidateParsedCache();
1232
1749
  },
1233
1750
  _deleteMemoryEntry: () => {
1234
- if (memoryExpiration) {
1235
- memoryExpiration.delete(storageKey);
1236
- }
1751
+ memoryExpirationDeadlines.delete(storageKey);
1237
1752
  memoryStore.delete(storageKey);
1238
1753
  invalidateParsedCache();
1239
1754
  },
1240
1755
  _hasValidation: validate !== undefined,
1241
1756
  _hasExpiration: expiration !== undefined,
1757
+ _hasRenameFrom: renameFromKeys.length > 0,
1758
+ _renameFromKeys: renameFromKeys,
1759
+ _getRenameMigrationState: () => renamesMigrated,
1760
+ _setRenameMigrationState: migrated => {
1761
+ renamesMigrated = migrated;
1762
+ },
1242
1763
  _readCacheEnabled: readCache,
1243
1764
  _isBiometric: isBiometric,
1244
1765
  _biometricLevel: resolvedBiometricLevel,
@@ -1260,6 +1781,14 @@ export function createStorageCore(buildAdapter) {
1260
1781
  }
1261
1782
  groupSet.add(storageItem);
1262
1783
  }
1784
+ if (isMemory) {
1785
+ let items = memoryItemsByKey.get(storageKey);
1786
+ if (!items) {
1787
+ items = new Set();
1788
+ memoryItemsByKey.set(storageKey, items);
1789
+ }
1790
+ items.add(storageItem);
1791
+ }
1263
1792
  const registryKey = `${config.scope}:${storageKey}`;
1264
1793
  registeredKeyCounts.set(registryKey, (registeredKeyCounts.get(registryKey) ?? 0) + 1);
1265
1794
  return storageItem;
@@ -1279,24 +1808,23 @@ export function createStorageCore(buildAdapter) {
1279
1808
  const keyIndexes = [];
1280
1809
  items.forEach((item, index) => {
1281
1810
  if (scope === StorageScope.Disk) {
1282
- const pending = durability.readPendingDiskWrite(item.key);
1811
+ const pending = durability.getPendingDiskWrite(item.key);
1283
1812
  if (pending !== undefined) {
1284
- rawValues[index] = pending;
1813
+ rawValues[index] = pending.value;
1285
1814
  return;
1286
1815
  }
1287
1816
  }
1288
1817
  if (scope === StorageScope.Secure) {
1289
- const pending = durability.readPendingSecureWrite(item.key);
1818
+ const pending = durability.getPendingSecureWrite(item.key);
1290
1819
  if (pending !== undefined) {
1291
- rawValues[index] = pending;
1820
+ rawValues[index] = pending.value;
1292
1821
  return;
1293
1822
  }
1294
1823
  }
1295
1824
  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;
1825
+ const cachedEntry = getCachedRawValueEntry(scope, item.key);
1826
+ if (cachedEntry?.has("plain")) {
1827
+ rawValues[index] = cachedEntry.get("plain");
1300
1828
  return;
1301
1829
  }
1302
1830
  }
@@ -1391,6 +1919,10 @@ export function createStorageCore(buildAdapter) {
1391
1919
  const keys = secureEntries.map(({
1392
1920
  item
1393
1921
  }) => item.key);
1922
+ keys.forEach(key => {
1923
+ invalidateRawCache(scope, key);
1924
+ });
1925
+ const serializedValues = [];
1394
1926
  const oldValues = shouldReadPreviousEventValues(scope) ? adapter.backend.getBatch(keys, scope).map(value => value === undefined ? undefined : unescapeCollidingRawValue(value)) : [];
1395
1927
  const groupedByAccessControl = new Map();
1396
1928
  secureEntries.forEach(({
@@ -1398,6 +1930,8 @@ export function createStorageCore(buildAdapter) {
1398
1930
  value,
1399
1931
  internal
1400
1932
  }) => {
1933
+ const serialized = item.serialize(value);
1934
+ serializedValues.push(serialized);
1401
1935
  const accessControl = internal._secureAccessControl ?? secureDefaultAccessControl;
1402
1936
  const existingGroup = groupedByAccessControl.get(accessControl);
1403
1937
  const group = existingGroup ?? {
@@ -1405,7 +1939,7 @@ export function createStorageCore(buildAdapter) {
1405
1939
  values: []
1406
1940
  };
1407
1941
  group.keys.push(item.key);
1408
- group.values.push(item.serialize(value));
1942
+ group.values.push(serialized);
1409
1943
  if (!existingGroup) {
1410
1944
  groupedByAccessControl.set(accessControl, group);
1411
1945
  }
@@ -1417,10 +1951,8 @@ export function createStorageCore(buildAdapter) {
1417
1951
  cacheRawValue(scope, key, group.values[index]);
1418
1952
  });
1419
1953
  });
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)));
1954
+ const willEmitChanges = storageEvents.hasListeners(scope) || eventObserver !== undefined;
1955
+ emitBatchChange(scope, "setBatch", adapter.changeSource, willEmitChanges ? keys.map((key, index) => createKeyChange(scope, key, oldValues[index], serializedValues[index], "setBatch", adapter.changeSource)) : []);
1424
1956
  return;
1425
1957
  }
1426
1958
  flushDiskWrites();
@@ -1460,6 +1992,12 @@ export function createStorageCore(buildAdapter) {
1460
1992
  emitBatchChange(scope, "removeBatch", "memory", changes);
1461
1993
  return;
1462
1994
  }
1995
+ if (items.some(item => asInternal(item)._hasRenameFrom)) {
1996
+ items.forEach(item => {
1997
+ asInternal(item).delete();
1998
+ });
1999
+ return;
2000
+ }
1463
2001
  const keys = items.map(item => item.key);
1464
2002
  if (scope === StorageScope.Disk) {
1465
2003
  flushDiskWrites();
@@ -1467,7 +2005,25 @@ export function createStorageCore(buildAdapter) {
1467
2005
  if (scope === StorageScope.Secure) {
1468
2006
  flushSecureWrites();
1469
2007
  }
1470
- const oldValues = shouldReadPreviousEventValues(scope) ? adapter.backend.getBatch(keys, scope).map(value => value === undefined ? undefined : unescapeCollidingRawValue(value)) : [];
2008
+ const oldValues = shouldReadPreviousEventValues(scope) ? (() => {
2009
+ const plainItems = items.filter(item => asInternal(item)._isBiometric !== true);
2010
+ const plainValues = plainItems.length === 0 ? [] : adapter.backend.getBatch(plainItems.map(item => item.key), scope).map(value => value === undefined ? undefined : unescapeCollidingRawValue(value));
2011
+ let plainIndex = 0;
2012
+ return items.map(item => {
2013
+ const internal = asInternal(item);
2014
+ if (internal._isBiometric === true) {
2015
+ return getEventRawValueForRepresentation(scope, item.key, "biometric");
2016
+ }
2017
+ const value = plainValues[plainIndex];
2018
+ plainIndex += 1;
2019
+ return value;
2020
+ });
2021
+ })() : [];
2022
+ if (scope === StorageScope.Secure) {
2023
+ keys.forEach(key => {
2024
+ invalidateRawCache(scope, key);
2025
+ });
2026
+ }
1471
2027
  adapter.backend.removeBatch(keys, scope);
1472
2028
  keys.forEach(key => {
1473
2029
  cacheRawValue(scope, key, undefined);
@@ -1515,33 +2071,74 @@ export function createStorageCore(buildAdapter) {
1515
2071
  }
1516
2072
  const NOT_SET = Symbol();
1517
2073
  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
2074
+ const itemRenameStates = new Map();
2075
+ const rememberRollback = (key, item, includeOtherSecureRepresentation = false) => {
2076
+ const internal = item ? item : undefined;
2077
+ const representation = scope === StorageScope.Secure && internal?._isBiometric === true ? "biometric" : "plain";
2078
+ const rememberRepresentation = representationToRemember => {
2079
+ const identity = JSON.stringify([scope, key, representationToRemember]);
2080
+ if (rollback.has(identity)) {
2081
+ return;
2082
+ }
2083
+ if (scope === StorageScope.Memory) {
2084
+ const expiresAt = memoryExpirationDeadlines.get(key);
2085
+ rollback.set(identity, {
2086
+ key,
2087
+ representation: representationToRemember,
2088
+ record: {
2089
+ kind: "memory",
2090
+ value: memoryStore.has(key) ? memoryStore.get(key) : NOT_SET,
2091
+ ...(expiresAt === undefined ? {} : {
2092
+ expiresAt
2093
+ })
2094
+ }
1534
2095
  });
1535
2096
  return;
1536
2097
  }
1537
- rollback.set(key, {
1538
- kind: "raw",
1539
- value: getStoredRawValue(key, scope),
1540
- ...(scope === StorageScope.Secure && internal?._secureAccessControl !== undefined ? {
1541
- accessControl: internal._secureAccessControl
1542
- } : {})
2098
+ if (representationToRemember === "biometric") {
2099
+ rollback.set(identity, {
2100
+ key,
2101
+ representation: representationToRemember,
2102
+ record: {
2103
+ kind: "biometric",
2104
+ value: getStoredRawValueForRepresentation(key, scope, representationToRemember),
2105
+ level: internal?._biometricLevel !== undefined && internal._biometricLevel !== BiometricLevel.None ? internal._biometricLevel : BiometricLevel.BiometryOnly
2106
+ }
2107
+ });
2108
+ return;
2109
+ }
2110
+ rollback.set(identity, {
2111
+ key,
2112
+ representation: representationToRemember,
2113
+ record: {
2114
+ kind: "raw",
2115
+ value: getStoredRawValueForRepresentation(key, scope, representationToRemember),
2116
+ ...(scope === StorageScope.Secure && internal?._secureAccessControl !== undefined ? {
2117
+ accessControl: internal._secureAccessControl
2118
+ } : {})
2119
+ }
1543
2120
  });
2121
+ };
2122
+ if (representation === "biometric" || includeOtherSecureRepresentation) {
2123
+ rememberRepresentation("plain");
2124
+ }
2125
+ rememberRepresentation(representation);
2126
+ if (includeOtherSecureRepresentation) {
2127
+ rememberRepresentation("biometric");
2128
+ }
2129
+ };
2130
+ const rememberItemRollback = item => {
2131
+ const internal = item;
2132
+ const includeOtherSecureRepresentation = scope === StorageScope.Secure && (internal._isBiometric === true || internal._hasRenameFrom === true);
2133
+ rememberRollback(item.key, item, includeOtherSecureRepresentation);
2134
+ if (internal._getRenameMigrationState) {
2135
+ if (!itemRenameStates.has(internal)) {
2136
+ itemRenameStates.set(internal, internal._getRenameMigrationState());
2137
+ }
1544
2138
  }
2139
+ (internal._renameFromKeys ?? EMPTY_KEYS).forEach(aliasKey => {
2140
+ rememberRollback(aliasKey, item, true);
2141
+ });
1545
2142
  };
1546
2143
  const tx = {
1547
2144
  scope,
@@ -1551,49 +2148,85 @@ export function createStorageCore(buildAdapter) {
1551
2148
  setRawValue(key, value, scope);
1552
2149
  },
1553
2150
  removeRaw: key => {
1554
- rememberRollback(key);
2151
+ rememberRollback(key, undefined, scope === StorageScope.Secure);
1555
2152
  removeRawValue(key, scope);
1556
2153
  },
1557
2154
  getItem: item => {
1558
2155
  assertBatchScope([item], scope);
2156
+ rememberItemRollback(item);
1559
2157
  return item.get();
1560
2158
  },
1561
2159
  setItem: (item, value) => {
1562
2160
  assertBatchScope([item], scope);
1563
- rememberRollback(item.key, item);
2161
+ rememberItemRollback(item);
1564
2162
  item.set(value);
1565
2163
  },
1566
2164
  removeItem: item => {
1567
2165
  assertBatchScope([item], scope);
1568
- rememberRollback(item.key, item);
2166
+ rememberItemRollback(item);
1569
2167
  item.delete();
1570
2168
  }
1571
2169
  };
1572
2170
  try {
1573
2171
  return transaction(tx);
1574
2172
  } catch (error) {
1575
- const rollbackEntries = Array.from(rollback.entries()).reverse();
2173
+ itemRenameStates.forEach((migrated, item) => {
2174
+ item._setRenameMigrationState(migrated);
2175
+ });
2176
+ const rollbackEntries = Array.from(rollback.values()).reverse();
1576
2177
  const rollbackSource = scope === StorageScope.Memory ? "memory" : adapter.changeSource;
1577
- const preRollbackValues = rollbackEntries.length > 0 ? rollbackEntries.map(([key]) => getEventRawValue(scope, key)) : [];
2178
+ const rollbackErrors = [];
2179
+ const readRollbackEventValue = entry => {
2180
+ try {
2181
+ return getEventRawValueForRepresentation(scope, entry.key, entry.representation);
2182
+ } catch (readError) {
2183
+ rollbackErrors.push({
2184
+ label: `rollback read ${entry.representation}:${entry.key}`,
2185
+ error: readError
2186
+ });
2187
+ return undefined;
2188
+ }
2189
+ };
2190
+ const preRollbackValues = rollbackEntries.map(readRollbackEventValue);
1578
2191
  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);
2192
+ rollbackEntries.forEach(entry => {
2193
+ try {
2194
+ const record = entry.record;
2195
+ if (record.kind !== "memory") {
2196
+ return;
2197
+ }
2198
+ if (record.value === NOT_SET) {
2199
+ memoryStore.delete(entry.key);
2200
+ } else {
2201
+ memoryStore.set(entry.key, record.value);
2202
+ }
2203
+ if (record.expiresAt === undefined) {
2204
+ memoryExpirationDeadlines.delete(entry.key);
2205
+ } else {
2206
+ memoryExpirationDeadlines.set(entry.key, record.expiresAt);
2207
+ }
2208
+ invalidateMemoryItemCaches(entry.key);
2209
+ notifyKeyListeners(memoryListeners, entry.key);
2210
+ } catch (rollbackError) {
2211
+ rollbackErrors.push({
2212
+ label: `rollback ${entry.representation}:${entry.key}`,
2213
+ error: rollbackError
2214
+ });
1584
2215
  }
1585
- notifyKeyListeners(memoryListeners, key);
1586
2216
  });
1587
2217
  } else {
1588
2218
  const groupedKeysToSet = new Map();
1589
2219
  const keysToRemove = [];
1590
- rollbackEntries.forEach(([key, record]) => {
2220
+ const biometricEntries = [];
2221
+ const biometricValues = new Map();
2222
+ rollbackEntries.forEach(entry => {
2223
+ const {
2224
+ key,
2225
+ record
2226
+ } = entry;
1591
2227
  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
- }
2228
+ biometricEntries.push(entry);
2229
+ biometricValues.set(key, record.value);
1597
2230
  return;
1598
2231
  }
1599
2232
  if (record.kind !== "raw") {
@@ -1616,29 +2249,108 @@ export function createStorageCore(buildAdapter) {
1616
2249
  }
1617
2250
  });
1618
2251
  if (scope === StorageScope.Disk) {
1619
- flushDiskWrites();
2252
+ try {
2253
+ flushDiskWrites();
2254
+ } catch (flushError) {
2255
+ rollbackErrors.push({
2256
+ label: "rollback flush disk",
2257
+ error: flushError
2258
+ });
2259
+ }
1620
2260
  }
1621
2261
  if (scope === StorageScope.Secure) {
1622
- flushSecureWrites();
2262
+ try {
2263
+ flushSecureWrites();
2264
+ } catch (flushError) {
2265
+ rollbackErrors.push({
2266
+ label: "rollback flush secure",
2267
+ error: flushError
2268
+ });
2269
+ }
2270
+ }
2271
+ if (scope === StorageScope.Secure) {
2272
+ rollbackEntries.forEach(entry => {
2273
+ invalidateRawCache(scope, entry.key);
2274
+ });
1623
2275
  }
2276
+ biometricEntries.forEach(entry => {
2277
+ const {
2278
+ key,
2279
+ record
2280
+ } = entry;
2281
+ if (record.kind !== "biometric" || record.value === undefined) {
2282
+ return;
2283
+ }
2284
+ try {
2285
+ adapter.backend.setSecureBiometricWithLevel(key, record.value, record.level);
2286
+ cacheRawValue(StorageScope.Secure, key, record.value, "biometric");
2287
+ } catch (rollbackError) {
2288
+ rollbackErrors.push({
2289
+ label: "rollback biometric",
2290
+ error: rollbackError
2291
+ });
2292
+ }
2293
+ });
1624
2294
  groupedKeysToSet.forEach((group, accessControl) => {
1625
- if (scope === StorageScope.Secure) {
1626
- adapter.backend.setSecureAccessControl(accessControl);
2295
+ try {
2296
+ if (scope === StorageScope.Secure) {
2297
+ adapter.backend.setSecureAccessControl(accessControl);
2298
+ }
2299
+ adapter.backend.setBatch(group.keys, group.values, scope);
2300
+ group.keys.forEach((key, index) => {
2301
+ cacheRawValue(scope, key, group.values[index], "plain");
2302
+ });
2303
+ } catch (rollbackError) {
2304
+ rollbackErrors.push({
2305
+ label: "rollback plain setBatch",
2306
+ error: rollbackError
2307
+ });
1627
2308
  }
1628
- adapter.backend.setBatch(group.keys, group.values, scope);
1629
- group.keys.forEach((key, index) => {
1630
- cacheRawValue(scope, key, group.values[index]);
1631
- });
1632
2309
  });
1633
2310
  if (keysToRemove.length > 0) {
1634
- adapter.backend.removeBatch(keysToRemove, scope);
1635
- keysToRemove.forEach(key => {
1636
- cacheRawValue(scope, key, undefined);
1637
- });
2311
+ const keysToRemoveWithoutBiometric = keysToRemove.filter(key => biometricValues.get(key) === undefined);
2312
+ try {
2313
+ if (keysToRemoveWithoutBiometric.length > 0) {
2314
+ adapter.backend.removeBatch(keysToRemoveWithoutBiometric, scope);
2315
+ }
2316
+ keysToRemove.forEach(key => {
2317
+ cacheRawValue(scope, key, undefined, "plain");
2318
+ });
2319
+ } catch (rollbackError) {
2320
+ rollbackErrors.push({
2321
+ label: "rollback plain removeBatch",
2322
+ error: rollbackError
2323
+ });
2324
+ }
1638
2325
  }
2326
+ biometricEntries.forEach(entry => {
2327
+ const {
2328
+ key,
2329
+ record
2330
+ } = entry;
2331
+ if (record.kind !== "biometric") {
2332
+ return;
2333
+ }
2334
+ if (record.value !== undefined) {
2335
+ return;
2336
+ }
2337
+ invalidateRawCache(StorageScope.Secure, key);
2338
+ try {
2339
+ adapter.backend.deleteSecureBiometric(key);
2340
+ cacheRawValue(StorageScope.Secure, key, record.value, "biometric");
2341
+ } catch (rollbackError) {
2342
+ rollbackErrors.push({
2343
+ label: "rollback biometric",
2344
+ error: rollbackError
2345
+ });
2346
+ }
2347
+ });
1639
2348
  }
1640
2349
  if (rollbackEntries.length > 0) {
1641
- emitBatchChange(scope, "rollback", rollbackSource, rollbackEntries.map(([key], index) => createKeyChange(scope, key, preRollbackValues[index], getEventRawValue(scope, key), "rollback", rollbackSource)));
2350
+ emitBatchChange(scope, "rollback", rollbackSource, rollbackEntries.map((entry, index) => createKeyChange(scope, entry.key, preRollbackValues[index], readRollbackEventValue(entry), "rollback", rollbackSource)));
2351
+ }
2352
+ if (rollbackErrors.length > 0) {
2353
+ throw createStorageCompositeError("transaction rollback", error, rollbackErrors);
1642
2354
  }
1643
2355
  throw error;
1644
2356
  }
@@ -1746,8 +2458,16 @@ export function createStorageCore(buildAdapter) {
1746
2458
  add(id);
1747
2459
  return true;
1748
2460
  };
2461
+ const getTyped = () => {
2462
+ const typed = {};
2463
+ for (const id of Object.keys(item.get())) {
2464
+ typed[id] = true;
2465
+ }
2466
+ return typed;
2467
+ };
1749
2468
  return {
1750
2469
  get: item.get,
2470
+ getTyped,
1751
2471
  has,
1752
2472
  add,
1753
2473
  delete: deleteId,