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
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,212 @@ 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();
428
618
  },
429
619
  };
430
620
 
621
+ export function setWebSecureStorageBackend(
622
+ _backend?: WebSecureStorageBackend,
623
+ ): void {
624
+ // Native platforms do not use web secure backends.
625
+ }
626
+
627
+ export function getWebSecureStorageBackend():
628
+ | WebSecureStorageBackend
629
+ | undefined {
630
+ return undefined;
631
+ }
632
+
431
633
  export interface StorageItemConfig<T> {
432
634
  key: string;
433
635
  scope: StorageScope;
@@ -442,12 +644,18 @@ export interface StorageItemConfig<T> {
442
644
  coalesceSecureWrites?: boolean;
443
645
  namespace?: string;
444
646
  biometric?: boolean;
647
+ biometricLevel?: BiometricLevel;
445
648
  accessControl?: AccessControl;
446
649
  }
447
650
 
448
651
  export interface StorageItem<T> {
449
652
  get: () => T;
653
+ getWithVersion: () => VersionedValue<T>;
450
654
  set: (value: T | ((prev: T) => T)) => void;
655
+ setIfVersion: (
656
+ version: StorageVersion,
657
+ value: T | ((prev: T) => T),
658
+ ) => boolean;
451
659
  delete: () => void;
452
660
  has: () => boolean;
453
661
  subscribe: (callback: () => void) => () => void;
@@ -463,6 +671,7 @@ type StorageItemInternal<T> = StorageItem<T> & {
463
671
  _hasExpiration: boolean;
464
672
  _readCacheEnabled: boolean;
465
673
  _isBiometric: boolean;
674
+ _defaultValue: T;
466
675
  _secureAccessControl?: AccessControl;
467
676
  };
468
677
 
@@ -498,8 +707,14 @@ export function createStorageItem<T = undefined>(
498
707
  const serialize = config.serialize ?? defaultSerialize;
499
708
  const deserialize = config.deserialize ?? defaultDeserialize;
500
709
  const isMemory = config.scope === StorageScope.Memory;
501
- const isBiometric =
502
- config.biometric === true && config.scope === StorageScope.Secure;
710
+ const resolvedBiometricLevel =
711
+ config.scope === StorageScope.Secure
712
+ ? (config.biometricLevel ??
713
+ (config.biometric === true
714
+ ? BiometricLevel.BiometryOnly
715
+ : BiometricLevel.None))
716
+ : BiometricLevel.None;
717
+ const isBiometric = resolvedBiometricLevel !== BiometricLevel.None;
503
718
  const secureAccessControl = config.accessControl;
504
719
  const validate = config.validate;
505
720
  const onValidationError = config.onValidationError;
@@ -512,8 +727,7 @@ export function createStorageItem<T = undefined>(
512
727
  const coalesceSecureWrites =
513
728
  config.scope === StorageScope.Secure &&
514
729
  config.coalesceSecureWrites === true &&
515
- !isBiometric &&
516
- secureAccessControl === undefined;
730
+ !isBiometric;
517
731
  const defaultValue = config.defaultValue as T;
518
732
  const nonMemoryScope: NonMemoryScope | null =
519
733
  config.scope === StorageScope.Disk
@@ -603,14 +817,22 @@ export function createStorageItem<T = undefined>(
603
817
 
604
818
  const writeStoredRaw = (rawValue: string): void => {
605
819
  if (isBiometric) {
606
- getStorageModule().setSecureBiometric(storageKey, rawValue);
820
+ getStorageModule().setSecureBiometricWithLevel(
821
+ storageKey,
822
+ rawValue,
823
+ resolvedBiometricLevel,
824
+ );
607
825
  return;
608
826
  }
609
827
 
610
828
  cacheRawValue(nonMemoryScope!, storageKey, rawValue);
611
829
 
612
830
  if (coalesceSecureWrites) {
613
- scheduleSecureWrite(storageKey, rawValue);
831
+ scheduleSecureWrite(
832
+ storageKey,
833
+ rawValue,
834
+ secureAccessControl ?? secureDefaultAccessControl,
835
+ );
614
836
  return;
615
837
  }
616
838
 
@@ -633,7 +855,11 @@ export function createStorageItem<T = undefined>(
633
855
  cacheRawValue(nonMemoryScope!, storageKey, undefined);
634
856
 
635
857
  if (coalesceSecureWrites) {
636
- scheduleSecureWrite(storageKey, undefined);
858
+ scheduleSecureWrite(
859
+ storageKey,
860
+ undefined,
861
+ secureAccessControl ?? secureDefaultAccessControl,
862
+ );
637
863
  return;
638
864
  }
639
865
 
@@ -694,7 +920,7 @@ export function createStorageItem<T = undefined>(
694
920
  return resolved;
695
921
  };
696
922
 
697
- const get = (): T => {
923
+ const getInternal = (): T => {
698
924
  const raw = readStoredRaw();
699
925
 
700
926
  if (!memoryExpiration && raw === lastRaw && hasLastValue) {
@@ -771,40 +997,74 @@ export function createStorageItem<T = undefined>(
771
997
  return lastValue;
772
998
  };
773
999
 
1000
+ const getCurrentVersion = (): StorageVersion => {
1001
+ const raw = readStoredRaw();
1002
+ return toVersionToken(raw);
1003
+ };
1004
+
1005
+ const get = (): T =>
1006
+ measureOperation("item:get", config.scope, () => getInternal());
1007
+
1008
+ const getWithVersion = (): VersionedValue<T> =>
1009
+ measureOperation("item:getWithVersion", config.scope, () => ({
1010
+ value: getInternal(),
1011
+ version: getCurrentVersion(),
1012
+ }));
1013
+
774
1014
  const set = (valueOrFn: T | ((prev: T) => T)): void => {
775
- const newValue = isUpdater(valueOrFn) ? valueOrFn(get()) : valueOrFn;
1015
+ measureOperation("item:set", config.scope, () => {
1016
+ const newValue = isUpdater(valueOrFn)
1017
+ ? valueOrFn(getInternal())
1018
+ : valueOrFn;
776
1019
 
777
- invalidateParsedCache();
1020
+ invalidateParsedCache();
778
1021
 
779
- if (validate && !validate(newValue)) {
780
- throw new Error(
781
- `Validation failed for key "${storageKey}" in scope "${StorageScope[config.scope]}".`,
782
- );
783
- }
1022
+ if (validate && !validate(newValue)) {
1023
+ throw new Error(
1024
+ `Validation failed for key "${storageKey}" in scope "${StorageScope[config.scope]}".`,
1025
+ );
1026
+ }
784
1027
 
785
- writeValueWithoutValidation(newValue);
1028
+ writeValueWithoutValidation(newValue);
1029
+ });
786
1030
  };
787
1031
 
1032
+ const setIfVersion = (
1033
+ version: StorageVersion,
1034
+ valueOrFn: T | ((prev: T) => T),
1035
+ ): boolean =>
1036
+ measureOperation("item:setIfVersion", config.scope, () => {
1037
+ const currentVersion = getCurrentVersion();
1038
+ if (currentVersion !== version) {
1039
+ return false;
1040
+ }
1041
+ set(valueOrFn);
1042
+ return true;
1043
+ });
1044
+
788
1045
  const deleteItem = (): void => {
789
- invalidateParsedCache();
1046
+ measureOperation("item:delete", config.scope, () => {
1047
+ invalidateParsedCache();
790
1048
 
791
- if (isMemory) {
792
- if (memoryExpiration) {
793
- memoryExpiration.delete(storageKey);
1049
+ if (isMemory) {
1050
+ if (memoryExpiration) {
1051
+ memoryExpiration.delete(storageKey);
1052
+ }
1053
+ memoryStore.delete(storageKey);
1054
+ notifyKeyListeners(memoryListeners, storageKey);
1055
+ return;
794
1056
  }
795
- memoryStore.delete(storageKey);
796
- notifyKeyListeners(memoryListeners, storageKey);
797
- return;
798
- }
799
1057
 
800
- removeStoredRaw();
1058
+ removeStoredRaw();
1059
+ });
801
1060
  };
802
1061
 
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
- };
1062
+ const hasItem = (): boolean =>
1063
+ measureOperation("item:has", config.scope, () => {
1064
+ if (isMemory) return memoryStore.has(storageKey);
1065
+ if (isBiometric) return getStorageModule().hasSecureBiometric(storageKey);
1066
+ return getStorageModule().has(storageKey, config.scope);
1067
+ });
808
1068
 
809
1069
  const subscribe = (callback: () => void): (() => void) => {
810
1070
  ensureSubscription();
@@ -823,7 +1083,9 @@ export function createStorageItem<T = undefined>(
823
1083
 
824
1084
  const storageItem: StorageItemInternal<T> = {
825
1085
  get,
1086
+ getWithVersion,
826
1087
  set,
1088
+ setIfVersion,
827
1089
  delete: deleteItem,
828
1090
  has: hasItem,
829
1091
  subscribe,
@@ -837,6 +1099,7 @@ export function createStorageItem<T = undefined>(
837
1099
  _hasExpiration: expiration !== undefined,
838
1100
  _readCacheEnabled: readCache,
839
1101
  _isBiometric: isBiometric,
1102
+ _defaultValue: defaultValue,
840
1103
  ...(secureAccessControl !== undefined
841
1104
  ? { _secureAccessControl: secureAccessControl }
842
1105
  : {}),
@@ -857,6 +1120,7 @@ type BatchReadItem<T> = Pick<
857
1120
  _hasExpiration?: boolean;
858
1121
  _readCacheEnabled?: boolean;
859
1122
  _isBiometric?: boolean;
1123
+ _defaultValue?: unknown;
860
1124
  _secureAccessControl?: AccessControl;
861
1125
  };
862
1126
  type BatchRemoveItem = Pick<StorageItem<unknown>, "key" | "scope" | "delete">;
@@ -870,159 +1134,179 @@ export function getBatch(
870
1134
  items: readonly BatchReadItem<unknown>[],
871
1135
  scope: StorageScope,
872
1136
  ): unknown[] {
873
- assertBatchScope(items, scope);
1137
+ return measureOperation(
1138
+ "batch:get",
1139
+ scope,
1140
+ () => {
1141
+ assertBatchScope(items, scope);
874
1142
 
875
- if (scope === StorageScope.Memory) {
876
- return items.map((item) => item.get());
877
- }
1143
+ if (scope === StorageScope.Memory) {
1144
+ return items.map((item) => item.get());
1145
+ }
878
1146
 
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);
1147
+ const useRawBatchPath = items.every((item) =>
1148
+ scope === StorageScope.Secure
1149
+ ? canUseSecureRawBatchPath(item)
1150
+ : canUseRawBatchPath(item),
1151
+ );
1152
+ if (!useRawBatchPath) {
1153
+ return items.map((item) => item.get());
1154
+ }
888
1155
 
889
- const rawValues = new Array<string | undefined>(items.length);
890
- const keysToFetch: string[] = [];
891
- const keyIndexes: number[] = [];
1156
+ const rawValues = new Array<string | undefined>(items.length);
1157
+ const keysToFetch: string[] = [];
1158
+ const keyIndexes: number[] = [];
892
1159
 
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
- }
1160
+ items.forEach((item, index) => {
1161
+ if (scope === StorageScope.Secure) {
1162
+ if (hasPendingSecureWrite(item.key)) {
1163
+ rawValues[index] = readPendingSecureWrite(item.key);
1164
+ return;
1165
+ }
1166
+ }
900
1167
 
901
- if (useBatchCache) {
902
- if (hasCachedRawValue(scope, item.key)) {
903
- rawValues[index] = readCachedRawValue(scope, item.key);
904
- return;
905
- }
906
- }
1168
+ if (item._readCacheEnabled === true) {
1169
+ if (hasCachedRawValue(scope, item.key)) {
1170
+ rawValues[index] = readCachedRawValue(scope, item.key);
1171
+ return;
1172
+ }
1173
+ }
907
1174
 
908
- keysToFetch.push(item.key);
909
- keyIndexes.push(index);
910
- });
1175
+ keysToFetch.push(item.key);
1176
+ keyIndexes.push(index);
1177
+ });
911
1178
 
912
- if (keysToFetch.length > 0) {
913
- const fetchedValues = getStorageModule()
914
- .getBatch(keysToFetch, scope)
915
- .map((value) => decodeNativeBatchValue(value));
1179
+ if (keysToFetch.length > 0) {
1180
+ const fetchedValues = getStorageModule()
1181
+ .getBatch(keysToFetch, scope)
1182
+ .map((value) => decodeNativeBatchValue(value));
916
1183
 
917
- fetchedValues.forEach((value, index) => {
918
- const key = keysToFetch[index];
919
- const targetIndex = keyIndexes[index];
920
- if (key === undefined || targetIndex === undefined) {
921
- return;
1184
+ fetchedValues.forEach((value, index) => {
1185
+ const key = keysToFetch[index];
1186
+ const targetIndex = keyIndexes[index];
1187
+ if (key === undefined || targetIndex === undefined) {
1188
+ return;
1189
+ }
1190
+ rawValues[targetIndex] = value;
1191
+ cacheRawValue(scope, key, value);
1192
+ });
922
1193
  }
923
- rawValues[targetIndex] = value;
924
- cacheRawValue(scope, key, value);
925
- });
926
- }
927
1194
 
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
- });
1195
+ return items.map((item, index) => {
1196
+ const raw = rawValues[index];
1197
+ if (raw === undefined) {
1198
+ return asInternal(item as StorageItem<unknown>)._defaultValue;
1199
+ }
1200
+ return item.deserialize(raw);
1201
+ });
1202
+ },
1203
+ items.length,
1204
+ );
935
1205
  }
936
1206
 
937
1207
  export function setBatch<T>(
938
1208
  items: readonly StorageBatchSetItem<T>[],
939
1209
  scope: StorageScope,
940
1210
  ): void {
941
- assertBatchScope(
942
- items.map((batchEntry) => batchEntry.item),
1211
+ measureOperation(
1212
+ "batch:set",
943
1213
  scope,
944
- );
1214
+ () => {
1215
+ assertBatchScope(
1216
+ items.map((batchEntry) => batchEntry.item),
1217
+ scope,
1218
+ );
945
1219
 
946
- if (scope === StorageScope.Memory) {
947
- items.forEach(({ item, value }) => item.set(value));
948
- return;
949
- }
1220
+ if (scope === StorageScope.Memory) {
1221
+ items.forEach(({ item, value }) => item.set(value));
1222
+ return;
1223
+ }
950
1224
 
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
- }
1225
+ if (scope === StorageScope.Secure) {
1226
+ const secureEntries = items.map(({ item, value }) => ({
1227
+ item,
1228
+ value,
1229
+ internal: asInternal(item),
1230
+ }));
1231
+ const canUseSecureBatchPath = secureEntries.every(({ internal }) =>
1232
+ canUseSecureRawBatchPath(internal),
1233
+ );
1234
+ if (!canUseSecureBatchPath) {
1235
+ items.forEach(({ item, value }) => item.set(value));
1236
+ return;
1237
+ }
964
1238
 
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);
1239
+ flushSecureWrites();
1240
+ const storageModule = getStorageModule();
1241
+ const groupedByAccessControl = new Map<
1242
+ number,
1243
+ { keys: string[]; values: string[] }
1244
+ >();
1245
+
1246
+ secureEntries.forEach(({ item, value, internal }) => {
1247
+ const accessControl =
1248
+ internal._secureAccessControl ?? secureDefaultAccessControl;
1249
+ const existingGroup = groupedByAccessControl.get(accessControl);
1250
+ const group = existingGroup ?? { keys: [], values: [] };
1251
+ group.keys.push(item.key);
1252
+ group.values.push(item.serialize(value));
1253
+ if (!existingGroup) {
1254
+ groupedByAccessControl.set(accessControl, group);
1255
+ }
1256
+ });
1257
+
1258
+ groupedByAccessControl.forEach((group, accessControl) => {
1259
+ storageModule.setSecureAccessControl(accessControl);
1260
+ storageModule.setBatch(group.keys, group.values, scope);
1261
+ group.keys.forEach((key, index) =>
1262
+ cacheRawValue(scope, key, group.values[index]),
1263
+ );
1264
+ });
1265
+ return;
981
1266
  }
982
- });
983
1267
 
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]),
1268
+ const useRawBatchPath = items.every(({ item }) =>
1269
+ canUseRawBatchPath(asInternal(item)),
989
1270
  );
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
- }
1271
+ if (!useRawBatchPath) {
1272
+ items.forEach(({ item, value }) => item.set(value));
1273
+ return;
1274
+ }
1001
1275
 
1002
- const keys = items.map((entry) => entry.item.key);
1003
- const values = items.map((entry) => entry.item.serialize(entry.value));
1276
+ const keys = items.map((entry) => entry.item.key);
1277
+ const values = items.map((entry) => entry.item.serialize(entry.value));
1004
1278
 
1005
- getStorageModule().setBatch(keys, values, scope);
1006
- keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
1279
+ getStorageModule().setBatch(keys, values, scope);
1280
+ keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
1281
+ },
1282
+ items.length,
1283
+ );
1007
1284
  }
1008
1285
 
1009
1286
  export function removeBatch(
1010
1287
  items: readonly BatchRemoveItem[],
1011
1288
  scope: StorageScope,
1012
1289
  ): void {
1013
- assertBatchScope(items, scope);
1290
+ measureOperation(
1291
+ "batch:remove",
1292
+ scope,
1293
+ () => {
1294
+ assertBatchScope(items, scope);
1014
1295
 
1015
- if (scope === StorageScope.Memory) {
1016
- items.forEach((item) => item.delete());
1017
- return;
1018
- }
1296
+ if (scope === StorageScope.Memory) {
1297
+ items.forEach((item) => item.delete());
1298
+ return;
1299
+ }
1019
1300
 
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));
1301
+ const keys = items.map((item) => item.key);
1302
+ if (scope === StorageScope.Secure) {
1303
+ flushSecureWrites();
1304
+ }
1305
+ getStorageModule().removeBatch(keys, scope);
1306
+ keys.forEach((key) => cacheRawValue(scope, key, undefined));
1307
+ },
1308
+ items.length,
1309
+ );
1026
1310
  }
