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
package/src/index.ts CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  decodeNativeBatchValue,
11
11
  serializeWithPrimitiveFastPath,
12
12
  deserializeWithPrimitiveFastPath,
13
+ toVersionToken,
13
14
  prefixKey,
14
15
  isNamespaced,
15
16
  } from "./internal";
@@ -22,6 +23,31 @@ export type Validator<T> = (value: unknown) => value is T;
22
23
  export type ExpirationConfig = {
23
24
  ttlMs: number;
24
25
  };
26
+ export type StorageVersion = string;
27
+ export type VersionedValue<T> = {
28
+ value: T;
29
+ version: StorageVersion;
30
+ };
31
+ export type StorageMetricsEvent = {
32
+ operation: string;
33
+ scope: StorageScope;
34
+ durationMs: number;
35
+ keysCount: number;
36
+ };
37
+ export type StorageMetricsObserver = (event: StorageMetricsEvent) => void;
38
+ export type StorageMetricSummary = {
39
+ count: number;
40
+ totalDurationMs: number;
41
+ avgDurationMs: number;
42
+ maxDurationMs: number;
43
+ };
44
+ export type WebSecureStorageBackend = {
45
+ getItem: (key: string) => string | null;
46
+ setItem: (key: string, value: string) => void;
47
+ removeItem: (key: string) => void;
48
+ clear: () => void;
49
+ getAllKeys: () => string[];
50
+ };
25
51
 
