react-native-nitro-storage 0.3.2 → 0.4.0

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 (36) hide show
  1. package/README.md +141 -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/java/com/nitrostorage/AndroidStorageAdapter.kt +54 -5
  5. package/cpp/bindings/HybridStorage.cpp +167 -22
  6. package/cpp/bindings/HybridStorage.hpp +12 -1
  7. package/cpp/core/NativeStorageAdapter.hpp +3 -0
  8. package/ios/IOSStorageAdapterCpp.hpp +16 -0
  9. package/ios/IOSStorageAdapterCpp.mm +135 -11
  10. package/lib/commonjs/index.js +466 -275
  11. package/lib/commonjs/index.js.map +1 -1
  12. package/lib/commonjs/index.web.js +564 -270
  13. package/lib/commonjs/index.web.js.map +1 -1
  14. package/lib/commonjs/internal.js +25 -0
  15. package/lib/commonjs/internal.js.map +1 -1
  16. package/lib/module/index.js +466 -277
  17. package/lib/module/index.js.map +1 -1
  18. package/lib/module/index.web.js +564 -272
  19. package/lib/module/index.web.js.map +1 -1
  20. package/lib/module/internal.js +24 -0
  21. package/lib/module/internal.js.map +1 -1
  22. package/lib/typescript/Storage.nitro.d.ts +2 -0
  23. package/lib/typescript/Storage.nitro.d.ts.map +1 -1
  24. package/lib/typescript/index.d.ts +38 -1
  25. package/lib/typescript/index.d.ts.map +1 -1
  26. package/lib/typescript/index.web.d.ts +40 -1
  27. package/lib/typescript/index.web.d.ts.map +1 -1
  28. package/lib/typescript/internal.d.ts +1 -0
  29. package/lib/typescript/internal.d.ts.map +1 -1
  30. package/nitrogen/generated/shared/c++/HybridStorageSpec.cpp +2 -0
  31. package/nitrogen/generated/shared/c++/HybridStorageSpec.hpp +2 -0
  32. package/package.json +1 -1
  33. package/src/Storage.nitro.ts +2 -0
  34. package/src/index.ts +616 -296
  35. package/src/index.web.ts +728 -288
  36. 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,167 @@ 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);
708
+ },
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;
505
723
  },
506
- setKeychainAccessGroup: _group => {}
724
+ resetMetrics: () => {
725
+ metricsCounters.clear();
726
+ }
507
727
  };
728
+ export function setWebSecureStorageBackend(backend) {
729
+ webSecureStorageBackend = backend ?? createLocalStorageWebSecureBackend();
730
+ hydratedWebScopeKeyIndex.delete(StorageScope.Secure);
731
+ clearScopeRawCache(StorageScope.Secure);
732
+ }
733
+ export function getWebSecureStorageBackend() {
734
+ return webSecureStorageBackend;
735
+ }
508
736
  function canUseRawBatchPath(item) {
509
737
  return item._hasExpiration === false && item._hasValidation === false && item._isBiometric !== true && item._secureAccessControl === undefined;
510
738
  }