1027
1311
 
1028
1312
  export function registerMigration(version: number, migration: Migration): void {
@@ -1040,92 +1324,124 @@ export function registerMigration(version: number, migration: Migration): void {
1040
1324
  export function migrateToLatest(
1041
1325
  scope: StorageScope = StorageScope.Disk,
1042
1326
  ): 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);
1327
+ return measureOperation("migration:run", scope, () => {
1328
+ assertValidScope(scope);
1329
+ const currentVersion = readMigrationVersion(scope);
1330
+ const versions = Array.from(registeredMigrations.keys())
1331
+ .filter((version) => version > currentVersion)
1332
+ .sort((a, b) => a - b);
1333
+
1334
+ let appliedVersion = currentVersion;
1335
+ const context: MigrationContext = {
1336
+ scope,
1337
+ getRaw: (key) => getRawValue(key, scope),
1338
+ setRaw: (key, value) => setRawValue(key, value, scope),
1339
+ removeRaw: (key) => removeRawValue(key, scope),
1340
+ };
1048
1341
 
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
- };
1342
+ versions.forEach((version) => {
1343
+ const migration = registeredMigrations.get(version);
1344
+ if (!migration) {
1345
+ return;
1346
+ }
1347
+ migration(context);
1348
+ writeMigrationVersion(scope, version);
1349
+ appliedVersion = version;
1350
+ });
1056
1351
 
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;
1352
+ return appliedVersion;
1065
1353
  });
