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,8 +1,8 @@
1
1
  "use strict";
2
2
 
3
3
  import { NitroModules } from "react-native-nitro-modules";
4
- import { StorageScope, AccessControl } from "./Storage.types";
5
- import { MIGRATION_VERSION_KEY, isStoredEnvelope, assertBatchScope, assertValidScope, decodeNativeBatchValue, serializeWithPrimitiveFastPath, deserializeWithPrimitiveFastPath, prefixKey, isNamespaced } from "./internal";
4
+ import { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
5
+ import { MIGRATION_VERSION_KEY, isStoredEnvelope, assertBatchScope, assertValidScope, decodeNativeBatchValue, serializeWithPrimitiveFastPath, deserializeWithPrimitiveFastPath, toVersionToken, prefixKey, isNamespaced } from "./internal";
6
6
  export { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
7
7
  export { migrateFromMMKV } from "./migration";
8
8
  function asInternal(item) {
@@ -33,6 +33,36 @@ const scopedRawCache = new Map([[StorageScope.Disk, new Map()], [StorageScope.Se
33
33
  const pendingSecureWrites = new Map();
34
34
  let secureFlushScheduled = false;
35
35
  let secureDefaultAccessControl = AccessControl.WhenUnlocked;
36
+ let metricsObserver;
37
+ const metricsCounters = new Map();
38
+ function recordMetric(operation, scope, durationMs, keysCount = 1) {
39
+ const existing = metricsCounters.get(operation);
40
+ if (!existing) {
41
+ metricsCounters.set(operation, {
42
+ count: 1,
43
+ totalDurationMs: durationMs,
44
+ maxDurationMs: durationMs
45
+ });
46
+ } else {
47
+ existing.count += 1;
48
+ existing.totalDurationMs += durationMs;
49
+ existing.maxDurationMs = Math.max(existing.maxDurationMs, durationMs);
50
+ }
51
+ metricsObserver?.({
52
+ operation,
53
+ scope,
54
+ durationMs,
55
+ keysCount
56
+ });
57
+ }
58
+ function measureOperation(operation, scope, fn, keysCount = 1) {
59
+ const start = Date.now();
60
+ try {
61
+ return fn();
62
+ } finally {
63
+ recordMetric(operation, scope, Date.now() - start, keysCount);
64
+ }
65
+ }
36
66
  function getScopedListeners(scope) {
37
67
  return scopedListeners.get(scope);
38
68
  }
@@ -93,34 +123,47 @@ function flushSecureWrites() {
93
123
  }
94
124
  const writes = Array.from(pendingSecureWrites.values());
95
125
  pendingSecureWrites.clear();
96
- const keysToSet = [];
97
- const valuesToSet = [];
126
+ const groupedSetWrites = new Map();
98
127
  const keysToRemove = [];
99
128
  writes.forEach(({
100
129
  key,
101
- value
130
+ value,
131
+ accessControl
102
132
  }) => {
103
133
  if (value === undefined) {
104
134
  keysToRemove.push(key);
105
135
  } else {
106
- keysToSet.push(key);
107
- valuesToSet.push(value);
136
+ const resolvedAccessControl = accessControl ?? secureDefaultAccessControl;
137
+ const existingGroup = groupedSetWrites.get(resolvedAccessControl);
138
+ const group = existingGroup ?? {
139
+ keys: [],
140
+ values: []
141
+ };
142
+ group.keys.push(key);
143
+ group.values.push(value);
144
+ if (!existingGroup) {
145
+ groupedSetWrites.set(resolvedAccessControl, group);
146
+ }
108
147
  }
109
148
  });
110
149
  const storageModule = getStorageModule();
111
- storageModule.setSecureAccessControl(secureDefaultAccessControl);
112
- if (keysToSet.length > 0) {
113
- storageModule.setBatch(keysToSet, valuesToSet, StorageScope.Secure);
114
- }
150
+ groupedSetWrites.forEach((group, accessControl) => {
151
+ storageModule.setSecureAccessControl(accessControl);
152
+ storageModule.setBatch(group.keys, group.values, StorageScope.Secure);
153
+ });
115
154
  if (keysToRemove.length > 0) {
116
155
  storageModule.removeBatch(keysToRemove, StorageScope.Secure);
117
156
  }
118
157
  }
119
- function scheduleSecureWrite(key, value) {
120
- pendingSecureWrites.set(key, {
158
+ function scheduleSecureWrite(key, value, accessControl) {
159
+ const pendingWrite = {
121
160
  key,
122
161
  value
123
- });
162
+ };
163
+ if (accessControl !== undefined) {
164
+ pendingWrite.accessControl = accessControl;
165
+ }
166
+ pendingSecureWrites.set(key, pendingWrite);
124
167
  if (secureFlushScheduled) {
125
168
  return;
126
169
  }
@@ -214,97 +257,180 @@ function writeMigrationVersion(scope, version) {
214
257
  }
215
258
  export const storage = {
216
259
  clear: scope => {
217
- if (scope === StorageScope.Memory) {
218
- memoryStore.clear();
219
- notifyAllListeners(memoryListeners);
220
- return;
221
- }
222
- if (scope === StorageScope.Secure) {
223
- flushSecureWrites();
224
- pendingSecureWrites.clear();
225
- }
226
- clearScopeRawCache(scope);
227
- getStorageModule().clear(scope);
260
+ measureOperation("storage:clear", scope, () => {
261
+ if (scope === StorageScope.Memory) {
262
+ memoryStore.clear();
263
+ notifyAllListeners(memoryListeners);
264
+ return;
265
+ }
266
+ if (scope === StorageScope.Secure) {
267
+ flushSecureWrites();
268
+ pendingSecureWrites.clear();
269
+ }
270
+ clearScopeRawCache(scope);
271
+ getStorageModule().clear(scope);
272
+ });
228
273
  },
229
274
  clearAll: () => {
230
- storage.clear(StorageScope.Memory);
231
- storage.clear(StorageScope.Disk);
232
- storage.clear(StorageScope.Secure);
275
+ measureOperation("storage:clearAll", StorageScope.Memory, () => {
276
+ storage.clear(StorageScope.Memory);
277
+ storage.clear(StorageScope.Disk);
278
+ storage.clear(StorageScope.Secure);
279
+ }, 3);
233
280
  },
234
281
  clearNamespace: (namespace, scope) => {
235
- assertValidScope(scope);
236
- if (scope === StorageScope.Memory) {
237
- for (const key of memoryStore.keys()) {
238
- if (isNamespaced(key, namespace)) {
239
- memoryStore.delete(key);
282
+ measureOperation("storage:clearNamespace", scope, () => {
283
+ assertValidScope(scope);
284
+ if (scope === StorageScope.Memory) {
285
+ for (const key of memoryStore.keys()) {
286
+ if (isNamespaced(key, namespace)) {
287
+ memoryStore.delete(key);
288
+ }
240
289
  }
290
+ notifyAllListeners(memoryListeners);
291
+ return;
241
292
  }
242
- notifyAllListeners(memoryListeners);
243
- return;
244
- }
245
- const keyPrefix = prefixKey(namespace, "");
246
- if (scope === StorageScope.Secure) {
247
- flushSecureWrites();
248
- }
249
- clearScopeRawCache(scope);
250
- getStorageModule().removeByPrefix(keyPrefix, scope);
293
+ const keyPrefix = prefixKey(namespace, "");
294
+ if (scope === StorageScope.Secure) {
295
+ flushSecureWrites();
296
+ }
297
+ clearScopeRawCache(scope);
298
+ getStorageModule().removeByPrefix(keyPrefix, scope);
299
+ });
251
300
  },
252
301
  clearBiometric: () => {
253
- getStorageModule().clearSecureBiometric();
302
+ measureOperation("storage:clearBiometric", StorageScope.Secure, () => {
303
+ getStorageModule().clearSecureBiometric();
304
+ });
254
305
  },
255
306
  has: (key, scope) => {
256
- assertValidScope(scope);
257
- if (scope === StorageScope.Memory) {
258
- return memoryStore.has(key);
259
- }
260
- return getStorageModule().has(key, scope);
307
+ return measureOperation("storage:has", scope, () => {
308
+ assertValidScope(scope);
309
+ if (scope === StorageScope.Memory) {
310
+ return memoryStore.has(key);
311
+ }
312
+ return getStorageModule().has(key, scope);
313
+ });
261
314
  },
262
315
  getAllKeys: scope => {
263
- assertValidScope(scope);
264
- if (scope === StorageScope.Memory) {
265
- return Array.from(memoryStore.keys());
266
- }
267
- return getStorageModule().getAllKeys(scope);
316
+ return measureOperation("storage:getAllKeys", scope, () => {
317
+ assertValidScope(scope);
318
+ if (scope === StorageScope.Memory) {
319
+ return Array.from(memoryStore.keys());
320
+ }
321
+ return getStorageModule().getAllKeys(scope);
322
+ });
323
+ },
324
+ getKeysByPrefix: (prefix, scope) => {
325
+ return measureOperation("storage:getKeysByPrefix", scope, () => {
326
+ assertValidScope(scope);
327
+ if (scope === StorageScope.Memory) {
328
+ return Array.from(memoryStore.keys()).filter(key => key.startsWith(prefix));
329
+ }
330
+ return getStorageModule().getKeysByPrefix(prefix, scope);
331
+ });
332
+ },
333
+ getByPrefix: (prefix, scope) => {
334
+ return measureOperation("storage:getByPrefix", scope, () => {
335
+ const result = {};
336
+ const keys = storage.getKeysByPrefix(prefix, scope);
337
+ if (keys.length === 0) {
338
+ return result;
339
+ }
340
+ if (scope === StorageScope.Memory) {
341
+ keys.forEach(key => {
342
+ const value = memoryStore.get(key);
343
+ if (typeof value === "string") {
344
+ result[key] = value;
345
+ }
346
+ });
347
+ return result;
348
+ }
349
+ const values = getStorageModule().getBatch(keys, scope);
350
+ keys.forEach((key, idx) => {
351
+ const value = decodeNativeBatchValue(values[idx]);
352
+ if (value !== undefined) {
353
+ result[key] = value;
354
+ }
355
+ });
356
+ return result;
357
+ });
268
358
  },
269
359
  getAll: scope => {
270
- assertValidScope(scope);
271
- const result = {};
272
- if (scope === StorageScope.Memory) {
273
- memoryStore.forEach((value, key) => {
274
- if (typeof value === "string") result[key] = value;
360
+ return measureOperation("storage:getAll", scope, () => {
361
+ assertValidScope(scope);
362
+ const result = {};
363
+ if (scope === StorageScope.Memory) {
364
+ memoryStore.forEach((value, key) => {
365
+ if (typeof value === "string") result[key] = value;
366
+ });
367
+ return result;
368
+ }
369
+ const keys = getStorageModule().getAllKeys(scope);
370
+ if (keys.length === 0) return result;
371
+ const values = getStorageModule().getBatch(keys, scope);
372
+ keys.forEach((key, idx) => {
373
+ const val = decodeNativeBatchValue(values[idx]);
374
+ if (val !== undefined) result[key] = val;
275
375
  });
276
376
  return result;
277
- }
278
- const keys = getStorageModule().getAllKeys(scope);
279
- if (keys.length === 0) return result;
280
- const values = getStorageModule().getBatch(keys, scope);
281
- keys.forEach((key, idx) => {
282
- const val = decodeNativeBatchValue(values[idx]);
283
- if (val !== undefined) result[key] = val;
284
377
  });
285
- return result;
286
378
  },
287
379
  size: scope => {
288
- assertValidScope(scope);
289
- if (scope === StorageScope.Memory) {
290
- return memoryStore.size;
291
- }
292
- return getStorageModule().size(scope);
380
+ return measureOperation("storage:size", scope, () => {
381
+ assertValidScope(scope);
382
+ if (scope === StorageScope.Memory) {
383
+ return memoryStore.size;
384
+ }
385
+ return getStorageModule().size(scope);
386
+ });
293
387
  },
294
388
  setAccessControl: level => {
295
- secureDefaultAccessControl = level;
296
- getStorageModule().setSecureAccessControl(level);
389
+ measureOperation("storage:setAccessControl", StorageScope.Secure, () => {
390
+ secureDefaultAccessControl = level;
391
+ getStorageModule().setSecureAccessControl(level);
392
+ });
297
393
  },
298
394
  setSecureWritesAsync: enabled => {
299
- getStorageModule().setSecureWritesAsync(enabled);
395
+ measureOperation("storage:setSecureWritesAsync", StorageScope.Secure, () => {
396
+ getStorageModule().setSecureWritesAsync(enabled);
397
+ });
300
398
  },
301
399
  flushSecureWrites: () => {
302
- flushSecureWrites();
400
+ measureOperation("storage:flushSecureWrites", StorageScope.Secure, () => {
401
+ flushSecureWrites();
402
+ });
303
403
  },
304
404
  setKeychainAccessGroup: group => {
305
- getStorageModule().setKeychainAccessGroup(group);
405
+ measureOperation("storage:setKeychainAccessGroup", StorageScope.Secure, () => {
406
+ getStorageModule().setKeychainAccessGroup(group);
407
+ });
408
+ },
409
+ setMetricsObserver: observer => {
410
+ metricsObserver = observer;
411
+ },
412
+ getMetricsSnapshot: () => {
413
+ const snapshot = {};
414
+ metricsCounters.forEach((value, key) => {
415
+ snapshot[key] = {
416
+ count: value.count,
417
+ totalDurationMs: value.totalDurationMs,
418
+ avgDurationMs: value.count === 0 ? 0 : value.totalDurationMs / value.count,
419
+ maxDurationMs: value.maxDurationMs
420
+ };
421
+ });
422
+ return snapshot;
423
+ },
424
+ resetMetrics: () => {
425
+ metricsCounters.clear();
306
426
  }
307
427
  };
428
+ export function setWebSecureStorageBackend(_backend) {
429
+ // Native platforms do not use web secure backends.
430
+ }
431
+ export function getWebSecureStorageBackend() {
432
+ return undefined;
433
+ }
308
434
  function canUseRawBatchPath(item) {
309
435
  return item._hasExpiration === false && item._hasValidation === false && item._isBiometric !== true && item._secureAccessControl === undefined;
310
436
  }
@@ -322,7 +448,8 @@ export function createStorageItem(config) {
322
448
  const serialize = config.serialize ?? defaultSerialize;
323
449
  const deserialize = config.deserialize ?? defaultDeserialize;
324
450
  const isMemory = config.scope === StorageScope.Memory;
325
- const isBiometric = config.biometric === true && config.scope === StorageScope.Secure;
451
+ const resolvedBiometricLevel = config.scope === StorageScope.Secure ? config.biometricLevel ?? (config.biometric === true ? BiometricLevel.BiometryOnly : BiometricLevel.None) : BiometricLevel.None;
452
+ const isBiometric = resolvedBiometricLevel !== BiometricLevel.None;
326
453
  const secureAccessControl = config.accessControl;
327
454
  const validate = config.validate;
328
455
  const onValidationError = config.onValidationError;
@@ -331,7 +458,7 @@ export function createStorageItem(config) {
331
458
  const expirationTtlMs = expiration?.ttlMs;
332
459
  const memoryExpiration = expiration && isMemory ? new Map() : null;
333
460
  const readCache = !isMemory && config.readCache === true;
334
- const coalesceSecureWrites = config.scope === StorageScope.Secure && config.coalesceSecureWrites === true && !isBiometric && secureAccessControl === undefined;
461
+ const coalesceSecureWrites = config.scope === StorageScope.Secure && config.coalesceSecureWrites === true && !isBiometric;
335
462
  const defaultValue = config.defaultValue;
336
463
  const nonMemoryScope = config.scope === StorageScope.Disk ? StorageScope.Disk : config.scope === StorageScope.Secure ? StorageScope.Secure : null;
337
464
  if (expiration && expiration.ttlMs <= 0) {
@@ -395,12 +522,12 @@ export function createStorageItem(config) {
395
522
  };
396
523
  const writeStoredRaw = rawValue => {
397
524
  if (isBiometric) {
398
- getStorageModule().setSecureBiometric(storageKey, rawValue);
525
+ getStorageModule().setSecureBiometricWithLevel(storageKey, rawValue, resolvedBiometricLevel);
399
526
  return;
400
527
  }
401
528
  cacheRawValue(nonMemoryScope, storageKey, rawValue);
402
529
  if (coalesceSecureWrites) {
403
- scheduleSecureWrite(storageKey, rawValue);
530
+ scheduleSecureWrite(storageKey, rawValue, secureAccessControl ?? secureDefaultAccessControl);
404
531
  return;
405
532
  }
406
533
  if (nonMemoryScope === StorageScope.Secure) {
@@ -416,7 +543,7 @@ export function createStorageItem(config) {
416
543
  }
417
544
  cacheRawValue(nonMemoryScope, storageKey, undefined);
418
545
  if (coalesceSecureWrites) {
419
- scheduleSecureWrite(storageKey, undefined);
546
+ scheduleSecureWrite(storageKey, undefined, secureAccessControl ?? secureDefaultAccessControl);
420
547
  return;
421
548
  }
422
549
  if (nonMemoryScope === StorageScope.Secure) {
@@ -464,7 +591,7 @@ export function createStorageItem(config) {
464
591
  }
465
592
  return resolved;
466
593
  };
467
- const get = () => {
594
+ const getInternal = () => {
468
595
  const raw = readStoredRaw();
469
596
  if (!memoryExpiration && raw === lastRaw && hasLastValue) {
470
597
  if (!expiration || lastExpiresAt === null) {
@@ -529,31 +656,52 @@ export function createStorageItem(config) {
529
656
  hasLastValue = true;
530
657
  return lastValue;
531
658
  };
659
+ const getCurrentVersion = () => {
660
+ const raw = readStoredRaw();
661
+ return toVersionToken(raw);
662
+ };
663
+ const get = () => measureOperation("item:get", config.scope, () => getInternal());
664
+ const getWithVersion = () => measureOperation("item:getWithVersion", config.scope, () => ({
665
+ value: getInternal(),
666
+ version: getCurrentVersion()
667
+ }));
532
668
  const set = valueOrFn => {
533
- const newValue = isUpdater(valueOrFn) ? valueOrFn(get()) : valueOrFn;
534
- invalidateParsedCache();
535
- if (validate && !validate(newValue)) {
536
- throw new Error(`Validation failed for key "${storageKey}" in scope "${StorageScope[config.scope]}".`);
537
- }
538
- writeValueWithoutValidation(newValue);
669
+ measureOperation("item:set", config.scope, () => {
670
+ const newValue = isUpdater(valueOrFn) ? valueOrFn(getInternal()) : valueOrFn;
671
+ invalidateParsedCache();
672
+ if (validate && !validate(newValue)) {
673
+ throw new Error(`Validation failed for key "${storageKey}" in scope "${StorageScope[config.scope]}".`);
674
+ }
675
+ writeValueWithoutValidation(newValue);
676
+ });
539
677
  };
678
+ const setIfVersion = (version, valueOrFn) => measureOperation("item:setIfVersion", config.scope, () => {
679
+ const currentVersion = getCurrentVersion();
680
+ if (currentVersion !== version) {
681
+ return false;
682
+ }
683
+ set(valueOrFn);
684
+ return true;
685
+ });
540
686
  const deleteItem = () => {
541
- invalidateParsedCache();
542
- if (isMemory) {
543
- if (memoryExpiration) {
544
- memoryExpiration.delete(storageKey);
687
+ measureOperation("item:delete", config.scope, () => {
688
+ invalidateParsedCache();
689
+ if (isMemory) {
690
+ if (memoryExpiration) {
691
+ memoryExpiration.delete(storageKey);
692
+ }
693
+ memoryStore.delete(storageKey);
694
+ notifyKeyListeners(memoryListeners, storageKey);
695
+ return;
545
696
  }
546
- memoryStore.delete(storageKey);
547
- notifyKeyListeners(memoryListeners, storageKey);
548
- return;
549
- }
550
- removeStoredRaw();
697
+ removeStoredRaw();
698
+ });
551
699
  };
552
- const hasItem = () => {
700
+ const hasItem = () => measureOperation("item:has", config.scope, () => {
553
701
  if (isMemory) return memoryStore.has(storageKey);
554
702
  if (isBiometric) return getStorageModule().hasSecureBiometric(storageKey);
555
703
  return getStorageModule().has(storageKey, config.scope);
556
- };
704
+ });
557
705
  const subscribe = callback => {
558
706
  ensureSubscription();
559
707
  listeners.add(callback);
@@ -570,7 +718,9 @@ export function createStorageItem(config) {
570
718
  };
571
719
  const storageItem = {
572
720
  get,
721
+ getWithVersion,
573
722
  set,
723
+ setIfVersion,
574
724
  delete: deleteItem,
575
725
  has: hasItem,
576
726
  subscribe,
@@ -584,6 +734,7 @@ export function createStorageItem(config) {
584
734
  _hasExpiration: expiration !== undefined,
585
735
  _readCacheEnabled: readCache,
586
736
  _isBiometric: isBiometric,
737
+ _defaultValue: defaultValue,
587
738
  ...(secureAccessControl !== undefined ? {
588
739
  _secureAccessControl: secureAccessControl
589
740
  } : {}),
@@ -594,136 +745,141 @@ export function createStorageItem(config) {
594
745
  }
595
746
  export { useStorage, useStorageSelector, useSetStorage } from "./storage-hooks";
596
747
  export function getBatch(items, scope) {
597
- assertBatchScope(items, scope);
598
- if (scope === StorageScope.Memory) {
599
- return items.map(item => item.get());
600
- }
601
- const useRawBatchPath = items.every(item => scope === StorageScope.Secure ? canUseSecureRawBatchPath(item) : canUseRawBatchPath(item));
602
- if (!useRawBatchPath) {
603
- return items.map(item => item.get());
604
- }
605
- const useBatchCache = items.every(item => item._readCacheEnabled === true);
606
- const rawValues = new Array(items.length);
607
- const keysToFetch = [];
608
- const keyIndexes = [];
609
- items.forEach((item, index) => {
610
- if (scope === StorageScope.Secure) {
611
- if (hasPendingSecureWrite(item.key)) {
612
- rawValues[index] = readPendingSecureWrite(item.key);
613
- return;
614
- }
748
+ return measureOperation("batch:get", scope, () => {
749
+ assertBatchScope(items, scope);
750
+ if (scope === StorageScope.Memory) {
751
+ return items.map(item => item.get());
615
752
  }
616
- if (useBatchCache) {
617
- if (hasCachedRawValue(scope, item.key)) {
618
- rawValues[index] = readCachedRawValue(scope, item.key);
619
- return;
620
- }
753
+ const useRawBatchPath = items.every(item => scope === StorageScope.Secure ? canUseSecureRawBatchPath(item) : canUseRawBatchPath(item));
754
+ if (!useRawBatchPath) {
755
+ return items.map(item => item.get());
621
756
  }
622
- keysToFetch.push(item.key);
623
- keyIndexes.push(index);
624
- });
625
- if (keysToFetch.length > 0) {
626
- const fetchedValues = getStorageModule().getBatch(keysToFetch, scope).map(value => decodeNativeBatchValue(value));
627
- fetchedValues.forEach((value, index) => {
628
- const key = keysToFetch[index];
629
- const targetIndex = keyIndexes[index];
630
- if (key === undefined || targetIndex === undefined) {
631
- return;
757
+ const rawValues = new Array(items.length);
758
+ const keysToFetch = [];
759
+ const keyIndexes = [];
760
+ items.forEach((item, index) => {
761
+ if (scope === StorageScope.Secure) {
762
+ if (hasPendingSecureWrite(item.key)) {
763
+ rawValues[index] = readPendingSecureWrite(item.key);
764
+ return;
765
+ }
632
766
  }
633
- rawValues[targetIndex] = value;
634
- cacheRawValue(scope, key, value);
767
+ if (item._readCacheEnabled === true) {
768
+ if (hasCachedRawValue(scope, item.key)) {
769
+ rawValues[index] = readCachedRawValue(scope, item.key);
770
+ return;
771
+ }
772
+ }
773
+ keysToFetch.push(item.key);
774
+ keyIndexes.push(index);
635
775
  });
636
- }
637
- return items.map((item, index) => {
638
- const raw = rawValues[index];
639
- if (raw === undefined) {
640
- return item.get();
776
+ if (keysToFetch.length > 0) {
777
+ const fetchedValues = getStorageModule().getBatch(keysToFetch, scope).map(value => decodeNativeBatchValue(value));
778
+ fetchedValues.forEach((value, index) => {
779
+ const key = keysToFetch[index];
780
+ const targetIndex = keyIndexes[index];
781
+ if (key === undefined || targetIndex === undefined) {
782
+ return;
783
+ }
784
+ rawValues[targetIndex] = value;
785
+ cacheRawValue(scope, key, value);
786
+ });
641
787
  }
642
- return item.deserialize(raw);
643
- });
788
+ return items.map((item, index) => {
789
+ const raw = rawValues[index];
790
+ if (raw === undefined) {
791
+ return asInternal(item)._defaultValue;
792
+ }
793
+ return item.deserialize(raw);
794
+ });
795
+ }, items.length);
644
796
  }
645
797
  export function setBatch(items, scope) {
646
- assertBatchScope(items.map(batchEntry => batchEntry.item), scope);
647
- if (scope === StorageScope.Memory) {
648
- items.forEach(({
649
- item,
650
- value
651
- }) => item.set(value));
652
- return;
653
- }
654
- if (scope === StorageScope.Secure) {
655
- const secureEntries = items.map(({
656
- item,
657
- value
658
- }) => ({
659
- item,
660
- value,
661
- internal: asInternal(item)
662
- }));
663
- const canUseSecureBatchPath = secureEntries.every(({
664
- internal
665
- }) => canUseSecureRawBatchPath(internal));
666
- if (!canUseSecureBatchPath) {
798
+ measureOperation("batch:set", scope, () => {
799
+ assertBatchScope(items.map(batchEntry => batchEntry.item), scope);
800
+ if (scope === StorageScope.Memory) {
667
801
  items.forEach(({
668
802
  item,
669
803
  value
670
804
  }) => item.set(value));
671
805
  return;
672
806
  }
673
- flushSecureWrites();
674
- const storageModule = getStorageModule();
675
- const groupedByAccessControl = new Map();
676
- secureEntries.forEach(({
677
- item,
678
- value,
679
- internal
680
- }) => {
681
- const accessControl = internal._secureAccessControl ?? secureDefaultAccessControl;
682
- const existingGroup = groupedByAccessControl.get(accessControl);
683
- const group = existingGroup ?? {
684
- keys: [],
685
- values: []
686
- };
687
- group.keys.push(item.key);
688
- group.values.push(item.serialize(value));
689
- if (!existingGroup) {
690
- groupedByAccessControl.set(accessControl, group);
807
+ if (scope === StorageScope.Secure) {
808
+ const secureEntries = items.map(({
809
+ item,
810
+ value
811
+ }) => ({
812
+ item,
813
+ value,
814
+ internal: asInternal(item)
815
+ }));
816
+ const canUseSecureBatchPath = secureEntries.every(({
817
+ internal
818
+ }) => canUseSecureRawBatchPath(internal));
819
+ if (!canUseSecureBatchPath) {
820
+ items.forEach(({
821
+ item,
822
+ value
823
+ }) => item.set(value));
824
+ return;
691
825
  }
692
- });
693
- groupedByAccessControl.forEach((group, accessControl) => {
694
- storageModule.setSecureAccessControl(accessControl);
695
- storageModule.setBatch(group.keys, group.values, scope);
696
- group.keys.forEach((key, index) => cacheRawValue(scope, key, group.values[index]));
697
- });
698
- return;
699
- }
700
- const useRawBatchPath = items.every(({
701
- item
702
- }) => canUseRawBatchPath(asInternal(item)));
703
- if (!useRawBatchPath) {
704
- items.forEach(({
705
- item,
706
- value
707
- }) => item.set(value));
708
- return;
709
- }
710
- const keys = items.map(entry => entry.item.key);
711
- const values = items.map(entry => entry.item.serialize(entry.value));
712
- getStorageModule().setBatch(keys, values, scope);
713
- keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
826
+ flushSecureWrites();
827
+ const storageModule = getStorageModule();
828
+ const groupedByAccessControl = new Map();
829
+ secureEntries.forEach(({
830
+ item,
831
+ value,
832
+ internal
833
+ }) => {
834
+ const accessControl = internal._secureAccessControl ?? secureDefaultAccessControl;
835
+ const existingGroup = groupedByAccessControl.get(accessControl);
836
+ const group = existingGroup ?? {
837
+ keys: [],
838
+ values: []
839
+ };
840
+ group.keys.push(item.key);
841
+ group.values.push(item.serialize(value));
842
+ if (!existingGroup) {
843
+ groupedByAccessControl.set(accessControl, group);
844
+ }
845
+ });
846
+ groupedByAccessControl.forEach((group, accessControl) => {
847
+ storageModule.setSecureAccessControl(accessControl);
848
+ storageModule.setBatch(group.keys, group.values, scope);
849
+ group.keys.forEach((key, index) => cacheRawValue(scope, key, group.values[index]));
850
+ });
851
+ return;
852
+ }
853
+ const useRawBatchPath = items.every(({
854
+ item
855
+ }) => canUseRawBatchPath(asInternal(item)));
856
+ if (!useRawBatchPath) {
857
+ items.forEach(({
858
+ item,
859
+ value
860
+ }) => item.set(value));
861
+ return;
862
+ }
863
+ const keys = items.map(entry => entry.item.key);
864
+ const values = items.map(entry => entry.item.serialize(entry.value));
865
+ getStorageModule().setBatch(keys, values, scope);
866
+ keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
867
+ }, items.length);
714
868
  }
715
869
  export function removeBatch(items, scope) {
716
- assertBatchScope(items, scope);
717
- if (scope === StorageScope.Memory) {
718
- items.forEach(item => item.delete());
719
- return;
720
- }
721
- const keys = items.map(item => item.key);
722
- if (scope === StorageScope.Secure) {
723
- flushSecureWrites();
724
- }
725
- getStorageModule().removeBatch(keys, scope);
726
- keys.forEach(key => cacheRawValue(scope, key, undefined));
870
+ measureOperation("batch:remove", scope, () => {
871
+ assertBatchScope(items, scope);
872
+ if (scope === StorageScope.Memory) {
873
+ items.forEach(item => item.delete());
874
+ return;
875
+ }
876
+ const keys = items.map(item => item.key);
877
+ if (scope === StorageScope.Secure) {
878
+ flushSecureWrites();
879
+ }
880
+ getStorageModule().removeBatch(keys, scope);
881
+ keys.forEach(key => cacheRawValue(scope, key, undefined));
882
+ }, items.length);
727
883
  }
728
884
  export function registerMigration(version, migration) {
729
885
  if (!Number.isInteger(version) || version <= 0) {
@@ -735,77 +891,107 @@ export function registerMigration(version, migration) {
735
891
  registeredMigrations.set(version, migration);
736
892
  }
737
893
  export function migrateToLatest(scope = StorageScope.Disk) {
738
- assertValidScope(scope);
739
- const currentVersion = readMigrationVersion(scope);
740
- const versions = Array.from(registeredMigrations.keys()).filter(version => version > currentVersion).sort((a, b) => a - b);
741
- let appliedVersion = currentVersion;
742
- const context = {
743
- scope,
744
- getRaw: key => getRawValue(key, scope),
745
- setRaw: (key, value) => setRawValue(key, value, scope),
746
- removeRaw: key => removeRawValue(key, scope)
747
- };
748
- versions.forEach(version => {
749
- const migration = registeredMigrations.get(version);
750
- if (!migration) {
751
- return;
752
- }
753
- migration(context);
754
- writeMigrationVersion(scope, version);
755
- appliedVersion = version;
894
+ return measureOperation("migration:run", scope, () => {
895
+ assertValidScope(scope);
896
+ const currentVersion = readMigrationVersion(scope);
897
+ const versions = Array.from(registeredMigrations.keys()).filter(version => version > currentVersion).sort((a, b) => a - b);
898
+ let appliedVersion = currentVersion;
899
+ const context = {
900
+ scope,
901
+ getRaw: key => getRawValue(key, scope),
902
+ setRaw: (key, value) => setRawValue(key, value, scope),
903
+ removeRaw: key => removeRawValue(key, scope)
904
+ };
905
+ versions.forEach(version => {
906
+ const migration = registeredMigrations.get(version);
907
+ if (!migration) {
908
+ return;
909
+ }
910
+ migration(context);
911
+ writeMigrationVersion(scope, version);
912
+ appliedVersion = version;
913
+ });
914
+ return appliedVersion;
756
915
  });
757
- return appliedVersion;
758
916
  }
759
917
  export function runTransaction(scope, transaction) {
760
- assertValidScope(scope);
761
- if (scope === StorageScope.Secure) {
762
- flushSecureWrites();
763
- }
764
- const rollback = new Map();
765
- const rememberRollback = key => {
766
- if (rollback.has(key)) {
767
- return;
768
- }
769
- rollback.set(key, getRawValue(key, scope));
770
- };
771
- const tx = {
772
- scope,
773
- getRaw: key => getRawValue(key, scope),
774
- setRaw: (key, value) => {
775
- rememberRollback(key);
776
- setRawValue(key, value, scope);
777
- },
778
- removeRaw: key => {
779
- rememberRollback(key);
780
- removeRawValue(key, scope);
781
- },
782
- getItem: item => {
783
- assertBatchScope([item], scope);
784
- return item.get();
785
- },
786
- setItem: (item, value) => {
787
- assertBatchScope([item], scope);
788
- rememberRollback(item.key);
789
- item.set(value);
790
- },
791
- removeItem: item => {
792
- assertBatchScope([item], scope);
793
- rememberRollback(item.key);
794
- item.delete();
918
+ return measureOperation("transaction:run", scope, () => {
919
+ assertValidScope(scope);
920
+ if (scope === StorageScope.Secure) {
921
+ flushSecureWrites();
795
922
  }
796
- };
797
- try {
798
- return transaction(tx);
799
- } catch (error) {
800
- Array.from(rollback.entries()).reverse().forEach(([key, previousValue]) => {
801
- if (previousValue === undefined) {
923
+ const rollback = new Map();
924
+ const rememberRollback = key => {
925
+ if (rollback.has(key)) {
926
+ return;
927
+ }
928
+ rollback.set(key, getRawValue(key, scope));
929
+ };
930
+ const tx = {
931
+ scope,
932
+ getRaw: key => getRawValue(key, scope),
933
+ setRaw: (key, value) => {
934
+ rememberRollback(key);
935
+ setRawValue(key, value, scope);
936
+ },
937
+ removeRaw: key => {
938
+ rememberRollback(key);
802
939
  removeRawValue(key, scope);
940
+ },
941
+ getItem: item => {
942
+ assertBatchScope([item], scope);
943
+ return item.get();
944
+ },
945
+ setItem: (item, value) => {
946
+ assertBatchScope([item], scope);
947
+ rememberRollback(item.key);
948
+ item.set(value);
949
+ },
950
+ removeItem: item => {
951
+ assertBatchScope([item], scope);
952
+ rememberRollback(item.key);
953
+ item.delete();
954
+ }
955
+ };
956
+ try {
957
+ return transaction(tx);
958
+ } catch (error) {
959
+ const rollbackEntries = Array.from(rollback.entries()).reverse();
960
+ if (scope === StorageScope.Memory) {
961
+ rollbackEntries.forEach(([key, previousValue]) => {
962
+ if (previousValue === undefined) {
963
+ removeRawValue(key, scope);
964
+ } else {
965
+ setRawValue(key, previousValue, scope);
966
+ }
967
+ });
803
968
  } else {
804
- setRawValue(key, previousValue, scope);
969
+ const keysToSet = [];
970
+ const valuesToSet = [];
971
+ const keysToRemove = [];
972
+ rollbackEntries.forEach(([key, previousValue]) => {
973
+ if (previousValue === undefined) {
974
+ keysToRemove.push(key);
975
+ } else {
976
+ keysToSet.push(key);
977
+ valuesToSet.push(previousValue);
978
+ }
979
+ });
980
+ if (scope === StorageScope.Secure) {
981
+ flushSecureWrites();
982
+ }
983
+ if (keysToSet.length > 0) {
984
+ getStorageModule().setBatch(keysToSet, valuesToSet, scope);
985
+ keysToSet.forEach((key, index) => cacheRawValue(scope, key, valuesToSet[index]));
986
+ }
987
+ if (keysToRemove.length > 0) {
988
+ getStorageModule().removeBatch(keysToRemove, scope);
989
+ keysToRemove.forEach(key => cacheRawValue(scope, key, undefined));
990
+ }
805
991
  }
806
- });
807
- throw error;
808
- }
992
+ throw error;
993
+ }
994
+ });
809
995
  }
810
996
  export function createSecureAuthStorage(config, options) {
811
997
  const ns = options?.namespace ?? "auth";
@@ -823,6 +1009,9 @@ export function createSecureAuthStorage(config, options) {
823
1009
  ...(itemConfig.biometric !== undefined ? {
824
1010
  biometric: itemConfig.biometric
825
1011
  } : {}),
1012
+ ...(itemConfig.biometricLevel !== undefined ? {
1013
+ biometricLevel: itemConfig.biometricLevel
1014
+ } : {}),
826
1015
  ...(itemConfig.accessControl !== undefined ? {
827
1016
  accessControl: itemConfig.accessControl
828
1017
  } : {}),