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
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
 
3
- import { StorageScope, AccessControl } from "./Storage.types";
4
- import { MIGRATION_VERSION_KEY, isStoredEnvelope, assertBatchScope, assertValidScope, serializeWithPrimitiveFastPath, deserializeWithPrimitiveFastPath, prefixKey, isNamespaced } from "./internal";
3
+ import { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
4
+ import { MIGRATION_VERSION_KEY, isStoredEnvelope, assertBatchScope, assertValidScope, serializeWithPrimitiveFastPath, deserializeWithPrimitiveFastPath, toVersionToken, prefixKey, isNamespaced } from "./internal";
5
5
  export { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
6
6
  export { migrateFromMMKV } from "./migration";
7
7
  function asInternal(item) {
@@ -28,12 +28,76 @@ let secureFlushScheduled = false;
28
28
  const SECURE_WEB_PREFIX = "__secure_";
29
29
  const BIOMETRIC_WEB_PREFIX = "__bio_";
30
30
  let hasWarnedAboutWebBiometricFallback = false;
31
+ let hasWebStorageEventSubscription = false;
32
+ let metricsObserver;
33
+ const metricsCounters = new Map();
34
+ function recordMetric(operation, scope, durationMs, keysCount = 1) {
35
+ const existing = metricsCounters.get(operation);
36
+ if (!existing) {
37
+ metricsCounters.set(operation, {
38
+ count: 1,
39
+ totalDurationMs: durationMs,
40
+ maxDurationMs: durationMs
41
+ });
42
+ } else {
43
+ existing.count += 1;
44
+ existing.totalDurationMs += durationMs;
45
+ existing.maxDurationMs = Math.max(existing.maxDurationMs, durationMs);
46
+ }
47
+ metricsObserver?.({
48
+ operation,
49
+ scope,
50
+ durationMs,
51
+ keysCount
52
+ });
53
+ }
54
+ function measureOperation(operation, scope, fn, keysCount = 1) {
55
+ const start = Date.now();
56
+ try {
57
+ return fn();
58
+ } finally {
59
+ recordMetric(operation, scope, Date.now() - start, keysCount);
60
+ }
61
+ }
62
+ function createLocalStorageWebSecureBackend() {
63
+ return {
64
+ getItem: key => globalThis.localStorage?.getItem(key) ?? null,
65
+ setItem: (key, value) => globalThis.localStorage?.setItem(key, value),
66
+ removeItem: key => globalThis.localStorage?.removeItem(key),
67
+ clear: () => globalThis.localStorage?.clear(),
68
+ getAllKeys: () => {
69
+ const storage = globalThis.localStorage;
70
+ if (!storage) return [];
71
+ const keys = [];
72
+ for (let index = 0; index < storage.length; index += 1) {
73
+ const key = storage.key(index);
74
+ if (key) {
75
+ keys.push(key);
76
+ }
77
+ }
78
+ return keys;
79
+ }
80
+ };
81
+ }
82
+ let webSecureStorageBackend = createLocalStorageWebSecureBackend();
31
83
  function getBrowserStorage(scope) {
32
84
  if (scope === StorageScope.Disk) {
33
85
  return globalThis.localStorage;
34
86
  }
35
87
  if (scope === StorageScope.Secure) {
36
- return globalThis.localStorage;
88
+ if (!webSecureStorageBackend) {
89
+ return undefined;
90
+ }
91
+ return {
92
+ setItem: (key, value) => webSecureStorageBackend?.setItem(key, value),
93
+ getItem: key => webSecureStorageBackend?.getItem(key) ?? null,
94
+ removeItem: key => webSecureStorageBackend?.removeItem(key),
95
+ clear: () => webSecureStorageBackend?.clear(),
96
+ key: index => webSecureStorageBackend?.getAllKeys()[index] ?? null,
97
+ get length() {
98
+ return webSecureStorageBackend?.getAllKeys().length ?? 0;
99
+ }
100
+ };
37
101
  }
38
102
  return undefined;
39
103
  }
@@ -86,6 +150,62 @@ function ensureWebScopeKeyIndex(scope) {
86
150
  hydrateWebScopeKeyIndex(scope);
87
151
  return getWebScopeKeyIndex(scope);
88
152
  }
153
+ function handleWebStorageEvent(event) {
154
+ const key = event.key;
155
+ if (key === null) {
156
+ clearScopeRawCache(StorageScope.Disk);
157
+ clearScopeRawCache(StorageScope.Secure);
158
+ ensureWebScopeKeyIndex(StorageScope.Disk).clear();
159
+ ensureWebScopeKeyIndex(StorageScope.Secure).clear();
160
+ notifyAllListeners(getScopedListeners(StorageScope.Disk));
161
+ notifyAllListeners(getScopedListeners(StorageScope.Secure));
162
+ return;
163
+ }
164
+ if (key.startsWith(SECURE_WEB_PREFIX)) {
165
+ const plainKey = fromSecureStorageKey(key);
166
+ if (event.newValue === null) {
167
+ ensureWebScopeKeyIndex(StorageScope.Secure).delete(plainKey);
168
+ cacheRawValue(StorageScope.Secure, plainKey, undefined);
169
+ } else {
170
+ ensureWebScopeKeyIndex(StorageScope.Secure).add(plainKey);
171
+ cacheRawValue(StorageScope.Secure, plainKey, event.newValue);
172
+ }
173
+ notifyKeyListeners(getScopedListeners(StorageScope.Secure), plainKey);
174
+ return;
175
+ }
176
+ if (key.startsWith(BIOMETRIC_WEB_PREFIX)) {
177
+ const plainKey = fromBiometricStorageKey(key);
178
+ if (event.newValue === null) {
179
+ if (getBrowserStorage(StorageScope.Secure)?.getItem(toSecureStorageKey(plainKey)) === null) {
180
+ ensureWebScopeKeyIndex(StorageScope.Secure).delete(plainKey);
181
+ }
182
+ cacheRawValue(StorageScope.Secure, plainKey, undefined);
183
+ } else {
184
+ ensureWebScopeKeyIndex(StorageScope.Secure).add(plainKey);
185
+ cacheRawValue(StorageScope.Secure, plainKey, event.newValue);
186
+ }
187
+ notifyKeyListeners(getScopedListeners(StorageScope.Secure), plainKey);
188
+ return;
189
+ }
190
+ if (event.newValue === null) {
191
+ ensureWebScopeKeyIndex(StorageScope.Disk).delete(key);
192
+ cacheRawValue(StorageScope.Disk, key, undefined);
193
+ } else {
194
+ ensureWebScopeKeyIndex(StorageScope.Disk).add(key);
195
+ cacheRawValue(StorageScope.Disk, key, event.newValue);
196
+ }
197
+ notifyKeyListeners(getScopedListeners(StorageScope.Disk), key);
198
+ }
199
+ function ensureWebStorageEventSubscription() {
200
+ if (hasWebStorageEventSubscription) {
201
+ return;
202
+ }
203
+ if (typeof window === "undefined" || typeof window.addEventListener !== "function") {
204
+ return;
205
+ }
206
+ window.addEventListener("storage", handleWebStorageEvent);
207
+ hasWebStorageEventSubscription = true;
208
+ }
89
209
  function getScopedListeners(scope) {
90
210
  return webScopeListeners.get(scope);
91
211
  }
@@ -146,32 +266,46 @@ function flushSecureWrites() {
146
266
  }
147
267
  const writes = Array.from(pendingSecureWrites.values());
148
268
  pendingSecureWrites.clear();
149
- const keysToSet = [];
150
- const valuesToSet = [];
269
+ const groupedSetWrites = new Map();
151
270
  const keysToRemove = [];
152
271
  writes.forEach(({
153
272
  key,
154
- value
273
+ value,
274
+ accessControl
155
275
  }) => {
156
276
  if (value === undefined) {
157
277
  keysToRemove.push(key);
158
278
  } else {
159
- keysToSet.push(key);
160
- valuesToSet.push(value);
279
+ const resolvedAccessControl = accessControl ?? AccessControl.WhenUnlocked;
280
+ const existingGroup = groupedSetWrites.get(resolvedAccessControl);
281
+ const group = existingGroup ?? {
282
+ keys: [],
283
+ values: []
284
+ };
285
+ group.keys.push(key);
286
+ group.values.push(value);
287
+ if (!existingGroup) {
288
+ groupedSetWrites.set(resolvedAccessControl, group);
289
+ }
161
290
  }
162
291
  });
163
- if (keysToSet.length > 0) {
164
- WebStorage.setBatch(keysToSet, valuesToSet, StorageScope.Secure);
165
- }
292
+ groupedSetWrites.forEach((group, accessControl) => {
293
+ WebStorage.setSecureAccessControl(accessControl);
294
+ WebStorage.setBatch(group.keys, group.values, StorageScope.Secure);
295
+ });
166
296
  if (keysToRemove.length > 0) {
167
297
  WebStorage.removeBatch(keysToRemove, StorageScope.Secure);
168
298
  }
169
299
  }
170
- function scheduleSecureWrite(key, value) {
171
- pendingSecureWrites.set(key, {
300
+ function scheduleSecureWrite(key, value, accessControl) {
301
+ const pendingWrite = {
172
302
  key,
173
303
  value
174
- });
304
+ };
305
+ if (accessControl !== undefined) {
306
+ pendingWrite.accessControl = accessControl;
307
+ }
308
+ pendingSecureWrites.set(key, pendingWrite);
175
309
  if (secureFlushScheduled) {
176
310
  return;
177
311
  }
@@ -322,6 +456,12 @@ const WebStorage = {
322
456
  }
323
457
  return Array.from(ensureWebScopeKeyIndex(scope));
324
458
  },
459
+ getKeysByPrefix: (prefix, scope) => {
460
+ if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
461
+ return [];
462
+ }
463
+ return Array.from(ensureWebScopeKeyIndex(scope)).filter(key => key.startsWith(prefix));
464
+ },
325
465
  size: scope => {
326
466
  if (scope === StorageScope.Disk || scope === StorageScope.Secure) {
327
467
  return ensureWebScopeKeyIndex(scope).size;
@@ -332,19 +472,22 @@ const WebStorage = {
332
472
  setSecureWritesAsync: _enabled => {},
333
473
  setKeychainAccessGroup: () => {},
334
474
  setSecureBiometric: (key, value) => {
475
+ WebStorage.setSecureBiometricWithLevel(key, value, BiometricLevel.BiometryOnly);
476
+ },
477
+ setSecureBiometricWithLevel: (key, value, _level) => {
335
478
  if (typeof __DEV__ !== "undefined" && __DEV__ && !hasWarnedAboutWebBiometricFallback) {
336
479
  hasWarnedAboutWebBiometricFallback = true;
337
480
  console.warn("[NitroStorage] Biometric storage is not supported on web. Using localStorage.");
338
481
  }
339
- globalThis.localStorage?.setItem(toBiometricStorageKey(key), value);
482
+ getBrowserStorage(StorageScope.Secure)?.setItem(toBiometricStorageKey(key), value);
340
483
  ensureWebScopeKeyIndex(StorageScope.Secure).add(key);
341
484
  notifyKeyListeners(getScopedListeners(StorageScope.Secure), key);
342
485
  },
343
486
  getSecureBiometric: key => {
344
- return globalThis.localStorage?.getItem(toBiometricStorageKey(key)) ?? undefined;
487
+ return getBrowserStorage(StorageScope.Secure)?.getItem(toBiometricStorageKey(key)) ?? undefined;
345
488
  },
346
489
  deleteSecureBiometric: key => {
347
- const storage = globalThis.localStorage;
490
+ const storage = getBrowserStorage(StorageScope.Secure);
348
491
  storage?.removeItem(toBiometricStorageKey(key));
349
492
  if (storage?.getItem(toSecureStorageKey(key)) === null) {
350
493
  ensureWebScopeKeyIndex(StorageScope.Secure).delete(key);
@@ -352,10 +495,10 @@ const WebStorage = {
352
495
  notifyKeyListeners(getScopedListeners(StorageScope.Secure), key);
353
496
  },
354
497
  hasSecureBiometric: key => {
355
- return globalThis.localStorage?.getItem(toBiometricStorageKey(key)) !== null;
498
+ return getBrowserStorage(StorageScope.Secure)?.getItem(toBiometricStorageKey(key)) !== null;
356
499
  },
357
500
  clearSecureBiometric: () => {
358
- const storage = globalThis.localStorage;
501
+ const storage = getBrowserStorage(StorageScope.Secure);
359
502
  if (!storage) return;
360
503
  const keysToNotify = [];
361
504
  const toRemove = [];
@@ -429,82 +572,183 @@ function writeMigrationVersion(scope, version) {
429
572
  }
430
573
  export const storage = {
431
574
  clear: scope => {
432
- if (scope === StorageScope.Memory) {
433
- memoryStore.clear();
434
- notifyAllListeners(memoryListeners);
435
- return;
436
- }
437
- if (scope === StorageScope.Secure) {
438
- flushSecureWrites();
439
- pendingSecureWrites.clear();
440
- }
441
- clearScopeRawCache(scope);
442
- WebStorage.clear(scope);
575
+ measureOperation("storage:clear", scope, () => {
576
+ if (scope === StorageScope.Memory) {
577
+ memoryStore.clear();
578
+ notifyAllListeners(memoryListeners);
579
+ return;
580
+ }
581
+ if (scope === StorageScope.Secure) {
582
+ flushSecureWrites();
583
+ pendingSecureWrites.clear();
584
+ }
585
+ clearScopeRawCache(scope);
586
+ WebStorage.clear(scope);
587
+ });
443
588
  },
444
589
  clearAll: () => {
445
- storage.clear(StorageScope.Memory);
446
- storage.clear(StorageScope.Disk);
447
- storage.clear(StorageScope.Secure);
590
+ measureOperation("storage:clearAll", StorageScope.Memory, () => {
591
+ storage.clear(StorageScope.Memory);
592
+ storage.clear(StorageScope.Disk);
593
+ storage.clear(StorageScope.Secure);
594
+ }, 3);
448
595
  },
449
596
  clearNamespace: (namespace, scope) => {
450
- assertValidScope(scope);
451
- if (scope === StorageScope.Memory) {
452
- for (const key of memoryStore.keys()) {
453
- if (isNamespaced(key, namespace)) {
454
- memoryStore.delete(key);
597
+ measureOperation("storage:clearNamespace", scope, () => {
598
+ assertValidScope(scope);
599
+ if (scope === StorageScope.Memory) {
600
+ for (const key of memoryStore.keys()) {
601
+ if (isNamespaced(key, namespace)) {
602
+ memoryStore.delete(key);
603
+ }
455
604
  }
605
+ notifyAllListeners(memoryListeners);
606
+ return;
456
607
  }
457
- notifyAllListeners(memoryListeners);
458
- return;
459
- }
460
- const keyPrefix = prefixKey(namespace, "");
461
- if (scope === StorageScope.Secure) {
462
- flushSecureWrites();
463
- }
464
- clearScopeRawCache(scope);
465
- WebStorage.removeByPrefix(keyPrefix, scope);
608
+ const keyPrefix = prefixKey(namespace, "");
609
+ if (scope === StorageScope.Secure) {
610
+ flushSecureWrites();
611
+ }
612
+ clearScopeRawCache(scope);
613
+ WebStorage.removeByPrefix(keyPrefix, scope);
614
+ });
466
615
  },
467
616
  clearBiometric: () => {
468
- WebStorage.clearSecureBiometric();
617
+ measureOperation("storage:clearBiometric", StorageScope.Secure, () => {
618
+ WebStorage.clearSecureBiometric();
619
+ });
469
620
  },
470
621
  has: (key, scope) => {
471
- assertValidScope(scope);
472
- if (scope === StorageScope.Memory) return memoryStore.has(key);
473
- return WebStorage.has(key, scope);
622
+ return measureOperation("storage:has", scope, () => {
623
+ assertValidScope(scope);
624
+ if (scope === StorageScope.Memory) return memoryStore.has(key);
625
+ return WebStorage.has(key, scope);
626
+ });
474
627
  },
475
628
  getAllKeys: scope => {
476
- assertValidScope(scope);
477
- if (scope === StorageScope.Memory) return Array.from(memoryStore.keys());
478
- return WebStorage.getAllKeys(scope);
629
+ return measureOperation("storage:getAllKeys", scope, () => {
630
+ assertValidScope(scope);
631
+ if (scope === StorageScope.Memory) return Array.from(memoryStore.keys());
632
+ return WebStorage.getAllKeys(scope);
633
+ });
634
+ },
635
+ getKeysByPrefix: (prefix, scope) => {
636
+ return measureOperation("storage:getKeysByPrefix", scope, () => {
637
+ assertValidScope(scope);
638
+ if (scope === StorageScope.Memory) {
639
+ return Array.from(memoryStore.keys()).filter(key => key.startsWith(prefix));
640
+ }
641
+ return WebStorage.getKeysByPrefix(prefix, scope);
642
+ });
643
+ },
644
+ getByPrefix: (prefix, scope) => {
645
+ return measureOperation("storage:getByPrefix", scope, () => {
646
+ const result = {};
647
+ const keys = storage.getKeysByPrefix(prefix, scope);
648
+ if (keys.length === 0) {
649
+ return result;
650
+ }
651
+ if (scope === StorageScope.Memory) {
652
+ keys.forEach(key => {
653
+ const value = memoryStore.get(key);
654
+ if (typeof value === "string") {
655
+ result[key] = value;
656
+ }
657
+ });
658
+ return result;
659
+ }
660
+ const values = WebStorage.getBatch(keys, scope);
661
+ keys.forEach((key, index) => {
662
+ const value = values[index];
663
+ if (value !== undefined) {
664
+ result[key] = value;
665
+ }
666
+ });
667
+ return result;
668
+ });
479
669
  },
480
670
  getAll: scope => {
481
- assertValidScope(scope);
482
- const result = {};
483
- if (scope === StorageScope.Memory) {
484
- memoryStore.forEach((value, key) => {
485
- if (typeof value === "string") result[key] = value;
671
+ return measureOperation("storage:getAll", scope, () => {
672
+ assertValidScope(scope);
673
+ const result = {};
674
+ if (scope === StorageScope.Memory) {
675
+ memoryStore.forEach((value, key) => {
676
+ if (typeof value === "string") result[key] = value;
677
+ });
678
+ return result;
679
+ }
680
+ const keys = WebStorage.getAllKeys(scope);
681
+ keys.forEach(key => {
682
+ const val = WebStorage.get(key, scope);
683
+ if (val !== undefined) result[key] = val;
486
684
  });
487
685
  return result;
488
- }
489
- const keys = WebStorage.getAllKeys(scope);
490
- keys.forEach(key => {
491
- const val = WebStorage.get(key, scope);
492
- if (val !== undefined) result[key] = val;
493
686
  });
494
- return result;
495
687
  },
496
688
  size: scope => {
497
- assertValidScope(scope);
498
- if (scope === StorageScope.Memory) return memoryStore.size;
499
- return WebStorage.size(scope);
689
+ return measureOperation("storage:size", scope, () => {
690
+ assertValidScope(scope);
691
+ if (scope === StorageScope.Memory) return memoryStore.size;
692
+ return WebStorage.size(scope);
693
+ });
694
+ },
695
+ setAccessControl: _level => {
696
+ recordMetric("storage:setAccessControl", StorageScope.Secure, 0);
697
+ },
698
+ setSecureWritesAsync: _enabled => {
699
+ recordMetric("storage:setSecureWritesAsync", StorageScope.Secure, 0);
500
700
  },
501
- setAccessControl: _level => {},
502
- setSecureWritesAsync: _enabled => {},
503
701
  flushSecureWrites: () => {
504
- flushSecureWrites();
702
+ measureOperation("storage:flushSecureWrites", StorageScope.Secure, () => {
703
+ flushSecureWrites();
704
+ });
705
+ },
706
+ setKeychainAccessGroup: _group => {
707
+ recordMetric("storage:setKeychainAccessGroup", StorageScope.Secure, 0);
505
708
  },
506
- setKeychainAccessGroup: _group => {}
709
+ setMetricsObserver: observer => {
710
+ metricsObserver = observer;
711
+ },
712
+ getMetricsSnapshot: () => {
713
+ const snapshot = {};
714
+ metricsCounters.forEach((value, key) => {
715
+ snapshot[key] = {
716
+ count: value.count,
717
+ totalDurationMs: value.totalDurationMs,
718
+ avgDurationMs: value.count === 0 ? 0 : value.totalDurationMs / value.count,
719
+ maxDurationMs: value.maxDurationMs
720
+ };
721
+ });
722
+ return snapshot;
723
+ },
724
+ resetMetrics: () => {
725
+ metricsCounters.clear();
726
+ },
727
+ import: (data, scope) => {
728
+ measureOperation("storage:import", scope, () => {
729
+ assertValidScope(scope);
730
+ const keys = Object.keys(data);
731
+ if (keys.length === 0) return;
732
+ const values = keys.map(k => data[k]);
733
+ if (scope === StorageScope.Memory) {
734
+ keys.forEach((key, index) => {
735
+ memoryStore.set(key, values[index]);
736
+ });
737
+ keys.forEach(key => notifyKeyListeners(memoryListeners, key));
738
+ return;
739
+ }
740
+ WebStorage.setBatch(keys, values, scope);
741
+ }, Object.keys(data).length);
742
+ }
507
743
  };
744
+ export function setWebSecureStorageBackend(backend) {
745
+ webSecureStorageBackend = backend ?? createLocalStorageWebSecureBackend();
746
+ hydratedWebScopeKeyIndex.delete(StorageScope.Secure);
747
+ clearScopeRawCache(StorageScope.Secure);
748
+ }
749
+ export function getWebSecureStorageBackend() {
750
+ return webSecureStorageBackend;
751
+ }
508
752
  function canUseRawBatchPath(item) {
509
753
  return item._hasExpiration === false && item._hasValidation === false && item._isBiometric !== true && item._secureAccessControl === undefined;
510
754
  }
@@ -522,7 +766,8 @@ export function createStorageItem(config) {
522
766
  const serialize = config.serialize ?? defaultSerialize;
523
767
  const deserialize = config.deserialize ?? defaultDeserialize;
524
768
  const isMemory = config.scope === StorageScope.Memory;
525
- const isBiometric = config.biometric === true && config.scope === StorageScope.Secure;
769
+ const resolvedBiometricLevel = config.scope === StorageScope.Secure ? config.biometricLevel ?? (config.biometric === true ? BiometricLevel.BiometryOnly : BiometricLevel.None) : BiometricLevel.None;
770
+ const isBiometric = resolvedBiometricLevel !== BiometricLevel.None;
526
771
  const secureAccessControl = config.accessControl;
527
772
  const validate = config.validate;
528
773
  const onValidationError = config.onValidationError;
@@ -531,7 +776,7 @@ export function createStorageItem(config) {
531
776
  const expirationTtlMs = expiration?.ttlMs;
532
777
  const memoryExpiration = expiration && isMemory ? new Map() : null;
533
778
  const readCache = !isMemory && config.readCache === true;
534
- const coalesceSecureWrites = config.scope === StorageScope.Secure && config.coalesceSecureWrites === true && !isBiometric && secureAccessControl === undefined;
779
+ const coalesceSecureWrites = config.scope === StorageScope.Secure && config.coalesceSecureWrites === true && !isBiometric;
535
780
  const defaultValue = config.defaultValue;
536
781
  const nonMemoryScope = config.scope === StorageScope.Disk ? StorageScope.Disk : config.scope === StorageScope.Secure ? StorageScope.Secure : null;
537
782
  if (expiration && expiration.ttlMs <= 0) {
@@ -561,6 +806,7 @@ export function createStorageItem(config) {
561
806
  unsubscribe = addKeyListener(memoryListeners, storageKey, listener);
562
807
  return;
563
808
  }
809
+ ensureWebStorageEventSubscription();
564
810
  unsubscribe = addKeyListener(getScopedListeners(nonMemoryScope), storageKey, listener);
565
811
  };
566
812
  const readStoredRaw = () => {
@@ -594,12 +840,12 @@ export function createStorageItem(config) {
594
840
  };
595
841
  const writeStoredRaw = rawValue => {
596
842
  if (isBiometric) {
597
- WebStorage.setSecureBiometric(storageKey, rawValue);
843
+ WebStorage.setSecureBiometricWithLevel(storageKey, rawValue, resolvedBiometricLevel);
598
844
  return;
599
845
  }
600
846
  cacheRawValue(nonMemoryScope, storageKey, rawValue);
601
847
  if (coalesceSecureWrites) {
602
- scheduleSecureWrite(storageKey, rawValue);
848
+ scheduleSecureWrite(storageKey, rawValue, secureAccessControl ?? AccessControl.WhenUnlocked);
603
849
  return;
604
850
  }
605
851
  if (nonMemoryScope === StorageScope.Secure) {
@@ -614,7 +860,7 @@ export function createStorageItem(config) {
614
860
  }
615
861
  cacheRawValue(nonMemoryScope, storageKey, undefined);
616
862
  if (coalesceSecureWrites) {
617
- scheduleSecureWrite(storageKey, undefined);
863
+ scheduleSecureWrite(storageKey, undefined, secureAccessControl ?? AccessControl.WhenUnlocked);
618
864
  return;
619
865
  }
620
866
  if (nonMemoryScope === StorageScope.Secure) {
@@ -662,7 +908,7 @@ export function createStorageItem(config) {
662
908
  }
663
909
  return resolved;
664
910
  };
665
- const get = () => {
911
+ const getInternal = () => {
666
912
  const raw = readStoredRaw();
667
913
  if (!memoryExpiration && raw === lastRaw && hasLastValue) {
668
914
  if (!expiration || lastExpiresAt === null) {
@@ -677,6 +923,7 @@ export function createStorageItem(config) {
677
923
  onExpired?.(storageKey);
678
924
  lastValue = ensureValidatedValue(defaultValue, false);
679
925
  hasLastValue = true;
926
+ listeners.forEach(cb => cb());
680
927
  return lastValue;
681
928
  }
682
929
  }
@@ -712,6 +959,7 @@ export function createStorageItem(config) {
712
959
  onExpired?.(storageKey);
713
960
  lastValue = ensureValidatedValue(defaultValue, false);
714
961
  hasLastValue = true;
962
+ listeners.forEach(cb => cb());
715
963
  return lastValue;
716
964
  }
717
965
  deserializableRaw = parsed.payload;
@@ -727,31 +975,52 @@ export function createStorageItem(config) {
727
975
  hasLastValue = true;
728
976
  return lastValue;
729
977
  };
978
+ const getCurrentVersion = () => {
979
+ const raw = readStoredRaw();
980
+ return toVersionToken(raw);
981
+ };
982
+ const get = () => measureOperation("item:get", config.scope, () => getInternal());
983
+ const getWithVersion = () => measureOperation("item:getWithVersion", config.scope, () => ({
984
+ value: getInternal(),
985
+ version: getCurrentVersion()
986
+ }));
730
987
  const set = valueOrFn => {
731
- const newValue = isUpdater(valueOrFn) ? valueOrFn(get()) : valueOrFn;
732
- invalidateParsedCache();
733
- if (validate && !validate(newValue)) {
734
- throw new Error(`Validation failed for key "${storageKey}" in scope "${StorageScope[config.scope]}".`);
735
- }
736
- writeValueWithoutValidation(newValue);
988
+ measureOperation("item:set", config.scope, () => {
989
+ const newValue = isUpdater(valueOrFn) ? valueOrFn(getInternal()) : valueOrFn;
990
+ invalidateParsedCache();
991
+ if (validate && !validate(newValue)) {
992
+ throw new Error(`Validation failed for key "${storageKey}" in scope "${StorageScope[config.scope]}".`);
993
+ }
994
+ writeValueWithoutValidation(newValue);
995
+ });
737
996
  };
997
+ const setIfVersion = (version, valueOrFn) => measureOperation("item:setIfVersion", config.scope, () => {
998
+ const currentVersion = getCurrentVersion();
999
+ if (currentVersion !== version) {
1000
+ return false;
1001
+ }
1002
+ set(valueOrFn);
1003
+ return true;
1004
+ });
738
1005
  const deleteItem = () => {
739
- invalidateParsedCache();
740
- if (isMemory) {
741
- if (memoryExpiration) {
742
- memoryExpiration.delete(storageKey);
1006
+ measureOperation("item:delete", config.scope, () => {
1007
+ invalidateParsedCache();
1008
+ if (isMemory) {
1009
+ if (memoryExpiration) {
1010
+ memoryExpiration.delete(storageKey);
1011
+ }
1012
+ memoryStore.delete(storageKey);
1013
+ notifyKeyListeners(memoryListeners, storageKey);
1014
+ return;
743
1015
  }
744
- memoryStore.delete(storageKey);
745
- notifyKeyListeners(memoryListeners, storageKey);
746
- return;
747
- }
748
- removeStoredRaw();
1016
+ removeStoredRaw();
1017
+ });
749
1018
  };
750
- const hasItem = () => {
1019
+ const hasItem = () => measureOperation("item:has", config.scope, () => {
751
1020
  if (isMemory) return memoryStore.has(storageKey);
752
1021
  if (isBiometric) return WebStorage.hasSecureBiometric(storageKey);
753
1022
  return WebStorage.has(storageKey, config.scope);
754
- };
1023
+ });
755
1024
  const subscribe = callback => {
756
1025
  ensureSubscription();
757
1026
  listeners.add(callback);
@@ -765,7 +1034,9 @@ export function createStorageItem(config) {
765
1034
  };
766
1035
  const storageItem = {
767
1036
  get,
1037
+ getWithVersion,
768
1038
  set,
1039
+ setIfVersion,
769
1040
  delete: deleteItem,
770
1041
  has: hasItem,
771
1042
  subscribe,
@@ -775,10 +1046,14 @@ export function createStorageItem(config) {
775
1046
  invalidateParsedCache();
776
1047
  listeners.forEach(listener => listener());
777
1048
  },
1049
+ _invalidateParsedCacheOnly: () => {
1050
+ invalidateParsedCache();
1051
+ },
778
1052
  _hasValidation: validate !== undefined,
779
1053
  _hasExpiration: expiration !== undefined,
780
1054
  _readCacheEnabled: readCache,
781
1055
  _isBiometric: isBiometric,
1056
+ _defaultValue: defaultValue,
782
1057
  ...(secureAccessControl !== undefined ? {
783
1058
  _secureAccessControl: secureAccessControl
784
1059
  } : {}),
@@ -788,136 +1063,164 @@ export function createStorageItem(config) {
788
1063
  return storageItem;
789
1064
  }
790
1065
  export { useStorage, useStorageSelector, useSetStorage } from "./storage-hooks";
1066
+ export { createIndexedDBBackend } from "./indexeddb-backend";
791
1067
  export function getBatch(items, scope) {
792
- assertBatchScope(items, scope);
793
- if (scope === StorageScope.Memory) {
794
- return items.map(item => item.get());
795
- }
796
- const useRawBatchPath = items.every(item => scope === StorageScope.Secure ? canUseSecureRawBatchPath(item) : canUseRawBatchPath(item));
797
- if (!useRawBatchPath) {
798
- return items.map(item => item.get());
799
- }
800
- const useBatchCache = items.every(item => item._readCacheEnabled === true);
801
- const rawValues = new Array(items.length);
802
- const keysToFetch = [];
803
- const keyIndexes = [];
804
- items.forEach((item, index) => {
805
- if (scope === StorageScope.Secure) {
806
- if (hasPendingSecureWrite(item.key)) {
807
- rawValues[index] = readPendingSecureWrite(item.key);
808
- return;
1068
+ return measureOperation("batch:get", scope, () => {
1069
+ assertBatchScope(items, scope);
1070
+ if (scope === StorageScope.Memory) {
1071
+ return items.map(item => item.get());
1072
+ }
1073
+ const useRawBatchPath = items.every(item => scope === StorageScope.Secure ? canUseSecureRawBatchPath(item) : canUseRawBatchPath(item));
1074
+ if (!useRawBatchPath) {
1075
+ return items.map(item => item.get());
1076
+ }
1077
+ const rawValues = new Array(items.length);
1078
+ const keysToFetch = [];
1079
+ const keyIndexes = [];
1080
+ items.forEach((item, index) => {
1081
+ if (scope === StorageScope.Secure) {
1082
+ if (hasPendingSecureWrite(item.key)) {
1083
+ rawValues[index] = readPendingSecureWrite(item.key);
1084
+ return;
1085
+ }
1086
+ }
1087
+ if (item._readCacheEnabled === true) {
1088
+ if (hasCachedRawValue(scope, item.key)) {
1089
+ rawValues[index] = readCachedRawValue(scope, item.key);
1090
+ return;
1091
+ }
809
1092
  }
1093
+ keysToFetch.push(item.key);
1094
+ keyIndexes.push(index);
1095
+ });
1096
+ if (keysToFetch.length > 0) {
1097
+ const fetchedValues = WebStorage.getBatch(keysToFetch, scope);
1098
+ fetchedValues.forEach((value, index) => {
1099
+ const key = keysToFetch[index];
1100
+ const targetIndex = keyIndexes[index];
1101
+ if (key === undefined || targetIndex === undefined) {
1102
+ return;
1103
+ }
1104
+ rawValues[targetIndex] = value;
1105
+ cacheRawValue(scope, key, value);
1106
+ });
810
1107
  }
811
- if (useBatchCache) {
812
- if (hasCachedRawValue(scope, item.key)) {
813
- rawValues[index] = readCachedRawValue(scope, item.key);
1108
+ return items.map((item, index) => {
1109
+ const raw = rawValues[index];
1110
+ if (raw === undefined) {
1111
+ return asInternal(item)._defaultValue;
1112
+ }
1113
+ return item.deserialize(raw);
1114
+ });
1115
+ }, items.length);
1116
+ }
1117
+ export function setBatch(items, scope) {
1118
+ measureOperation("batch:set", scope, () => {
1119
+ assertBatchScope(items.map(batchEntry => batchEntry.item), scope);
1120
+ if (scope === StorageScope.Memory) {
1121
+ // Determine if any item needs per-item handling (validation or TTL)
1122
+ const needsIndividualSets = items.some(({
1123
+ item
1124
+ }) => {
1125
+ const internal = asInternal(item);
1126
+ return internal._hasValidation || internal._hasExpiration;
1127
+ });
1128
+ if (needsIndividualSets) {
1129
+ items.forEach(({
1130
+ item,
1131
+ value
1132
+ }) => item.set(value));
814
1133
  return;
815
1134
  }
1135
+
1136
+ // Atomic write: update all values in memoryStore, invalidate caches, then batch-notify
1137
+ items.forEach(({
1138
+ item,
1139
+ value
1140
+ }) => {
1141
+ memoryStore.set(item.key, value);
1142
+ asInternal(item)._invalidateParsedCacheOnly();
1143
+ });
1144
+ items.forEach(({
1145
+ item
1146
+ }) => notifyKeyListeners(memoryListeners, item.key));
1147
+ return;
816
1148
  }
817
- keysToFetch.push(item.key);
818
- keyIndexes.push(index);
819
- });
820
- if (keysToFetch.length > 0) {
821
- const fetchedValues = WebStorage.getBatch(keysToFetch, scope);
822
- fetchedValues.forEach((value, index) => {
823
- const key = keysToFetch[index];
824
- const targetIndex = keyIndexes[index];
825
- if (key === undefined || targetIndex === undefined) {
1149
+ if (scope === StorageScope.Secure) {
1150
+ const secureEntries = items.map(({
1151
+ item,
1152
+ value
1153
+ }) => ({
1154
+ item,
1155
+ value,
1156
+ internal: asInternal(item)
1157
+ }));
1158
+ const canUseSecureBatchPath = secureEntries.every(({
1159
+ internal
1160
+ }) => canUseSecureRawBatchPath(internal));
1161
+ if (!canUseSecureBatchPath) {
1162
+ items.forEach(({
1163
+ item,
1164
+ value
1165
+ }) => item.set(value));
826
1166
  return;
827
1167
  }
828
- rawValues[targetIndex] = value;
829
- cacheRawValue(scope, key, value);
830
- });
831
- }
832
- return items.map((item, index) => {
833
- const raw = rawValues[index];
834
- if (raw === undefined) {
835
- return item.get();
1168
+ flushSecureWrites();
1169
+ const groupedByAccessControl = new Map();
1170
+ secureEntries.forEach(({
1171
+ item,
1172
+ value,
1173
+ internal
1174
+ }) => {
1175
+ const accessControl = internal._secureAccessControl ?? AccessControl.WhenUnlocked;
1176
+ const existingGroup = groupedByAccessControl.get(accessControl);
1177
+ const group = existingGroup ?? {
1178
+ keys: [],
1179
+ values: []
1180
+ };
1181
+ group.keys.push(item.key);
1182
+ group.values.push(item.serialize(value));
1183
+ if (!existingGroup) {
1184
+ groupedByAccessControl.set(accessControl, group);
1185
+ }
1186
+ });
1187
+ groupedByAccessControl.forEach((group, accessControl) => {
1188
+ WebStorage.setSecureAccessControl(accessControl);
1189
+ WebStorage.setBatch(group.keys, group.values, scope);
1190
+ group.keys.forEach((key, index) => cacheRawValue(scope, key, group.values[index]));
1191
+ });
1192
+ return;
836
1193
  }
837
- return item.deserialize(raw);
838
- });
839
- }
840
- export function setBatch(items, scope) {
841
- assertBatchScope(items.map(batchEntry => batchEntry.item), scope);
842
- if (scope === StorageScope.Memory) {
843
- items.forEach(({
844
- item,
845
- value
846
- }) => item.set(value));
847
- return;
848
- }
849
- if (scope === StorageScope.Secure) {
850
- const secureEntries = items.map(({
851
- item,
852
- value
853
- }) => ({
854
- item,
855
- value,
856
- internal: asInternal(item)
857
- }));
858
- const canUseSecureBatchPath = secureEntries.every(({
859
- internal
860
- }) => canUseSecureRawBatchPath(internal));
861
- if (!canUseSecureBatchPath) {
1194
+ const useRawBatchPath = items.every(({
1195
+ item
1196
+ }) => canUseRawBatchPath(asInternal(item)));
1197
+ if (!useRawBatchPath) {
862
1198
  items.forEach(({
863
1199
  item,
864
1200
  value
865
1201
  }) => item.set(value));
866
1202
  return;
867
1203
  }
868
- flushSecureWrites();
869
- const groupedByAccessControl = new Map();
870
- secureEntries.forEach(({
871
- item,
872
- value,
873
- internal
874
- }) => {
875
- const accessControl = internal._secureAccessControl ?? AccessControl.WhenUnlocked;
876
- const existingGroup = groupedByAccessControl.get(accessControl);
877
- const group = existingGroup ?? {
878
- keys: [],
879
- values: []
880
- };
881
- group.keys.push(item.key);
882
- group.values.push(item.serialize(value));
883
- if (!existingGroup) {
884
- groupedByAccessControl.set(accessControl, group);
885
- }
886
- });
887
- groupedByAccessControl.forEach((group, accessControl) => {
888
- WebStorage.setSecureAccessControl(accessControl);
889
- WebStorage.setBatch(group.keys, group.values, scope);
890
- group.keys.forEach((key, index) => cacheRawValue(scope, key, group.values[index]));
891
- });
892
- return;
893
- }
894
- const useRawBatchPath = items.every(({
895
- item
896
- }) => canUseRawBatchPath(asInternal(item)));
897
- if (!useRawBatchPath) {
898
- items.forEach(({
899
- item,
900
- value
901
- }) => item.set(value));
902
- return;
903
- }
904
- const keys = items.map(entry => entry.item.key);
905
- const values = items.map(entry => entry.item.serialize(entry.value));
906
- WebStorage.setBatch(keys, values, scope);
907
- keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
1204
+ const keys = items.map(entry => entry.item.key);
1205
+ const values = items.map(entry => entry.item.serialize(entry.value));
1206
+ WebStorage.setBatch(keys, values, scope);
1207
+ keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
1208
+ }, items.length);
908
1209
  }
909
1210
  export function removeBatch(items, scope) {
910
- assertBatchScope(items, scope);
911
- if (scope === StorageScope.Memory) {
912
- items.forEach(item => item.delete());
913
- return;
914
- }
915
- const keys = items.map(item => item.key);
916
- if (scope === StorageScope.Secure) {
917
- flushSecureWrites();
918
- }
919
- WebStorage.removeBatch(keys, scope);
920
- keys.forEach(key => cacheRawValue(scope, key, undefined));
1211
+ measureOperation("batch:remove", scope, () => {
1212
+ assertBatchScope(items, scope);
1213
+ if (scope === StorageScope.Memory) {
1214
+ items.forEach(item => item.delete());
1215
+ return;
1216
+ }
1217
+ const keys = items.map(item => item.key);
1218
+ if (scope === StorageScope.Secure) {
1219
+ flushSecureWrites();
1220
+ }
1221
+ WebStorage.removeBatch(keys, scope);
1222
+ keys.forEach(key => cacheRawValue(scope, key, undefined));
1223
+ }, items.length);
921
1224
  }
922
1225
  export function registerMigration(version, migration) {
923
1226
  if (!Number.isInteger(version) || version <= 0) {
@@ -929,77 +1232,107 @@ export function registerMigration(version, migration) {
929
1232
  registeredMigrations.set(version, migration);
930
1233
  }
931
1234
  export function migrateToLatest(scope = StorageScope.Disk) {
932
- assertValidScope(scope);
933
- const currentVersion = readMigrationVersion(scope);
934
- const versions = Array.from(registeredMigrations.keys()).filter(version => version > currentVersion).sort((a, b) => a - b);
935
- let appliedVersion = currentVersion;
936
- const context = {
937
- scope,
938
- getRaw: key => getRawValue(key, scope),
939
- setRaw: (key, value) => setRawValue(key, value, scope),
940
- removeRaw: key => removeRawValue(key, scope)
941
- };
942
- versions.forEach(version => {
943
- const migration = registeredMigrations.get(version);
944
- if (!migration) {
945
- return;
946
- }
947
- migration(context);
948
- writeMigrationVersion(scope, version);
949
- appliedVersion = version;
1235
+ return measureOperation("migration:run", scope, () => {
1236
+ assertValidScope(scope);
1237
+ const currentVersion = readMigrationVersion(scope);
1238
+ const versions = Array.from(registeredMigrations.keys()).filter(version => version > currentVersion).sort((a, b) => a - b);
1239
+ let appliedVersion = currentVersion;
1240
+ const context = {
1241
+ scope,
1242
+ getRaw: key => getRawValue(key, scope),
1243
+ setRaw: (key, value) => setRawValue(key, value, scope),
1244
+ removeRaw: key => removeRawValue(key, scope)
1245
+ };
1246
+ versions.forEach(version => {
1247
+ const migration = registeredMigrations.get(version);
1248
+ if (!migration) {
1249
+ return;
1250
+ }
1251
+ migration(context);
1252
+ writeMigrationVersion(scope, version);
1253
+ appliedVersion = version;
1254
+ });
1255
+ return appliedVersion;
950
1256
  });
951
- return appliedVersion;
952
1257
  }
953
1258
  export function runTransaction(scope, transaction) {
954
- assertValidScope(scope);
955
- if (scope === StorageScope.Secure) {
956
- flushSecureWrites();
957
- }
958
- const rollback = new Map();
959
- const rememberRollback = key => {
960
- if (rollback.has(key)) {
961
- return;
962
- }
963
- rollback.set(key, getRawValue(key, scope));
964
- };
965
- const tx = {
966
- scope,
967
- getRaw: key => getRawValue(key, scope),
968
- setRaw: (key, value) => {
969
- rememberRollback(key);
970
- setRawValue(key, value, scope);
971
- },
972
- removeRaw: key => {
973
- rememberRollback(key);
974
- removeRawValue(key, scope);
975
- },
976
- getItem: item => {
977
- assertBatchScope([item], scope);
978
- return item.get();
979
- },
980
- setItem: (item, value) => {
981
- assertBatchScope([item], scope);
982
- rememberRollback(item.key);
983
- item.set(value);
984
- },
985
- removeItem: item => {
986
- assertBatchScope([item], scope);
987
- rememberRollback(item.key);
988
- item.delete();
1259
+ return measureOperation("transaction:run", scope, () => {
1260
+ assertValidScope(scope);
1261
+ if (scope === StorageScope.Secure) {
1262
+ flushSecureWrites();
989
1263
  }
990
- };
991
- try {
992
- return transaction(tx);
993
- } catch (error) {
994
- Array.from(rollback.entries()).reverse().forEach(([key, previousValue]) => {
995
- if (previousValue === undefined) {
1264
+ const rollback = new Map();
1265
+ const rememberRollback = key => {
1266
+ if (rollback.has(key)) {
1267
+ return;
1268
+ }
1269
+ rollback.set(key, getRawValue(key, scope));
1270
+ };
1271
+ const tx = {
1272
+ scope,
1273
+ getRaw: key => getRawValue(key, scope),
1274
+ setRaw: (key, value) => {
1275
+ rememberRollback(key);
1276
+ setRawValue(key, value, scope);
1277
+ },
1278
+ removeRaw: key => {
1279
+ rememberRollback(key);
996
1280
  removeRawValue(key, scope);
1281
+ },
1282
+ getItem: item => {
1283
+ assertBatchScope([item], scope);
1284
+ return item.get();
1285
+ },
1286
+ setItem: (item, value) => {
1287
+ assertBatchScope([item], scope);
1288
+ rememberRollback(item.key);
1289
+ item.set(value);
1290
+ },
1291
+ removeItem: item => {
1292
+ assertBatchScope([item], scope);
1293
+ rememberRollback(item.key);
1294
+ item.delete();
1295
+ }
1296
+ };
1297
+ try {
1298
+ return transaction(tx);
1299
+ } catch (error) {
1300
+ const rollbackEntries = Array.from(rollback.entries()).reverse();
1301
+ if (scope === StorageScope.Memory) {
1302
+ rollbackEntries.forEach(([key, previousValue]) => {
1303
+ if (previousValue === undefined) {
1304
+ removeRawValue(key, scope);
1305
+ } else {
1306
+ setRawValue(key, previousValue, scope);
1307
+ }
1308
+ });
997
1309
  } else {
998
- setRawValue(key, previousValue, scope);
1310
+ const keysToSet = [];
1311
+ const valuesToSet = [];
1312
+ const keysToRemove = [];
1313
+ rollbackEntries.forEach(([key, previousValue]) => {
1314
+ if (previousValue === undefined) {
1315
+ keysToRemove.push(key);
1316
+ } else {
1317
+ keysToSet.push(key);
1318
+ valuesToSet.push(previousValue);
1319
+ }
1320
+ });
1321
+ if (scope === StorageScope.Secure) {
1322
+ flushSecureWrites();
1323
+ }
1324
+ if (keysToSet.length > 0) {
1325
+ WebStorage.setBatch(keysToSet, valuesToSet, scope);
1326
+ keysToSet.forEach((key, index) => cacheRawValue(scope, key, valuesToSet[index]));
1327
+ }
1328
+ if (keysToRemove.length > 0) {
1329
+ WebStorage.removeBatch(keysToRemove, scope);
1330
+ keysToRemove.forEach(key => cacheRawValue(scope, key, undefined));
1331
+ }
999
1332
  }
1000
- });
1001
- throw error;
1002
- }
1333
+ throw error;
1334
+ }
1335
+ });
1003
1336
  }
1004
1337
  export function createSecureAuthStorage(config, options) {
1005
1338
  const ns = options?.namespace ?? "auth";
@@ -1017,6 +1350,9 @@ export function createSecureAuthStorage(config, options) {
1017
1350
  ...(itemConfig.biometric !== undefined ? {
1018
1351
  biometric: itemConfig.biometric
1019
1352
  } : {}),
1353
+ ...(itemConfig.biometricLevel !== undefined ? {
1354
+ biometricLevel: itemConfig.biometricLevel
1355
+ } : {}),
1020
1356
  ...(itemConfig.accessControl !== undefined ? {
1021
1357
  accessControl: itemConfig.accessControl
1022
1358
  } : {}),