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