react-native-nitro-storage 0.3.2 → 0.4.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 (46) hide show
  1. package/README.md +192 -30
  2. package/android/src/main/cpp/AndroidStorageAdapterCpp.cpp +22 -2
  3. package/android/src/main/cpp/AndroidStorageAdapterCpp.hpp +3 -0
  4. package/android/src/main/cpp/cpp-adapter.cpp +3 -1
  5. package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +54 -5
  6. package/cpp/bindings/HybridStorage.cpp +167 -22
  7. package/cpp/bindings/HybridStorage.hpp +12 -1
  8. package/cpp/core/NativeStorageAdapter.hpp +3 -0
  9. package/ios/IOSStorageAdapterCpp.hpp +16 -0
  10. package/ios/IOSStorageAdapterCpp.mm +135 -11
  11. package/lib/commonjs/index.js +522 -275
  12. package/lib/commonjs/index.js.map +1 -1
  13. package/lib/commonjs/index.web.js +614 -270
  14. package/lib/commonjs/index.web.js.map +1 -1
  15. package/lib/commonjs/indexeddb-backend.js +130 -0
  16. package/lib/commonjs/indexeddb-backend.js.map +1 -0
  17. package/lib/commonjs/internal.js +25 -0
  18. package/lib/commonjs/internal.js.map +1 -1
  19. package/lib/module/index.js +516 -277
  20. package/lib/module/index.js.map +1 -1
  21. package/lib/module/index.web.js +608 -272
  22. package/lib/module/index.web.js.map +1 -1
  23. package/lib/module/indexeddb-backend.js +126 -0
  24. package/lib/module/indexeddb-backend.js.map +1 -0
  25. package/lib/module/internal.js +24 -0
  26. package/lib/module/internal.js.map +1 -1
  27. package/lib/typescript/Storage.nitro.d.ts +2 -0
  28. package/lib/typescript/Storage.nitro.d.ts.map +1 -1
  29. package/lib/typescript/index.d.ts +40 -1
  30. package/lib/typescript/index.d.ts.map +1 -1
  31. package/lib/typescript/index.web.d.ts +42 -1
  32. package/lib/typescript/index.web.d.ts.map +1 -1
  33. package/lib/typescript/indexeddb-backend.d.ts +29 -0
  34. package/lib/typescript/indexeddb-backend.d.ts.map +1 -0
  35. package/lib/typescript/internal.d.ts +1 -0
  36. package/lib/typescript/internal.d.ts.map +1 -1
  37. package/nitrogen/generated/android/NitroStorageOnLoad.cpp +22 -17
  38. package/nitrogen/generated/android/NitroStorageOnLoad.hpp +13 -4
  39. package/nitrogen/generated/shared/c++/HybridStorageSpec.cpp +2 -0
  40. package/nitrogen/generated/shared/c++/HybridStorageSpec.hpp +2 -0
  41. package/package.json +7 -3
  42. package/src/Storage.nitro.ts +2 -0
  43. package/src/index.ts +671 -296
  44. package/src/index.web.ts +776 -288
  45. package/src/indexeddb-backend.ts +143 -0
  46. package/src/internal.ts +28 -0
@@ -21,9 +21,16 @@ Object.defineProperty(exports, "StorageScope", {
21
21
  return _Storage.StorageScope;
22
22
  }
23
23
  });
24
+ Object.defineProperty(exports, "createIndexedDBBackend", {
25
+ enumerable: true,
26
+ get: function () {
27
+ return _indexeddbBackend.createIndexedDBBackend;
28
+ }
29
+ });
24
30
  exports.createSecureAuthStorage = createSecureAuthStorage;
25
31
  exports.createStorageItem = createStorageItem;
26
32
  exports.getBatch = getBatch;