@@ -522,7 +750,8 @@ export function createStorageItem(config) {
522
750
  const serialize = config.serialize ?? defaultSerialize;
523
751
  const deserialize = config.deserialize ?? defaultDeserialize;
524
752
  const isMemory = config.scope === StorageScope.Memory;
525
- const isBiometric = config.biometric === true && config.scope === StorageScope.Secure;
753
+ const resolvedBiometricLevel = config.scope === StorageScope.Secure ? config.biometricLevel ?? (config.biometric === true ? BiometricLevel.BiometryOnly : BiometricLevel.None) : BiometricLevel.None;
754
+ const isBiometric = resolvedBiometricLevel !== BiometricLevel.None;
526
755
  const secureAccessControl = config.accessControl;
527
756
  const validate = config.validate;
528
757
  const onValidationError = config.onValidationError;
@@ -531,7 +760,7 @@ export function createStorageItem(config) {
531
760
  const expirationTtlMs = expiration?.ttlMs;
532
761
  const memoryExpiration = expiration && isMemory ? new Map() : null;
533
762
  const readCache = !isMemory && config.readCache === true;
534
- const coalesceSecureWrites = config.scope === StorageScope.Secure && config.coalesceSecureWrites === true && !isBiometric && secureAccessControl === undefined;
763
+ const coalesceSecureWrites = config.scope === StorageScope.Secure && config.coalesceSecureWrites === true && !isBiometric;
535
764
  const defaultValue = config.defaultValue;
536
765
  const nonMemoryScope = config.scope === StorageScope.Disk ? StorageScope.Disk : config.scope === StorageScope.Secure ? StorageScope.Secure : null;
537
766
  if (expiration && expiration.ttlMs <= 0) {
@@ -561,6 +790,7 @@ export function createStorageItem(config) {
561
790
  unsubscribe = addKeyListener(memoryListeners, storageKey, listener);
562
791
  return;
563
792
  }
793
+ ensureWebStorageEventSubscription();
564
794
  unsubscribe = addKeyListener(getScopedListeners(nonMemoryScope), storageKey, listener);
565
795
  };
566
796
  const readStoredRaw = () => {
@@ -594,12 +824,12 @@ export function createStorageItem(config) {
594
824
  };
595
825
  const writeStoredRaw = rawValue => {
596
826
  if (isBiometric) {
597
- WebStorage.setSecureBiometric(storageKey, rawValue);
827
+ WebStorage.setSecureBiometricWithLevel(storageKey, rawValue, resolvedBiometricLevel);
598
828
  return;
599
829
  }
600
830
  cacheRawValue(nonMemoryScope, storageKey, rawValue);
601
831
  if (coalesceSecureWrites) {
602
- scheduleSecureWrite(storageKey, rawValue);
832
+ scheduleSecureWrite(storageKey, rawValue, secureAccessControl ?? AccessControl.WhenUnlocked);
603
833
  return;
604
834
  }
605
835
  if (nonMemoryScope === StorageScope.Secure) {
@@ -614,7 +844,7 @@ export function createStorageItem(config) {
614
844
  }
615
845
  cacheRawValue(nonMemoryScope, storageKey, undefined);
616
846
  if (coalesceSecureWrites) {
617
- scheduleSecureWrite(storageKey, undefined);
847
+ scheduleSecureWrite(storageKey, undefined, secureAccessControl ?? AccessControl.WhenUnlocked);
618
848
  return;
619
849
  }
620
850
  if (nonMemoryScope === StorageScope.Secure) {
@@ -662,7 +892,7 @@ export function createStorageItem(config) {
662
892
  }
663
893
  return resolved;
664
894
  };
665
- const get = () => {
895
+ const getInternal = () => {
666
896
  const raw = readStoredRaw();
667
897
  if (!memoryExpiration && raw === lastRaw && hasLastValue) {
668
898
  if (!expiration || lastExpiresAt === null) {
@@ -727,31 +957,52 @@ export function createStorageItem(config) {
727
957
  hasLastValue = true;
728
958
  return lastValue;
729
959
  };
960
+ const getCurrentVersion = () => {
961
+ const raw = readStoredRaw();
962
+ return toVersionToken(raw);
963
+ };
964
+ const get = () => measureOperation("item:get", config.scope, () => getInternal());
965
+ const getWithVersion = () => measureOperation("item:getWithVersion", config.scope, () => ({
966
+ value: getInternal(),
967
+ version: getCurrentVersion()
968
+ }));
730
969
  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);
970
+ measureOperation("item:set", config.scope, () => {
971
+ const newValue = isUpdater(valueOrFn) ? valueOrFn(getInternal()) : valueOrFn;
972
+ invalidateParsedCache();
973
+ if (validate && !validate(newValue)) {
974
+ throw new Error(`Validation failed for key "${storageKey}" in scope "${StorageScope[config.scope]}".`);
975
+ }
976
+ writeValueWithoutValidation(newValue);
977
+ });
737
978
  };
979
+ const setIfVersion = (version, valueOrFn) => measureOperation("item:setIfVersion", config.scope, () => {
980
+ const currentVersion = getCurrentVersion();
981
+ if (currentVersion !== version) {
982
+ return false;
983
+ }
984
+ set(valueOrFn);
985
+ return true;
986
+ });
738
987
  const deleteItem = () => {
739
- invalidateParsedCache();
740
- if (isMemory) {
741
- if (memoryExpiration) {
742
- memoryExpiration.delete(storageKey);
988
+ measureOperation("item:delete", config.scope, () => {
989
+ invalidateParsedCache();
990
+ if (isMemory) {
991
+ if (memoryExpiration) {
992
+ memoryExpiration.delete(storageKey);
993
+ }
994
+ memoryStore.delete(storageKey);
995
+ notifyKeyListeners(memoryListeners, storageKey);
996
+ return;
743
997
  }
744
- memoryStore.delete(storageKey);
745
- notifyKeyListeners(memoryListeners, storageKey);
746
- return;
747
- }
748
- removeStoredRaw();
998
+ removeStoredRaw();
999
+ });
749
1000
  };
750
- const hasItem = () => {
1001
+ const hasItem = () => measureOperation("item:has", config.scope, () => {
751
1002
  if (isMemory) return memoryStore.has(storageKey);
752
1003
  if (isBiometric) return WebStorage.hasSecureBiometric(storageKey);
753
1004
  return WebStorage.has(storageKey, config.scope);
754
- };
1005
+ });
755
1006
  const subscribe = callback => {
756
1007
  ensureSubscription();
757
1008
  listeners.add(callback);
@@ -765,7 +1016,9 @@ export function createStorageItem(config) {
765
1016
  };
766
1017
  const storageItem = {
767
1018
  get,
1019
+ getWithVersion,
768
1020
  set,
1021
+ setIfVersion,
769
1022
  delete: deleteItem,
770
1023
  has: hasItem,
771
1024
  subscribe,
@@ -779,6 +1032,7 @@ export function createStorageItem(config) {
779
1032
  _hasExpiration: expiration !== undefined,
780
1033
  _readCacheEnabled: readCache,
781
1034
  _isBiometric: isBiometric,
1035
+ _defaultValue: defaultValue,
782
1036
  ...(secureAccessControl !== undefined ? {
783
1037
  _secureAccessControl: secureAccessControl
784
1038
  } : {}),
@@ -789,135 +1043,140 @@ export function createStorageItem(config) {
789
1043
  }
790
1044
  export { useStorage, useStorageSelector, useSetStorage } from "./storage-hooks";
791
1045
  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;
809
- }
1046
+ return measureOperation("batch:get", scope, () => {
1047
+ assertBatchScope(items, scope);
1048
+ if (scope === StorageScope.Memory) {
1049
+ return items.map(item => item.get());
810
1050
  }
811
- if (useBatchCache) {
812
- if (hasCachedRawValue(scope, item.key)) {
813
- rawValues[index] = readCachedRawValue(scope, item.key);
814
- return;
815
- }
1051
+ const useRawBatchPath = items.every(item => scope === StorageScope.Secure ? canUseSecureRawBatchPath(item) : canUseRawBatchPath(item));
1052
+ if (!useRawBatchPath) {
1053
+ return items.map(item => item.get());
816
1054
  }
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) {
826
- return;
1055
+ const rawValues = new Array(items.length);
1056
+ const keysToFetch = [];
1057
+ const keyIndexes = [];
1058
+ items.forEach((item, index) => {
1059
+ if (scope === StorageScope.Secure) {
1060
+ if (hasPendingSecureWrite(item.key)) {
1061
+ rawValues[index] = readPendingSecureWrite(item.key);
1062
+ return;
1063
+ }
1064
+ }
1065
+ if (item._readCacheEnabled === true) {
1066
+ if (hasCachedRawValue(scope, item.key)) {
1067
+ rawValues[index] = readCachedRawValue(scope, item.key);
1068
+ return;
1069
+ }
827
1070
  }
828
- rawValues[targetIndex] = value;
829
- cacheRawValue(scope, key, value);
1071
+ keysToFetch.push(item.key);
1072
+ keyIndexes.push(index);
830
1073
  });
831
- }
832
- return items.map((item, index) => {
833
- const raw = rawValues[index];
834
- if (raw === undefined) {
835
- return item.get();
1074
+ if (keysToFetch.length > 0) {
1075
+ const fetchedValues = WebStorage.getBatch(keysToFetch, scope);
1076
+ fetchedValues.forEach((value, index) => {
1077
+ const key = keysToFetch[index];
1078
+ const targetIndex = keyIndexes[index];
1079
+ if (key === undefined || targetIndex === undefined) {
1080
+ return;
1081
+ }
1082
+ rawValues[targetIndex] = value;
1083
+ cacheRawValue(scope, key, value);
1084
+ });
836
1085
  }
837
- return item.deserialize(raw);
838
- });
1086
+ return items.map((item, index) => {
1087
+ const raw = rawValues[index];
1088
+ if (raw === undefined) {
1089
+ return asInternal(item)._defaultValue;
1090
+ }
1091
+ return item.deserialize(raw);
1092
+ });
1093
+ }, items.length);
839
1094
  }
840
1095
  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) {
1096
+ measureOperation("batch:set", scope, () => {
1097
+ assertBatchScope(items.map(batchEntry => batchEntry.item), scope);
1098
+ if (scope === StorageScope.Memory) {
862
1099
  items.forEach(({
863
1100
  item,
864
1101
  value
865
1102
  }) => item.set(value));
866
1103
  return;
867
1104
  }
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);
1105
+ if (scope === StorageScope.Secure) {
1106
+ const secureEntries = items.map(({
1107
+ item,
1108
+ value
1109
+ }) => ({
1110
+ item,
1111
+ value,
1112
+ internal: asInternal(item)
1113
+ }));
1114
+ const canUseSecureBatchPath = secureEntries.every(({
1115
+ internal
1116
+ }) => canUseSecureRawBatchPath(internal));
1117
+ if (!canUseSecureBatchPath) {
1118
+ items.forEach(({
1119
+ item,
1120
+ value
1121
+ }) => item.set(value));
1122
+ return;
885
1123
  }
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]));
1124
+ flushSecureWrites();
1125
+ const groupedByAccessControl = new Map();
1126
+ secureEntries.forEach(({
1127
+ item,
1128
+ value,
1129
+ internal
1130
+ }) => {
1131
+ const accessControl = internal._secureAccessControl ?? AccessControl.WhenUnlocked;
1132
+ const existingGroup = groupedByAccessControl.get(accessControl);
1133
+ const group = existingGroup ?? {
1134
+ keys: [],
1135
+ values: []
1136
+ };
1137
+ group.keys.push(item.key);
1138
+ group.values.push(item.serialize(value));
1139
+ if (!existingGroup) {
1140
+ groupedByAccessControl.set(accessControl, group);
1141
+ }
1142
+ });
1143
+ groupedByAccessControl.forEach((group, accessControl) => {
1144
+ WebStorage.setSecureAccessControl(accessControl);
1145
+ WebStorage.setBatch(group.keys, group.values, scope);
1146
+ group.keys.forEach((key, index) => cacheRawValue(scope, key, group.values[index]));
1147
+ });
1148
+ return;
1149
+ }
1150
+ const useRawBatchPath = items.every(({
1151
+ item
1152
+ }) => canUseRawBatchPath(asInternal(item)));
1153
+ if (!useRawBatchPath) {
1154
+ items.forEach(({
1155
+ item,
1156
+ value
1157
+ }) => item.set(value));
1158
+ return;
1159
+ }
1160
+ const keys = items.map(entry => entry.item.key);
1161
+ const values = items.map(entry => entry.item.serialize(entry.value));
1162
+ WebStorage.setBatch(keys, values, scope);
1163
+ keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
1164
+ }, items.length);
908
1165
  }
909
1166
  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));
1167
+ measureOperation("batch:remove", scope, () => {
1168
+ assertBatchScope(items, scope);
1169
+ if (scope === StorageScope.Memory) {
1170
+ items.forEach(item => item.delete());
1171
+ return;
1172
+ }
1173
+ const keys = items.map(item => item.key);
1174
+ if (scope === StorageScope.Secure) {
1175
+ flushSecureWrites();
1176
+ }
1177
+ WebStorage.removeBatch(keys, scope);
1178
+ keys.forEach(key => cacheRawValue(scope, key, undefined));
1179
+ }, items.length);
921
1180
  }
922
1181
  export function registerMigration(version, migration) {
923
1182
  if (!Number.isInteger(version) || version <= 0) {
@@ -929,77 +1188,107 @@ export function registerMigration(version, migration) {
929
1188
  registeredMigrations.set(version, migration);
930
1189
  }
931
1190
  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;
1191
+ return measureOperation("migration:run", scope, () => {
1192
+ assertValidScope(scope);
1193
+ const currentVersion = readMigrationVersion(scope);
1194
+ const versions = Array.from(registeredMigrations.keys()).filter(version => version > currentVersion).sort((a, b) => a - b);
1195
+ let appliedVersion = currentVersion;
1196
+ const context = {
1197
+ scope,
1198
+ getRaw: key => getRawValue(key, scope),
1199
+ setRaw: (key, value) => setRawValue(key, value, scope),
1200
+ removeRaw: key => removeRawValue(key, scope)
1201
+ };
1202
+ versions.forEach(version => {
1203
+ const migration = registeredMigrations.get(version);
1204
+ if (!migration) {
1205
+ return;
1206
+ }
1207
+ migration(context);
1208
+ writeMigrationVersion(scope, version);
1209
+ appliedVersion = version;
1210
+ });
1211
+ return appliedVersion;
950
1212
  });
951
- return appliedVersion;
952
1213
  }
953
1214
  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();
1215
+ return measureOperation("transaction:run", scope, () => {
1216
+ assertValidScope(scope);
1217
+ if (scope === StorageScope.Secure) {
1218
+ flushSecureWrites();
989
1219
  }
990
- };
991
- try {
992
- return transaction(tx);
993
- } catch (error) {
994
- Array.from(rollback.entries()).reverse().forEach(([key, previousValue]) => {
995
- if (previousValue === undefined) {
1220
+ const rollback = new Map();
1221
+ const rememberRollback = key => {
1222
+ if (rollback.has(key)) {
1223
+ return;
1224
+ }
1225
+ rollback.set(key, getRawValue(key, scope));
1226
+ };
1227
+ const tx = {
1228
+ scope,
1229
+ getRaw: key => getRawValue(key, scope),
1230
+ setRaw: (key, value) => {
1231
+ rememberRollback(key);
1232
+ setRawValue(key, value, scope);
1233
+ },
1234
+ removeRaw: key => {
1235
+ rememberRollback(key);
996
1236
  removeRawValue(key, scope);
1237
+ },
1238
+ getItem: item => {
1239
+ assertBatchScope([item], scope);
1240
+ return item.get();
1241
+ },
1242
+ setItem: (item, value) => {
1243
+ assertBatchScope([item], scope);
1244
+ rememberRollback(item.key);
1245
+ item.set(value);
1246
+ },
1247
+ removeItem: item => {
1248
+ assertBatchScope([item], scope);
1249
+ rememberRollback(item.key);
1250
+ item.delete();
1251
+ }
1252
+ };
1253
+ try {
1254
+ return transaction(tx);
1255
+ } catch (error) {
1256
+ const rollbackEntries = Array.from(rollback.entries()).reverse();
1257
+ if (scope === StorageScope.Memory) {
1258
+ rollbackEntries.forEach(([key, previousValue]) => {
1259
+ if (previousValue === undefined) {
1260
+ removeRawValue(key, scope);
1261
+ } else {
1262
+ setRawValue(key, previousValue, scope);
1263
+ }
1264
+ });
997
1265
  } else {
998
- setRawValue(key, previousValue, scope);
1266
+ const keysToSet = [];
1267
+ const valuesToSet = [];
1268
+ const keysToRemove = [];
1269
+ rollbackEntries.forEach(([key, previousValue]) => {
1270
+ if (previousValue === undefined) {
1271
+ keysToRemove.push(key);
1272
+ } else {
1273
+ keysToSet.push(key);
1274
+ valuesToSet.push(previousValue);
1275
+ }
1276
+ });
1277
+ if (scope === StorageScope.Secure) {
1278
+ flushSecureWrites();
1279
+ }
1280
+ if (keysToSet.length > 0) {
1281
+ WebStorage.setBatch(keysToSet, valuesToSet, scope);
1282
+ keysToSet.forEach((key, index) => cacheRawValue(scope, key, valuesToSet[index]));
1283
+ }
1284
+ if (keysToRemove.length > 0) {
1285
+ WebStorage.removeBatch(keysToRemove, scope);
1286
+ keysToRemove.forEach(key => cacheRawValue(scope, key, undefined));
1287
+ }
999
1288
  }
1000
- });
1001
- throw error;
1002
- }
1289
+ throw error;
1290
+ }
1291
+ });
1003
1292
  }
1004
1293
  export function createSecureAuthStorage(config, options) {
1005
1294
  const ns = options?.namespace ?? "auth";
@@ -1017,6 +1306,9 @@ export function createSecureAuthStorage(config, options) {
1017
1306
  ...(itemConfig.biometric !== undefined ? {
1018
1307
  biometric: itemConfig.biometric
1019
1308
  } : {}),
1309
+ ...(itemConfig.biometricLevel !== undefined ? {
1310
+ biometricLevel: itemConfig.biometricLevel
1311
+ } : {}),
1020
1312
  ...(itemConfig.accessControl !== undefined ? {
1021
1313
  accessControl: itemConfig.accessControl
1022
1314
  } : {}),