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
@@ -19,6 +19,8 @@ function createStorageCore(buildAdapter) {
19
19
  const itemGroups = new Map();
20
20
  const registeredKeyCounts = new Map();
21
21
  const memoryStore = new Map();
22
+ const memoryExpirationDeadlines = new Map();
23
+ const memoryItemsByKey = new Map();
22
24
  const memoryListeners = new Map();
23
25
  const scopedListeners = {
24
26
  [_Storage.StorageScope.Disk]: new Map(),
@@ -53,15 +55,33 @@ function createStorageCore(buildAdapter) {
53
55
  function getScopeRawCache(scope) {
54
56
  return scopedRawCache[scope];
55
57
  }
56
- function cacheRawValue(scope, key, value) {
57
- getScopeRawCache(scope).set(key, value);
58
+ function getCachedRawValueEntry(scope, key, create = false) {
59
+ const scopeCache = getScopeRawCache(scope);
60
+ const existing = scopeCache.get(key);
61
+ if (existing || !create) {
62
+ return existing;
63
+ }
64
+ const entry = new Map();
65
+ scopeCache.set(key, entry);
66
+ return entry;
67
+ }
68
+ function cacheRawValue(scope, key, value, representation = "plain") {
69
+ getCachedRawValueEntry(scope, key, true)?.set(representation, value);
70
+ }
71
+ function readCachedRawValue(scope, key, representation = "plain") {
72
+ return getCachedRawValueEntry(scope, key)?.get(representation);
58
73
  }
59
- function readCachedRawValue(scope, key) {
60
- return getScopeRawCache(scope).get(key);
74
+ function invalidateRawCache(scope, key) {
75
+ getScopeRawCache(scope).delete(key);
61
76
  }
62
77
  function clearScopeRawCache(scope) {
63
78
  getScopeRawCache(scope).clear();
64
79
  }
80
+ function invalidateMemoryItemCaches(key) {
81
+ memoryItemsByKey.get(key)?.forEach(item => {
82
+ item._invalidateParsedCacheOnly();
83
+ });
84
+ }
65
85
  function addKeyListener(registry, key, listener) {
66
86
  let listeners = registry.get(key);
67
87
  if (!listeners) {
@@ -87,6 +107,13 @@ function createStorageCore(buildAdapter) {
87
107
  }
88
108
  return getRawValue(key, scope);
89
109
  }
110
+ function getEventRawValueForRepresentation(scope, key, representation) {
111
+ if (representation === "plain") {
112
+ return getEventRawValue(scope, key);
113
+ }
114
+ const raw = adapter.backend.getSecureBiometric(key);
115
+ return raw === undefined ? undefined : (0, _internal.unescapeCollidingRawValue)(raw);
116
+ }
90
117
  function shouldReadPreviousEventValues(scope) {
91
118
  if (storageEvents.hasListeners(scope)) {
92
119
  return true;
@@ -135,18 +162,6 @@ function createStorageCore(buildAdapter) {
135
162
  function recordMetric(operation, scope, durationMs, keysCount = 1) {
136
163
  metrics.record(operation, scope, durationMs, keysCount);
137
164
  }
138
- function readPendingSecureWrite(key) {
139
- return durability.readPendingSecureWrite(key);
140
- }
141
- function readPendingDiskWrite(key) {
142
- return durability.readPendingDiskWrite(key);
143
- }
144
- function hasPendingDiskWrite(key) {
145
- return durability.hasPendingDiskWrite(key);
146
- }
147
- function hasPendingSecureWrite(key) {
148
- return durability.hasPendingSecureWrite(key);
149
- }
150
165
  function clearPendingDiskWrite(key) {
151
166
  durability.clearPendingDiskWrite(key);
152
167
  }
@@ -159,11 +174,14 @@ function createStorageCore(buildAdapter) {
159
174
  function flushSecureWrites() {
160
175
  durability.flushSecureWrites();
161
176
  }
177
+ function runSecurePromotion(key, promotion) {
178
+ return durability.runSecurePromotion(key, promotion);
179
+ }
162
180
  function scheduleDiskWrite(key, value) {
163
- durability.scheduleDiskWrite(key, value);
181
+ return durability.scheduleDiskWrite(key, value);
164
182
  }
165
183
  function scheduleSecureWrite(key, value, accessControl) {
166
- durability.scheduleSecureWrite(key, value, accessControl);
184
+ return durability.scheduleSecureWrite(key, value, accessControl);
167
185
  }
168
186
  function setDiskWritesAsyncMode(enabled) {
169
187
  durability.setDiskWritesAsync(enabled);
@@ -176,27 +194,43 @@ function createStorageCore(buildAdapter) {
176
194
  const value = memoryStore.get(key);
177
195
  return typeof value === "string" ? value : undefined;
178
196
  }
179
- if (scope === _Storage.StorageScope.Disk && hasPendingDiskWrite(key)) {
180
- return readPendingDiskWrite(key);
197
+ if (scope === _Storage.StorageScope.Disk) {
198
+ const pending = durability.getPendingDiskWrite(key);
199
+ if (pending !== undefined) {
200
+ return pending.value;
201
+ }
181
202
  }
182
- if (scope === _Storage.StorageScope.Secure && hasPendingSecureWrite(key)) {
183
- return readPendingSecureWrite(key);
203
+ if (scope === _Storage.StorageScope.Secure) {
204
+ const pending = durability.getPendingSecureWrite(key);
205
+ if (pending !== undefined) {
206
+ return pending.value;
207
+ }
184
208
  }
185
209
  return adapter.backend.get(key, scope);
186
210
  }
211
+ function getStoredRawValueForRepresentation(key, scope, representation) {
212
+ if (representation === "plain") {
213
+ return getStoredRawValue(key, scope);
214
+ }
215
+ return adapter.backend.getSecureBiometric(key);
216
+ }
187
217
  function getRawValue(key, scope) {
188
218
  (0, _internal.assertValidScope)(scope);
189
219
  if (scope === _Storage.StorageScope.Memory) {
190
220
  const value = memoryStore.get(key);
191
221
  return typeof value === "string" ? (0, _internal.unescapeCollidingRawValue)(value) : undefined;
192
222
  }
193
- if (scope === _Storage.StorageScope.Disk && hasPendingDiskWrite(key)) {
194
- const pending = readPendingDiskWrite(key);
195
- return pending === undefined ? undefined : (0, _internal.unescapeCollidingRawValue)(pending);
223
+ if (scope === _Storage.StorageScope.Disk) {
224
+ const pending = durability.getPendingDiskWrite(key);
225
+ if (pending !== undefined) {
226
+ return pending.value === undefined ? undefined : (0, _internal.unescapeCollidingRawValue)(pending.value);
227
+ }
196
228
  }
197
- if (scope === _Storage.StorageScope.Secure && hasPendingSecureWrite(key)) {
198
- const pending = readPendingSecureWrite(key);
199
- return pending === undefined ? undefined : (0, _internal.unescapeCollidingRawValue)(pending);
229
+ if (scope === _Storage.StorageScope.Secure) {
230
+ const pending = durability.getPendingSecureWrite(key);
231
+ if (pending !== undefined) {
232
+ return pending.value === undefined ? undefined : (0, _internal.unescapeCollidingRawValue)(pending.value);
233
+ }
200
234
  }
201
235
  const raw = adapter.backend.get(key, scope);
202
236
  return raw === undefined ? undefined : (0, _internal.unescapeCollidingRawValue)(raw);
@@ -222,6 +256,7 @@ function createStorageCore(buildAdapter) {
222
256
  clearPendingDiskWrite(key);
223
257
  }
224
258
  if (scope === _Storage.StorageScope.Secure) {
259
+ invalidateRawCache(scope, key);
225
260
  flushSecureWrites();
226
261
  clearPendingSecureWrite(key);
227
262
  if (adapter.applyAccessControlOnSecureRawWrite) {
@@ -252,6 +287,7 @@ function createStorageCore(buildAdapter) {
252
287
  clearPendingDiskWrite(key);
253
288
  }
254
289
  if (scope === _Storage.StorageScope.Secure) {
290
+ invalidateRawCache(scope, key);
255
291
  flushSecureWrites();
256
292
  clearPendingSecureWrite(key);
257
293
  }
@@ -271,6 +307,7 @@ function createStorageCore(buildAdapter) {
271
307
  getScopedListeners,
272
308
  cacheRawValue,
273
309
  readCachedRawValue,
310
+ invalidateRawCache,
274
311
  clearScopeRawCache,
275
312
  clearPendingDiskWrite,
276
313
  clearPendingSecureWrite,
@@ -324,6 +361,11 @@ function createStorageCore(buildAdapter) {
324
361
  return;
325
362
  }
326
363
  const previousValues = shouldReadPreviousEventValues(scope) ? adapter.backend.getBatch(removeKeys, scope).map(value => value === undefined ? undefined : (0, _internal.unescapeCollidingRawValue)(value)) : [];
364
+ if (scope === _Storage.StorageScope.Secure) {
365
+ removeKeys.forEach(key => {
366
+ invalidateRawCache(scope, key);
367
+ });
368
+ }
327
369
  adapter.backend.removeBatch(removeKeys, scope);
328
370
  removeKeys.forEach(key => {
329
371
  cacheRawValue(scope, key, undefined);
@@ -391,6 +433,7 @@ function createStorageCore(buildAdapter) {
391
433
  const previousValues = shouldReadPreviousEventValues(scope) ? storage.getAll(scope) : {};
392
434
  if (scope === _Storage.StorageScope.Memory) {
393
435
  memoryStore.clear();
436
+ memoryExpirationDeadlines.clear();
394
437
  (0, _shared.notifyAllListeners)(memoryListeners);
395
438
  emitBatchChange(scope, "clear", "memory", Object.keys(previousValues).map(key => (0, _shared.createKeyChange)(scope, key, previousValues[key], undefined, "clear", "memory")));
396
439
  return;
@@ -494,6 +537,7 @@ function createStorageCore(buildAdapter) {
494
537
  }
495
538
  affectedKeys.forEach(key => {
496
539
  memoryStore.delete(key);
540
+ memoryExpirationDeadlines.delete(key);
497
541
  });
498
542
  affectedKeys.forEach(key => {
499
543
  (0, _shared.notifyKeyListeners)(memoryListeners, key);
@@ -524,7 +568,21 @@ function createStorageCore(buildAdapter) {
524
568
  },
525
569
  clearBiometric: () => {
526
570
  measureOperation("storage:clearBiometric", _Storage.StorageScope.Secure, () => {
527
- adapter.backend.clearSecureBiometric();
571
+ flushSecureWrites();
572
+ const readEventValues = shouldReadPreviousEventValues(_Storage.StorageScope.Secure);
573
+ const shouldEmitChanges = storageEvents.hasListeners(_Storage.StorageScope.Secure) || eventObserver !== undefined;
574
+ const biometricKeys = shouldEmitChanges ? adapter.backend.getAllKeys(_Storage.StorageScope.Secure).filter(key => adapter.backend.hasSecureBiometric(key)) : [];
575
+ const previousValues = readEventValues ? biometricKeys.map(key => {
576
+ const raw = adapter.backend.getSecureBiometric(key);
577
+ return raw === undefined ? undefined : (0, _internal.unescapeCollidingRawValue)(raw);
578
+ }) : [];
579
+ clearScopeRawCache(_Storage.StorageScope.Secure);
580
+ try {
581
+ adapter.backend.clearSecureBiometric();
582
+ } finally {
583
+ clearScopeRawCache(_Storage.StorageScope.Secure);
584
+ }
585
+ emitBatchChange(_Storage.StorageScope.Secure, "clear", adapter.changeSource, biometricKeys.map((key, index) => (0, _shared.createKeyChange)(_Storage.StorageScope.Secure, key, readEventValues ? previousValues[index] : undefined, undefined, "clear", adapter.changeSource)));
528
586
  });
529
587
  },
530
588
  has: (key, scope) => {
@@ -749,6 +807,9 @@ function createStorageCore(buildAdapter) {
749
807
  if (scope === _Storage.StorageScope.Secure) {
750
808
  flushSecureWrites();
751
809
  adapter.backend.setSecureAccessControl(secureDefaultAccessControl);
810
+ keys.forEach(key => {
811
+ invalidateRawCache(scope, key);
812
+ });
752
813
  }
753
814
  if (scope === _Storage.StorageScope.Disk) {
754
815
  flushDiskWrites();
@@ -774,8 +835,9 @@ function createStorageCore(buildAdapter) {
774
835
  const expiration = config.expiration;
775
836
  const onExpired = config.onExpired;
776
837
  const expirationTtlMs = expiration?.ttlMs;
777
- const memoryExpiration = expiration && isMemory ? new Map() : null;
838
+ const memoryExpiration = expiration && isMemory ? memoryExpirationDeadlines : null;
778
839
  const readCache = !isMemory && config.readCache === true;
840
+ const rawCacheRepresentation = isBiometric ? "biometric" : "plain";
779
841
  const coalesceDiskWrites = config.scope === _Storage.StorageScope.Disk && config.coalesceDiskWrites === true;
780
842
  const coalesceSecureWrites = config.scope === _Storage.StorageScope.Secure && config.coalesceSecureWrites === true && !isBiometric;
781
843
  const defaultValue = config.defaultValue;
@@ -845,28 +907,44 @@ function createStorageCore(buildAdapter) {
845
907
  const memoryStored = memoryStore.get(storageKey);
846
908
  return typeof memoryStored === "string" ? (0, _internal.unescapeCollidingRawValue)(memoryStored) : memoryStored;
847
909
  }
910
+ if (nonMemoryScope === _Storage.StorageScope.Disk) {
911
+ const pending = durability.getPendingDiskWrite(storageKey);
912
+ if (pending !== undefined) {
913
+ return pending.value;
914
+ }
915
+ }
916
+ if (nonMemoryScope === _Storage.StorageScope.Secure && !isBiometric) {
917
+ const pending = durability.getPendingSecureWrite(storageKey);
918
+ if (pending !== undefined) {
919
+ return pending.value;
920
+ }
921
+ }
848
922
  migrateRenamesIfNeeded();
849
923
  if (nonMemoryScope === _Storage.StorageScope.Disk) {
850
- const pending = durability.readPendingDiskWrite(storageKey);
924
+ const pending = durability.getPendingDiskWrite(storageKey);
851
925
  if (pending !== undefined) {
852
- return pending;
926
+ return pending.value;
853
927
  }
854
928
  }
855
929
  if (nonMemoryScope === _Storage.StorageScope.Secure && !isBiometric) {
856
- const pending = durability.readPendingSecureWrite(storageKey);
930
+ const pending = durability.getPendingSecureWrite(storageKey);
857
931
  if (pending !== undefined) {
858
- return pending;
932
+ return pending.value;
859
933
  }
860
934
  }
861
935
  if (readCache) {
862
- const cache = getScopeRawCache(resolveNonMemoryScope());
863
- const cached = cache.get(storageKey);
864
- if (cached !== undefined || cache.has(storageKey)) {
865
- return cached;
936
+ const scope = resolveNonMemoryScope();
937
+ const cachedEntry = getCachedRawValueEntry(scope, storageKey);
938
+ if (cachedEntry?.has(rawCacheRepresentation)) {
939
+ return cachedEntry.get(rawCacheRepresentation);
866
940
  }
867
941
  }
868
942
  if (isBiometric) {
869
- return readBackendRaw(() => adapter.backend.getSecureBiometric(storageKey));
943
+ const raw = readBackendRaw(() => adapter.backend.getSecureBiometric(storageKey));
944
+ if (readCache) {
945
+ cacheRawValue(resolveNonMemoryScope(), storageKey, raw, rawCacheRepresentation);
946
+ }
947
+ return raw;
870
948
  }
871
949
  const raw = readBackendRaw(() => adapter.backend.get(storageKey, config.scope));
872
950
  cacheRawValue(resolveNonMemoryScope(), storageKey, raw);
@@ -878,7 +956,8 @@ function createStorageCore(buildAdapter) {
878
956
  } catch (error) {
879
957
  onReadError?.(error);
880
958
  if (fallbackToCacheOnReadError) {
881
- const cached = getScopeRawCache(resolveNonMemoryScope()).get(storageKey);
959
+ const scope = resolveNonMemoryScope();
960
+ const cached = readCachedRawValue(scope, storageKey, rawCacheRepresentation);
882
961
  if (cached !== undefined) {
883
962
  return cached;
884
963
  }
@@ -887,26 +966,378 @@ function createStorageCore(buildAdapter) {
887
966
  throw error;
888
967
  }
889
968
  };
890
- const writeStoredRaw = rawValue => {
969
+ function getRenameSourceKeys() {
970
+ return renameFromKeys.filter(key => key !== storageKey);
971
+ }
972
+ function getRenameSourceRaw(key) {
973
+ return isBiometric ? adapter.backend.getSecureBiometric(key) : adapter.backend.get(key, config.scope);
974
+ }
975
+ function invalidateRenameSourceCache(key) {
976
+ if (nonMemoryScope !== null) {
977
+ invalidateRawCache(nonMemoryScope, key);
978
+ }
979
+ }
980
+ function removeRenameSource(key) {
981
+ invalidateRenameSourceCache(key);
982
+ if (isBiometric) {
983
+ adapter.backend.deleteSecureBiometric(key);
984
+ invalidateRenameSourceCache(key);
985
+ return;
986
+ }
987
+ adapter.backend.remove(key, config.scope);
988
+ invalidateRenameSourceCache(key);
989
+ }
990
+ function restoreRenameSource(snapshot) {
991
+ invalidateRenameSourceCache(snapshot.key);
992
+ if (nonMemoryScope === _Storage.StorageScope.Secure) {
993
+ if (snapshot.biometricValue !== undefined) {
994
+ adapter.backend.setSecureBiometricWithLevel(snapshot.key, snapshot.biometricValue, resolvedBiometricLevel === _Storage.BiometricLevel.None ? _Storage.BiometricLevel.BiometryOnly : resolvedBiometricLevel);
995
+ } else {
996
+ adapter.backend.deleteSecureBiometric(snapshot.key);
997
+ }
998
+ if (snapshot.plainValue !== undefined) {
999
+ if (adapter.applyAccessControlOnSecureRawWrite) {
1000
+ adapter.backend.setSecureAccessControl(secureAccessControl ?? secureDefaultAccessControl);
1001
+ }
1002
+ adapter.backend.set(snapshot.key, snapshot.plainValue, config.scope);
1003
+ } else if (snapshot.biometricValue === undefined) {
1004
+ adapter.backend.remove(snapshot.key, config.scope);
1005
+ }
1006
+ invalidateRenameSourceCache(snapshot.key);
1007
+ return;
1008
+ }
1009
+ if (snapshot.plainValue !== undefined) {
1010
+ adapter.backend.set(snapshot.key, snapshot.plainValue, config.scope);
1011
+ } else {
1012
+ adapter.backend.remove(snapshot.key, config.scope);
1013
+ }
1014
+ invalidateRenameSourceCache(snapshot.key);
1015
+ }
1016
+ function readRenameSnapshots() {
1017
+ return getRenameSourceKeys().flatMap(key => {
1018
+ const plainValue = nonMemoryScope === _Storage.StorageScope.Secure ? adapter.backend.get(key, config.scope) : getRenameSourceRaw(key);
1019
+ const biometricValue = nonMemoryScope === _Storage.StorageScope.Secure ? adapter.backend.getSecureBiometric(key) : undefined;
1020
+ if (plainValue === undefined && biometricValue === undefined) {
1021
+ return [];
1022
+ }
1023
+ return [{
1024
+ key,
1025
+ ...(plainValue === undefined ? {} : {
1026
+ plainValue
1027
+ }),
1028
+ ...(biometricValue === undefined ? {} : {
1029
+ biometricValue
1030
+ })
1031
+ }];
1032
+ });
1033
+ }
1034
+ function selectedRenameValue(snapshot) {
1035
+ return isBiometric ? snapshot.biometricValue : snapshot.plainValue;
1036
+ }
1037
+ function hasPendingCurrentWrite() {
1038
+ if (nonMemoryScope === _Storage.StorageScope.Disk) {
1039
+ return durability.hasPendingDiskWrite(storageKey);
1040
+ }
1041
+ if (nonMemoryScope === _Storage.StorageScope.Secure && !isBiometric) {
1042
+ return durability.hasPendingSecureWrite(storageKey);
1043
+ }
1044
+ return false;
1045
+ }
1046
+ function hasPendingRenameSourceWrite() {
1047
+ if (isBiometric || nonMemoryScope === null) {
1048
+ return false;
1049
+ }
1050
+ return getRenameSourceKeys().some(key => nonMemoryScope === _Storage.StorageScope.Disk ? durability.hasPendingDiskWrite(key) : durability.hasPendingSecureWrite(key));
1051
+ }
1052
+ function flushPendingRenameSourceWrites() {
1053
+ if (!hasPendingRenameSourceWrite()) {
1054
+ return;
1055
+ }
1056
+ if (nonMemoryScope === _Storage.StorageScope.Disk) {
1057
+ flushDiskWrites();
1058
+ return;
1059
+ }
1060
+ flushSecureWrites();
1061
+ }
1062
+ function clearPendingCurrentWriteIf(write) {
1063
+ if (write === undefined) {
1064
+ return;
1065
+ }
1066
+ if (nonMemoryScope === _Storage.StorageScope.Disk && "generation" in write) {
1067
+ durability.clearPendingDiskWriteIf(write);
1068
+ return;
1069
+ }
1070
+ if (nonMemoryScope === _Storage.StorageScope.Secure && !isBiometric) {
1071
+ durability.clearPendingSecureWriteIf(write);
1072
+ }
1073
+ }
1074
+ function flushPendingCurrentWrite() {
1075
+ if (nonMemoryScope === _Storage.StorageScope.Disk) {
1076
+ flushDiskWrites();
1077
+ return;
1078
+ }
1079
+ if (nonMemoryScope === _Storage.StorageScope.Secure && !isBiometric) {
1080
+ flushSecureWrites();
1081
+ }
1082
+ }
1083
+ function removeCurrentBackendValue() {
1084
+ invalidateRawCache(resolveNonMemoryScope(), storageKey);
1085
+ if (isBiometric) {
1086
+ adapter.backend.deleteSecureBiometric(storageKey);
1087
+ return;
1088
+ }
1089
+ adapter.backend.remove(storageKey, config.scope);
1090
+ }
1091
+ function scheduleRenameSourceCleanup() {
1092
+ const sourceKeys = getRenameSourceKeys();
1093
+ sourceKeys.forEach(invalidateRenameSourceCache);
1094
+ if (isBiometric) {
1095
+ sourceKeys.forEach(removeRenameSource);
1096
+ return;
1097
+ }
1098
+ if (nonMemoryScope === _Storage.StorageScope.Disk && (coalesceDiskWrites || isDiskWritesAsync())) {
1099
+ sourceKeys.forEach(key => {
1100
+ scheduleDiskWrite(key, undefined);
1101
+ });
1102
+ return;
1103
+ }
1104
+ if (nonMemoryScope === _Storage.StorageScope.Secure && coalesceSecureWrites) {
1105
+ sourceKeys.forEach(key => {
1106
+ scheduleSecureWrite(key, undefined, secureAccessControl ?? secureDefaultAccessControl);
1107
+ });
1108
+ return;
1109
+ }
1110
+ sourceKeys.forEach(removeRenameSource);
1111
+ }
1112
+ let atomicMutationDepth = 0;
1113
+ function getItemStateKeys() {
1114
+ return Array.from(new Set([storageKey, ...getRenameSourceKeys()]));
1115
+ }
1116
+ function captureItemState() {
1117
+ const records = getItemStateKeys().map(key => {
1118
+ let pending;
1119
+ if (nonMemoryScope === _Storage.StorageScope.Disk) {
1120
+ const pendingWrite = durability.getPendingDiskWrite(key);
1121
+ if (pendingWrite !== undefined) {
1122
+ pending = {
1123
+ value: pendingWrite.value
1124
+ };
1125
+ }
1126
+ } else if (nonMemoryScope === _Storage.StorageScope.Secure && !isBiometric) {
1127
+ const pendingWrite = durability.getPendingSecureWrite(key);
1128
+ if (pendingWrite !== undefined) {
1129
+ pending = {
1130
+ value: pendingWrite.value,
1131
+ ...(pendingWrite.accessControl === undefined ? {} : {
1132
+ accessControl: pendingWrite.accessControl
1133
+ })
1134
+ };
1135
+ }
1136
+ }
1137
+ return {
1138
+ key,
1139
+ plainValue: nonMemoryScope === null ? undefined : adapter.backend.get(key, config.scope),
1140
+ biometricValue: nonMemoryScope === _Storage.StorageScope.Secure ? adapter.backend.getSecureBiometric(key) : undefined,
1141
+ ...(pending === undefined ? {} : {
1142
+ pending
1143
+ })
1144
+ };
1145
+ });
1146
+ return {
1147
+ records,
1148
+ renamesMigrated
1149
+ };
1150
+ }
1151
+ function clearPendingItemWrite(key) {
1152
+ if (nonMemoryScope === _Storage.StorageScope.Disk) {
1153
+ durability.clearPendingDiskWrite(key);
1154
+ } else if (nonMemoryScope === _Storage.StorageScope.Secure && !isBiometric) {
1155
+ durability.clearPendingSecureWrite(key);
1156
+ }
1157
+ }
1158
+ function restoreItemState(snapshot) {
1159
+ const rollbackErrors = [];
1160
+ const recordsByKey = new Map(snapshot.records.map(record => [record.key, record]));
1161
+ snapshot.records.forEach(({
1162
+ key
1163
+ }) => {
1164
+ clearPendingItemWrite(key);
1165
+ if (nonMemoryScope !== null) {
1166
+ invalidateRawCache(nonMemoryScope, key);
1167
+ }
1168
+ });
1169
+ const biometricRecords = snapshot.records.filter(({
1170
+ biometricValue
1171
+ }) => biometricValue !== undefined);
1172
+ if (nonMemoryScope === _Storage.StorageScope.Secure) {
1173
+ biometricRecords.forEach(({
1174
+ key,
1175
+ biometricValue
1176
+ }) => {
1177
+ try {
1178
+ adapter.backend.setSecureBiometricWithLevel(key, biometricValue, resolvedBiometricLevel === _Storage.BiometricLevel.None ? _Storage.BiometricLevel.BiometryOnly : resolvedBiometricLevel);
1179
+ cacheRawValue(_Storage.StorageScope.Secure, key, biometricValue, "biometric");
1180
+ } catch (error) {
1181
+ rollbackErrors.push({
1182
+ label: "rollback biometric",
1183
+ error
1184
+ });
1185
+ }
1186
+ });
1187
+ }
1188
+ const plainSets = new Map();
1189
+ const plainRemoves = [];
1190
+ snapshot.records.forEach(({
1191
+ key,
1192
+ plainValue,
1193
+ biometricValue
1194
+ }) => {
1195
+ if (plainValue === undefined) {
1196
+ if (biometricValue === undefined) {
1197
+ plainRemoves.push(key);
1198
+ }
1199
+ return;
1200
+ }
1201
+ const accessControl = secureAccessControl ?? secureDefaultAccessControl;
1202
+ const group = plainSets.get(accessControl) ?? {
1203
+ keys: [],
1204
+ values: []
1205
+ };
1206
+ group.keys.push(key);
1207
+ group.values.push(plainValue);
1208
+ plainSets.set(accessControl, group);
1209
+ });
1210
+ plainSets.forEach((group, accessControl) => {
1211
+ try {
1212
+ if (nonMemoryScope === _Storage.StorageScope.Secure) {
1213
+ adapter.backend.setSecureAccessControl(accessControl);
1214
+ }
1215
+ adapter.backend.setBatch(group.keys, group.values, config.scope);
1216
+ group.keys.forEach((key, index) => {
1217
+ cacheRawValue(resolveNonMemoryScope(), key, group.values[index], "plain");
1218
+ });
1219
+ } catch (error) {
1220
+ rollbackErrors.push({
1221
+ label: "rollback plain set",
1222
+ error
1223
+ });
1224
+ }
1225
+ });
1226
+ if (plainRemoves.length > 0) {
1227
+ try {
1228
+ adapter.backend.removeBatch(plainRemoves, config.scope);
1229
+ plainRemoves.forEach(key => {
1230
+ cacheRawValue(resolveNonMemoryScope(), key, undefined, "plain");
1231
+ });
1232
+ } catch (error) {
1233
+ rollbackErrors.push({
1234
+ label: "rollback plain remove",
1235
+ error
1236
+ });
1237
+ }
1238
+ }
1239
+ if (nonMemoryScope === _Storage.StorageScope.Secure) {
1240
+ snapshot.records.forEach(({
1241
+ key,
1242
+ biometricValue
1243
+ }) => {
1244
+ if (biometricValue !== undefined) {
1245
+ return;
1246
+ }
1247
+ try {
1248
+ adapter.backend.deleteSecureBiometric(key);
1249
+ cacheRawValue(_Storage.StorageScope.Secure, key, undefined, "biometric");
1250
+ } catch (error) {
1251
+ rollbackErrors.push({
1252
+ label: "rollback biometric remove",
1253
+ error
1254
+ });
1255
+ }
1256
+ });
1257
+ }
1258
+ snapshot.records.forEach(({
1259
+ key,
1260
+ pending
1261
+ }) => {
1262
+ if (pending === undefined || nonMemoryScope === null) {
1263
+ return;
1264
+ }
1265
+ if (nonMemoryScope === _Storage.StorageScope.Disk) {
1266
+ scheduleDiskWrite(key, pending.value);
1267
+ } else if (!isBiometric) {
1268
+ scheduleSecureWrite(key, pending.value, pending.accessControl ?? secureAccessControl ?? secureDefaultAccessControl);
1269
+ }
1270
+ });
1271
+ recordsByKey.forEach(({
1272
+ key
1273
+ }) => {
1274
+ if (nonMemoryScope !== null) {
1275
+ invalidateRawCache(nonMemoryScope, key);
1276
+ }
1277
+ });
1278
+ renamesMigrated = snapshot.renamesMigrated;
1279
+ invalidateParsedCache();
1280
+ return rollbackErrors;
1281
+ }
1282
+ function runAtomicItemMutation(mutation) {
1283
+ if (isMemory || renameFromKeys.length === 0 || atomicMutationDepth > 0) {
1284
+ mutation();
1285
+ return;
1286
+ }
1287
+ const snapshot = captureItemState();
1288
+ atomicMutationDepth += 1;
1289
+ try {
1290
+ mutation();
1291
+ } catch (primaryError) {
1292
+ const rollbackErrors = restoreItemState(snapshot);
1293
+ if (rollbackErrors.length > 0) {
1294
+ throw (0, _shared.createStorageCompositeError)("item mutation rollback", primaryError, rollbackErrors);
1295
+ }
1296
+ throw primaryError;
1297
+ } finally {
1298
+ atomicMutationDepth -= 1;
1299
+ }
1300
+ }
1301
+ const writeStoredRaw = (rawValue, options = {}) => {
1302
+ const cleanupRenameSources = options.cleanupRenameSources !== false;
891
1303
  const oldValue = undefined;
892
1304
  if (isBiometric) {
893
- adapter.backend.setSecureBiometricWithLevel(storageKey, rawValue, resolvedBiometricLevel);
1305
+ invalidateRawCache(_Storage.StorageScope.Secure, storageKey);
1306
+ runSecurePromotion(storageKey, () => {
1307
+ try {
1308
+ adapter.backend.setSecureBiometricWithLevel(storageKey, rawValue, resolvedBiometricLevel);
1309
+ } catch (error) {
1310
+ throw (0, _shared.normalizeStorageError)(error);
1311
+ }
1312
+ });
1313
+ if (cleanupRenameSources) {
1314
+ scheduleRenameSourceCleanup();
1315
+ }
894
1316
  emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", adapter.changeSource);
895
- return;
1317
+ return undefined;
1318
+ }
1319
+ if (nonMemoryScope === _Storage.StorageScope.Secure) {
1320
+ invalidateRawCache(_Storage.StorageScope.Secure, storageKey);
896
1321
  }
897
1322
  cacheRawValue(resolveNonMemoryScope(), storageKey, rawValue);
898
1323
  if (nonMemoryScope === _Storage.StorageScope.Disk) {
899
1324
  if (coalesceDiskWrites || isDiskWritesAsync()) {
900
- scheduleDiskWrite(storageKey, rawValue);
1325
+ const pendingWrite = scheduleDiskWrite(storageKey, rawValue);
1326
+ if (cleanupRenameSources) {
1327
+ scheduleRenameSourceCleanup();
1328
+ }
901
1329
  emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", adapter.changeSource);
902
- return;
1330
+ return pendingWrite;
903
1331
  }
904
1332
  clearPendingDiskWrite(storageKey);
905
1333
  }
906
1334
  if (coalesceSecureWrites) {
907
- scheduleSecureWrite(storageKey, rawValue, secureAccessControl ?? secureDefaultAccessControl);
1335
+ const pendingWrite = scheduleSecureWrite(storageKey, rawValue, secureAccessControl ?? secureDefaultAccessControl);
1336
+ if (cleanupRenameSources) {
1337
+ scheduleRenameSourceCleanup();
1338
+ }
908
1339
  emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", adapter.changeSource);
909
- return;
1340
+ return pendingWrite;
910
1341
  }
911
1342
  if (nonMemoryScope === _Storage.StorageScope.Secure) {
912
1343
  clearPendingSecureWrite(storageKey);
@@ -915,41 +1346,102 @@ function createStorageCore(buildAdapter) {
915
1346
  }
916
1347
  }
917
1348
  adapter.backend.set(storageKey, rawValue, config.scope);
1349
+ if (cleanupRenameSources) {
1350
+ scheduleRenameSourceCleanup();
1351
+ }
918
1352
  emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", adapter.changeSource);
1353
+ return undefined;
919
1354
  };
920
1355
  const migrateRenamesIfNeeded = () => {
921
1356
  if (renamesMigrated) {
922
1357
  return;
923
1358
  }
924
1359
  try {
1360
+ flushPendingRenameSourceWrites();
925
1361
  const hasCurrent = isBiometric ? adapter.backend.hasSecureBiometric(storageKey) : adapter.backend.has(storageKey, config.scope);
1362
+ const snapshots = readRenameSnapshots();
926
1363
  if (hasCurrent) {
927
- for (const legacyKey of renameFromKeys) {
928
- if (isBiometric) {
929
- if (adapter.backend.hasSecureBiometric(legacyKey)) {
930
- adapter.backend.deleteSecureBiometric(legacyKey);
1364
+ try {
1365
+ snapshots.forEach(({
1366
+ key
1367
+ }) => {
1368
+ removeRenameSource(key);
1369
+ });
1370
+ } catch (primaryError) {
1371
+ const rollbackErrors = [];
1372
+ snapshots.forEach(snapshot => {
1373
+ try {
1374
+ restoreRenameSource(snapshot);
1375
+ } catch (error) {
1376
+ rollbackErrors.push({
1377
+ label: "rollback rename source",
1378
+ error
1379
+ });
931
1380
  }
932
- } else if (adapter.backend.has(legacyKey, config.scope)) {
933
- adapter.backend.remove(legacyKey, config.scope);
1381
+ });
1382
+ if (rollbackErrors.length > 0) {
1383
+ throw (0, _shared.createStorageCompositeError)("rename cleanup", primaryError, rollbackErrors);
934
1384
  }
1385
+ throw primaryError;
935
1386
  }
936
1387
  renamesMigrated = true;
937
1388
  return;
938
1389
  }
939
- for (const legacyKey of renameFromKeys) {
940
- const legacyRaw = isBiometric ? adapter.backend.getSecureBiometric(legacyKey) : adapter.backend.get(legacyKey, config.scope);
941
- if (legacyRaw === undefined) {
942
- continue;
1390
+ const snapshot = snapshots.find(candidate => selectedRenameValue(candidate) !== undefined);
1391
+ if (snapshot === undefined) {
1392
+ renamesMigrated = true;
1393
+ return;
1394
+ }
1395
+ let writeAttempted = false;
1396
+ let pendingCurrentWrite;
1397
+ try {
1398
+ writeAttempted = true;
1399
+ pendingCurrentWrite = writeStoredRaw(selectedRenameValue(snapshot), {
1400
+ cleanupRenameSources: false
1401
+ });
1402
+ if (hasPendingCurrentWrite()) {
1403
+ flushPendingCurrentWrite();
943
1404
  }
944
- writeStoredRaw(legacyRaw);
945
- if (isBiometric) {
946
- adapter.backend.deleteSecureBiometric(legacyKey);
947
- } else {
948
- adapter.backend.remove(legacyKey, config.scope);
1405
+ snapshots.forEach(({
1406
+ key
1407
+ }) => {
1408
+ removeRenameSource(key);
1409
+ });
1410
+ renamesMigrated = true;
1411
+ } catch (primaryError) {
1412
+ const rollbackErrors = [];
1413
+ clearPendingCurrentWriteIf(pendingCurrentWrite);
1414
+ if (writeAttempted) {
1415
+ try {
1416
+ removeCurrentBackendValue();
1417
+ } catch (error) {
1418
+ rollbackErrors.push({
1419
+ label: "rollback current value",
1420
+ error
1421
+ });
1422
+ }
1423
+ }
1424
+ snapshots.forEach(renameSnapshot => {
1425
+ try {
1426
+ restoreRenameSource(renameSnapshot);
1427
+ } catch (error) {
1428
+ rollbackErrors.push({
1429
+ label: "rollback rename source",
1430
+ error
1431
+ });
1432
+ }
1433
+ });
1434
+ invalidateRawCache(resolveNonMemoryScope(), storageKey);
1435
+ snapshots.forEach(({
1436
+ key
1437
+ }) => {
1438
+ invalidateRenameSourceCache(key);
1439
+ });
1440
+ if (rollbackErrors.length > 0) {
1441
+ throw (0, _shared.createStorageCompositeError)("rename migration", primaryError, rollbackErrors);
949
1442
  }
950
- break;
1443
+ throw primaryError;
951
1444
  }
952
- renamesMigrated = true;
953
1445
  } catch (error) {
954
1446
  if ((0, _shared.isKeychainLockedError)(error)) {
955
1447
  onReadError?.(error);
@@ -959,16 +1451,22 @@ function createStorageCore(buildAdapter) {
959
1451
  }
960
1452
  };
961
1453
  const removeStoredRaw = (operation = "remove") => {
962
- const oldValue = getEventRawValue(config.scope, storageKey);
1454
+ const oldValue = isBiometric ? getEventRawValueForRepresentation(config.scope, storageKey, "biometric") : getEventRawValue(config.scope, storageKey);
963
1455
  if (isBiometric) {
1456
+ invalidateRawCache(_Storage.StorageScope.Secure, storageKey);
1457
+ scheduleRenameSourceCleanup();
964
1458
  adapter.backend.deleteSecureBiometric(storageKey);
965
1459
  emitKeyChange(config.scope, storageKey, oldValue, undefined, operation, adapter.changeSource);
966
1460
  return;
967
1461
  }
1462
+ if (nonMemoryScope === _Storage.StorageScope.Secure) {
1463
+ invalidateRawCache(_Storage.StorageScope.Secure, storageKey);
1464
+ }
968
1465
  cacheRawValue(resolveNonMemoryScope(), storageKey, undefined);
969
1466
  if (nonMemoryScope === _Storage.StorageScope.Disk) {
970
1467
  if (coalesceDiskWrites || isDiskWritesAsync()) {
971
1468
  scheduleDiskWrite(storageKey, undefined);
1469
+ scheduleRenameSourceCleanup();
972
1470
  emitKeyChange(config.scope, storageKey, oldValue, undefined, operation, adapter.changeSource);
973
1471
  return;
974
1472
  }
@@ -976,12 +1474,14 @@ function createStorageCore(buildAdapter) {
976
1474
  }
977
1475
  if (coalesceSecureWrites) {
978
1476
  scheduleSecureWrite(storageKey, undefined, secureAccessControl ?? secureDefaultAccessControl);
1477
+ scheduleRenameSourceCleanup();
979
1478
  emitKeyChange(config.scope, storageKey, oldValue, undefined, operation, adapter.changeSource);
980
1479
  return;
981
1480
  }
982
1481
  if (nonMemoryScope === _Storage.StorageScope.Secure) {
983
1482
  clearPendingSecureWrite(storageKey);
984
1483
  }
1484
+ scheduleRenameSourceCleanup();
985
1485
  adapter.backend.remove(storageKey, config.scope);
986
1486
  emitKeyChange(config.scope, storageKey, oldValue, undefined, operation, adapter.changeSource);
987
1487
  };
@@ -990,6 +1490,8 @@ function createStorageCore(buildAdapter) {
990
1490
  const oldValue = getEventRawValue(config.scope, storageKey);
991
1491
  if (memoryExpiration) {
992
1492
  memoryExpiration.set(storageKey, Date.now() + (expirationTtlMs ?? 0));
1493
+ } else {
1494
+ memoryExpirationDeadlines.delete(storageKey);
993
1495
  }
994
1496
  const storedValue = typeof value === "string" ? (0, _internal.escapeCollidingRawValue)(value) : value;
995
1497
  memoryStore.set(storageKey, storedValue);
@@ -1038,7 +1540,9 @@ function createStorageCore(buildAdapter) {
1038
1540
  if (lastExpiresAt > Date.now()) {
1039
1541
  return lastValue;
1040
1542
  }
1041
- removeStoredRaw("expire");
1543
+ runAtomicItemMutation(() => {
1544
+ removeStoredRaw("expire");
1545
+ });
1042
1546
  invalidateParsedCache();
1043
1547
  onExpired?.(storageKey);
1044
1548
  lastValue = ensureValidatedValue(defaultValue, false);
@@ -1076,7 +1580,9 @@ function createStorageCore(buildAdapter) {
1076
1580
  if ((0, _internal.isStoredEnvelope)(parsed)) {
1077
1581
  envelopeExpiresAt = parsed.expiresAt;
1078
1582
  if (parsed.expiresAt <= Date.now()) {
1079
- removeStoredRaw("expire");
1583
+ runAtomicItemMutation(() => {
1584
+ removeStoredRaw("expire");
1585
+ });
1080
1586
  invalidateParsedCache();
1081
1587
  onExpired?.(storageKey);
1082
1588
  lastValue = ensureValidatedValue(defaultValue, false);
@@ -1113,36 +1619,45 @@ function createStorageCore(buildAdapter) {
1113
1619
  }));
1114
1620
  const set = valueOrFn => {
1115
1621
  measureOperation("item:set", config.scope, () => {
1116
- const newValue = (0, _shared.isUpdater)(valueOrFn) ? valueOrFn(getInternal()) : valueOrFn;
1117
- if (validate && !validate(newValue)) {
1118
- throw new Error(`Validation failed for key "${storageKey}" in scope "${_Storage.StorageScope[config.scope]}".`);
1119
- }
1120
- invalidateParsedCache();
1121
- writeValueWithoutValidation(newValue);
1622
+ runAtomicItemMutation(() => {
1623
+ const newValue = (0, _shared.isUpdater)(valueOrFn) ? valueOrFn(getInternal()) : valueOrFn;
1624
+ if (validate && !validate(newValue)) {
1625
+ throw new Error(`Validation failed for key "${storageKey}" in scope "${_Storage.StorageScope[config.scope]}".`);
1626
+ }
1627
+ invalidateParsedCache();
1628
+ writeValueWithoutValidation(newValue);
1629
+ });
1122
1630
  });
1123
1631
  };
1124
1632
  const setIfVersion = (version, valueOrFn) => measureOperation("item:setIfVersion", config.scope, () => {
1125
- const currentVersion = getCurrentVersion();
1126
- if (currentVersion !== version) {
1127
- return false;
1128
- }
1129
- set(valueOrFn);
1130
- return true;
1633
+ let didSet = false;
1634
+ runAtomicItemMutation(() => {
1635
+ const currentVersion = getCurrentVersion();
1636
+ if (currentVersion !== version) {
1637
+ return;
1638
+ }
1639
+ set(valueOrFn);
1640
+ didSet = true;
1641
+ });
1642
+ return didSet;
1131
1643
  });
1132
1644
  const deleteItem = () => {
1133
1645
  measureOperation("item:delete", config.scope, () => {
1134
- invalidateParsedCache();
1135
- if (isMemory) {
1136
- const oldValue = getEventRawValue(config.scope, storageKey);
1137
- if (memoryExpiration) {
1138
- memoryExpiration.delete(storageKey);
1646
+ runAtomicItemMutation(() => {
1647
+ invalidateParsedCache();
1648
+ if (isMemory) {
1649
+ const oldValue = getEventRawValue(config.scope, storageKey);
1650
+ if (memoryExpiration) {
1651
+ memoryExpiration.delete(storageKey);
1652
+ }
1653
+ memoryExpirationDeadlines.delete(storageKey);
1654
+ memoryStore.delete(storageKey);
1655
+ (0, _shared.notifyKeyListeners)(memoryListeners, storageKey);
1656
+ emitKeyChange(config.scope, storageKey, oldValue, undefined, "remove", "memory");
1657
+ return;
1139
1658
  }
1140
- memoryStore.delete(storageKey);
1141
- (0, _shared.notifyKeyListeners)(memoryListeners, storageKey);
1142
- emitKeyChange(config.scope, storageKey, oldValue, undefined, "remove", "memory");
1143
- return;
1144
- }
1145
- removeStoredRaw();
1659
+ removeStoredRaw();
1660
+ });
1146
1661
  });
1147
1662
  };
1148
1663
  const merge = partial => {
@@ -1170,13 +1685,15 @@ function createStorageCore(buildAdapter) {
1170
1685
  if (isMemory) return memoryStore.has(storageKey);
1171
1686
  if (isBiometric) return adapter.backend.hasSecureBiometric(storageKey);
1172
1687
  if (nonMemoryScope === _Storage.StorageScope.Disk) {
1173
- if (durability.hasPendingDiskWrite(storageKey)) {
1174
- return durability.readPendingDiskWrite(storageKey) !== undefined;
1688
+ const pending = durability.getPendingDiskWrite(storageKey);
1689
+ if (pending !== undefined) {
1690
+ return pending.value !== undefined;
1175
1691
  }
1176
1692
  }
1177
1693
  if (nonMemoryScope === _Storage.StorageScope.Secure) {
1178
- if (durability.hasPendingSecureWrite(storageKey)) {
1179
- return durability.readPendingSecureWrite(storageKey) !== undefined;
1694
+ const pending = durability.getPendingSecureWrite(storageKey);
1695
+ if (pending !== undefined) {
1696
+ return pending.value !== undefined;
1180
1697
  }
1181
1698
  }
1182
1699
  return adapter.backend.has(storageKey, config.scope);
@@ -1235,14 +1752,18 @@ function createStorageCore(buildAdapter) {
1235
1752
  invalidateParsedCache();
1236
1753
  },
1237
1754
  _deleteMemoryEntry: () => {
1238
- if (memoryExpiration) {
1239
- memoryExpiration.delete(storageKey);
1240
- }
1755
+ memoryExpirationDeadlines.delete(storageKey);
1241
1756
  memoryStore.delete(storageKey);
1242
1757
  invalidateParsedCache();
1243
1758
  },
1244
1759
  _hasValidation: validate !== undefined,
1245
1760
  _hasExpiration: expiration !== undefined,
1761
+ _hasRenameFrom: renameFromKeys.length > 0,
1762
+ _renameFromKeys: renameFromKeys,
1763
+ _getRenameMigrationState: () => renamesMigrated,
1764
+ _setRenameMigrationState: migrated => {
1765
+ renamesMigrated = migrated;
1766
+ },
1246
1767
  _readCacheEnabled: readCache,
1247
1768
  _isBiometric: isBiometric,
1248
1769
  _biometricLevel: resolvedBiometricLevel,
@@ -1264,6 +1785,14 @@ function createStorageCore(buildAdapter) {
1264
1785
  }
1265
1786
  groupSet.add(storageItem);
1266
1787
  }
1788
+ if (isMemory) {
1789
+ let items = memoryItemsByKey.get(storageKey);
1790
+ if (!items) {
1791
+ items = new Set();
1792
+ memoryItemsByKey.set(storageKey, items);
1793
+ }
1794
+ items.add(storageItem);
1795
+ }
1267
1796
  const registryKey = `${config.scope}:${storageKey}`;
1268
1797
  registeredKeyCounts.set(registryKey, (registeredKeyCounts.get(registryKey) ?? 0) + 1);
1269
1798
  return storageItem;
@@ -1283,24 +1812,23 @@ function createStorageCore(buildAdapter) {
1283
1812
  const keyIndexes = [];
1284
1813
  items.forEach((item, index) => {
1285
1814
  if (scope === _Storage.StorageScope.Disk) {
1286
- const pending = durability.readPendingDiskWrite(item.key);
1815
+ const pending = durability.getPendingDiskWrite(item.key);
1287
1816
  if (pending !== undefined) {
1288
- rawValues[index] = pending;
1817
+ rawValues[index] = pending.value;
1289
1818
  return;
1290
1819
  }
1291
1820
  }
1292
1821
  if (scope === _Storage.StorageScope.Secure) {
1293
- const pending = durability.readPendingSecureWrite(item.key);
1822
+ const pending = durability.getPendingSecureWrite(item.key);
1294
1823
  if (pending !== undefined) {
1295
- rawValues[index] = pending;
1824
+ rawValues[index] = pending.value;
1296
1825
  return;
1297
1826
  }
1298
1827
  }
1299
1828
  if (item._readCacheEnabled === true) {
1300
- const cache = getScopeRawCache(scope);
1301
- const cached = cache.get(item.key);
1302
- if (cached !== undefined || cache.has(item.key)) {
1303
- rawValues[index] = cached;
1829
+ const cachedEntry = getCachedRawValueEntry(scope, item.key);
1830
+ if (cachedEntry?.has("plain")) {
1831
+ rawValues[index] = cachedEntry.get("plain");
1304
1832
  return;
1305
1833
  }
1306
1834
  }
@@ -1395,6 +1923,10 @@ function createStorageCore(buildAdapter) {
1395
1923
  const keys = secureEntries.map(({
1396
1924
  item
1397
1925
  }) => item.key);
1926
+ keys.forEach(key => {
1927
+ invalidateRawCache(scope, key);
1928
+ });
1929
+ const serializedValues = [];
1398
1930
  const oldValues = shouldReadPreviousEventValues(scope) ? adapter.backend.getBatch(keys, scope).map(value => value === undefined ? undefined : (0, _internal.unescapeCollidingRawValue)(value)) : [];
1399
1931
  const groupedByAccessControl = new Map();
1400
1932
  secureEntries.forEach(({
@@ -1402,6 +1934,8 @@ function createStorageCore(buildAdapter) {
1402
1934
  value,
1403
1935
  internal
1404
1936
  }) => {
1937
+ const serialized = item.serialize(value);
1938
+ serializedValues.push(serialized);
1405
1939
  const accessControl = internal._secureAccessControl ?? secureDefaultAccessControl;
1406
1940
  const existingGroup = groupedByAccessControl.get(accessControl);
1407
1941
  const group = existingGroup ?? {
@@ -1409,7 +1943,7 @@ function createStorageCore(buildAdapter) {
1409
1943
  values: []
1410
1944
  };
1411
1945
  group.keys.push(item.key);
1412
- group.values.push(item.serialize(value));
1946
+ group.values.push(serialized);
1413
1947
  if (!existingGroup) {
1414
1948
  groupedByAccessControl.set(accessControl, group);
1415
1949
  }
@@ -1421,10 +1955,8 @@ function createStorageCore(buildAdapter) {
1421
1955
  cacheRawValue(scope, key, group.values[index]);
1422
1956
  });
1423
1957
  });
1424
- emitBatchChange(scope, "setBatch", adapter.changeSource, secureEntries.map(({
1425
- item,
1426
- value
1427
- }, index) => (0, _shared.createKeyChange)(scope, item.key, oldValues[index], item.serialize(value), "setBatch", adapter.changeSource)));
1958
+ const willEmitChanges = storageEvents.hasListeners(scope) || eventObserver !== undefined;
1959
+ emitBatchChange(scope, "setBatch", adapter.changeSource, willEmitChanges ? keys.map((key, index) => (0, _shared.createKeyChange)(scope, key, oldValues[index], serializedValues[index], "setBatch", adapter.changeSource)) : []);
1428
1960
  return;
1429
1961
  }
1430
1962
  flushDiskWrites();
@@ -1464,6 +1996,12 @@ function createStorageCore(buildAdapter) {
1464
1996
  emitBatchChange(scope, "removeBatch", "memory", changes);
1465
1997
  return;
1466
1998
  }
1999
+ if (items.some(item => asInternal(item)._hasRenameFrom)) {
2000
+ items.forEach(item => {
2001
+ asInternal(item).delete();
2002
+ });
2003
+ return;
2004
+ }
1467
2005
  const keys = items.map(item => item.key);
1468
2006
  if (scope === _Storage.StorageScope.Disk) {
1469
2007
  flushDiskWrites();
@@ -1471,7 +2009,25 @@ function createStorageCore(buildAdapter) {
1471
2009
  if (scope === _Storage.StorageScope.Secure) {
1472
2010
  flushSecureWrites();
1473
2011
  }
1474
- const oldValues = shouldReadPreviousEventValues(scope) ? adapter.backend.getBatch(keys, scope).map(value => value === undefined ? undefined : (0, _internal.unescapeCollidingRawValue)(value)) : [];
2012
+ const oldValues = shouldReadPreviousEventValues(scope) ? (() => {
2013
+ const plainItems = items.filter(item => asInternal(item)._isBiometric !== true);
2014
+ const plainValues = plainItems.length === 0 ? [] : adapter.backend.getBatch(plainItems.map(item => item.key), scope).map(value => value === undefined ? undefined : (0, _internal.unescapeCollidingRawValue)(value));
2015
+ let plainIndex = 0;
2016
+ return items.map(item => {
2017
+ const internal = asInternal(item);
2018
+ if (internal._isBiometric === true) {
2019
+ return getEventRawValueForRepresentation(scope, item.key, "biometric");
2020
+ }
2021
+ const value = plainValues[plainIndex];
2022
+ plainIndex += 1;
2023
+ return value;
2024
+ });
2025
+ })() : [];
2026
+ if (scope === _Storage.StorageScope.Secure) {
2027
+ keys.forEach(key => {
2028
+ invalidateRawCache(scope, key);
2029
+ });
2030
+ }
1475
2031
  adapter.backend.removeBatch(keys, scope);
1476
2032
  keys.forEach(key => {
1477
2033
  cacheRawValue(scope, key, undefined);
@@ -1519,33 +2075,74 @@ function createStorageCore(buildAdapter) {
1519
2075
  }
1520
2076
  const NOT_SET = Symbol();
1521
2077
  const rollback = new Map();
1522
- const rememberRollback = (key, item) => {
1523
- if (rollback.has(key)) {
1524
- return;
1525
- }
1526
- if (scope === _Storage.StorageScope.Memory) {
1527
- rollback.set(key, {
1528
- kind: "memory",
1529
- value: memoryStore.has(key) ? memoryStore.get(key) : NOT_SET
1530
- });
1531
- } else {
1532
- const internal = item ? item : undefined;
1533
- if (scope === _Storage.StorageScope.Secure && internal?._isBiometric === true) {
1534
- rollback.set(key, {
1535
- kind: "biometric",
1536
- value: adapter.backend.getSecureBiometric(key),
1537
- level: internal._biometricLevel
2078
+ const itemRenameStates = new Map();
2079
+ const rememberRollback = (key, item, includeOtherSecureRepresentation = false) => {
2080
+ const internal = item ? item : undefined;
2081
+ const representation = scope === _Storage.StorageScope.Secure && internal?._isBiometric === true ? "biometric" : "plain";
2082
+ const rememberRepresentation = representationToRemember => {
2083
+ const identity = JSON.stringify([scope, key, representationToRemember]);
2084
+ if (rollback.has(identity)) {
2085
+ return;
2086
+ }
2087
+ if (scope === _Storage.StorageScope.Memory) {
2088
+ const expiresAt = memoryExpirationDeadlines.get(key);
2089
+ rollback.set(identity, {
2090
+ key,
2091
+ representation: representationToRemember,
2092
+ record: {
2093
+ kind: "memory",
2094
+ value: memoryStore.has(key) ? memoryStore.get(key) : NOT_SET,
2095
+ ...(expiresAt === undefined ? {} : {
2096
+ expiresAt
2097
+ })
2098
+ }
1538
2099
  });
1539
2100
  return;
1540
2101
  }
1541
- rollback.set(key, {
1542
- kind: "raw",
1543
- value: getStoredRawValue(key, scope),
1544
- ...(scope === _Storage.StorageScope.Secure && internal?._secureAccessControl !== undefined ? {
1545
- accessControl: internal._secureAccessControl
1546
- } : {})
2102
+ if (representationToRemember === "biometric") {
2103
+ rollback.set(identity, {
2104
+ key,
2105
+ representation: representationToRemember,
2106
+ record: {
2107
+ kind: "biometric",
2108
+ value: getStoredRawValueForRepresentation(key, scope, representationToRemember),
2109
+ level: internal?._biometricLevel !== undefined && internal._biometricLevel !== _Storage.BiometricLevel.None ? internal._biometricLevel : _Storage.BiometricLevel.BiometryOnly
2110
+ }
2111
+ });
2112
+ return;
2113
+ }
2114
+ rollback.set(identity, {
2115
+ key,
2116
+ representation: representationToRemember,
2117
+ record: {
2118
+ kind: "raw",
2119
+ value: getStoredRawValueForRepresentation(key, scope, representationToRemember),
2120
+ ...(scope === _Storage.StorageScope.Secure && internal?._secureAccessControl !== undefined ? {
2121
+ accessControl: internal._secureAccessControl
2122
+ } : {})
2123
+ }
1547
2124
  });
2125
+ };
2126
+ if (representation === "biometric" || includeOtherSecureRepresentation) {
2127
+ rememberRepresentation("plain");
2128
+ }
2129
+ rememberRepresentation(representation);
2130
+ if (includeOtherSecureRepresentation) {
2131
+ rememberRepresentation("biometric");
2132
+ }
2133
+ };
2134
+ const rememberItemRollback = item => {
2135
+ const internal = item;
2136
+ const includeOtherSecureRepresentation = scope === _Storage.StorageScope.Secure && (internal._isBiometric === true || internal._hasRenameFrom === true);
2137
+ rememberRollback(item.key, item, includeOtherSecureRepresentation);
2138
+ if (internal._getRenameMigrationState) {
2139
+ if (!itemRenameStates.has(internal)) {
2140
+ itemRenameStates.set(internal, internal._getRenameMigrationState());
2141
+ }
1548
2142
  }
2143
+ (internal._renameFromKeys ?? EMPTY_KEYS).forEach(aliasKey => {
2144
+ rememberRollback(aliasKey, item, true);
2145
+ });
1549
2146
  };
1550
2147
  const tx = {
1551
2148
  scope,
@@ -1555,49 +2152,85 @@ function createStorageCore(buildAdapter) {
1555
2152
  setRawValue(key, value, scope);
1556
2153
  },
1557
2154
  removeRaw: key => {
1558
- rememberRollback(key);
2155
+ rememberRollback(key, undefined, scope === _Storage.StorageScope.Secure);
1559
2156
  removeRawValue(key, scope);
1560
2157
  },
1561
2158
  getItem: item => {
1562
2159
  (0, _internal.assertBatchScope)([item], scope);
2160
+ rememberItemRollback(item);
1563
2161
  return item.get();
1564
2162
  },
1565
2163
  setItem: (item, value) => {
1566
2164
  (0, _internal.assertBatchScope)([item], scope);
1567
- rememberRollback(item.key, item);
2165
+ rememberItemRollback(item);
1568
2166
  item.set(value);
1569
2167
  },
1570
2168
  removeItem: item => {
1571
2169
  (0, _internal.assertBatchScope)([item], scope);
1572
- rememberRollback(item.key, item);
2170
+ rememberItemRollback(item);
1573
2171
  item.delete();
1574
2172
  }
1575
2173
  };
1576
2174
  try {
1577
2175
  return transaction(tx);
1578
2176
  } catch (error) {
1579
- const rollbackEntries = Array.from(rollback.entries()).reverse();
2177
+ itemRenameStates.forEach((migrated, item) => {
2178
+ item._setRenameMigrationState(migrated);
2179
+ });
2180
+ const rollbackEntries = Array.from(rollback.values()).reverse();
1580
2181
  const rollbackSource = scope === _Storage.StorageScope.Memory ? "memory" : adapter.changeSource;
1581
- const preRollbackValues = rollbackEntries.length > 0 ? rollbackEntries.map(([key]) => getEventRawValue(scope, key)) : [];
2182
+ const rollbackErrors = [];
2183
+ const readRollbackEventValue = entry => {
2184
+ try {
2185
+ return getEventRawValueForRepresentation(scope, entry.key, entry.representation);
2186
+ } catch (readError) {
2187
+ rollbackErrors.push({
2188
+ label: `rollback read ${entry.representation}:${entry.key}`,
2189
+ error: readError
2190
+ });
2191
+ return undefined;
2192
+ }
2193
+ };
2194
+ const preRollbackValues = rollbackEntries.map(readRollbackEventValue);
1582
2195
  if (scope === _Storage.StorageScope.Memory) {
1583
- rollbackEntries.forEach(([key, record]) => {
1584
- if (record.value === NOT_SET) {
1585
- memoryStore.delete(key);
1586
- } else {
1587
- memoryStore.set(key, record.value);
2196
+ rollbackEntries.forEach(entry => {
2197
+ try {
2198
+ const record = entry.record;
2199
+ if (record.kind !== "memory") {
2200
+ return;
2201
+ }
2202
+ if (record.value === NOT_SET) {
2203
+ memoryStore.delete(entry.key);
2204
+ } else {
2205
+ memoryStore.set(entry.key, record.value);
2206
+ }
2207
+ if (record.expiresAt === undefined) {
2208
+ memoryExpirationDeadlines.delete(entry.key);
2209
+ } else {
2210
+ memoryExpirationDeadlines.set(entry.key, record.expiresAt);
2211
+ }
2212
+ invalidateMemoryItemCaches(entry.key);
2213
+ (0, _shared.notifyKeyListeners)(memoryListeners, entry.key);
2214
+ } catch (rollbackError) {
2215
+ rollbackErrors.push({
2216
+ label: `rollback ${entry.representation}:${entry.key}`,
2217
+ error: rollbackError
2218
+ });
1588
2219
  }
1589
- (0, _shared.notifyKeyListeners)(memoryListeners, key);
1590
2220
  });
1591
2221
  } else {
1592
2222
  const groupedKeysToSet = new Map();
1593
2223
  const keysToRemove = [];
1594
- rollbackEntries.forEach(([key, record]) => {
2224
+ const biometricEntries = [];
2225
+ const biometricValues = new Map();
2226
+ rollbackEntries.forEach(entry => {
2227
+ const {
2228
+ key,
2229
+ record
2230
+ } = entry;
1595
2231
  if (record.kind === "biometric") {
1596
- if (record.value === undefined) {
1597
- adapter.backend.deleteSecureBiometric(key);
1598
- } else {
1599
- adapter.backend.setSecureBiometricWithLevel(key, record.value, record.level);
1600
- }
2232
+ biometricEntries.push(entry);
2233
+ biometricValues.set(key, record.value);
1601
2234
  return;
1602
2235
  }
1603
2236
  if (record.kind !== "raw") {
@@ -1620,29 +2253,108 @@ function createStorageCore(buildAdapter) {
1620
2253
  }
1621
2254
  });
1622
2255
  if (scope === _Storage.StorageScope.Disk) {
1623
- flushDiskWrites();
2256
+ try {
2257
+ flushDiskWrites();
2258
+ } catch (flushError) {
2259
+ rollbackErrors.push({
2260
+ label: "rollback flush disk",
2261
+ error: flushError
2262
+ });
2263
+ }
1624
2264
  }
1625
2265
  if (scope === _Storage.StorageScope.Secure) {
1626
- flushSecureWrites();
2266
+ try {
2267
+ flushSecureWrites();
2268
+ } catch (flushError) {
2269
+ rollbackErrors.push({
2270
+ label: "rollback flush secure",
2271
+ error: flushError
2272
+ });
2273
+ }
2274
+ }
2275
+ if (scope === _Storage.StorageScope.Secure) {
2276
+ rollbackEntries.forEach(entry => {
2277
+ invalidateRawCache(scope, entry.key);
2278
+ });
1627
2279
  }
2280
+ biometricEntries.forEach(entry => {
2281
+ const {
2282
+ key,
2283
+ record
2284
+ } = entry;
2285
+ if (record.kind !== "biometric" || record.value === undefined) {
2286
+ return;
2287
+ }
2288
+ try {
2289
+ adapter.backend.setSecureBiometricWithLevel(key, record.value, record.level);
2290
+ cacheRawValue(_Storage.StorageScope.Secure, key, record.value, "biometric");
2291
+ } catch (rollbackError) {
2292
+ rollbackErrors.push({
2293
+ label: "rollback biometric",
2294
+ error: rollbackError
2295
+ });
2296
+ }
2297
+ });
1628
2298
  groupedKeysToSet.forEach((group, accessControl) => {
1629
- if (scope === _Storage.StorageScope.Secure) {
1630
- adapter.backend.setSecureAccessControl(accessControl);
2299
+ try {
2300
+ if (scope === _Storage.StorageScope.Secure) {
2301
+ adapter.backend.setSecureAccessControl(accessControl);
2302
+ }
2303
+ adapter.backend.setBatch(group.keys, group.values, scope);
2304
+ group.keys.forEach((key, index) => {
2305
+ cacheRawValue(scope, key, group.values[index], "plain");
2306
+ });
2307
+ } catch (rollbackError) {
2308
+ rollbackErrors.push({
2309
+ label: "rollback plain setBatch",
2310
+ error: rollbackError
2311
+ });
1631
2312
  }
1632
- adapter.backend.setBatch(group.keys, group.values, scope);
1633
- group.keys.forEach((key, index) => {
1634
- cacheRawValue(scope, key, group.values[index]);
1635
- });
1636
2313
  });
1637
2314
  if (keysToRemove.length > 0) {
1638
- adapter.backend.removeBatch(keysToRemove, scope);
1639
- keysToRemove.forEach(key => {
1640
- cacheRawValue(scope, key, undefined);
1641
- });
2315
+ const keysToRemoveWithoutBiometric = keysToRemove.filter(key => biometricValues.get(key) === undefined);
2316
+ try {
2317
+ if (keysToRemoveWithoutBiometric.length > 0) {
2318
+ adapter.backend.removeBatch(keysToRemoveWithoutBiometric, scope);
2319
+ }
2320
+ keysToRemove.forEach(key => {
2321
+ cacheRawValue(scope, key, undefined, "plain");
2322
+ });
2323
+ } catch (rollbackError) {
2324
+ rollbackErrors.push({
2325
+ label: "rollback plain removeBatch",
2326
+ error: rollbackError
2327
+ });
2328
+ }
1642
2329
  }
2330
+ biometricEntries.forEach(entry => {
2331
+ const {
2332
+ key,
2333
+ record
2334
+ } = entry;
2335
+ if (record.kind !== "biometric") {
2336
+ return;
2337
+ }
2338
+ if (record.value !== undefined) {
2339
+ return;
2340
+ }
2341
+ invalidateRawCache(_Storage.StorageScope.Secure, key);
2342
+ try {
2343
+ adapter.backend.deleteSecureBiometric(key);
2344
+ cacheRawValue(_Storage.StorageScope.Secure, key, record.value, "biometric");
2345
+ } catch (rollbackError) {
2346
+ rollbackErrors.push({
2347
+ label: "rollback biometric",
2348
+ error: rollbackError
2349
+ });
2350
+ }
2351
+ });
1643
2352
  }
1644
2353
  if (rollbackEntries.length > 0) {
1645
- emitBatchChange(scope, "rollback", rollbackSource, rollbackEntries.map(([key], index) => (0, _shared.createKeyChange)(scope, key, preRollbackValues[index], getEventRawValue(scope, key), "rollback", rollbackSource)));
2354
+ emitBatchChange(scope, "rollback", rollbackSource, rollbackEntries.map((entry, index) => (0, _shared.createKeyChange)(scope, entry.key, preRollbackValues[index], readRollbackEventValue(entry), "rollback", rollbackSource)));
2355
+ }
2356
+ if (rollbackErrors.length > 0) {
2357
+ throw (0, _shared.createStorageCompositeError)("transaction rollback", error, rollbackErrors);
1646
2358
  }
1647
2359
  throw error;
1648
2360
  }
@@ -1750,8 +2462,16 @@ function createStorageCore(buildAdapter) {
1750
2462
  add(id);
1751
2463
  return true;
1752
2464
  };
2465
+ const getTyped = () => {
2466
+ const typed = {};
2467
+ for (const id of Object.keys(item.get())) {
2468
+ typed[id] = true;
2469
+ }
2470
+ return typed;
2471
+ };
1753
2472
  return {
1754
2473
  get: item.get,
2474
+ getTyped,
1755
2475
  has,
1756
2476
  add,
1757
2477
  delete: deleteId,