33
+ exports.getWebSecureStorageBackend = getWebSecureStorageBackend;
27
34
  Object.defineProperty(exports, "migrateFromMMKV", {
28
35
  enumerable: true,
29
36
  get: function () {
@@ -35,6 +42,7 @@ exports.registerMigration = registerMigration;
35
42
  exports.removeBatch = removeBatch;
36
43
  exports.runTransaction = runTransaction;
37
44
  exports.setBatch = setBatch;
45
+ exports.setWebSecureStorageBackend = setWebSecureStorageBackend;
38
46
  exports.storage = void 0;
39
47
  Object.defineProperty(exports, "useSetStorage", {
40
48
  enumerable: true,
@@ -58,6 +66,7 @@ var _Storage = require("./Storage.types");
58
66
  var _internal = require("./internal");
59
67
  var _migration = require("./migration");
60
68
  var _storageHooks = require("./storage-hooks");
69
+ var _indexeddbBackend = require("./indexeddb-backend");
61
70
  function asInternal(item) {
62
71
  return item;
63
72
  }
@@ -82,12 +91,76 @@ let secureFlushScheduled = false;
82
91
  const SECURE_WEB_PREFIX = "__secure_";
83
92
  const BIOMETRIC_WEB_PREFIX = "__bio_";
84
93
  let hasWarnedAboutWebBiometricFallback = false;
94
+ let hasWebStorageEventSubscription = false;
95
+ let metricsObserver;
96
+ const metricsCounters = new Map();
97
+ function recordMetric(operation, scope, durationMs, keysCount = 1) {
98
+ const existing = metricsCounters.get(operation);
99
+ if (!existing) {
100
+ metricsCounters.set(operation, {
101
+ count: 1,
102
+ totalDurationMs: durationMs,
103
+ maxDurationMs: durationMs
104
+ });
105
+ } else {
106
+ existing.count += 1;
107
+ existing.totalDurationMs += durationMs;
108
+ existing.maxDurationMs = Math.max(existing.maxDurationMs, durationMs);
109
+ }
110
+ metricsObserver?.({
111
+ operation,
112
+ scope,
113
+ durationMs,
114
+ keysCount
115
+ });
116
+ }
117
+ function measureOperation(operation, scope, fn, keysCount = 1) {
118
+ const start = Date.now();
119
+ try {
120
+ return fn();
121
+ } finally {
122
+ recordMetric(operation, scope, Date.now() - start, keysCount);
123
+ }
124
+ }
125
+ function createLocalStorageWebSecureBackend() {
126
+ return {
127
+ getItem: key => globalThis.localStorage?.getItem(key) ?? null,
128
+ setItem: (key, value) => globalThis.localStorage?.setItem(key, value),
129
+ removeItem: key => globalThis.localStorage?.removeItem(key),
130
+ clear: () => globalThis.localStorage?.clear(),
131
+ getAllKeys: () => {
132
+ const storage = globalThis.localStorage;
133
+ if (!storage) return [];
134
+ const keys = [];
135
+ for (let index = 0; index < storage.length; index += 1) {
136
+ const key = storage.key(index);
137
+ if (key) {
138
+ keys.push(key);
139
+ }
140
+ }
141
+ return keys;
142
+ }
143
+ };
144
+ }
145
+ let webSecureStorageBackend = createLocalStorageWebSecureBackend();
85
146
  function getBrowserStorage(scope) {
86
147
  if (scope === _Storage.StorageScope.Disk) {
87
148
  return globalThis.localStorage;
88
149
  }
89
150
  if (scope === _Storage.StorageScope.Secure) {
90
- return globalThis.localStorage;
151
+ if (!webSecureStorageBackend) {
152
+ return undefined;
153
+ }
154
+ return {
155
+ setItem: (key, value) => webSecureStorageBackend?.setItem(key, value),
156
+ getItem: key => webSecureStorageBackend?.getItem(key) ?? null,
157
+ removeItem: key => webSecureStorageBackend?.removeItem(key),
158
+ clear: () => webSecureStorageBackend?.clear(),
159
+ key: index => webSecureStorageBackend?.getAllKeys()[index] ?? null,
160
+ get length() {
161
+ return webSecureStorageBackend?.getAllKeys().length ?? 0;
162
+ }
163
+ };
91
164
  }
92
165
  return undefined;
93
166
  }
@@ -140,6 +213,62 @@ function ensureWebScopeKeyIndex(scope) {
140
213
  hydrateWebScopeKeyIndex(scope);
141
214
  return getWebScopeKeyIndex(scope);
142
215
  }
216
+ function handleWebStorageEvent(event) {
217
+ const key = event.key;
218
+ if (key === null) {
219
+ clearScopeRawCache(_Storage.StorageScope.Disk);
220
+ clearScopeRawCache(_Storage.StorageScope.Secure);
221
+ ensureWebScopeKeyIndex(_Storage.StorageScope.Disk).clear();
222
+ ensureWebScopeKeyIndex(_Storage.StorageScope.Secure).clear();
223
+ notifyAllListeners(getScopedListeners(_Storage.StorageScope.Disk));
224
+ notifyAllListeners(getScopedListeners(_Storage.StorageScope.Secure));
225
+ return;
226
+ }
227
+ if (key.startsWith(SECURE_WEB_PREFIX)) {
228
+ const plainKey = fromSecureStorageKey(key);
229
+ if (event.newValue === null) {
230
+ ensureWebScopeKeyIndex(_Storage.StorageScope.Secure).delete(plainKey);
231
+ cacheRawValue(_Storage.StorageScope.Secure, plainKey, undefined);
232
+ } else {
233
+ ensureWebScopeKeyIndex(_Storage.StorageScope.Secure).add(plainKey);
234
+ cacheRawValue(_Storage.StorageScope.Secure, plainKey, event.newValue);
235
+ }
236
+ notifyKeyListeners(getScopedListeners(_Storage.StorageScope.Secure), plainKey);
237
+ return;
238
+ }
239
+ if (key.startsWith(BIOMETRIC_WEB_PREFIX)) {
240
+ const plainKey = fromBiometricStorageKey(key);
241
+ if (event.newValue === null) {
242
+ if (getBrowserStorage(_Storage.StorageScope.Secure)?.getItem(toSecureStorageKey(plainKey)) === null) {
243
+ ensureWebScopeKeyIndex(_Storage.StorageScope.Secure).delete(plainKey);
244
+ }
245
+ cacheRawValue(_Storage.StorageScope.Secure, plainKey, undefined);
246
+ } else {
247
+ ensureWebScopeKeyIndex(_Storage.StorageScope.Secure).add(plainKey);
248
+ cacheRawValue(_Storage.StorageScope.Secure, plainKey, event.newValue);
249
+ }
250
+ notifyKeyListeners(getScopedListeners(_Storage.StorageScope.Secure), plainKey);
251
+ return;
252
+ }
253
+ if (event.newValue === null) {
254
+ ensureWebScopeKeyIndex(_Storage.StorageScope.Disk).delete(key);
255
+ cacheRawValue(_Storage.StorageScope.Disk, key, undefined);
256
+ } else {
257
+ ensureWebScopeKeyIndex(_Storage.StorageScope.Disk).add(key);
258
+ cacheRawValue(_Storage.StorageScope.Disk, key, event.newValue);
259
+ }
260
+ notifyKeyListeners(getScopedListeners(_Storage.StorageScope.Disk), key);
261
+ }
262
+ function ensureWebStorageEventSubscription() {
263
+ if (hasWebStorageEventSubscription) {
264
+ return;
265
+ }
266
+ if (typeof window === "undefined" || typeof window.addEventListener !== "function") {
267
+ return;
268
+ }
269
+ window.addEventListener("storage", handleWebStorageEvent);
270
+ hasWebStorageEventSubscription = true;
271
+ }
143
272
  function getScopedListeners(scope) {
144
273
  return webScopeListeners.get(scope);
145
274
  }
@@ -200,32 +329,46 @@ function flushSecureWrites() {
200
329
  }
201
330
  const writes = Array.from(pendingSecureWrites.values());
202
331
  pendingSecureWrites.clear();
203
- const keysToSet = [];
204
- const valuesToSet = [];
332
+ const groupedSetWrites = new Map();
205
333
  const keysToRemove = [];
206
334
  writes.forEach(({
207
335
  key,
208
- value
336
+ value,
337
+ accessControl
209
338
  }) => {
210
339
  if (value === undefined) {
211
340
  keysToRemove.push(key);
212
341
  } else {
213
- keysToSet.push(key);
214
- valuesToSet.push(value);
342
+ const resolvedAccessControl = accessControl ?? _Storage.AccessControl.WhenUnlocked;
343
+ const existingGroup = groupedSetWrites.get(resolvedAccessControl);
344
+ const group = existingGroup ?? {
345
+ keys: [],
346
+ values: []
347
+ };
348
+ group.keys.push(key);
349
+ group.values.push(value);
350
+ if (!existingGroup) {
351
+ groupedSetWrites.set(resolvedAccessControl, group);
352
+ }
215
353
  }
216
354
  });
217
- if (keysToSet.length > 0) {
218
- WebStorage.setBatch(keysToSet, valuesToSet, _Storage.StorageScope.Secure);
219
- }
355
+ groupedSetWrites.forEach((group, accessControl) => {
356
+ WebStorage.setSecureAccessControl(accessControl);
357
+ WebStorage.setBatch(group.keys, group.values, _Storage.StorageScope.Secure);
358
+ });
220
359
  if (keysToRemove.length > 0) {
221
360
  WebStorage.removeBatch(keysToRemove, _Storage.StorageScope.Secure);
222
361
  }
223
362
  }
224
- function scheduleSecureWrite(key, value) {
225
- pendingSecureWrites.set(key, {
363
+ function scheduleSecureWrite(key, value, accessControl) {
364
+ const pendingWrite = {
226
365
  key,
227
366
  value
228
- });
367
+ };
368
+ if (accessControl !== undefined) {
369
+ pendingWrite.accessControl = accessControl;
370
+ }
371
+ pendingSecureWrites.set(key, pendingWrite);
229
372
  if (secureFlushScheduled) {
230
373
  return;
231
374
  }
@@ -376,6 +519,12 @@ const WebStorage = {
376
519
  }
377
520
  return Array.from(ensureWebScopeKeyIndex(scope));
378
521
  },
522
+ getKeysByPrefix: (prefix, scope) => {
523
+ if (scope !== _Storage.StorageScope.Disk && scope !== _Storage.StorageScope.Secure) {
524
+ return [];
525
+ }
526
+ return Array.from(ensureWebScopeKeyIndex(scope)).filter(key => key.startsWith(prefix));
527
+ },
379
528
  size: scope => {
380
529
  if (scope === _Storage.StorageScope.Disk || scope === _Storage.StorageScope.Secure) {
381
530
  return ensureWebScopeKeyIndex(scope).size;
@@ -386,19 +535,22 @@ const WebStorage = {
386
535
  setSecureWritesAsync: _enabled => {},
387
536
  setKeychainAccessGroup: () => {},
388
537
  setSecureBiometric: (key, value) => {
538
+ WebStorage.setSecureBiometricWithLevel(key, value, _Storage.BiometricLevel.BiometryOnly);
539
+ },
540
+ setSecureBiometricWithLevel: (key, value, _level) => {
389
541
  if (typeof __DEV__ !== "undefined" && __DEV__ && !hasWarnedAboutWebBiometricFallback) {
390
542
  hasWarnedAboutWebBiometricFallback = true;
391
543
  console.warn("[NitroStorage] Biometric storage is not supported on web. Using localStorage.");
392
544
  }
393
- globalThis.localStorage?.setItem(toBiometricStorageKey(key), value);
545
+ getBrowserStorage(_Storage.StorageScope.Secure)?.setItem(toBiometricStorageKey(key), value);
394
546
  ensureWebScopeKeyIndex(_Storage.StorageScope.Secure).add(key);
395
547
  notifyKeyListeners(getScopedListeners(_Storage.StorageScope.Secure), key);
396
548
  },
397
549
  getSecureBiometric: key => {
398
- return globalThis.localStorage?.getItem(toBiometricStorageKey(key)) ?? undefined;
550
+ return getBrowserStorage(_Storage.StorageScope.Secure)?.getItem(toBiometricStorageKey(key)) ?? undefined;
399
551
  },
400
552
  deleteSecureBiometric: key => {
401
- const storage = globalThis.localStorage;
553
+ const storage = getBrowserStorage(_Storage.StorageScope.Secure);
402
554
  storage?.removeItem(toBiometricStorageKey(key));
403
555
  if (storage?.getItem(toSecureStorageKey(key)) === null) {
404
556
  ensureWebScopeKeyIndex(_Storage.StorageScope.Secure).delete(key);
@@ -406,10 +558,10 @@ const WebStorage = {
406
558
  notifyKeyListeners(getScopedListeners(_Storage.StorageScope.Secure), key);
407
559
  },
408
560
  hasSecureBiometric: key => {
409
- return globalThis.localStorage?.getItem(toBiometricStorageKey(key)) !== null;
561
+ return getBrowserStorage(_Storage.StorageScope.Secure)?.getItem(toBiometricStorageKey(key)) !== null;
410
562
  },
411
563
  clearSecureBiometric: () => {
412
- const storage = globalThis.localStorage;
564
+ const storage = getBrowserStorage(_Storage.StorageScope.Secure);
413
565
  if (!storage) return;
414
566
  const keysToNotify = [];
415
567
  const toRemove = [];
@@ -483,82 +635,183 @@ function writeMigrationVersion(scope, version) {
483
635
  }
484
636
  const storage = exports.storage = {
485
637
  clear: scope => {
486
- if (scope === _Storage.StorageScope.Memory) {
487
- memoryStore.clear();
488
- notifyAllListeners(memoryListeners);
489
- return;
490
- }
491
- if (scope === _Storage.StorageScope.Secure) {
492
- flushSecureWrites();
493
- pendingSecureWrites.clear();
494
- }
495
- clearScopeRawCache(scope);
496
- WebStorage.clear(scope);
638
+ measureOperation("storage:clear", scope, () => {
639
+ if (scope === _Storage.StorageScope.Memory) {
640
+ memoryStore.clear();
641
+ notifyAllListeners(memoryListeners);
642
+ return;
643
+ }
644
+ if (scope === _Storage.StorageScope.Secure) {
645
+ flushSecureWrites();
646
+ pendingSecureWrites.clear();
647
+ }
648
+ clearScopeRawCache(scope);
649
+ WebStorage.clear(scope);
650
+ });
497
651
  },
498
652
  clearAll: () => {
499
- storage.clear(_Storage.StorageScope.Memory);
500
- storage.clear(_Storage.StorageScope.Disk);
501
- storage.clear(_Storage.StorageScope.Secure);
653
+ measureOperation("storage:clearAll", _Storage.StorageScope.Memory, () => {
654
+ storage.clear(_Storage.StorageScope.Memory);
655
+ storage.clear(_Storage.StorageScope.Disk);
656
+ storage.clear(_Storage.StorageScope.Secure);
657
+ }, 3);
502
658
  },
503
659
  clearNamespace: (namespace, scope) => {
504
- (0, _internal.assertValidScope)(scope);
505
- if (scope === _Storage.StorageScope.Memory) {
506
- for (const key of memoryStore.keys()) {
507
- if ((0, _internal.isNamespaced)(key, namespace)) {
508
- memoryStore.delete(key);
660
+ measureOperation("storage:clearNamespace", scope, () => {
661
+ (0, _internal.assertValidScope)(scope);
662
+ if (scope === _Storage.StorageScope.Memory) {
663
+ for (const key of memoryStore.keys()) {
664
+ if ((0, _internal.isNamespaced)(key, namespace)) {
665
+ memoryStore.delete(key);
666
+ }
509
667
  }
668
+ notifyAllListeners(memoryListeners);
669
+ return;
510
670
  }
511
- notifyAllListeners(memoryListeners);
512
- return;
513
- }
514
- const keyPrefix = (0, _internal.prefixKey)(namespace, "");
515
- if (scope === _Storage.StorageScope.Secure) {
516
- flushSecureWrites();
517
- }
518
- clearScopeRawCache(scope);
519
- WebStorage.removeByPrefix(keyPrefix, scope);
671
+ const keyPrefix = (0, _internal.prefixKey)(namespace, "");
672
+ if (scope === _Storage.StorageScope.Secure) {
673
+ flushSecureWrites();
674
+ }
675
+ clearScopeRawCache(scope);
676
+ WebStorage.removeByPrefix(keyPrefix, scope);
677
+ });
520
678
  },
521
679
  clearBiometric: () => {
522
- WebStorage.clearSecureBiometric();
680
+ measureOperation("storage:clearBiometric", _Storage.StorageScope.Secure, () => {
681
+ WebStorage.clearSecureBiometric();
682
+ });
523
683
  },
524
684
  has: (key, scope) => {
525
- (0, _internal.assertValidScope)(scope);
526
- if (scope === _Storage.StorageScope.Memory) return memoryStore.has(key);
527
- return WebStorage.has(key, scope);
685
+ return measureOperation("storage:has", scope, () => {
686
+ (0, _internal.assertValidScope)(scope);
687
+ if (scope === _Storage.StorageScope.Memory) return memoryStore.has(key);
688
+ return WebStorage.has(key, scope);
689
+ });
528
690
  },
529
691
  getAllKeys: scope => {
530
- (0, _internal.assertValidScope)(scope);
531
- if (scope === _Storage.StorageScope.Memory) return Array.from(memoryStore.keys());
532
- return WebStorage.getAllKeys(scope);
692
+ return measureOperation("storage:getAllKeys", scope, () => {
693
+ (0, _internal.assertValidScope)(scope);
694
+ if (scope === _Storage.StorageScope.Memory) return Array.from(memoryStore.keys());
695
+ return WebStorage.getAllKeys(scope);
696
+ });
697
+ },
698
+ getKeysByPrefix: (prefix, scope) => {
699
+ return measureOperation("storage:getKeysByPrefix", scope, () => {
700
+ (0, _internal.assertValidScope)(scope);
701
+ if (scope === _Storage.StorageScope.Memory) {
702
+ return Array.from(memoryStore.keys()).filter(key => key.startsWith(prefix));
703
+ }
704
+ return WebStorage.getKeysByPrefix(prefix, scope);
705
+ });
706
+ },
707
+ getByPrefix: (prefix, scope) => {
708
+ return measureOperation("storage:getByPrefix", scope, () => {
709
+ const result = {};
710
+ const keys = storage.getKeysByPrefix(prefix, scope);
711
+ if (keys.length === 0) {
712
+ return result;
713
+ }
714
+ if (scope === _Storage.StorageScope.Memory) {
715
+ keys.forEach(key => {
716
+ const value = memoryStore.get(key);
717
+ if (typeof value === "string") {
718
+ result[key] = value;
719
+ }
720
+ });
721
+ return result;
722
+ }
723
+ const values = WebStorage.getBatch(keys, scope);
724
+ keys.forEach((key, index) => {
725
+ const value = values[index];
726
+ if (value !== undefined) {
727
+ result[key] = value;
728
+ }
729
+ });
730
+ return result;
731
+ });
533
732
  },
534
733
  getAll: scope => {
535
- (0, _internal.assertValidScope)(scope);
536
- const result = {};
537
- if (scope === _Storage.StorageScope.Memory) {
538
- memoryStore.forEach((value, key) => {
539
- if (typeof value === "string") result[key] = value;
734
+ return measureOperation("storage:getAll", scope, () => {
735
+ (0, _internal.assertValidScope)(scope);
736
+ const result = {};
737
+ if (scope === _Storage.StorageScope.Memory) {
738
+ memoryStore.forEach((value, key) => {
739
+ if (typeof value === "string") result[key] = value;
740
+ });
741
+ return result;
742
+ }
743
+ const keys = WebStorage.getAllKeys(scope);
744
+ keys.forEach(key => {
745
+ const val = WebStorage.get(key, scope);
746
+ if (val !== undefined) result[key] = val;
540
747
  });
541
748
  return result;
542
- }
543
- const keys = WebStorage.getAllKeys(scope);
544
- keys.forEach(key => {
545
- const val = WebStorage.get(key, scope);
546
- if (val !== undefined) result[key] = val;
547
749
  });
548
- return result;
549
750
  },
550
751
  size: scope => {
551
- (0, _internal.assertValidScope)(scope);
552
- if (scope === _Storage.StorageScope.Memory) return memoryStore.size;
553
- return WebStorage.size(scope);
752
+ return measureOperation("storage:size", scope, () => {
753
+ (0, _internal.assertValidScope)(scope);
754
+ if (scope === _Storage.StorageScope.Memory) return memoryStore.size;
755
+ return WebStorage.size(scope);
756
+ });
757
+ },
758
+ setAccessControl: _level => {
759
+ recordMetric("storage:setAccessControl", _Storage.StorageScope.Secure, 0);
760
+ },
761
+ setSecureWritesAsync: _enabled => {
762
+ recordMetric("storage:setSecureWritesAsync", _Storage.StorageScope.Secure, 0);
554
763
  },
555
- setAccessControl: _level => {},
556
- setSecureWritesAsync: _enabled => {},
557
764
  flushSecureWrites: () => {
558
- flushSecureWrites();
765
+ measureOperation("storage:flushSecureWrites", _Storage.StorageScope.Secure, () => {
766
+ flushSecureWrites();
767
+ });
768
+ },
769
+ setKeychainAccessGroup: _group => {
770
+ recordMetric("storage:setKeychainAccessGroup", _Storage.StorageScope.Secure, 0);
771
+ },
772
+ setMetricsObserver: observer => {
773
+ metricsObserver = observer;
774
+ },
775
+ getMetricsSnapshot: () => {
776
+ const snapshot = {};
777
+ metricsCounters.forEach((value, key) => {
778
+ snapshot[key] = {
779
+ count: value.count,
780
+ totalDurationMs: value.totalDurationMs,
781
+ avgDurationMs: value.count === 0 ? 0 : value.totalDurationMs / value.count,
782
+ maxDurationMs: value.maxDurationMs
783
+ };
784
+ });
785
+ return snapshot;
786
+ },
787
+ resetMetrics: () => {
788
+ metricsCounters.clear();
559
789
  },
560
- setKeychainAccessGroup: _group => {}
790
+ import: (data, scope) => {
791
+ measureOperation("storage:import", scope, () => {
792
+ (0, _internal.assertValidScope)(scope);
793
+ const keys = Object.keys(data);
794
+ if (keys.length === 0) return;
795
+ const values = keys.map(k => data[k]);
796
+ if (scope === _Storage.StorageScope.Memory) {
797
+ keys.forEach((key, index) => {
798
+ memoryStore.set(key, values[index]);
799
+ });
800
+ keys.forEach(key => notifyKeyListeners(memoryListeners, key));
801
+ return;
802
+ }
803
+ WebStorage.setBatch(keys, values, scope);
804
+ }, Object.keys(data).length);
805
+ }
561
806
  };
807
+ function setWebSecureStorageBackend(backend) {
808
+ webSecureStorageBackend = backend ?? createLocalStorageWebSecureBackend();
809
+ hydratedWebScopeKeyIndex.delete(_Storage.StorageScope.Secure);
810
+ clearScopeRawCache(_Storage.StorageScope.Secure);
811
+ }
812
+ function getWebSecureStorageBackend() {
813
+ return webSecureStorageBackend;
814
+ }
562
815
  function canUseRawBatchPath(item) {
563
816
  return item._hasExpiration === false && item._hasValidation === false && item._isBiometric !== true && item._secureAccessControl === undefined;
564
817
  }
@@ -576,7 +829,8 @@ function createStorageItem(config) {
576
829
  const serialize = config.serialize ?? defaultSerialize;
577
830
  const deserialize = config.deserialize ?? defaultDeserialize;
578
831
  const isMemory = config.scope === _Storage.StorageScope.Memory;
579
- const isBiometric = config.biometric === true && config.scope === _Storage.StorageScope.Secure;
832
+ const resolvedBiometricLevel = config.scope === _Storage.StorageScope.Secure ? config.biometricLevel ?? (config.biometric === true ? _Storage.BiometricLevel.BiometryOnly : _Storage.BiometricLevel.None) : _Storage.BiometricLevel.None;
833
+ const isBiometric = resolvedBiometricLevel !== _Storage.BiometricLevel.None;
580
834
  const secureAccessControl = config.accessControl;
581
835
  const validate = config.validate;
582
836
  const onValidationError = config.onValidationError;
@@ -585,7 +839,7 @@ function createStorageItem(config) {
585
839
  const expirationTtlMs = expiration?.ttlMs;
586
840
  const memoryExpiration = expiration && isMemory ? new Map() : null;
587
841
  const readCache = !isMemory && config.readCache === true;
588
- const coalesceSecureWrites = config.scope === _Storage.StorageScope.Secure && config.coalesceSecureWrites === true && !isBiometric && secureAccessControl === undefined;
842
+ const coalesceSecureWrites = config.scope === _Storage.StorageScope.Secure && config.coalesceSecureWrites === true && !isBiometric;
589
843
  const defaultValue = config.defaultValue;
590
844
  const nonMemoryScope = config.scope === _Storage.StorageScope.Disk ? _Storage.StorageScope.Disk : config.scope === _Storage.StorageScope.Secure ? _Storage.StorageScope.Secure : null;
591
845
  if (expiration && expiration.ttlMs <= 0) {
@@ -615,6 +869,7 @@ function createStorageItem(config) {
615
869
  unsubscribe = addKeyListener(memoryListeners, storageKey, listener);
616
870
  return;
617
871
  }
872
+ ensureWebStorageEventSubscription();
618
873
  unsubscribe = addKeyListener(getScopedListeners(nonMemoryScope), storageKey, listener);
619
874
  };
620
875
  const readStoredRaw = () => {
@@ -648,12 +903,12 @@ function createStorageItem(config) {
648
903
  };
649
904
  const writeStoredRaw = rawValue => {
650
905
  if (isBiometric) {
651
- WebStorage.setSecureBiometric(storageKey, rawValue);
906
+ WebStorage.setSecureBiometricWithLevel(storageKey, rawValue, resolvedBiometricLevel);
652
907
  return;
653
908
  }
654
909
  cacheRawValue(nonMemoryScope, storageKey, rawValue);
655
910
  if (coalesceSecureWrites) {
656
- scheduleSecureWrite(storageKey, rawValue);
911
+ scheduleSecureWrite(storageKey, rawValue, secureAccessControl ?? _Storage.AccessControl.WhenUnlocked);
657
912
  return;
658
913
  }
659
914
  if (nonMemoryScope === _Storage.StorageScope.Secure) {
@@ -668,7 +923,7 @@ function createStorageItem(config) {
668
923
  }
669
924
  cacheRawValue(nonMemoryScope, storageKey, undefined);
670
925
  if (coalesceSecureWrites) {
671
- scheduleSecureWrite(storageKey, undefined);
926
+ scheduleSecureWrite(storageKey, undefined, secureAccessControl ?? _Storage.AccessControl.WhenUnlocked);
672
927
  return;
673
928
  }
674
929
  if (nonMemoryScope === _Storage.StorageScope.Secure) {
@@ -716,7 +971,7 @@ function createStorageItem(config) {
716
971
  }
717
972
  return resolved;
718
973
  };
719
- const get = () => {
974
+ const getInternal = () => {
720
975
  const raw = readStoredRaw();
721
976
  if (!memoryExpiration && raw === lastRaw && hasLastValue) {
722
977
  if (!expiration || lastExpiresAt === null) {
@@ -731,6 +986,7 @@ function createStorageItem(config) {
731
986
  onExpired?.(storageKey);
732
987
  lastValue = ensureValidatedValue(defaultValue, false);
733
988
  hasLastValue = true;
989
+ listeners.forEach(cb => cb());
734
990
  return lastValue;
735
991
  }
736
992
  }
@@ -766,6 +1022,7 @@ function createStorageItem(config) {
766
1022
  onExpired?.(storageKey);
767
1023
  lastValue = ensureValidatedValue(defaultValue, false);
768
1024
  hasLastValue = true;
1025
+ listeners.forEach(cb => cb());
769
1026
  return lastValue;
770
1027
  }
771
1028
  deserializableRaw = parsed.payload;
@@ -781,31 +1038,52 @@ function createStorageItem(config) {
781
1038
  hasLastValue = true;
782
1039
  return lastValue;
783
1040
  };
1041
+ const getCurrentVersion = () => {
1042
+ const raw = readStoredRaw();
1043
+ return (0, _internal.toVersionToken)(raw);
1044
+ };
1045
+ const get = () => measureOperation("item:get", config.scope, () => getInternal());
1046
+ const getWithVersion = () => measureOperation("item:getWithVersion", config.scope, () => ({
1047
+ value: getInternal(),
1048
+ version: getCurrentVersion()
1049
+ }));
784
1050
  const set = valueOrFn => {
785
- const newValue = isUpdater(valueOrFn) ? valueOrFn(get()) : valueOrFn;
786
- invalidateParsedCache();
787
- if (validate && !validate(newValue)) {
788
- throw new Error(`Validation failed for key "${storageKey}" in scope "${_Storage.StorageScope[config.scope]}".`);
789
- }
790
- writeValueWithoutValidation(newValue);
1051
+ measureOperation("item:set", config.scope, () => {
1052
+ const newValue = isUpdater(valueOrFn) ? valueOrFn(getInternal()) : valueOrFn;
1053
+ invalidateParsedCache();
1054
+ if (validate && !validate(newValue)) {
1055
+ throw new Error(`Validation failed for key "${storageKey}" in scope "${_Storage.StorageScope[config.scope]}".`);
1056
+ }
1057
+ writeValueWithoutValidation(newValue);
1058
+ });
791
1059
  };
1060
+ const setIfVersion = (version, valueOrFn) => measureOperation("item:setIfVersion", config.scope, () => {
1061
+ const currentVersion = getCurrentVersion();
1062
+ if (currentVersion !== version) {
1063
+ return false;
1064
+ }
1065
+ set(valueOrFn);
1066
+ return true;
1067
+ });
792
1068
  const deleteItem = () => {
793
- invalidateParsedCache();
794
- if (isMemory) {
795
- if (memoryExpiration) {
796
- memoryExpiration.delete(storageKey);
1069
+ measureOperation("item:delete", config.scope, () => {
1070
+ invalidateParsedCache();
1071
+ if (isMemory) {
1072
+ if (memoryExpiration) {
1073
+ memoryExpiration.delete(storageKey);
1074
+ }
1075
+ memoryStore.delete(storageKey);
1076
+ notifyKeyListeners(memoryListeners, storageKey);
1077
+ return;
797
1078
  }
798
- memoryStore.delete(storageKey);
799
- notifyKeyListeners(memoryListeners, storageKey);
800
- return;
801
- }
802
- removeStoredRaw();
1079
+ removeStoredRaw();
1080
+ });
803
1081
  };
804
- const hasItem = () => {
1082
+ const hasItem = () => measureOperation("item:has", config.scope, () => {
805
1083
  if (isMemory) return memoryStore.has(storageKey);
806
1084
  if (isBiometric) return WebStorage.hasSecureBiometric(storageKey);
807
1085
  return WebStorage.has(storageKey, config.scope);
808
- };
1086
+ });
809
1087
  const subscribe = callback => {
810
1088
  ensureSubscription();
811
1089
  listeners.add(callback);
@@ -819,7 +1097,9 @@ function createStorageItem(config) {
819
1097
  };
820
1098
  const storageItem = {
821
1099
  get,
1100
+ getWithVersion,
822
1101
  set,
1102
+ setIfVersion,
823
1103
  delete: deleteItem,
824
1104
  has: hasItem,
825
1105
  subscribe,
@@ -829,10 +1109,14 @@ function createStorageItem(config) {
829
1109
  invalidateParsedCache();
830
1110
  listeners.forEach(listener => listener());
831
1111
  },
1112
+ _invalidateParsedCacheOnly: () => {
1113
+ invalidateParsedCache();
1114
+ },
832
1115
  _hasValidation: validate !== undefined,
833
1116
  _hasExpiration: expiration !== undefined,
834
1117
  _readCacheEnabled: readCache,
835
1118
  _isBiometric: isBiometric,
1119
+ _defaultValue: defaultValue,
836
1120
  ...(secureAccessControl !== undefined ? {
837
1121
  _secureAccessControl: secureAccessControl
838
1122
  } : {}),
@@ -842,135 +1126,162 @@ function createStorageItem(config) {
842
1126
  return storageItem;
843
1127
  }
844
1128
  function getBatch(items, scope) {
845
- (0, _internal.assertBatchScope)(items, scope);
846
- if (scope === _Storage.StorageScope.Memory) {
847
- return items.map(item => item.get());
848
- }
849
- const useRawBatchPath = items.every(item => scope === _Storage.StorageScope.Secure ? canUseSecureRawBatchPath(item) : canUseRawBatchPath(item));
850
- if (!useRawBatchPath) {
851
- return items.map(item => item.get());
852
- }
853
- const useBatchCache = items.every(item => item._readCacheEnabled === true);
854
- const rawValues = new Array(items.length);
855
- const keysToFetch = [];
856
- const keyIndexes = [];
857
- items.forEach((item, index) => {
858
- if (scope === _Storage.StorageScope.Secure) {
859
- if (hasPendingSecureWrite(item.key)) {
860
- rawValues[index] = readPendingSecureWrite(item.key);
861
- return;
1129
+ return measureOperation("batch:get", scope, () => {
1130
+ (0, _internal.assertBatchScope)(items, scope);
1131
+ if (scope === _Storage.StorageScope.Memory) {
1132
+ return items.map(item => item.get());
1133
+ }
1134
+ const useRawBatchPath = items.every(item => scope === _Storage.StorageScope.Secure ? canUseSecureRawBatchPath(item) : canUseRawBatchPath(item));
1135
+ if (!useRawBatchPath) {
1136
+ return items.map(item => item.get());
1137
+ }
1138
+ const rawValues = new Array(items.length);
1139
+ const keysToFetch = [];
1140
+ const keyIndexes = [];
1141
+ items.forEach((item, index) => {
1142
+ if (scope === _Storage.StorageScope.Secure) {
1143
+ if (hasPendingSecureWrite(item.key)) {
1144
+ rawValues[index] = readPendingSecureWrite(item.key);
1145
+ return;
1146
+ }
862
1147
  }
1148
+ if (item._readCacheEnabled === true) {
1149
+ if (hasCachedRawValue(scope, item.key)) {
1150
+ rawValues[index] = readCachedRawValue(scope, item.key);
1151
+ return;
1152
+ }
1153
+ }
1154
+ keysToFetch.push(item.key);
1155
+ keyIndexes.push(index);
1156
+ });
1157
+ if (keysToFetch.length > 0) {
1158
+ const fetchedValues = WebStorage.getBatch(keysToFetch, scope);
1159
+ fetchedValues.forEach((value, index) => {
1160
+ const key = keysToFetch[index];
1161
+ const targetIndex = keyIndexes[index];
1162
+ if (key === undefined || targetIndex === undefined) {
1163
+ return;
1164
+ }
1165
+ rawValues[targetIndex] = value;
1166
+ cacheRawValue(scope, key, value);
1167
+ });
863
1168
  }
864
- if (useBatchCache) {
865
- if (hasCachedRawValue(scope, item.key)) {
866
- rawValues[index] = readCachedRawValue(scope, item.key);
1169
+ return items.map((item, index) => {
1170
+ const raw = rawValues[index];
1171
+ if (raw === undefined) {
1172
+ return asInternal(item)._defaultValue;
1173
+ }
1174
+ return item.deserialize(raw);
1175
+ });
1176
+ }, items.length);
1177
+ }
1178
+ function setBatch(items, scope) {
1179
+ measureOperation("batch:set", scope, () => {
1180
+ (0, _internal.assertBatchScope)(items.map(batchEntry => batchEntry.item), scope);
1181
+ if (scope === _Storage.StorageScope.Memory) {
1182
+ // Determine if any item needs per-item handling (validation or TTL)
1183
+ const needsIndividualSets = items.some(({
1184
+ item
1185
+ }) => {
1186
+ const internal = asInternal(item);
1187
+ return internal._hasValidation || internal._hasExpiration;
1188
+ });
1189
+ if (needsIndividualSets) {
1190
+ items.forEach(({
1191
+ item,
1192
+ value
1193
+ }) => item.set(value));
867
1194
  return;
868
1195
  }
1196
+
1197
+ // Atomic write: update all values in memoryStore, invalidate caches, then batch-notify
1198
+ items.forEach(({
1199
+ item,
1200
+ value
1201
+ }) => {
1202
+ memoryStore.set(item.key, value);
1203
+ asInternal(item)._invalidateParsedCacheOnly();
1204
+ });
1205
+ items.forEach(({
1206
+ item
1207
+ }) => notifyKeyListeners(memoryListeners, item.key));
1208
+ return;
869
1209
  }
870
- keysToFetch.push(item.key);
871
- keyIndexes.push(index);
872
- });
873
- if (keysToFetch.length > 0) {
874
- const fetchedValues = WebStorage.getBatch(keysToFetch, scope);
875
- fetchedValues.forEach((value, index) => {
876
- const key = keysToFetch[index];
877
- const targetIndex = keyIndexes[index];
878
- if (key === undefined || targetIndex === undefined) {
1210
+ if (scope === _Storage.StorageScope.Secure) {
1211
+ const secureEntries = items.map(({
1212
+ item,
1213
+ value
1214
+ }) => ({
1215
+ item,
1216
+ value,
1217
+ internal: asInternal(item)
1218
+ }));
1219
+ const canUseSecureBatchPath = secureEntries.every(({
1220
+ internal
1221
+ }) => canUseSecureRawBatchPath(internal));
1222
+ if (!canUseSecureBatchPath) {
1223
+ items.forEach(({
1224
+ item,
1225
+ value
1226
+ }) => item.set(value));
879
1227
  return;
880
1228
  }
881
- rawValues[targetIndex] = value;
882
- cacheRawValue(scope, key, value);
883
- });
884
- }
885
- return items.map((item, index) => {
886
- const raw = rawValues[index];
887
- if (raw === undefined) {
888
- return item.get();
1229
+ flushSecureWrites();
1230
+ const groupedByAccessControl = new Map();
1231
+ secureEntries.forEach(({
1232
+ item,
1233
+ value,
1234
+ internal
1235
+ }) => {
1236
+ const accessControl = internal._secureAccessControl ?? _Storage.AccessControl.WhenUnlocked;
1237
+ const existingGroup = groupedByAccessControl.get(accessControl);
1238
+ const group = existingGroup ?? {
1239
+ keys: [],
1240
+ values: []
1241
+ };
1242
+ group.keys.push(item.key);
1243
+ group.values.push(item.serialize(value));
1244
+ if (!existingGroup) {
1245
+ groupedByAccessControl.set(accessControl, group);
1246
+ }
1247
+ });
1248
+ groupedByAccessControl.forEach((group, accessControl) => {
1249
+ WebStorage.setSecureAccessControl(accessControl);
1250
+ WebStorage.setBatch(group.keys, group.values, scope);
1251
+ group.keys.forEach((key, index) => cacheRawValue(scope, key, group.values[index]));
1252
+ });
1253
+ return;
889
1254
  }
890
- return item.deserialize(raw);
891
- });
892
- }
893
- function setBatch(items, scope) {
894
- (0, _internal.assertBatchScope)(items.map(batchEntry => batchEntry.item), scope);
895
- if (scope === _Storage.StorageScope.Memory) {
896
- items.forEach(({
897
- item,
898
- value
899
- }) => item.set(value));
900
- return;
901
- }
902
- if (scope === _Storage.StorageScope.Secure) {
903
- const secureEntries = items.map(({
904
- item,
905
- value
906
- }) => ({
907
- item,
908
- value,
909
- internal: asInternal(item)
910
- }));
911
- const canUseSecureBatchPath = secureEntries.every(({
912
- internal
913
- }) => canUseSecureRawBatchPath(internal));
914
- if (!canUseSecureBatchPath) {
1255
+ const useRawBatchPath = items.every(({
1256
+ item
1257
+ }) => canUseRawBatchPath(asInternal(item)));
1258
+ if (!useRawBatchPath) {
915
1259
  items.forEach(({
916
1260
  item,
917
1261
  value
918
1262
  }) => item.set(value));
919
1263
  return;
920
1264
  }
921
- flushSecureWrites();
922
- const groupedByAccessControl = new Map();
923
- secureEntries.forEach(({
924
- item,
925
- value,
926
- internal
927
- }) => {
928
- const accessControl = internal._secureAccessControl ?? _Storage.AccessControl.WhenUnlocked;
929
- const existingGroup = groupedByAccessControl.get(accessControl);
930
- const group = existingGroup ?? {
931
- keys: [],
932
- values: []
933
- };
934
- group.keys.push(item.key);
935
- group.values.push(item.serialize(value));
936
- if (!existingGroup) {
937
- groupedByAccessControl.set(accessControl, group);
938
- }
939
- });
940
- groupedByAccessControl.forEach((group, accessControl) => {
941
- WebStorage.setSecureAccessControl(accessControl);
942
- WebStorage.setBatch(group.keys, group.values, scope);
943
- group.keys.forEach((key, index) => cacheRawValue(scope, key, group.values[index]));
944
- });
945
- return;
946
- }
947
- const useRawBatchPath = items.every(({
948
- item
949
- }) => canUseRawBatchPath(asInternal(item)));
950
- if (!useRawBatchPath) {
951
- items.forEach(({
952
- item,
953
- value
954
- }) => item.set(value));
955
- return;
956
- }
957
- const keys = items.map(entry => entry.item.key);
958
- const values = items.map(entry => entry.item.serialize(entry.value));
959
- WebStorage.setBatch(keys, values, scope);
960
- keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
1265
+ const keys = items.map(entry => entry.item.key);
1266
+ const values = items.map(entry => entry.item.serialize(entry.value));
1267
+ WebStorage.setBatch(keys, values, scope);
1268
+ keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
1269
+ }, items.length);
961
1270
  }
962
1271
  function removeBatch(items, scope) {
963
- (0, _internal.assertBatchScope)(items, scope);
964
- if (scope === _Storage.StorageScope.Memory) {
965
- items.forEach(item => item.delete());
966
- return;
967
- }
968
- const keys = items.map(item => item.key);
969
- if (scope === _Storage.StorageScope.Secure) {
970
- flushSecureWrites();
971
- }
972
- WebStorage.removeBatch(keys, scope);
973
- keys.forEach(key => cacheRawValue(scope, key, undefined));
1272
+ measureOperation("batch:remove", scope, () => {
1273
+ (0, _internal.assertBatchScope)(items, scope);
1274
+ if (scope === _Storage.StorageScope.Memory) {
1275
+ items.forEach(item => item.delete());
1276
+ return;
1277
+ }
1278
+ const keys = items.map(item => item.key);
1279
+ if (scope === _Storage.StorageScope.Secure) {
1280
+ flushSecureWrites();
1281
+ }
1282
+ WebStorage.removeBatch(keys, scope);
1283
+ keys.forEach(key => cacheRawValue(scope, key, undefined));
1284
+ }, items.length);
974
1285
  }
975
1286
  function registerMigration(version, migration) {
976
1287
  if (!Number.isInteger(version) || version <= 0) {
@@ -982,77 +1293,107 @@ function registerMigration(version, migration) {
982
1293
  registeredMigrations.set(version, migration);
983
1294
  }
984
1295
  function migrateToLatest(scope = _Storage.StorageScope.Disk) {
985
- (0, _internal.assertValidScope)(scope);
986
- const currentVersion = readMigrationVersion(scope);
987
- const versions = Array.from(registeredMigrations.keys()).filter(version => version > currentVersion).sort((a, b) => a - b);
988
- let appliedVersion = currentVersion;
989
- const context = {
990
- scope,
991
- getRaw: key => getRawValue(key, scope),
992
- setRaw: (key, value) => setRawValue(key, value, scope),
993
- removeRaw: key => removeRawValue(key, scope)
994
- };
995
- versions.forEach(version => {
996
- const migration = registeredMigrations.get(version);
997
- if (!migration) {
998
- return;
999
- }
1000
- migration(context);
1001
- writeMigrationVersion(scope, version);
1002
- appliedVersion = version;
1296
+ return measureOperation("migration:run", scope, () => {
1297
+ (0, _internal.assertValidScope)(scope);
1298
+ const currentVersion = readMigrationVersion(scope);
1299
+ const versions = Array.from(registeredMigrations.keys()).filter(version => version > currentVersion).sort((a, b) => a - b);
1300
+ let appliedVersion = currentVersion;
1301
+ const context = {
1302
+ scope,
1303
+ getRaw: key => getRawValue(key, scope),
1304
+ setRaw: (key, value) => setRawValue(key, value, scope),
1305
+ removeRaw: key => removeRawValue(key, scope)
1306
+ };
1307
+ versions.forEach(version => {
1308
+ const migration = registeredMigrations.get(version);
1309
+ if (!migration) {
1310
+ return;
1311
+ }
1312
+ migration(context);
1313
+ writeMigrationVersion(scope, version);
1314
+ appliedVersion = version;
1315
+ });
1316
+ return appliedVersion;
1003
1317
  });
1004
- return appliedVersion;
1005
1318
  }
1006
1319
  function runTransaction(scope, transaction) {
1007
- (0, _internal.assertValidScope)(scope);
1008
- if (scope === _Storage.StorageScope.Secure) {
1009
- flushSecureWrites();
1010
- }
1011
- const rollback = new Map();
1012
- const rememberRollback = key => {
1013
- if (rollback.has(key)) {
1014
- return;
1015
- }
1016
- rollback.set(key, getRawValue(key, scope));
1017
- };
1018
- const tx = {
1019
- scope,
1020
- getRaw: key => getRawValue(key, scope),
1021
- setRaw: (key, value) => {
1022
- rememberRollback(key);
1023
- setRawValue(key, value, scope);
1024
- },
1025
- removeRaw: key => {
1026
- rememberRollback(key);
1027
- removeRawValue(key, scope);
1028
- },
1029
- getItem: item => {
1030
- (0, _internal.assertBatchScope)([item], scope);
1031
- return item.get();
1032
- },
1033
- setItem: (item, value) => {
1034
- (0, _internal.assertBatchScope)([item], scope);
1035
- rememberRollback(item.key);
1036
- item.set(value);
1037
- },
1038
- removeItem: item => {
1039
- (0, _internal.assertBatchScope)([item], scope);
1040
- rememberRollback(item.key);
1041
- item.delete();
1320
+ return measureOperation("transaction:run", scope, () => {
1321
+ (0, _internal.assertValidScope)(scope);
1322
+ if (scope === _Storage.StorageScope.Secure) {
1323
+ flushSecureWrites();
1042
1324
  }
1043
- };
1044
- try {
1045
- return transaction(tx);
1046
- } catch (error) {
1047
- Array.from(rollback.entries()).reverse().forEach(([key, previousValue]) => {
1048
- if (previousValue === undefined) {
1325
+ const rollback = new Map();
1326
+ const rememberRollback = key => {
1327
+ if (rollback.has(key)) {
1328
+ return;
1329
+ }
1330
+ rollback.set(key, getRawValue(key, scope));
1331
+ };
1332
+ const tx = {
1333
+ scope,
1334
+ getRaw: key => getRawValue(key, scope),
1335
+ setRaw: (key, value) => {
1336
+ rememberRollback(key);
1337
+ setRawValue(key, value, scope);
1338
+ },
1339
+ removeRaw: key => {
1340
+ rememberRollback(key);
1049
1341
  removeRawValue(key, scope);
1342
+ },
1343
+ getItem: item => {
1344
+ (0, _internal.assertBatchScope)([item], scope);
1345
+ return item.get();
1346
+ },
1347
+ setItem: (item, value) => {
1348
+ (0, _internal.assertBatchScope)([item], scope);
1349
+ rememberRollback(item.key);
1350
+ item.set(value);
1351
+ },
1352
+ removeItem: item => {
1353
+ (0, _internal.assertBatchScope)([item], scope);
1354
+ rememberRollback(item.key);
1355
+ item.delete();
1356
+ }
1357
+ };
1358
+ try {
1359
+ return transaction(tx);
1360
+ } catch (error) {
1361
+ const rollbackEntries = Array.from(rollback.entries()).reverse();
1362
+ if (scope === _Storage.StorageScope.Memory) {
1363
+ rollbackEntries.forEach(([key, previousValue]) => {
1364
+ if (previousValue === undefined) {
1365
+ removeRawValue(key, scope);
1366
+ } else {
1367
+ setRawValue(key, previousValue, scope);
1368
+ }
1369
+ });
1050
1370
  } else {
1051
- setRawValue(key, previousValue, scope);
1371
+ const keysToSet = [];
1372
+ const valuesToSet = [];
1373
+ const keysToRemove = [];
1374
+ rollbackEntries.forEach(([key, previousValue]) => {
1375
+ if (previousValue === undefined) {
1376
+ keysToRemove.push(key);
1377
+ } else {
1378
+ keysToSet.push(key);
1379
+ valuesToSet.push(previousValue);
1380
+ }
1381
+ });
1382
+ if (scope === _Storage.StorageScope.Secure) {
1383
+ flushSecureWrites();
1384
+ }
1385
+ if (keysToSet.length > 0) {
1386
+ WebStorage.setBatch(keysToSet, valuesToSet, scope);
1387
+ keysToSet.forEach((key, index) => cacheRawValue(scope, key, valuesToSet[index]));
1388
+ }
1389
+ if (keysToRemove.length > 0) {
1390
+ WebStorage.removeBatch(keysToRemove, scope);
1391
+ keysToRemove.forEach(key => cacheRawValue(scope, key, undefined));
1392
+ }
1052
1393
  }
1053
- });
1054
- throw error;
1055
- }
1394
+ throw error;
1395
+ }
1396
+ });
1056
1397
  }
1057
1398
  function createSecureAuthStorage(config, options) {
1058
1399
  const ns = options?.namespace ?? "auth";
@@ -1070,6 +1411,9 @@ function createSecureAuthStorage(config, options) {
1070
1411
  ...(itemConfig.biometric !== undefined ? {
1071
1412
  biometric: itemConfig.biometric
1072
1413
  } : {}),
1414
+ ...(itemConfig.biometricLevel !== undefined ? {
1415
+ biometricLevel: itemConfig.biometricLevel
1416
+ } : {}),
1073
1417
  ...(itemConfig.accessControl !== undefined ? {
1074
1418
  accessControl: itemConfig.accessControl
1075
1419
  } : {}),