react-native-nitro-storage 0.3.2 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +192 -30
  2. package/android/src/main/cpp/AndroidStorageAdapterCpp.cpp +22 -2
  3. package/android/src/main/cpp/AndroidStorageAdapterCpp.hpp +3 -0
  4. package/android/src/main/cpp/cpp-adapter.cpp +3 -1
  5. package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +54 -5
  6. package/cpp/bindings/HybridStorage.cpp +167 -22
  7. package/cpp/bindings/HybridStorage.hpp +12 -1
  8. package/cpp/core/NativeStorageAdapter.hpp +3 -0
  9. package/ios/IOSStorageAdapterCpp.hpp +16 -0
  10. package/ios/IOSStorageAdapterCpp.mm +135 -11
  11. package/lib/commonjs/index.js +522 -275
  12. package/lib/commonjs/index.js.map +1 -1
  13. package/lib/commonjs/index.web.js +614 -270
  14. package/lib/commonjs/index.web.js.map +1 -1
  15. package/lib/commonjs/indexeddb-backend.js +130 -0
  16. package/lib/commonjs/indexeddb-backend.js.map +1 -0
  17. package/lib/commonjs/internal.js +25 -0
  18. package/lib/commonjs/internal.js.map +1 -1
  19. package/lib/module/index.js +516 -277
  20. package/lib/module/index.js.map +1 -1
  21. package/lib/module/index.web.js +608 -272
  22. package/lib/module/index.web.js.map +1 -1
  23. package/lib/module/indexeddb-backend.js +126 -0
  24. package/lib/module/indexeddb-backend.js.map +1 -0
  25. package/lib/module/internal.js +24 -0
  26. package/lib/module/internal.js.map +1 -1
  27. package/lib/typescript/Storage.nitro.d.ts +2 -0
  28. package/lib/typescript/Storage.nitro.d.ts.map +1 -1
  29. package/lib/typescript/index.d.ts +40 -1
  30. package/lib/typescript/index.d.ts.map +1 -1
  31. package/lib/typescript/index.web.d.ts +42 -1
  32. package/lib/typescript/index.web.d.ts.map +1 -1
  33. package/lib/typescript/indexeddb-backend.d.ts +29 -0
  34. package/lib/typescript/indexeddb-backend.d.ts.map +1 -0
  35. package/lib/typescript/internal.d.ts +1 -0
  36. package/lib/typescript/internal.d.ts.map +1 -1
  37. package/nitrogen/generated/android/NitroStorageOnLoad.cpp +22 -17
  38. package/nitrogen/generated/android/NitroStorageOnLoad.hpp +13 -4
  39. package/nitrogen/generated/shared/c++/HybridStorageSpec.cpp +2 -0
  40. package/nitrogen/generated/shared/c++/HybridStorageSpec.hpp +2 -0
  41. package/package.json +7 -3
  42. package/src/Storage.nitro.ts +2 -0
  43. package/src/index.ts +671 -296
  44. package/src/index.web.ts +776 -288
  45. package/src/indexeddb-backend.ts +143 -0
  46. package/src/internal.ts +28 -0
@@ -21,9 +21,16 @@ Object.defineProperty(exports, "StorageScope", {
21
21
  return _Storage.StorageScope;
22
22
  }
23
23
  });
24
+ Object.defineProperty(exports, "createIndexedDBBackend", {
25
+ enumerable: true,
26
+ get: function () {
27
+ return _indexeddbBackend.createIndexedDBBackend;
28
+ }
29
+ });
24
30
  exports.createSecureAuthStorage = createSecureAuthStorage;
25
31
  exports.createStorageItem = createStorageItem;
26
32
  exports.getBatch = getBatch;