26
52
  export type MigrationContext = {
27
53
  scope: StorageScope;
@@ -69,7 +95,11 @@ function typedKeys<K extends string, V>(record: Record<K, V>): K[] {
69
95
  return Object.keys(record) as K[];
70
96
  }
71
97
  type NonMemoryScope = StorageScope.Disk | StorageScope.Secure;
72
- type PendingSecureWrite = { key: string; value: string | undefined };
98
+ type PendingSecureWrite = {
99
+ key: string;
100
+ value: string | undefined;
101
+ accessControl?: AccessControl;
102
+ };
73
103
 
74
104
  const registeredMigrations = new Map<number, Migration>();
75
105
  const runMicrotask =
@@ -104,6 +134,52 @@ const scopedRawCache = new Map<NonMemoryScope, Map<string, string | undefined>>(
104
134
  const pendingSecureWrites = new Map<string, PendingSecureWrite>();
105
135
  let secureFlushScheduled = false;
106
136
  let secureDefaultAccessControl: AccessControl = AccessControl.WhenUnlocked;
137
+ let metricsObserver: StorageMetricsObserver | undefined;
138
+ const metricsCounters = new Map<
139
+ string,
140
+ { count: number; totalDurationMs: number; maxDurationMs: number }
141
+ >();
142
+
143
+ function recordMetric(
144
+ operation: string,
145
+ scope: StorageScope,
146
+ durationMs: number,
147
+ keysCount = 1,
148
+ ): void {
149
+ const existing = metricsCounters.get(operation);
150
+ if (!existing) {
151
+ metricsCounters.set(operation, {
152
+ count: 1,
153
+ totalDurationMs: durationMs,
154
+ maxDurationMs: durationMs,
155
+ });
156
+ } else {
157
+ existing.count += 1;
158
+ existing.totalDurationMs += durationMs;
159
+ existing.maxDurationMs = Math.max(existing.maxDurationMs, durationMs);
160
+ }
161
+
162
+ metricsObserver?.({
163
+ operation,
164
+ scope,
165
+ durationMs,
166
+ keysCount,
167
+ });
168
+ }
169
+
170
+ function measureOperation<T>(
171
+ operation: string,
172
+ scope: StorageScope,
173
+ fn: () => T,
174
+ keysCount = 1,
175
+ ): T {
176
+ const start = Date.now();
177
+ try {
178
+ return fn();
179
+ } finally {
180
+ recordMetric(operation, scope, Date.now() - start, keysCount);
181
+ }
182
+ }
107
183
 
108
184
  function getScopedListeners(scope: NonMemoryScope): KeyListenerRegistry {
109
185
  return scopedListeners.get(scope)!;
@@ -194,31 +270,47 @@ function flushSecureWrites(): void {
194
270
  const writes = Array.from(pendingSecureWrites.values());
195
271
  pendingSecureWrites.clear();
196
272
 
197
- const keysToSet: string[] = [];
198
- const valuesToSet: string[] = [];
273
+ const groupedSetWrites = new Map<
274
+ AccessControl,
275
+ { keys: string[]; values: string[] }
276
+ >();
199
277
  const keysToRemove: string[] = [];
200
278
 
201
- writes.forEach(({ key, value }) => {
279
+ writes.forEach(({ key, value, accessControl }) => {
202
280
  if (value === undefined) {
203
281
  keysToRemove.push(key);
204
282
  } else {
205
- keysToSet.push(key);
206
- valuesToSet.push(value);
283
+ const resolvedAccessControl = accessControl ?? secureDefaultAccessControl;
284
+ const existingGroup = groupedSetWrites.get(resolvedAccessControl);
285
+ const group = existingGroup ?? { keys: [], values: [] };
286
+ group.keys.push(key);
287
+ group.values.push(value);
288
+ if (!existingGroup) {
289
+ groupedSetWrites.set(resolvedAccessControl, group);
290
+ }
207
291
  }
208
292
  });
209
293
 
210
294
  const storageModule = getStorageModule();
211
- storageModule.setSecureAccessControl(secureDefaultAccessControl);
212
- if (keysToSet.length > 0) {
213
- storageModule.setBatch(keysToSet, valuesToSet, StorageScope.Secure);
214
- }
295
+ groupedSetWrites.forEach((group, accessControl) => {
296
+ storageModule.setSecureAccessControl(accessControl);
297
+ storageModule.setBatch(group.keys, group.values, StorageScope.Secure);
298
+ });
215
299
  if (keysToRemove.length > 0) {
216
300
  storageModule.removeBatch(keysToRemove, StorageScope.Secure);
217
301
  }
218
302
  }
219
303
 
220
- function scheduleSecureWrite(key: string, value: string | undefined): void {
221
- pendingSecureWrites.set(key, { key, value });
304
+ function scheduleSecureWrite(
305
+ key: string,
306
+ value: string | undefined,
307
+ accessControl?: AccessControl,
308
+ ): void {
309
+ const pendingWrite: PendingSecureWrite = { key, value };
310
+ if (accessControl !== undefined) {
311
+ pendingWrite.accessControl = accessControl;
312
+ }
313
+ pendingSecureWrites.set(key, pendingWrite);
222
314
  if (secureFlushScheduled) {
223
315
  return;
224
316
  }
@@ -332,102 +424,241 @@ function writeMigrationVersion(scope: StorageScope, version: number): void {
332
424
 
333
425
  export const storage = {
334
426
  clear: (scope: StorageScope) => {
335
- if (scope === StorageScope.Memory) {
336
- memoryStore.clear();
337
- notifyAllListeners(memoryListeners);
338
- return;
339
- }
427
+ measureOperation("storage:clear", scope, () => {
428
+ if (scope === StorageScope.Memory) {
429
+ memoryStore.clear();
430
+ notifyAllListeners(memoryListeners);
431
+ return;
432
+ }
340
433
 
341
- if (scope === StorageScope.Secure) {
342
- flushSecureWrites();
343
- pendingSecureWrites.clear();
344
- }
434
+ if (scope === StorageScope.Secure) {
435
+ flushSecureWrites();
436
+ pendingSecureWrites.clear();
437
+ }
345
438
 
346
- clearScopeRawCache(scope);
347
- getStorageModule().clear(scope);
439
+ clearScopeRawCache(scope);
440
+ getStorageModule().clear(scope);
441
+ });
348
442
  },
349
443
  clearAll: () => {
350
- storage.clear(StorageScope.Memory);
351
- storage.clear(StorageScope.Disk);
352
- storage.clear(StorageScope.Secure);
444
+ measureOperation(
445
+ "storage:clearAll",
446
+ StorageScope.Memory,
447
+ () => {
448
+ storage.clear(StorageScope.Memory);
449
+ storage.clear(StorageScope.Disk);
450
+ storage.clear(StorageScope.Secure);
451
+ },
452
+ 3,
453
+ );
353
454
  },
354
455
  clearNamespace: (namespace: string, scope: StorageScope) => {
355
- assertValidScope(scope);
356
- if (scope === StorageScope.Memory) {
357
- for (const key of memoryStore.keys()) {
358
- if (isNamespaced(key, namespace)) {
359
- memoryStore.delete(key);
456
+ measureOperation("storage:clearNamespace", scope, () => {
457
+ assertValidScope(scope);
458
+ if (scope === StorageScope.Memory) {
459
+ for (const key of memoryStore.keys()) {
460
+ if (isNamespaced(key, namespace)) {
461
+ memoryStore.delete(key);
462
+ }
360
463
  }
464
+ notifyAllListeners(memoryListeners);
465
+ return;
361
466
  }
362
- notifyAllListeners(memoryListeners);
363
- return;
364
- }
365
467
 
366
- const keyPrefix = prefixKey(namespace, "");
367
- if (scope === StorageScope.Secure) {
368
- flushSecureWrites();
369
- }
468
+ const keyPrefix = prefixKey(namespace, "");
469
+ if (scope === StorageScope.Secure) {
470
+ flushSecureWrites();
471
+ }
370
472
 
371
- clearScopeRawCache(scope);
372
- getStorageModule().removeByPrefix(keyPrefix, scope);
473
+ clearScopeRawCache(scope);
474
+ getStorageModule().removeByPrefix(keyPrefix, scope);
475
+ });
373
476
  },
374
477
  clearBiometric: () => {
375
- getStorageModule().clearSecureBiometric();
478
+ measureOperation("storage:clearBiometric", StorageScope.Secure, () => {
479
+ getStorageModule().clearSecureBiometric();
480
+ });
376
481
  },
377
482
  has: (key: string, scope: StorageScope): boolean => {
378
- assertValidScope(scope);
379
- if (scope === StorageScope.Memory) {
380
- return memoryStore.has(key);
381
- }
382
- return getStorageModule().has(key, scope);
483
+ return measureOperation("storage:has", scope, () => {
484
+ assertValidScope(scope);
485
+ if (scope === StorageScope.Memory) {
486
+ return memoryStore.has(key);
487
+ }
488
+ return getStorageModule().has(key, scope);
489
+ });
383
490
  },
384
491
  getAllKeys: (scope: StorageScope): string[] => {
385
- assertValidScope(scope);
386
- if (scope === StorageScope.Memory) {
387
- return Array.from(memoryStore.keys());
388
- }
389
- return getStorageModule().getAllKeys(scope);
492
+ return measureOperation("storage:getAllKeys", scope, () => {
493
+ assertValidScope(scope);
494
+ if (scope === StorageScope.Memory) {
495
+ return Array.from(memoryStore.keys());
496
+ }
497
+ return getStorageModule().getAllKeys(scope);
498
+ });
499
+ },
500
+ getKeysByPrefix: (prefix: string, scope: StorageScope): string[] => {
501
+ return measureOperation("storage:getKeysByPrefix", scope, () => {
502
+ assertValidScope(scope);
503
+ if (scope === StorageScope.Memory) {
504
+ return Array.from(memoryStore.keys()).filter((key) =>
505
+ key.startsWith(prefix),
506
+ );
507
+ }
508
+ return getStorageModule().getKeysByPrefix(prefix, scope);
509
+ });
510
+ },
511
+ getByPrefix: (
512
+ prefix: string,
513
+ scope: StorageScope,
514
+ ): Record<string, string> => {
515
+ return measureOperation("storage:getByPrefix", scope, () => {
516
+ const result: Record<string, string> = {};
517
+ const keys = storage.getKeysByPrefix(prefix, scope);
518
+ if (keys.length === 0) {
519
+ return result;
520
+ }
521
+
522
+ if (scope === StorageScope.Memory) {
523
+ keys.forEach((key) => {
524
+ const value = memoryStore.get(key);
525
+ if (typeof value === "string") {
526
+ result[key] = value;
527
+ }
528
+ });
529
+ return result;
530
+ }
531
+
532
+ const values = getStorageModule().getBatch(keys, scope);
533
+ keys.forEach((key, idx) => {
534
+ const value = decodeNativeBatchValue(values[idx]);
535
+ if (value !== undefined) {
536
+ result[key] = value;
537
+ }
538
+ });
539
+ return result;
540
+ });
390
541
  },
391
542
  getAll: (scope: StorageScope): Record<string, string> => {
392
- assertValidScope(scope);
393
- const result: Record<string, string> = {};
394
- if (scope === StorageScope.Memory) {
395
- memoryStore.forEach((value, key) => {
396
- if (typeof value === "string") result[key] = value;
543
+ return measureOperation("storage:getAll", scope, () => {
544
+ assertValidScope(scope);
545
+ const result: Record<string, string> = {};
546
+ if (scope === StorageScope.Memory) {
547
+ memoryStore.forEach((value, key) => {
548
+ if (typeof value === "string") result[key] = value;
549
+ });
550
+ return result;
551
+ }
552
+ const keys = getStorageModule().getAllKeys(scope);
553
+ if (keys.length === 0) return result;
554
+ const values = getStorageModule().getBatch(keys, scope);
555
+ keys.forEach((key, idx) => {
556
+ const val = decodeNativeBatchValue(values[idx]);
557
+ if (val !== undefined) result[key] = val;
397
558
  });
398
559
  return result;
399
- }
400
- const keys = getStorageModule().getAllKeys(scope);
401
- if (keys.length === 0) return result;
402
- const values = getStorageModule().getBatch(keys, scope);
403
- keys.forEach((key, idx) => {
404
- const val = decodeNativeBatchValue(values[idx]);
405
- if (val !== undefined) result[key] = val;
406
560
  });
407
- return result;
408
561
  },
409
562
  size: (scope: StorageScope): number => {
410
- assertValidScope(scope);
411
- if (scope === StorageScope.Memory) {
412
- return memoryStore.size;
413
- }
414
- return getStorageModule().size(scope);
563
+ return measureOperation("storage:size", scope, () => {
564
+ assertValidScope(scope);
565
+ if (scope === StorageScope.Memory) {
566
+ return memoryStore.size;
567
+ }
568
+ return getStorageModule().size(scope);
569
+ });
415
570
  },
416
571
  setAccessControl: (level: AccessControl) => {
417
- secureDefaultAccessControl = level;
418
- getStorageModule().setSecureAccessControl(level);
572
+ measureOperation("storage:setAccessControl", StorageScope.Secure, () => {
573
+ secureDefaultAccessControl = level;
574
+ getStorageModule().setSecureAccessControl(level);
575
+ });
419
576
  },
420
577
  setSecureWritesAsync: (enabled: boolean) => {
421
- getStorageModule().setSecureWritesAsync(enabled);
578
+ measureOperation(
579
+ "storage:setSecureWritesAsync",
580
+ StorageScope.Secure,
581
+ () => {
582
+ getStorageModule().setSecureWritesAsync(enabled);
583
+ },
584
+ );
422
585
  },
423
586
  flushSecureWrites: () => {
424
- flushSecureWrites();
587
+ measureOperation("storage:flushSecureWrites", StorageScope.Secure, () => {
588
+ flushSecureWrites();
589
+ });
425
590
  },
426
591
  setKeychainAccessGroup: (group: string) => {
427
- getStorageModule().setKeychainAccessGroup(group);
592
+ measureOperation(
593
+ "storage:setKeychainAccessGroup",
594
+ StorageScope.Secure,
595
+ () => {
596
+ getStorageModule().setKeychainAccessGroup(group);
597
+ },
598
+ );
599
+ },
600
+ setMetricsObserver: (observer?: StorageMetricsObserver) => {
601
+ metricsObserver = observer;
602
+ },
603
+ getMetricsSnapshot: (): Record<string, StorageMetricSummary> => {
604
+ const snapshot: Record<string, StorageMetricSummary> = {};
605
+ metricsCounters.forEach((value, key) => {
606
+ snapshot[key] = {
607
+ count: value.count,
608
+ totalDurationMs: value.totalDurationMs,
609
+ avgDurationMs:
610
+ value.count === 0 ? 0 : value.totalDurationMs / value.count,
611
+ maxDurationMs: value.maxDurationMs,
612
+ };
613
+ });
614
+ return snapshot;
615
+ },
616
+ resetMetrics: () => {
617
+ metricsCounters.clear();
618
+ },
619
+ import: (data: Record<string, string>, scope: StorageScope): void => {
620
+ measureOperation(
621
+ "storage:import",
622
+ scope,
623
+ () => {
624
+ assertValidScope(scope);
625
+ const keys = Object.keys(data);
626
+ if (keys.length === 0) return;
627
+ const values = keys.map((k) => data[k]!);
628
+
629
+ if (scope === StorageScope.Memory) {
630
+ keys.forEach((key, index) => {
631
+ memoryStore.set(key, values[index]);
632
+ });
633
+ keys.forEach((key) => notifyKeyListeners(memoryListeners, key));
634
+ return;
635
+ }
636
+
637
+ if (scope === StorageScope.Secure) {
638
+ flushSecureWrites();
639
+ getStorageModule().setSecureAccessControl(secureDefaultAccessControl);
640
+ }
641
+
642
+ getStorageModule().setBatch(keys, values, scope);
643
+ keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
644
+ },
645
+ Object.keys(data).length,
646
+ );
428
647
  },
429
648
  };
430
649
 
650
+ export function setWebSecureStorageBackend(
651
+ _backend?: WebSecureStorageBackend,
652
+ ): void {
653
+ // Native platforms do not use web secure backends.
654
+ }
655
+
656
+ export function getWebSecureStorageBackend():
657
+ | WebSecureStorageBackend
658
+ | undefined {
659
+ return undefined;
660
+ }
661
+
431
662
  export interface StorageItemConfig<T> {
432
663
  key: string;
433
664
  scope: StorageScope;
@@ -442,12 +673,18 @@ export interface StorageItemConfig<T> {
442
673
  coalesceSecureWrites?: boolean;
443
674
  namespace?: string;
444
675
  biometric?: boolean;
676
+ biometricLevel?: BiometricLevel;
445
677
  accessControl?: AccessControl;
446
678
  }
447
679
 
448
680
  export interface StorageItem<T> {
449
681
  get: () => T;
682
+ getWithVersion: () => VersionedValue<T>;
450
683
  set: (value: T | ((prev: T) => T)) => void;
684
+ setIfVersion: (
685
+ version: StorageVersion,
686
+ value: T | ((prev: T) => T),
687
+ ) => boolean;
451
688
  delete: () => void;
452
689
  has: () => boolean;
453
690
  subscribe: (callback: () => void) => () => void;
@@ -459,10 +696,12 @@ export interface StorageItem<T> {
459
696
 
460
697
  type StorageItemInternal<T> = StorageItem<T> & {
461
698
  _triggerListeners: () => void;
699
+ _invalidateParsedCacheOnly: () => void;
462
700
  _hasValidation: boolean;
463
701
  _hasExpiration: boolean;
464
702
  _readCacheEnabled: boolean;
465
703
  _isBiometric: boolean;
704
+ _defaultValue: T;
466
705
  _secureAccessControl?: AccessControl;
467
706
  };
468
707
 
@@ -498,8 +737,14 @@ export function createStorageItem<T = undefined>(
498
737
  const serialize = config.serialize ?? defaultSerialize;
499
738
  const deserialize = config.deserialize ?? defaultDeserialize;
500
739
  const isMemory = config.scope === StorageScope.Memory;
501
- const isBiometric =
502
- config.biometric === true && config.scope === StorageScope.Secure;
740
+ const resolvedBiometricLevel =
741
+ config.scope === StorageScope.Secure
742
+ ? (config.biometricLevel ??
743
+ (config.biometric === true
744
+ ? BiometricLevel.BiometryOnly
745
+ : BiometricLevel.None))
746
+ : BiometricLevel.None;
747
+ const isBiometric = resolvedBiometricLevel !== BiometricLevel.None;
503
748
  const secureAccessControl = config.accessControl;
504
749
  const validate = config.validate;
505
750
  const onValidationError = config.onValidationError;
@@ -512,8 +757,7 @@ export function createStorageItem<T = undefined>(
512
757
  const coalesceSecureWrites =
513
758
  config.scope === StorageScope.Secure &&
514
759
  config.coalesceSecureWrites === true &&
515
- !isBiometric &&
516
- secureAccessControl === undefined;
760
+ !isBiometric;
517
761
  const defaultValue = config.defaultValue as T;
518
762
  const nonMemoryScope: NonMemoryScope | null =
519
763
  config.scope === StorageScope.Disk
@@ -603,14 +847,22 @@ export function createStorageItem<T = undefined>(
603
847
 
604
848
  const writeStoredRaw = (rawValue: string): void => {
605
849
  if (isBiometric) {
606
- getStorageModule().setSecureBiometric(storageKey, rawValue);
850
+ getStorageModule().setSecureBiometricWithLevel(
851
+ storageKey,
852
+ rawValue,
853
+ resolvedBiometricLevel,
854
+ );
607
855
  return;
608
856
  }
609
857
 
610
858
  cacheRawValue(nonMemoryScope!, storageKey, rawValue);
611
859
 
612
860
  if (coalesceSecureWrites) {
613
- scheduleSecureWrite(storageKey, rawValue);
861
+ scheduleSecureWrite(
862
+ storageKey,
863
+ rawValue,
864
+ secureAccessControl ?? secureDefaultAccessControl,
865
+ );
614
866
  return;
615
867
  }
616
868
 
@@ -633,7 +885,11 @@ export function createStorageItem<T = undefined>(
633
885
  cacheRawValue(nonMemoryScope!, storageKey, undefined);
634
886
 
635
887
  if (coalesceSecureWrites) {
636
- scheduleSecureWrite(storageKey, undefined);
888
+ scheduleSecureWrite(
889
+ storageKey,
890
+ undefined,
891
+ secureAccessControl ?? secureDefaultAccessControl,
892
+ );
637
893
  return;
638
894
  }
639
895
 
@@ -694,7 +950,7 @@ export function createStorageItem<T = undefined>(
694
950
  return resolved;
695
951
  };
696
952
 
697
- const get = (): T => {
953
+ const getInternal = (): T => {
698
954
  const raw = readStoredRaw();
699
955
 
700
956
  if (!memoryExpiration && raw === lastRaw && hasLastValue) {
@@ -712,6 +968,7 @@ export function createStorageItem<T = undefined>(
712
968
  onExpired?.(storageKey);
713
969
  lastValue = ensureValidatedValue(defaultValue, false);
714
970
  hasLastValue = true;
971
+ listeners.forEach((cb) => cb());
715
972
  return lastValue;
716
973
  }
717
974
  }
@@ -753,6 +1010,7 @@ export function createStorageItem<T = undefined>(
753
1010
  onExpired?.(storageKey);
754
1011
  lastValue = ensureValidatedValue(defaultValue, false);
755
1012
  hasLastValue = true;
1013
+ listeners.forEach((cb) => cb());
756
1014
  return lastValue;
757
1015
  }
758
1016
 
@@ -771,40 +1029,74 @@ export function createStorageItem<T = undefined>(
771
1029
  return lastValue;
772
1030
  };
773
1031
 
1032
+ const getCurrentVersion = (): StorageVersion => {
1033
+ const raw = readStoredRaw();
1034
+ return toVersionToken(raw);
1035
+ };
1036
+
1037
+ const get = (): T =>
1038
+ measureOperation("item:get", config.scope, () => getInternal());
1039
+
1040
+ const getWithVersion = (): VersionedValue<T> =>
1041
+ measureOperation("item:getWithVersion", config.scope, () => ({
1042
+ value: getInternal(),
1043
+ version: getCurrentVersion(),
1044
+ }));
1045
+
774
1046
  const set = (valueOrFn: T | ((prev: T) => T)): void => {
775
- const newValue = isUpdater(valueOrFn) ? valueOrFn(get()) : valueOrFn;
1047
+ measureOperation("item:set", config.scope, () => {
1048
+ const newValue = isUpdater(valueOrFn)
1049
+ ? valueOrFn(getInternal())
1050
+ : valueOrFn;
776
1051
 
777
- invalidateParsedCache();
1052
+ invalidateParsedCache();
778
1053
 
779
- if (validate && !validate(newValue)) {
780
- throw new Error(
781
- `Validation failed for key "${storageKey}" in scope "${StorageScope[config.scope]}".`,
782
- );
783
- }
1054
+ if (validate && !validate(newValue)) {
1055
+ throw new Error(
1056
+ `Validation failed for key "${storageKey}" in scope "${StorageScope[config.scope]}".`,
1057
+ );
1058
+ }
784
1059
 
785
- writeValueWithoutValidation(newValue);
1060
+ writeValueWithoutValidation(newValue);
1061
+ });
786
1062
  };
787
1063
 
1064
+ const setIfVersion = (
1065
+ version: StorageVersion,
1066
+ valueOrFn: T | ((prev: T) => T),
1067
+ ): boolean =>
1068
+ measureOperation("item:setIfVersion", config.scope, () => {
1069
+ const currentVersion = getCurrentVersion();
1070
+ if (currentVersion !== version) {
1071
+ return false;
1072
+ }
1073
+ set(valueOrFn);
1074
+ return true;
1075
+ });
1076
+
788
1077
  const deleteItem = (): void => {
789
- invalidateParsedCache();
1078
+ measureOperation("item:delete", config.scope, () => {
1079
+ invalidateParsedCache();
790
1080
 
791
- if (isMemory) {
792
- if (memoryExpiration) {
793
- memoryExpiration.delete(storageKey);
1081
+ if (isMemory) {
1082
+ if (memoryExpiration) {
1083
+ memoryExpiration.delete(storageKey);
1084
+ }
1085
+ memoryStore.delete(storageKey);
1086
+ notifyKeyListeners(memoryListeners, storageKey);
1087
+ return;
794
1088
  }
795
- memoryStore.delete(storageKey);
796
- notifyKeyListeners(memoryListeners, storageKey);
797
- return;
798
- }
799
1089
 
800
- removeStoredRaw();
1090
+ removeStoredRaw();
1091
+ });
801
1092
  };
802
1093
 
803
- const hasItem = (): boolean => {
804
- if (isMemory) return memoryStore.has(storageKey);
805
- if (isBiometric) return getStorageModule().hasSecureBiometric(storageKey);
806
- return getStorageModule().has(storageKey, config.scope);
807
- };
1094
+ const hasItem = (): boolean =>
1095
+ measureOperation("item:has", config.scope, () => {
1096
+ if (isMemory) return memoryStore.has(storageKey);
1097
+ if (isBiometric) return getStorageModule().hasSecureBiometric(storageKey);
1098
+ return getStorageModule().has(storageKey, config.scope);
1099
+ });
808
1100
 
809
1101
  const subscribe = (callback: () => void): (() => void) => {
810
1102
  ensureSubscription();
@@ -823,7 +1115,9 @@ export function createStorageItem<T = undefined>(
823
1115
 
824
1116
  const storageItem: StorageItemInternal<T> = {
825
1117
  get,
1118
+ getWithVersion,
826
1119
  set,
1120
+ setIfVersion,
827
1121
  delete: deleteItem,
828
1122
  has: hasItem,
829
1123
  subscribe,
@@ -833,10 +1127,14 @@ export function createStorageItem<T = undefined>(
833
1127
  invalidateParsedCache();
834
1128
  listeners.forEach((listener) => listener());
835
1129
  },
1130
+ _invalidateParsedCacheOnly: () => {
1131
+ invalidateParsedCache();
1132
+ },
836
1133
  _hasValidation: validate !== undefined,
837
1134
  _hasExpiration: expiration !== undefined,
838
1135
  _readCacheEnabled: readCache,
839
1136
  _isBiometric: isBiometric,
1137
+ _defaultValue: defaultValue,
840
1138
  ...(secureAccessControl !== undefined
841
1139
  ? { _secureAccessControl: secureAccessControl }
842
1140
  : {}),
@@ -848,6 +1146,7 @@ export function createStorageItem<T = undefined>(
848
1146
  }
849
1147
 
850
1148
  export { useStorage, useStorageSelector, useSetStorage } from "./storage-hooks";
1149
+ export { createIndexedDBBackend } from "./indexeddb-backend";
851
1150
 
852
1151
  type BatchReadItem<T> = Pick<
853
1152
  StorageItem<T>,
@@ -857,6 +1156,7 @@ type BatchReadItem<T> = Pick<
857
1156
  _hasExpiration?: boolean;
858
1157
  _readCacheEnabled?: boolean;
859
1158
  _isBiometric?: boolean;
1159
+ _defaultValue?: unknown;
860
1160
  _secureAccessControl?: AccessControl;
861
1161
  };
862
1162
  type BatchRemoveItem = Pick<StorageItem<unknown>, "key" | "scope" | "delete">;
@@ -870,159 +1170,198 @@ export function getBatch(
870
1170
  items: readonly BatchReadItem<unknown>[],
871
1171
  scope: StorageScope,
872
1172
  ): unknown[] {
873
- assertBatchScope(items, scope);
1173
+ return measureOperation(
1174
+ "batch:get",
1175
+ scope,
1176
+ () => {
1177
+ assertBatchScope(items, scope);
874
1178
 
875
- if (scope === StorageScope.Memory) {
876
- return items.map((item) => item.get());
877
- }
1179
+ if (scope === StorageScope.Memory) {
1180
+ return items.map((item) => item.get());
1181
+ }
878
1182
 
879
- const useRawBatchPath = items.every((item) =>
880
- scope === StorageScope.Secure
881
- ? canUseSecureRawBatchPath(item)
882
- : canUseRawBatchPath(item),
883
- );
884
- if (!useRawBatchPath) {
885
- return items.map((item) => item.get());
886
- }
887
- const useBatchCache = items.every((item) => item._readCacheEnabled === true);
1183
+ const useRawBatchPath = items.every((item) =>
1184
+ scope === StorageScope.Secure
1185
+ ? canUseSecureRawBatchPath(item)
1186
+ : canUseRawBatchPath(item),
1187
+ );
1188
+ if (!useRawBatchPath) {
1189
+ return items.map((item) => item.get());
1190
+ }
888
1191
 
889
- const rawValues = new Array<string | undefined>(items.length);
890
- const keysToFetch: string[] = [];
891
- const keyIndexes: number[] = [];
1192
+ const rawValues = new Array<string | undefined>(items.length);
1193
+ const keysToFetch: string[] = [];
1194
+ const keyIndexes: number[] = [];
892
1195
 
893
- items.forEach((item, index) => {
894
- if (scope === StorageScope.Secure) {
895
- if (hasPendingSecureWrite(item.key)) {
896
- rawValues[index] = readPendingSecureWrite(item.key);
897
- return;
898
- }
899
- }
1196
+ items.forEach((item, index) => {
1197
+ if (scope === StorageScope.Secure) {
1198
+ if (hasPendingSecureWrite(item.key)) {
1199
+ rawValues[index] = readPendingSecureWrite(item.key);
1200
+ return;
1201
+ }
1202
+ }
900
1203
 
901
- if (useBatchCache) {
902
- if (hasCachedRawValue(scope, item.key)) {
903
- rawValues[index] = readCachedRawValue(scope, item.key);
904
- return;
905
- }
906
- }
1204
+ if (item._readCacheEnabled === true) {
1205
+ if (hasCachedRawValue(scope, item.key)) {
1206
+ rawValues[index] = readCachedRawValue(scope, item.key);
1207
+ return;
1208
+ }
1209
+ }
907
1210
 
908
- keysToFetch.push(item.key);
909
- keyIndexes.push(index);
910
- });
1211
+ keysToFetch.push(item.key);
1212
+ keyIndexes.push(index);
1213
+ });
911
1214
 
912
- if (keysToFetch.length > 0) {
913
- const fetchedValues = getStorageModule()
914
- .getBatch(keysToFetch, scope)
915
- .map((value) => decodeNativeBatchValue(value));
1215
+ if (keysToFetch.length > 0) {
1216
+ const fetchedValues = getStorageModule()
1217
+ .getBatch(keysToFetch, scope)
1218
+ .map((value) => decodeNativeBatchValue(value));
916
1219
 
917
- fetchedValues.forEach((value, index) => {
918
- const key = keysToFetch[index];
919
- const targetIndex = keyIndexes[index];
920
- if (key === undefined || targetIndex === undefined) {
921
- return;
1220
+ fetchedValues.forEach((value, index) => {
1221
+ const key = keysToFetch[index];
1222
+ const targetIndex = keyIndexes[index];
1223
+ if (key === undefined || targetIndex === undefined) {
1224
+ return;
1225
+ }
1226
+ rawValues[targetIndex] = value;
1227
+ cacheRawValue(scope, key, value);
1228
+ });
922
1229
  }
923
- rawValues[targetIndex] = value;
924
- cacheRawValue(scope, key, value);
925
- });
926
- }
927
1230
 
928
- return items.map((item, index) => {
929
- const raw = rawValues[index];
930
- if (raw === undefined) {
931
- return item.get();
932
- }
933
- return item.deserialize(raw);
934
- });
1231
+ return items.map((item, index) => {
1232
+ const raw = rawValues[index];
1233
+ if (raw === undefined) {
1234
+ return asInternal(item as StorageItem<unknown>)._defaultValue;
1235
+ }
1236
+ return item.deserialize(raw);
1237
+ });
1238
+ },
1239
+ items.length,
1240
+ );
935
1241
  }
936
1242
 
937
1243
  export function setBatch<T>(
938
1244
  items: readonly StorageBatchSetItem<T>[],
939
1245
  scope: StorageScope,
940
1246
  ): void {
941
- assertBatchScope(
942
- items.map((batchEntry) => batchEntry.item),
1247
+ measureOperation(
1248
+ "batch:set",
943
1249
  scope,
944
- );
1250
+ () => {
1251
+ assertBatchScope(
1252
+ items.map((batchEntry) => batchEntry.item),
1253
+ scope,
1254
+ );
945
1255
 
946
- if (scope === StorageScope.Memory) {
947
- items.forEach(({ item, value }) => item.set(value));
948
- return;
949
- }
1256
+ if (scope === StorageScope.Memory) {
1257
+ // Determine if any item needs per-item handling (validation or TTL)
1258
+ const needsIndividualSets = items.some(({ item }) => {
1259
+ const internal = asInternal(item as StorageItem<unknown>);
1260
+ return internal._hasValidation || internal._hasExpiration;
1261
+ });
1262
+
1263
+ if (needsIndividualSets) {
1264
+ // Fall back to individual sets to preserve validation and TTL semantics
1265
+ items.forEach(({ item, value }) => item.set(value));
1266
+ return;
1267
+ }
950
1268
 
951
- if (scope === StorageScope.Secure) {
952
- const secureEntries = items.map(({ item, value }) => ({
953
- item,
954
- value,
955
- internal: asInternal(item),
956
- }));
957
- const canUseSecureBatchPath = secureEntries.every(({ internal }) =>
958
- canUseSecureRawBatchPath(internal),
959
- );
960
- if (!canUseSecureBatchPath) {
961
- items.forEach(({ item, value }) => item.set(value));
962
- return;
963
- }
1269
+ // Atomic write: update all values in memoryStore, invalidate caches, then batch-notify
1270
+ items.forEach(({ item, value }) => {
1271
+ memoryStore.set(item.key, value);
1272
+ asInternal(item as StorageItem<unknown>)._invalidateParsedCacheOnly();
1273
+ });
1274
+ items.forEach(({ item }) =>
1275
+ notifyKeyListeners(memoryListeners, item.key),
1276
+ );
1277
+ return;
1278
+ }
964
1279
 
965
- flushSecureWrites();
966
- const storageModule = getStorageModule();
967
- const groupedByAccessControl = new Map<
968
- number,
969
- { keys: string[]; values: string[] }
970
- >();
971
-
972
- secureEntries.forEach(({ item, value, internal }) => {
973
- const accessControl =
974
- internal._secureAccessControl ?? secureDefaultAccessControl;
975
- const existingGroup = groupedByAccessControl.get(accessControl);
976
- const group = existingGroup ?? { keys: [], values: [] };
977
- group.keys.push(item.key);
978
- group.values.push(item.serialize(value));
979
- if (!existingGroup) {
980
- groupedByAccessControl.set(accessControl, group);
1280
+ if (scope === StorageScope.Secure) {
1281
+ const secureEntries = items.map(({ item, value }) => ({
1282
+ item,
1283
+ value,
1284
+ internal: asInternal(item),
1285
+ }));
1286
+ const canUseSecureBatchPath = secureEntries.every(({ internal }) =>
1287
+ canUseSecureRawBatchPath(internal),
1288
+ );
1289
+ if (!canUseSecureBatchPath) {
1290
+ items.forEach(({ item, value }) => item.set(value));
1291
+ return;
1292
+ }
1293
+
1294
+ flushSecureWrites();
1295
+ const storageModule = getStorageModule();
1296
+ const groupedByAccessControl = new Map<
1297
+ number,
1298
+ { keys: string[]; values: string[] }
1299
+ >();
1300
+
1301
+ secureEntries.forEach(({ item, value, internal }) => {
1302
+ const accessControl =
1303
+ internal._secureAccessControl ?? secureDefaultAccessControl;
1304
+ const existingGroup = groupedByAccessControl.get(accessControl);
1305
+ const group = existingGroup ?? { keys: [], values: [] };
1306
+ group.keys.push(item.key);
1307
+ group.values.push(item.serialize(value));
1308
+ if (!existingGroup) {
1309
+ groupedByAccessControl.set(accessControl, group);
1310
+ }
1311
+ });
1312
+
1313
+ groupedByAccessControl.forEach((group, accessControl) => {
1314
+ storageModule.setSecureAccessControl(accessControl);
1315
+ storageModule.setBatch(group.keys, group.values, scope);
1316
+ group.keys.forEach((key, index) =>
1317
+ cacheRawValue(scope, key, group.values[index]),
1318
+ );
1319
+ });
1320
+ return;
981
1321
  }
982
- });
983
1322
 
984
- groupedByAccessControl.forEach((group, accessControl) => {
985
- storageModule.setSecureAccessControl(accessControl);
986
- storageModule.setBatch(group.keys, group.values, scope);
987
- group.keys.forEach((key, index) =>
988
- cacheRawValue(scope, key, group.values[index]),
1323
+ const useRawBatchPath = items.every(({ item }) =>
1324
+ canUseRawBatchPath(asInternal(item)),
989
1325
  );
990
- });
991
- return;
992
- }
993
-
994
- const useRawBatchPath = items.every(({ item }) =>
995
- canUseRawBatchPath(asInternal(item)),
996
- );
997
- if (!useRawBatchPath) {
998
- items.forEach(({ item, value }) => item.set(value));
999
- return;
1000
- }
1326
+ if (!useRawBatchPath) {
1327
+ items.forEach(({ item, value }) => item.set(value));
1328
+ return;
1329
+ }
1001
1330
 
1002
- const keys = items.map((entry) => entry.item.key);
1003
- const values = items.map((entry) => entry.item.serialize(entry.value));
1331
+ const keys = items.map((entry) => entry.item.key);
1332
+ const values = items.map((entry) => entry.item.serialize(entry.value));
1004
1333
 
1005
- getStorageModule().setBatch(keys, values, scope);
1006
- keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
1334
+ getStorageModule().setBatch(keys, values, scope);
1335
+ keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
1336
+ },
1337
+ items.length,
1338
+ );
1007
1339
  }
1008
1340
 
1009
1341
  export function removeBatch(
1010
1342
  items: readonly BatchRemoveItem[],
1011
1343
  scope: StorageScope,
1012
1344
  ): void {
1013
- assertBatchScope(items, scope);
1345
+ measureOperation(
1346
+ "batch:remove",
1347
+ scope,
1348
+ () => {
1349
+ assertBatchScope(items, scope);
1014
1350
 
1015
- if (scope === StorageScope.Memory) {
1016
- items.forEach((item) => item.delete());
1017
- return;
1018
- }
1351
+ if (scope === StorageScope.Memory) {
1352
+ items.forEach((item) => item.delete());
1353
+ return;
1354
+ }
1019
1355
 
1020
- const keys = items.map((item) => item.key);
1021
- if (scope === StorageScope.Secure) {
1022
- flushSecureWrites();
1023
- }
1024
- getStorageModule().removeBatch(keys, scope);
1025
- keys.forEach((key) => cacheRawValue(scope, key, undefined));
1356
+ const keys = items.map((item) => item.key);
1357
+ if (scope === StorageScope.Secure) {
1358
+ flushSecureWrites();
1359
+ }
1360
+ getStorageModule().removeBatch(keys, scope);
1361
+ keys.forEach((key) => cacheRawValue(scope, key, undefined));
1362
+ },
1363
+ items.length,
1364
+ );
1026
1365
  }
1027
1366
 
1028
1367
  export function registerMigration(version: number, migration: Migration): void {
@@ -1040,92 +1379,124 @@ export function registerMigration(version: number, migration: Migration): void {
1040
1379
  export function migrateToLatest(
1041
1380
  scope: StorageScope = StorageScope.Disk,
1042
1381
  ): number {
1043
- assertValidScope(scope);
1044
- const currentVersion = readMigrationVersion(scope);
1045
- const versions = Array.from(registeredMigrations.keys())
1046
- .filter((version) => version > currentVersion)
1047
- .sort((a, b) => a - b);
1382
+ return measureOperation("migration:run", scope, () => {
1383
+ assertValidScope(scope);
1384
+ const currentVersion = readMigrationVersion(scope);
1385
+ const versions = Array.from(registeredMigrations.keys())
1386
+ .filter((version) => version > currentVersion)
1387
+ .sort((a, b) => a - b);
1388
+
1389
+ let appliedVersion = currentVersion;
1390
+ const context: MigrationContext = {
1391
+ scope,
1392
+ getRaw: (key) => getRawValue(key, scope),
1393
+ setRaw: (key, value) => setRawValue(key, value, scope),
1394
+ removeRaw: (key) => removeRawValue(key, scope),
1395
+ };
1048
1396
 
1049
- let appliedVersion = currentVersion;
1050
- const context: MigrationContext = {
1051
- scope,
1052
- getRaw: (key) => getRawValue(key, scope),
1053
- setRaw: (key, value) => setRawValue(key, value, scope),
1054
- removeRaw: (key) => removeRawValue(key, scope),
1055
- };
1397
+ versions.forEach((version) => {
1398
+ const migration = registeredMigrations.get(version);
1399
+ if (!migration) {
1400
+ return;
1401
+ }
1402
+ migration(context);
1403
+ writeMigrationVersion(scope, version);
1404
+ appliedVersion = version;
1405
+ });
1056
1406
 
1057
- versions.forEach((version) => {
1058
- const migration = registeredMigrations.get(version);
1059
- if (!migration) {
1060
- return;
1061
- }
1062
- migration(context);
1063
- writeMigrationVersion(scope, version);
1064
- appliedVersion = version;
1407
+ return appliedVersion;
1065
1408
  });
1066
-
1067
- return appliedVersion;
1068
1409
  }
1069
1410
 
1070
1411
  export function runTransaction<T>(
1071
1412
  scope: StorageScope,
1072
1413
  transaction: (context: TransactionContext) => T,
1073
1414
  ): T {
1074
- assertValidScope(scope);
1075
- if (scope === StorageScope.Secure) {
1076
- flushSecureWrites();
1077
- }
1415
+ return measureOperation("transaction:run", scope, () => {
1416
+ assertValidScope(scope);
1417
+ if (scope === StorageScope.Secure) {
1418
+ flushSecureWrites();
1419
+ }
1078
1420
 
1079
- const rollback = new Map<string, string | undefined>();
1421
+ const rollback = new Map<string, string | undefined>();
1080
1422
 
1081
- const rememberRollback = (key: string) => {
1082
- if (rollback.has(key)) {
1083
- return;
1084
- }
1085
- rollback.set(key, getRawValue(key, scope));
1086
- };
1423
+ const rememberRollback = (key: string) => {
1424
+ if (rollback.has(key)) {
1425
+ return;
1426
+ }
1427
+ rollback.set(key, getRawValue(key, scope));
1428
+ };
1087
1429
 
1088
- const tx: TransactionContext = {
1089
- scope,
1090
- getRaw: (key) => getRawValue(key, scope),
1091
- setRaw: (key, value) => {
1092
- rememberRollback(key);
1093
- setRawValue(key, value, scope);
1094
- },
1095
- removeRaw: (key) => {
1096
- rememberRollback(key);
1097
- removeRawValue(key, scope);
1098
- },
1099
- getItem: (item) => {
1100
- assertBatchScope([item], scope);
1101
- return item.get();
1102
- },
1103
- setItem: (item, value) => {
1104
- assertBatchScope([item], scope);
1105
- rememberRollback(item.key);
1106
- item.set(value);
1107
- },
1108
- removeItem: (item) => {
1109
- assertBatchScope([item], scope);
1110
- rememberRollback(item.key);
1111
- item.delete();
1112
- },
1113
- };
1430
+ const tx: TransactionContext = {
1431
+ scope,
1432
+ getRaw: (key) => getRawValue(key, scope),
1433
+ setRaw: (key, value) => {
1434
+ rememberRollback(key);
1435
+ setRawValue(key, value, scope);
1436
+ },
1437
+ removeRaw: (key) => {
1438
+ rememberRollback(key);
1439
+ removeRawValue(key, scope);
1440
+ },
1441
+ getItem: (item) => {
1442
+ assertBatchScope([item], scope);
1443
+ return item.get();
1444
+ },
1445
+ setItem: (item, value) => {
1446
+ assertBatchScope([item], scope);
1447
+ rememberRollback(item.key);
1448
+ item.set(value);
1449
+ },
1450
+ removeItem: (item) => {
1451
+ assertBatchScope([item], scope);
1452
+ rememberRollback(item.key);
1453
+ item.delete();
1454
+ },
1455
+ };
1114
1456
 
1115
- try {
1116
- return transaction(tx);
1117
- } catch (error) {
1118
- Array.from(rollback.entries())
1119
- .reverse()
1120
- .forEach(([key, previousValue]) => {
1121
- if (previousValue === undefined) {
1122
- removeRawValue(key, scope);
1123
- } else {
1124
- setRawValue(key, previousValue, scope);
1457
+ try {
1458
+ return transaction(tx);
1459
+ } catch (error) {
1460
+ const rollbackEntries = Array.from(rollback.entries()).reverse();
1461
+ if (scope === StorageScope.Memory) {
1462
+ rollbackEntries.forEach(([key, previousValue]) => {
1463
+ if (previousValue === undefined) {
1464
+ removeRawValue(key, scope);
1465
+ } else {
1466
+ setRawValue(key, previousValue, scope);
1467
+ }
1468
+ });
1469
+ } else {
1470
+ const keysToSet: string[] = [];
1471
+ const valuesToSet: string[] = [];
1472
+ const keysToRemove: string[] = [];
1473
+
1474
+ rollbackEntries.forEach(([key, previousValue]) => {
1475
+ if (previousValue === undefined) {
1476
+ keysToRemove.push(key);
1477
+ } else {
1478
+ keysToSet.push(key);
1479
+ valuesToSet.push(previousValue);
1480
+ }
1481
+ });
1482
+
1483
+ if (scope === StorageScope.Secure) {
1484
+ flushSecureWrites();
1125
1485
  }
1126
- });
1127
- throw error;
1128
- }
1486
+ if (keysToSet.length > 0) {
1487
+ getStorageModule().setBatch(keysToSet, valuesToSet, scope);
1488
+ keysToSet.forEach((key, index) =>
1489
+ cacheRawValue(scope, key, valuesToSet[index]),
1490
+ );
1491
+ }
1492
+ if (keysToRemove.length > 0) {
1493
+ getStorageModule().removeBatch(keysToRemove, scope);
1494
+ keysToRemove.forEach((key) => cacheRawValue(scope, key, undefined));
1495
+ }
1496
+ }
1497
+ throw error;
1498
+ }
1499
+ });
1129
1500
  }
1130
1501
 
1131
1502
  export type SecureAuthStorageConfig<K extends string = string> = Record<
@@ -1133,6 +1504,7 @@ export type SecureAuthStorageConfig<K extends string = string> = Record<
1133
1504
  {
1134
1505
  ttlMs?: number;
1135
1506
  biometric?: boolean;
1507
+ biometricLevel?: BiometricLevel;
1136
1508
  accessControl?: AccessControl;
1137
1509
  }
1138
1510
  >;
@@ -1156,6 +1528,9 @@ export function createSecureAuthStorage<K extends string>(
1156
1528
  ...(itemConfig.biometric !== undefined
1157
1529
  ? { biometric: itemConfig.biometric }
1158
1530
  : {}),
1531
+ ...(itemConfig.biometricLevel !== undefined
1532
+ ? { biometricLevel: itemConfig.biometricLevel }
1533
+ : {}),
1159
1534
  ...(itemConfig.accessControl !== undefined
1160
1535
  ? { accessControl: itemConfig.accessControl }
1161
1536
  : {}),