1066
-
1067
- return appliedVersion;
1068
1354
  }
1069
1355
 
1070
1356
  export function runTransaction<T>(
1071
1357
  scope: StorageScope,
1072
1358
  transaction: (context: TransactionContext) => T,
1073
1359
  ): T {
1074
- assertValidScope(scope);
1075
- if (scope === StorageScope.Secure) {
1076
- flushSecureWrites();
1077
- }
1360
+ return measureOperation("transaction:run", scope, () => {
1361
+ assertValidScope(scope);
1362
+ if (scope === StorageScope.Secure) {
1363
+ flushSecureWrites();
1364
+ }
1078
1365
 
1079
- const rollback = new Map<string, string | undefined>();
1366
+ const rollback = new Map<string, string | undefined>();
1080
1367
 
1081
- const rememberRollback = (key: string) => {
1082
- if (rollback.has(key)) {
1083
- return;
1084
- }
1085
- rollback.set(key, getRawValue(key, scope));
1086
- };
1368
+ const rememberRollback = (key: string) => {
1369
+ if (rollback.has(key)) {
1370
+ return;
1371
+ }
1372
+ rollback.set(key, getRawValue(key, scope));
1373
+ };
1087
1374
 
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
- };
1375
+ const tx: TransactionContext = {
1376
+ scope,
1377
+ getRaw: (key) => getRawValue(key, scope),
1378
+ setRaw: (key, value) => {
1379
+ rememberRollback(key);
1380
+ setRawValue(key, value, scope);
1381
+ },
1382
+ removeRaw: (key) => {
1383
+ rememberRollback(key);
1384
+ removeRawValue(key, scope);
1385
+ },
1386
+ getItem: (item) => {
1387
+ assertBatchScope([item], scope);
1388
+ return item.get();
1389
+ },
1390
+ setItem: (item, value) => {
1391
+ assertBatchScope([item], scope);
1392
+ rememberRollback(item.key);
1393
+ item.set(value);
1394
+ },
1395
+ removeItem: (item) => {
1396
+ assertBatchScope([item], scope);
1397
+ rememberRollback(item.key);
1398
+ item.delete();
1399
+ },
1400
+ };
1114
1401
 
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);
1402
+ try {
1403
+ return transaction(tx);
1404
+ } catch (error) {
1405
+ const rollbackEntries = Array.from(rollback.entries()).reverse();
1406
+ if (scope === StorageScope.Memory) {
1407
+ rollbackEntries.forEach(([key, previousValue]) => {
1408
+ if (previousValue === undefined) {
1409
+ removeRawValue(key, scope);
1410
+ } else {
1411
+ setRawValue(key, previousValue, scope);
1412
+ }
1413
+ });
1414
+ } else {
1415
+ const keysToSet: string[] = [];
1416
+ const valuesToSet: string[] = [];
1417
+ const keysToRemove: string[] = [];
1418
+
1419
+ rollbackEntries.forEach(([key, previousValue]) => {
1420
+ if (previousValue === undefined) {
1421
+ keysToRemove.push(key);
1422
+ } else {
1423
+ keysToSet.push(key);
1424
+ valuesToSet.push(previousValue);
1425
+ }
1426
+ });
1427
+
1428
+ if (scope === StorageScope.Secure) {
1429
+ flushSecureWrites();
1125
1430
  }
1126
- });
1127
- throw error;
1128
- }
1431
+ if (keysToSet.length > 0) {
1432
+ getStorageModule().setBatch(keysToSet, valuesToSet, scope);
1433
+ keysToSet.forEach((key, index) =>
1434
+ cacheRawValue(scope, key, valuesToSet[index]),
1435
+ );
1436
+ }
1437
+ if (keysToRemove.length > 0) {
1438
+ getStorageModule().removeBatch(keysToRemove, scope);
1439
+ keysToRemove.forEach((key) => cacheRawValue(scope, key, undefined));
1440
+ }
1441
+ }
1442
+ throw error;
1443
+ }
1444
+ });
1129
1445
  }
1130
1446
 
1131
1447
  export type SecureAuthStorageConfig<K extends string = string> = Record<
@@ -1133,6 +1449,7 @@ export type SecureAuthStorageConfig<K extends string = string> = Record<
1133
1449
  {
1134
1450
  ttlMs?: number;
1135
1451
  biometric?: boolean;
1452
+ biometricLevel?: BiometricLevel;
1136
1453
  accessControl?: AccessControl;
1137
1454
  }
1138
1455
  >;
@@ -1156,6 +1473,9 @@ export function createSecureAuthStorage<K extends string>(
1156
1473
  ...(itemConfig.biometric !== undefined
1157
1474
  ? { biometric: itemConfig.biometric }
1158
1475
  : {}),
1476
+ ...(itemConfig.biometricLevel !== undefined
1477
+ ? { biometricLevel: itemConfig.biometricLevel }
1478
+ : {}),
1159
1479
  ...(itemConfig.accessControl !== undefined
1160
1480
  ? { accessControl: itemConfig.accessControl }
1161
1481
  : {}),