33
+ exports.getWebSecureStorageBackend = getWebSecureStorageBackend;
27
34
  Object.defineProperty(exports, "migrateFromMMKV", {
28
35
  enumerable: true,
29
36
  get: function () {
@@ -35,6 +42,7 @@ exports.registerMigration = registerMigration;
35
42
  exports.removeBatch = removeBatch;
36
43
  exports.runTransaction = runTransaction;
37
44
  exports.setBatch = setBatch;
45
+ exports.setWebSecureStorageBackend = setWebSecureStorageBackend;
38
46
  exports.storage = void 0;
39
47
  Object.defineProperty(exports, "useSetStorage", {
40
48
  enumerable: true,
@@ -59,6 +67,7 @@ var _Storage = require("./Storage.types");
59
67
  var _internal = require("./internal");
60
68
  var _migration = require("./migration");
61
69
  var _storageHooks = require("./storage-hooks");
70
+ var _indexeddbBackend = require("./indexeddb-backend");
62
71
  function asInternal(item) {
63
72
  return item;
64
73
  }
@@ -87,6 +96,36 @@ const scopedRawCache = new Map([[_Storage.StorageScope.Disk, new Map()], [_Stora
87
96
  const pendingSecureWrites = new Map();
88
97
  let secureFlushScheduled = false;
89
98
  let secureDefaultAccessControl = _Storage.AccessControl.WhenUnlocked;
99
+ let metricsObserver;
100
+ const metricsCounters = new Map();
101
+ function recordMetric(operation, scope, durationMs, keysCount = 1) {
102
+ const existing = metricsCounters.get(operation);
103
+ if (!existing) {
104
+ metricsCounters.set(operation, {
105
+ count: 1,
106
+ totalDurationMs: durationMs,
107
+ maxDurationMs: durationMs
108
+ });
109
+ } else {
110
+ existing.count += 1;
111
+ existing.totalDurationMs += durationMs;
112
+ existing.maxDurationMs = Math.max(existing.maxDurationMs, durationMs);
113
+ }
114
+ metricsObserver?.({
115
+ operation,
116
+ scope,
117
+ durationMs,
118
+ keysCount
119
+ });
120
+ }
121
+ function measureOperation(operation, scope, fn, keysCount = 1) {
122
+ const start = Date.now();
123
+ try {
124
+ return fn();
125
+ } finally {
126
+ recordMetric(operation, scope, Date.now() - start, keysCount);
127
+ }
128
+ }
90
129
  function getScopedListeners(scope) {
91
130
  return scopedListeners.get(scope);
92
131
  }
@@ -147,34 +186,47 @@ function flushSecureWrites() {
147
186
  }
148
187
  const writes = Array.from(pendingSecureWrites.values());
149
188
  pendingSecureWrites.clear();
150
- const keysToSet = [];
151
- const valuesToSet = [];
189
+ const groupedSetWrites = new Map();
152
190
  const keysToRemove = [];
153
191
  writes.forEach(({
154
192
  key,
155
- value
193
+ value,
194
+ accessControl
156
195
  }) => {
157
196
  if (value === undefined) {
158
197
  keysToRemove.push(key);
159
198
  } else {
160
- keysToSet.push(key);
161
- valuesToSet.push(value);
199
+ const resolvedAccessControl = accessControl ?? secureDefaultAccessControl;
200
+ const existingGroup = groupedSetWrites.get(resolvedAccessControl);
201
+ const group = existingGroup ?? {
202
+ keys: [],
203
+ values: []
204
+ };
205
+ group.keys.push(key);
206
+ group.values.push(value);
207
+ if (!existingGroup) {
208
+ groupedSetWrites.set(resolvedAccessControl, group);
209
+ }
162
210
  }
163
211
  });
164
212
  const storageModule = getStorageModule();
165
- storageModule.setSecureAccessControl(secureDefaultAccessControl);
166
- if (keysToSet.length > 0) {
167
- storageModule.setBatch(keysToSet, valuesToSet, _Storage.StorageScope.Secure);
168
- }
213
+ groupedSetWrites.forEach((group, accessControl) => {
214
+ storageModule.setSecureAccessControl(accessControl);
215
+ storageModule.setBatch(group.keys, group.values, _Storage.StorageScope.Secure);
216
+ });
169
217
  if (keysToRemove.length > 0) {
170
218
  storageModule.removeBatch(keysToRemove, _Storage.StorageScope.Secure);
171
219
  }
172
220
  }
173
- function scheduleSecureWrite(key, value) {
174
- pendingSecureWrites.set(key, {
221
+ function scheduleSecureWrite(key, value, accessControl) {
222
+ const pendingWrite = {
175
223
  key,
176
224
  value
177
- });
225
+ };
226
+ if (accessControl !== undefined) {
227
+ pendingWrite.accessControl = accessControl;
228
+ }
229
+ pendingSecureWrites.set(key, pendingWrite);
178
230
  if (secureFlushScheduled) {
179
231
  return;
180
232
  }
@@ -268,97 +320,201 @@ function writeMigrationVersion(scope, version) {
268
320
  }
269
321
  const storage = exports.storage = {
270
322
  clear: scope => {
271
- if (scope === _Storage.StorageScope.Memory) {
272
- memoryStore.clear();
273
- notifyAllListeners(memoryListeners);
274
- return;
275
- }
276
- if (scope === _Storage.StorageScope.Secure) {
277
- flushSecureWrites();
278
- pendingSecureWrites.clear();
279
- }
280
- clearScopeRawCache(scope);
281
- getStorageModule().clear(scope);
323
+ measureOperation("storage:clear", scope, () => {
324
+ if (scope === _Storage.StorageScope.Memory) {
325
+ memoryStore.clear();
326
+ notifyAllListeners(memoryListeners);
327
+ return;
328
+ }
329
+ if (scope === _Storage.StorageScope.Secure) {
330
+ flushSecureWrites();
331
+ pendingSecureWrites.clear();
332
+ }
333
+ clearScopeRawCache(scope);
334
+ getStorageModule().clear(scope);
335
+ });
282
336
  },
283
337
  clearAll: () => {
284
- storage.clear(_Storage.StorageScope.Memory);
285
- storage.clear(_Storage.StorageScope.Disk);
286
- storage.clear(_Storage.StorageScope.Secure);
338
+ measureOperation("storage:clearAll", _Storage.StorageScope.Memory, () => {
339
+ storage.clear(_Storage.StorageScope.Memory);
340
+ storage.clear(_Storage.StorageScope.Disk);
341
+ storage.clear(_Storage.StorageScope.Secure);
342
+ }, 3);
287
343
  },
288
344
  clearNamespace: (namespace, scope) => {
289
- (0, _internal.assertValidScope)(scope);
290
- if (scope === _Storage.StorageScope.Memory) {
291
- for (const key of memoryStore.keys()) {
292
- if ((0, _internal.isNamespaced)(key, namespace)) {
293
- memoryStore.delete(key);
345
+ measureOperation("storage:clearNamespace", scope, () => {
346
+ (0, _internal.assertValidScope)(scope);
347
+ if (scope === _Storage.StorageScope.Memory) {
348
+ for (const key of memoryStore.keys()) {
349
+ if ((0, _internal.isNamespaced)(key, namespace)) {
350
+ memoryStore.delete(key);
351
+ }
294
352
  }
353
+ notifyAllListeners(memoryListeners);
354
+ return;
295
355
  }
296
- notifyAllListeners(memoryListeners);
297
- return;
298
- }
299
- const keyPrefix = (0, _internal.prefixKey)(namespace, "");
300
- if (scope === _Storage.StorageScope.Secure) {
301
- flushSecureWrites();
302
- }
303
- clearScopeRawCache(scope);
304
- getStorageModule().removeByPrefix(keyPrefix, scope);
356
+ const keyPrefix = (0, _internal.prefixKey)(namespace, "");
357
+ if (scope === _Storage.StorageScope.Secure) {
358
+ flushSecureWrites();
359
+ }
360
+ clearScopeRawCache(scope);
361
+ getStorageModule().removeByPrefix(keyPrefix, scope);
362
+ });
305
363
  },
306
364
  clearBiometric: () => {
307
- getStorageModule().clearSecureBiometric();
365
+ measureOperation("storage:clearBiometric", _Storage.StorageScope.Secure, () => {
366
+ getStorageModule().clearSecureBiometric();
367
+ });
308
368
  },
309
369
  has: (key, scope) => {
310
- (0, _internal.assertValidScope)(scope);
311
- if (scope === _Storage.StorageScope.Memory) {
312
- return memoryStore.has(key);
313
- }
314
- return getStorageModule().has(key, scope);
370
+ return measureOperation("storage:has", scope, () => {
371
+ (0, _internal.assertValidScope)(scope);
372
+ if (scope === _Storage.StorageScope.Memory) {
373
+ return memoryStore.has(key);
374
+ }
375
+ return getStorageModule().has(key, scope);
376
+ });
315
377
  },
316
378
  getAllKeys: scope => {
317
- (0, _internal.assertValidScope)(scope);
318
- if (scope === _Storage.StorageScope.Memory) {
319
- return Array.from(memoryStore.keys());
320
- }
321
- return getStorageModule().getAllKeys(scope);
379
+ return measureOperation("storage:getAllKeys", scope, () => {
380
+ (0, _internal.assertValidScope)(scope);
381
+ if (scope === _Storage.StorageScope.Memory) {
382
+ return Array.from(memoryStore.keys());
383
+ }
384
+ return getStorageModule().getAllKeys(scope);
385
+ });
386
+ },
387
+ getKeysByPrefix: (prefix, scope) => {
388
+ return measureOperation("storage:getKeysByPrefix", scope, () => {
389
+ (0, _internal.assertValidScope)(scope);
390
+ if (scope === _Storage.StorageScope.Memory) {
391
+ return Array.from(memoryStore.keys()).filter(key => key.startsWith(prefix));
392
+ }
393
+ return getStorageModule().getKeysByPrefix(prefix, scope);
394
+ });
395
+ },
396
+ getByPrefix: (prefix, scope) => {
397
+ return measureOperation("storage:getByPrefix", scope, () => {
398
+ const result = {};
399
+ const keys = storage.getKeysByPrefix(prefix, scope);
400
+ if (keys.length === 0) {
401
+ return result;
402
+ }
403
+ if (scope === _Storage.StorageScope.Memory) {
404
+ keys.forEach(key => {
405
+ const value = memoryStore.get(key);
406
+ if (typeof value === "string") {
407
+ result[key] = value;
408
+ }
409
+ });
410
+ return result;
411
+ }
412
+ const values = getStorageModule().getBatch(keys, scope);
413
+ keys.forEach((key, idx) => {
414
+ const value = (0, _internal.decodeNativeBatchValue)(values[idx]);
415
+ if (value !== undefined) {
416
+ result[key] = value;
417
+ }
418
+ });
419
+ return result;
420
+ });
322
421
  },
323
422
  getAll: scope => {
324
- (0, _internal.assertValidScope)(scope);
325
- const result = {};
326
- if (scope === _Storage.StorageScope.Memory) {
327
- memoryStore.forEach((value, key) => {
328
- if (typeof value === "string") result[key] = value;
423
+ return measureOperation("storage:getAll", scope, () => {
424
+ (0, _internal.assertValidScope)(scope);
425
+ const result = {};
426
+ if (scope === _Storage.StorageScope.Memory) {
427
+ memoryStore.forEach((value, key) => {
428
+ if (typeof value === "string") result[key] = value;
429
+ });
430
+ return result;
431
+ }
432
+ const keys = getStorageModule().getAllKeys(scope);
433
+ if (keys.length === 0) return result;
434
+ const values = getStorageModule().getBatch(keys, scope);
435
+ keys.forEach((key, idx) => {
436
+ const val = (0, _internal.decodeNativeBatchValue)(values[idx]);
437
+ if (val !== undefined) result[key] = val;
329
438
  });
330
439
  return result;
331
- }
332
- const keys = getStorageModule().getAllKeys(scope);
333
- if (keys.length === 0) return result;
334
- const values = getStorageModule().getBatch(keys, scope);
335
- keys.forEach((key, idx) => {
336
- const val = (0, _internal.decodeNativeBatchValue)(values[idx]);
337
- if (val !== undefined) result[key] = val;
338
440
  });
339
- return result;
340
441
  },
341
442
  size: scope => {
342
- (0, _internal.assertValidScope)(scope);
343
- if (scope === _Storage.StorageScope.Memory) {
344
- return memoryStore.size;
345
- }
346
- return getStorageModule().size(scope);
443
+ return measureOperation("storage:size", scope, () => {
444
+ (0, _internal.assertValidScope)(scope);
445
+ if (scope === _Storage.StorageScope.Memory) {
446
+ return memoryStore.size;
447
+ }
448
+ return getStorageModule().size(scope);
449
+ });
347
450
  },
348
451
  setAccessControl: level => {
349
- secureDefaultAccessControl = level;
350
- getStorageModule().setSecureAccessControl(level);
452
+ measureOperation("storage:setAccessControl", _Storage.StorageScope.Secure, () => {
453
+ secureDefaultAccessControl = level;
454
+ getStorageModule().setSecureAccessControl(level);
455
+ });
351
456
  },
352
457
  setSecureWritesAsync: enabled => {
353
- getStorageModule().setSecureWritesAsync(enabled);
458
+ measureOperation("storage:setSecureWritesAsync", _Storage.StorageScope.Secure, () => {
459
+ getStorageModule().setSecureWritesAsync(enabled);
460
+ });
354
461
  },
355
462
  flushSecureWrites: () => {
356
- flushSecureWrites();
463
+ measureOperation("storage:flushSecureWrites", _Storage.StorageScope.Secure, () => {
464
+ flushSecureWrites();
465
+ });
357
466
  },
358
467
  setKeychainAccessGroup: group => {
359
- getStorageModule().setKeychainAccessGroup(group);
468
+ measureOperation("storage:setKeychainAccessGroup", _Storage.StorageScope.Secure, () => {
469
+ getStorageModule().setKeychainAccessGroup(group);
470
+ });
471
+ },
472
+ setMetricsObserver: observer => {
473
+ metricsObserver = observer;
474
+ },
475
+ getMetricsSnapshot: () => {
476
+ const snapshot = {};
477
+ metricsCounters.forEach((value, key) => {
478
+ snapshot[key] = {
479
+ count: value.count,
480
+ totalDurationMs: value.totalDurationMs,
481
+ avgDurationMs: value.count === 0 ? 0 : value.totalDurationMs / value.count,
482
+ maxDurationMs: value.maxDurationMs
483
+ };
484
+ });
485
+ return snapshot;
486
+ },
487
+ resetMetrics: () => {
488
+ metricsCounters.clear();
489
+ },
490
+ import: (data, scope) => {
491
+ measureOperation("storage:import", scope, () => {
492
+ (0, _internal.assertValidScope)(scope);
493
+ const keys = Object.keys(data);
494
+ if (keys.length === 0) return;
495
+ const values = keys.map(k => data[k]);
496
+ if (scope === _Storage.StorageScope.Memory) {
497
+ keys.forEach((key, index) => {
498
+ memoryStore.set(key, values[index]);
499
+ });
500
+ keys.forEach(key => notifyKeyListeners(memoryListeners, key));
501
+ return;
502
+ }
503
+ if (scope === _Storage.StorageScope.Secure) {
504
+ flushSecureWrites();
505
+ getStorageModule().setSecureAccessControl(secureDefaultAccessControl);
506
+ }
507
+ getStorageModule().setBatch(keys, values, scope);
508
+ keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
509
+ }, Object.keys(data).length);
360
510
  }
361
511
  };
512
+ function setWebSecureStorageBackend(_backend) {
513
+ // Native platforms do not use web secure backends.
514
+ }
515
+ function getWebSecureStorageBackend() {
516
+ return undefined;
517
+ }
362
518
  function canUseRawBatchPath(item) {
363
519
  return item._hasExpiration === false && item._hasValidation === false && item._isBiometric !== true && item._secureAccessControl === undefined;
364
520
  }
@@ -376,7 +532,8 @@ function createStorageItem(config) {
376
532
  const serialize = config.serialize ?? defaultSerialize;
377
533
  const deserialize = config.deserialize ?? defaultDeserialize;
378
534
  const isMemory = config.scope === _Storage.StorageScope.Memory;
379
- const isBiometric = config.biometric === true && config.scope === _Storage.StorageScope.Secure;
535
+ const resolvedBiometricLevel = config.scope === _Storage.StorageScope.Secure ? config.biometricLevel ?? (config.biometric === true ? _Storage.BiometricLevel.BiometryOnly : _Storage.BiometricLevel.None) : _Storage.BiometricLevel.None;
536
+ const isBiometric = resolvedBiometricLevel !== _Storage.BiometricLevel.None;
380
537
  const secureAccessControl = config.accessControl;
381
538
  const validate = config.validate;
382
539
  const onValidationError = config.onValidationError;
@@ -385,7 +542,7 @@ function createStorageItem(config) {
385
542
  const expirationTtlMs = expiration?.ttlMs;
386
543
  const memoryExpiration = expiration && isMemory ? new Map() : null;
387
544
  const readCache = !isMemory && config.readCache === true;
388
- const coalesceSecureWrites = config.scope === _Storage.StorageScope.Secure && config.coalesceSecureWrites === true && !isBiometric && secureAccessControl === undefined;
545
+ const coalesceSecureWrites = config.scope === _Storage.StorageScope.Secure && config.coalesceSecureWrites === true && !isBiometric;
389
546
  const defaultValue = config.defaultValue;
390
547
  const nonMemoryScope = config.scope === _Storage.StorageScope.Disk ? _Storage.StorageScope.Disk : config.scope === _Storage.StorageScope.Secure ? _Storage.StorageScope.Secure : null;
391
548
  if (expiration && expiration.ttlMs <= 0) {
@@ -449,12 +606,12 @@ function createStorageItem(config) {
449
606
  };
450
607
  const writeStoredRaw = rawValue => {
451
608
  if (isBiometric) {
452
- getStorageModule().setSecureBiometric(storageKey, rawValue);
609
+ getStorageModule().setSecureBiometricWithLevel(storageKey, rawValue, resolvedBiometricLevel);
453
610
  return;
454
611
  }
455
612
  cacheRawValue(nonMemoryScope, storageKey, rawValue);
456
613
  if (coalesceSecureWrites) {
457
- scheduleSecureWrite(storageKey, rawValue);
614
+ scheduleSecureWrite(storageKey, rawValue, secureAccessControl ?? secureDefaultAccessControl);
458
615
  return;
459
616
  }
460
617
  if (nonMemoryScope === _Storage.StorageScope.Secure) {
@@ -470,7 +627,7 @@ function createStorageItem(config) {
470
627
  }
471
628
  cacheRawValue(nonMemoryScope, storageKey, undefined);
472
629
  if (coalesceSecureWrites) {
473
- scheduleSecureWrite(storageKey, undefined);
630
+ scheduleSecureWrite(storageKey, undefined, secureAccessControl ?? secureDefaultAccessControl);
474
631
  return;
475
632
  }
476
633
  if (nonMemoryScope === _Storage.StorageScope.Secure) {
@@ -518,7 +675,7 @@ function createStorageItem(config) {
518
675
  }
519
676
  return resolved;
520
677
  };
521
- const get = () => {
678
+ const getInternal = () => {
522
679
  const raw = readStoredRaw();
523
680
  if (!memoryExpiration && raw === lastRaw && hasLastValue) {
524
681
  if (!expiration || lastExpiresAt === null) {
@@ -533,6 +690,7 @@ function createStorageItem(config) {
533
690
  onExpired?.(storageKey);
534
691
  lastValue = ensureValidatedValue(defaultValue, false);
535
692
  hasLastValue = true;
693
+ listeners.forEach(cb => cb());
536
694
  return lastValue;
537
695
  }
538
696
  }
@@ -568,6 +726,7 @@ function createStorageItem(config) {
568
726
  onExpired?.(storageKey);
569
727
  lastValue = ensureValidatedValue(defaultValue, false);
570
728
  hasLastValue = true;
729
+ listeners.forEach(cb => cb());
571
730
  return lastValue;
572
731
  }
573
732
  deserializableRaw = parsed.payload;
@@ -583,31 +742,52 @@ function createStorageItem(config) {
583
742
  hasLastValue = true;
584
743
  return lastValue;
585
744
  };
745
+ const getCurrentVersion = () => {
746
+ const raw = readStoredRaw();
747
+ return (0, _internal.toVersionToken)(raw);
748
+ };
749
+ const get = () => measureOperation("item:get", config.scope, () => getInternal());
750
+ const getWithVersion = () => measureOperation("item:getWithVersion", config.scope, () => ({
751
+ value: getInternal(),
752
+ version: getCurrentVersion()
753
+ }));
586
754
  const set = valueOrFn => {
587
- const newValue = isUpdater(valueOrFn) ? valueOrFn(get()) : valueOrFn;
588
- invalidateParsedCache();
589
- if (validate && !validate(newValue)) {
590
- throw new Error(`Validation failed for key "${storageKey}" in scope "${_Storage.StorageScope[config.scope]}".`);
591
- }
592
- writeValueWithoutValidation(newValue);
755
+ measureOperation("item:set", config.scope, () => {
756
+ const newValue = isUpdater(valueOrFn) ? valueOrFn(getInternal()) : valueOrFn;
757
+ invalidateParsedCache();
758
+ if (validate && !validate(newValue)) {
759
+ throw new Error(`Validation failed for key "${storageKey}" in scope "${_Storage.StorageScope[config.scope]}".`);
760
+ }
761
+ writeValueWithoutValidation(newValue);
762
+ });
593
763
  };
764
+ const setIfVersion = (version, valueOrFn) => measureOperation("item:setIfVersion", config.scope, () => {
765
+ const currentVersion = getCurrentVersion();
766
+ if (currentVersion !== version) {
767
+ return false;
768
+ }
769
+ set(valueOrFn);
770
+ return true;
771
+ });
594
772
  const deleteItem = () => {
595
- invalidateParsedCache();
596
- if (isMemory) {
597
- if (memoryExpiration) {
598
- memoryExpiration.delete(storageKey);
773
+ measureOperation("item:delete", config.scope, () => {
774
+ invalidateParsedCache();
775
+ if (isMemory) {
776
+ if (memoryExpiration) {
777
+ memoryExpiration.delete(storageKey);
778
+ }
779
+ memoryStore.delete(storageKey);
780
+ notifyKeyListeners(memoryListeners, storageKey);
781
+ return;
599
782
  }
600
- memoryStore.delete(storageKey);
601
- notifyKeyListeners(memoryListeners, storageKey);
602
- return;
603
- }
604
- removeStoredRaw();
783
+ removeStoredRaw();
784
+ });
605
785
  };
606
- const hasItem = () => {
786
+ const hasItem = () => measureOperation("item:has", config.scope, () => {
607
787
  if (isMemory) return memoryStore.has(storageKey);
608
788
  if (isBiometric) return getStorageModule().hasSecureBiometric(storageKey);
609
789
  return getStorageModule().has(storageKey, config.scope);
610
- };
790
+ });
611
791
  const subscribe = callback => {
612
792
  ensureSubscription();
613
793
  listeners.add(callback);
@@ -624,7 +804,9 @@ function createStorageItem(config) {
624
804
  };
625
805
  const storageItem = {
626
806
  get,
807
+ getWithVersion,
627
808
  set,
809
+ setIfVersion,
628
810
  delete: deleteItem,
629
811
  has: hasItem,
630
812
  subscribe,
@@ -634,10 +816,14 @@ function createStorageItem(config) {
634
816
  invalidateParsedCache();
635
817
  listeners.forEach(listener => listener());
636
818
  },
819
+ _invalidateParsedCacheOnly: () => {
820
+ invalidateParsedCache();
821
+ },
637
822
  _hasValidation: validate !== undefined,
638
823
  _hasExpiration: expiration !== undefined,
639
824
  _readCacheEnabled: readCache,
640
825
  _isBiometric: isBiometric,
826
+ _defaultValue: defaultValue,
641
827
  ...(secureAccessControl !== undefined ? {
642
828
  _secureAccessControl: secureAccessControl
643
829
  } : {}),
@@ -647,136 +833,164 @@ function createStorageItem(config) {
647
833
  return storageItem;
648
834
  }
649
835
  function getBatch(items, scope) {
650
- (0, _internal.assertBatchScope)(items, scope);
651
- if (scope === _Storage.StorageScope.Memory) {
652
- return items.map(item => item.get());
653
- }
654
- const useRawBatchPath = items.every(item => scope === _Storage.StorageScope.Secure ? canUseSecureRawBatchPath(item) : canUseRawBatchPath(item));
655
- if (!useRawBatchPath) {
656
- return items.map(item => item.get());
657
- }
658
- const useBatchCache = items.every(item => item._readCacheEnabled === true);
659
- const rawValues = new Array(items.length);
660
- const keysToFetch = [];
661
- const keyIndexes = [];
662
- items.forEach((item, index) => {
663
- if (scope === _Storage.StorageScope.Secure) {
664
- if (hasPendingSecureWrite(item.key)) {
665
- rawValues[index] = readPendingSecureWrite(item.key);
666
- return;
836
+ return measureOperation("batch:get", scope, () => {
837
+ (0, _internal.assertBatchScope)(items, scope);
838
+ if (scope === _Storage.StorageScope.Memory) {
839
+ return items.map(item => item.get());
840
+ }
841
+ const useRawBatchPath = items.every(item => scope === _Storage.StorageScope.Secure ? canUseSecureRawBatchPath(item) : canUseRawBatchPath(item));
842
+ if (!useRawBatchPath) {
843
+ return items.map(item => item.get());
844
+ }
845
+ const rawValues = new Array(items.length);
846
+ const keysToFetch = [];
847
+ const keyIndexes = [];
848
+ items.forEach((item, index) => {
849
+ if (scope === _Storage.StorageScope.Secure) {
850
+ if (hasPendingSecureWrite(item.key)) {
851
+ rawValues[index] = readPendingSecureWrite(item.key);
852
+ return;
853
+ }
854
+ }
855
+ if (item._readCacheEnabled === true) {
856
+ if (hasCachedRawValue(scope, item.key)) {
857
+ rawValues[index] = readCachedRawValue(scope, item.key);
858
+ return;
859
+ }
667
860
  }
861
+ keysToFetch.push(item.key);
862
+ keyIndexes.push(index);
863
+ });
864
+ if (keysToFetch.length > 0) {
865
+ const fetchedValues = getStorageModule().getBatch(keysToFetch, scope).map(value => (0, _internal.decodeNativeBatchValue)(value));
866
+ fetchedValues.forEach((value, index) => {
867
+ const key = keysToFetch[index];
868
+ const targetIndex = keyIndexes[index];
869
+ if (key === undefined || targetIndex === undefined) {
870
+ return;
871
+ }
872
+ rawValues[targetIndex] = value;
873
+ cacheRawValue(scope, key, value);
874
+ });
668
875
  }
669
- if (useBatchCache) {
670
- if (hasCachedRawValue(scope, item.key)) {
671
- rawValues[index] = readCachedRawValue(scope, item.key);
876
+ return items.map((item, index) => {
877
+ const raw = rawValues[index];
878
+ if (raw === undefined) {
879
+ return asInternal(item)._defaultValue;
880
+ }
881
+ return item.deserialize(raw);
882
+ });
883
+ }, items.length);
884
+ }
885
+ function setBatch(items, scope) {
886
+ measureOperation("batch:set", scope, () => {
887
+ (0, _internal.assertBatchScope)(items.map(batchEntry => batchEntry.item), scope);
888
+ if (scope === _Storage.StorageScope.Memory) {
889
+ // Determine if any item needs per-item handling (validation or TTL)
890
+ const needsIndividualSets = items.some(({
891
+ item
892
+ }) => {
893
+ const internal = asInternal(item);
894
+ return internal._hasValidation || internal._hasExpiration;
895
+ });
896
+ if (needsIndividualSets) {
897
+ // Fall back to individual sets to preserve validation and TTL semantics
898
+ items.forEach(({
899
+ item,
900
+ value
901
+ }) => item.set(value));
672
902
  return;
673
903
  }
904
+
905
+ // Atomic write: update all values in memoryStore, invalidate caches, then batch-notify
906
+ items.forEach(({
907
+ item,
908
+ value
909
+ }) => {
910
+ memoryStore.set(item.key, value);
911
+ asInternal(item)._invalidateParsedCacheOnly();
912
+ });
913
+ items.forEach(({
914
+ item
915
+ }) => notifyKeyListeners(memoryListeners, item.key));
916
+ return;
674
917
  }
675
- keysToFetch.push(item.key);
676
- keyIndexes.push(index);
677
- });
678
- if (keysToFetch.length > 0) {
679
- const fetchedValues = getStorageModule().getBatch(keysToFetch, scope).map(value => (0, _internal.decodeNativeBatchValue)(value));
680
- fetchedValues.forEach((value, index) => {
681
- const key = keysToFetch[index];
682
- const targetIndex = keyIndexes[index];
683
- if (key === undefined || targetIndex === undefined) {
918
+ if (scope === _Storage.StorageScope.Secure) {
919
+ const secureEntries = items.map(({
920
+ item,
921
+ value
922
+ }) => ({
923
+ item,
924
+ value,
925
+ internal: asInternal(item)
926
+ }));
927
+ const canUseSecureBatchPath = secureEntries.every(({
928
+ internal
929
+ }) => canUseSecureRawBatchPath(internal));
930
+ if (!canUseSecureBatchPath) {
931
+ items.forEach(({
932
+ item,
933
+ value
934
+ }) => item.set(value));
684
935
  return;
685
936
  }
686
- rawValues[targetIndex] = value;
687
- cacheRawValue(scope, key, value);
688
- });
689
- }
690
- return items.map((item, index) => {
691
- const raw = rawValues[index];
692
- if (raw === undefined) {
693
- return item.get();
937
+ flushSecureWrites();
938
+ const storageModule = getStorageModule();
939
+ const groupedByAccessControl = new Map();
940
+ secureEntries.forEach(({
941
+ item,
942
+ value,
943
+ internal
944
+ }) => {
945
+ const accessControl = internal._secureAccessControl ?? secureDefaultAccessControl;
946
+ const existingGroup = groupedByAccessControl.get(accessControl);
947
+ const group = existingGroup ?? {
948
+ keys: [],
949
+ values: []
950
+ };
951
+ group.keys.push(item.key);
952
+ group.values.push(item.serialize(value));
953
+ if (!existingGroup) {
954
+ groupedByAccessControl.set(accessControl, group);
955
+ }
956
+ });
957
+ groupedByAccessControl.forEach((group, accessControl) => {
958
+ storageModule.setSecureAccessControl(accessControl);
959
+ storageModule.setBatch(group.keys, group.values, scope);
960
+ group.keys.forEach((key, index) => cacheRawValue(scope, key, group.values[index]));
961
+ });
962
+ return;
694
963
  }
695
- return item.deserialize(raw);
696
- });
697
- }
698
- function setBatch(items, scope) {
699
- (0, _internal.assertBatchScope)(items.map(batchEntry => batchEntry.item), scope);
700
- if (scope === _Storage.StorageScope.Memory) {
701
- items.forEach(({
702
- item,
703
- value
704
- }) => item.set(value));
705
- return;
706
- }
707
- if (scope === _Storage.StorageScope.Secure) {
708
- const secureEntries = items.map(({
709
- item,
710
- value
711
- }) => ({
712
- item,
713
- value,
714
- internal: asInternal(item)
715
- }));
716
- const canUseSecureBatchPath = secureEntries.every(({
717
- internal
718
- }) => canUseSecureRawBatchPath(internal));
719
- if (!canUseSecureBatchPath) {
964
+ const useRawBatchPath = items.every(({
965
+ item
966
+ }) => canUseRawBatchPath(asInternal(item)));
967
+ if (!useRawBatchPath) {
720
968
  items.forEach(({
721
969
  item,
722
970
  value
723
971
  }) => item.set(value));
724
972
  return;
725
973
  }
726
- flushSecureWrites();
727
- const storageModule = getStorageModule();
728
- const groupedByAccessControl = new Map();
729
- secureEntries.forEach(({
730
- item,
731
- value,
732
- internal
733
- }) => {
734
- const accessControl = internal._secureAccessControl ?? secureDefaultAccessControl;
735
- const existingGroup = groupedByAccessControl.get(accessControl);
736
- const group = existingGroup ?? {
737
- keys: [],
738
- values: []
739
- };
740
- group.keys.push(item.key);
741
- group.values.push(item.serialize(value));
742
- if (!existingGroup) {
743
- groupedByAccessControl.set(accessControl, group);
744
- }
745
- });
746
- groupedByAccessControl.forEach((group, accessControl) => {
747
- storageModule.setSecureAccessControl(accessControl);
748
- storageModule.setBatch(group.keys, group.values, scope);
749
- group.keys.forEach((key, index) => cacheRawValue(scope, key, group.values[index]));
750
- });
751
- return;
752
- }
753
- const useRawBatchPath = items.every(({
754
- item
755
- }) => canUseRawBatchPath(asInternal(item)));
756
- if (!useRawBatchPath) {
757
- items.forEach(({
758
- item,
759
- value
760
- }) => item.set(value));
761
- return;
762
- }
763
- const keys = items.map(entry => entry.item.key);
764
- const values = items.map(entry => entry.item.serialize(entry.value));
765
- getStorageModule().setBatch(keys, values, scope);
766
- keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
974
+ const keys = items.map(entry => entry.item.key);
975
+ const values = items.map(entry => entry.item.serialize(entry.value));
976
+ getStorageModule().setBatch(keys, values, scope);
977
+ keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
978
+ }, items.length);
767
979
  }
768
980
  function removeBatch(items, scope) {
769
- (0, _internal.assertBatchScope)(items, scope);
770
- if (scope === _Storage.StorageScope.Memory) {
771
- items.forEach(item => item.delete());
772
- return;
773
- }
774
- const keys = items.map(item => item.key);
775
- if (scope === _Storage.StorageScope.Secure) {
776
- flushSecureWrites();
777
- }
778
- getStorageModule().removeBatch(keys, scope);
779
- keys.forEach(key => cacheRawValue(scope, key, undefined));
981
+ measureOperation("batch:remove", scope, () => {
982
+ (0, _internal.assertBatchScope)(items, scope);
983
+ if (scope === _Storage.StorageScope.Memory) {
984
+ items.forEach(item => item.delete());
985
+ return;
986
+ }
987
+ const keys = items.map(item => item.key);
988
+ if (scope === _Storage.StorageScope.Secure) {
989
+ flushSecureWrites();
990
+ }
991
+ getStorageModule().removeBatch(keys, scope);
992
+ keys.forEach(key => cacheRawValue(scope, key, undefined));
993
+ }, items.length);
780
994
  }
781
995
  function registerMigration(version, migration) {
782
996
  if (!Number.isInteger(version) || version <= 0) {
@@ -788,77 +1002,107 @@ function registerMigration(version, migration) {
788
1002
  registeredMigrations.set(version, migration);
789
1003
  }
790
1004
  function migrateToLatest(scope = _Storage.StorageScope.Disk) {
791
- (0, _internal.assertValidScope)(scope);
792
- const currentVersion = readMigrationVersion(scope);
793
- const versions = Array.from(registeredMigrations.keys()).filter(version => version > currentVersion).sort((a, b) => a - b);
794
- let appliedVersion = currentVersion;
795
- const context = {
796
- scope,
797
- getRaw: key => getRawValue(key, scope),
798
- setRaw: (key, value) => setRawValue(key, value, scope),
799
- removeRaw: key => removeRawValue(key, scope)
800
- };
801
- versions.forEach(version => {
802
- const migration = registeredMigrations.get(version);
803
- if (!migration) {
804
- return;
805
- }
806
- migration(context);
807
- writeMigrationVersion(scope, version);
808
- appliedVersion = version;
1005
+ return measureOperation("migration:run", scope, () => {
1006
+ (0, _internal.assertValidScope)(scope);
1007
+ const currentVersion = readMigrationVersion(scope);
1008
+ const versions = Array.from(registeredMigrations.keys()).filter(version => version > currentVersion).sort((a, b) => a - b);
1009
+ let appliedVersion = currentVersion;
1010
+ const context = {
1011
+ scope,
1012
+ getRaw: key => getRawValue(key, scope),
1013
+ setRaw: (key, value) => setRawValue(key, value, scope),
1014
+ removeRaw: key => removeRawValue(key, scope)
1015
+ };
1016
+ versions.forEach(version => {
1017
+ const migration = registeredMigrations.get(version);
1018
+ if (!migration) {
1019
+ return;
1020
+ }
1021
+ migration(context);
1022
+ writeMigrationVersion(scope, version);
1023
+ appliedVersion = version;
1024
+ });
1025
+ return appliedVersion;
809
1026
  });
810
- return appliedVersion;
811
1027
  }
812
1028
  function runTransaction(scope, transaction) {
813
- (0, _internal.assertValidScope)(scope);
814
- if (scope === _Storage.StorageScope.Secure) {
815
- flushSecureWrites();
816
- }
817
- const rollback = new Map();
818
- const rememberRollback = key => {
819
- if (rollback.has(key)) {
820
- return;
821
- }
822
- rollback.set(key, getRawValue(key, scope));
823
- };
824
- const tx = {
825
- scope,
826
- getRaw: key => getRawValue(key, scope),
827
- setRaw: (key, value) => {
828
- rememberRollback(key);
829
- setRawValue(key, value, scope);
830
- },
831
- removeRaw: key => {
832
- rememberRollback(key);
833
- removeRawValue(key, scope);
834
- },
835
- getItem: item => {
836
- (0, _internal.assertBatchScope)([item], scope);
837
- return item.get();
838
- },
839
- setItem: (item, value) => {
840
- (0, _internal.assertBatchScope)([item], scope);
841
- rememberRollback(item.key);
842
- item.set(value);
843
- },
844
- removeItem: item => {
845
- (0, _internal.assertBatchScope)([item], scope);
846
- rememberRollback(item.key);
847
- item.delete();
1029
+ return measureOperation("transaction:run", scope, () => {
1030
+ (0, _internal.assertValidScope)(scope);
1031
+ if (scope === _Storage.StorageScope.Secure) {
1032
+ flushSecureWrites();
848
1033
  }
849
- };
850
- try {
851
- return transaction(tx);
852
- } catch (error) {
853
- Array.from(rollback.entries()).reverse().forEach(([key, previousValue]) => {
854
- if (previousValue === undefined) {
1034
+ const rollback = new Map();
1035
+ const rememberRollback = key => {
1036
+ if (rollback.has(key)) {
1037
+ return;
1038
+ }
1039
+ rollback.set(key, getRawValue(key, scope));
1040
+ };
1041
+ const tx = {
1042
+ scope,
1043
+ getRaw: key => getRawValue(key, scope),
1044
+ setRaw: (key, value) => {
1045
+ rememberRollback(key);
1046
+ setRawValue(key, value, scope);
1047
+ },
1048
+ removeRaw: key => {
1049
+ rememberRollback(key);
855
1050
  removeRawValue(key, scope);
1051
+ },
1052
+ getItem: item => {
1053
+ (0, _internal.assertBatchScope)([item], scope);
1054
+ return item.get();
1055
+ },
1056
+ setItem: (item, value) => {
1057
+ (0, _internal.assertBatchScope)([item], scope);
1058
+ rememberRollback(item.key);
1059
+ item.set(value);
1060
+ },
1061
+ removeItem: item => {
1062
+ (0, _internal.assertBatchScope)([item], scope);
1063
+ rememberRollback(item.key);
1064
+ item.delete();
1065
+ }
1066
+ };
1067
+ try {
1068
+ return transaction(tx);
1069
+ } catch (error) {
1070
+ const rollbackEntries = Array.from(rollback.entries()).reverse();
1071
+ if (scope === _Storage.StorageScope.Memory) {
1072
+ rollbackEntries.forEach(([key, previousValue]) => {
1073
+ if (previousValue === undefined) {
1074
+ removeRawValue(key, scope);
1075
+ } else {
1076
+ setRawValue(key, previousValue, scope);
1077
+ }
1078
+ });
856
1079
  } else {
857
- setRawValue(key, previousValue, scope);
1080
+ const keysToSet = [];
1081
+ const valuesToSet = [];
1082
+ const keysToRemove = [];
1083
+ rollbackEntries.forEach(([key, previousValue]) => {
1084
+ if (previousValue === undefined) {
1085
+ keysToRemove.push(key);
1086
+ } else {
1087
+ keysToSet.push(key);
1088
+ valuesToSet.push(previousValue);
1089
+ }
1090
+ });
1091
+ if (scope === _Storage.StorageScope.Secure) {
1092
+ flushSecureWrites();
1093
+ }
1094
+ if (keysToSet.length > 0) {
1095
+ getStorageModule().setBatch(keysToSet, valuesToSet, scope);
1096
+ keysToSet.forEach((key, index) => cacheRawValue(scope, key, valuesToSet[index]));
1097
+ }
1098
+ if (keysToRemove.length > 0) {
1099
+ getStorageModule().removeBatch(keysToRemove, scope);
1100
+ keysToRemove.forEach(key => cacheRawValue(scope, key, undefined));
1101
+ }
858
1102
  }
859
- });
860
- throw error;
861
- }
1103
+ throw error;
1104
+ }
1105
+ });
862
1106
  }
863
1107
  function createSecureAuthStorage(config, options) {
864
1108
  const ns = options?.namespace ?? "auth";
@@ -876,6 +1120,9 @@ function createSecureAuthStorage(config, options) {
876
1120
  ...(itemConfig.biometric !== undefined ? {
877
1121
  biometric: itemConfig.biometric
878
1122
  } : {}),
1123
+ ...(itemConfig.biometricLevel !== undefined ? {
1124
+ biometricLevel: itemConfig.biometricLevel
1125
+ } : {}),
879
1126
  ...(itemConfig.accessControl !== undefined ? {
880
1127
  accessControl: itemConfig.accessControl
881
1128
  } : {}),