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
@@ -1,14 +1,19 @@
1
1
  "use strict";
2
2
 
3
- import { useRef, useSyncExternalStore } from "react";
4
3
  import { NitroModules } from "react-native-nitro-modules";
5
- import { StorageScope, AccessControl } from "./Storage.types";
6
- 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";
7
6
  export { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
8
7
  export { migrateFromMMKV } from "./migration";
9
8
  function asInternal(item) {
10
9
  return item;
11
10
  }
11
+ function isUpdater(valueOrFn) {
12
+ return typeof valueOrFn === "function";
13
+ }
14
+ function typedKeys(record) {
15
+ return Object.keys(record);
16
+ }
12
17
  const registeredMigrations = new Map();
13
18
  const runMicrotask = typeof queueMicrotask === "function" ? queueMicrotask : task => {
14
19
  Promise.resolve().then(task);
@@ -28,6 +33,36 @@ const scopedRawCache = new Map([[StorageScope.Disk, new Map()], [StorageScope.Se
28
33
  const pendingSecureWrites = new Map();
29
34
  let secureFlushScheduled = false;
30
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
+ }
31
66
  function getScopedListeners(scope) {
32
67
  return scopedListeners.get(scope);
33
68
  }
@@ -88,34 +123,47 @@ function flushSecureWrites() {
88
123
  }
89
124
  const writes = Array.from(pendingSecureWrites.values());
90
125
  pendingSecureWrites.clear();
91
- const keysToSet = [];
92
- const valuesToSet = [];
126
+ const groupedSetWrites = new Map();
93
127
  const keysToRemove = [];
94
128
  writes.forEach(({
95
129
  key,
96
- value
130
+ value,
131
+ accessControl
97
132
  }) => {
98
133
  if (value === undefined) {
99
134
  keysToRemove.push(key);
100
135
  } else {
101
- keysToSet.push(key);
102
- 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
+ }
103
147
  }
104
148
  });
105
149
  const storageModule = getStorageModule();
106
- storageModule.setSecureAccessControl(secureDefaultAccessControl);
107
- if (keysToSet.length > 0) {
108
- storageModule.setBatch(keysToSet, valuesToSet, StorageScope.Secure);
109
- }
150
+ groupedSetWrites.forEach((group, accessControl) => {
151
+ storageModule.setSecureAccessControl(accessControl);
152
+ storageModule.setBatch(group.keys, group.values, StorageScope.Secure);
153
+ });
110
154
  if (keysToRemove.length > 0) {
111
155
  storageModule.removeBatch(keysToRemove, StorageScope.Secure);
112
156
  }
113
157
  }
114
- function scheduleSecureWrite(key, value) {
115
- pendingSecureWrites.set(key, {
158
+ function scheduleSecureWrite(key, value, accessControl) {
159
+ const pendingWrite = {
116
160
  key,
117
161
  value
118
- });
162
+ };
163
+ if (accessControl !== undefined) {
164
+ pendingWrite.accessControl = accessControl;
165
+ }
166
+ pendingSecureWrites.set(key, pendingWrite);
119
167
  if (secureFlushScheduled) {
120
168
  return;
121
169
  }
@@ -209,103 +257,186 @@ function writeMigrationVersion(scope, version) {
209
257
  }
210
258
  export const storage = {
211
259
  clear: scope => {
212
- if (scope === StorageScope.Memory) {
213
- memoryStore.clear();
214
- notifyAllListeners(memoryListeners);
215
- return;
216
- }
217
- if (scope === StorageScope.Secure) {
218
- flushSecureWrites();
219
- pendingSecureWrites.clear();
220
- }
221
- clearScopeRawCache(scope);
222
- getStorageModule().clear(scope);
223
- if (scope === StorageScope.Secure) {
224
- getStorageModule().clearSecureBiometric();
225
- }
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
+ });
226
273
  },
227
274
  clearAll: () => {
228
- storage.clear(StorageScope.Memory);
229
- storage.clear(StorageScope.Disk);
230
- 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);
231
280
  },
232
281
  clearNamespace: (namespace, scope) => {
233
- assertValidScope(scope);
234
- if (scope === StorageScope.Memory) {
235
- for (const key of memoryStore.keys()) {
236
- if (isNamespaced(key, namespace)) {
237
- 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
+ }
238
289
  }
290
+ notifyAllListeners(memoryListeners);
291
+ return;
239
292
  }
240
- notifyAllListeners(memoryListeners);
241
- return;
242
- }
243
- if (scope === StorageScope.Secure) {
244
- flushSecureWrites();
245
- }
246
- const keys = getStorageModule().getAllKeys(scope);
247
- const namespacedKeys = keys.filter(k => isNamespaced(k, namespace));
248
- if (namespacedKeys.length > 0) {
249
- getStorageModule().removeBatch(namespacedKeys, scope);
250
- namespacedKeys.forEach(k => cacheRawValue(scope, k, undefined));
293
+ const keyPrefix = prefixKey(namespace, "");
251
294
  if (scope === StorageScope.Secure) {
252
- namespacedKeys.forEach(k => clearPendingSecureWrite(k));
295
+ flushSecureWrites();
253
296
  }
254
- }
297
+ clearScopeRawCache(scope);
298
+ getStorageModule().removeByPrefix(keyPrefix, scope);
299
+ });
255
300
  },
256
301
  clearBiometric: () => {
257
- getStorageModule().clearSecureBiometric();
302
+ measureOperation("storage:clearBiometric", StorageScope.Secure, () => {
303
+ getStorageModule().clearSecureBiometric();
304
+ });
258
305
  },
259
306
  has: (key, scope) => {
260
- assertValidScope(scope);
261
- if (scope === StorageScope.Memory) {
262
- return memoryStore.has(key);
263
- }
264
- 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
+ });
265
314
  },
266
315
  getAllKeys: scope => {
267
- assertValidScope(scope);
268
- if (scope === StorageScope.Memory) {
269
- return Array.from(memoryStore.keys());
270
- }
271
- 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
+ });
272
358
  },
273
359
  getAll: scope => {
274
- assertValidScope(scope);
275
- const result = {};
276
- if (scope === StorageScope.Memory) {
277
- memoryStore.forEach((value, key) => {
278
- 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;
279
375
  });
280
376
  return result;
281
- }
282
- const keys = getStorageModule().getAllKeys(scope);
283
- if (keys.length === 0) return result;
284
- const values = getStorageModule().getBatch(keys, scope);
285
- keys.forEach((key, idx) => {
286
- const val = decodeNativeBatchValue(values[idx]);
287
- if (val !== undefined) result[key] = val;
288
377
  });
289
- return result;
290
378
  },
291
379
  size: scope => {
292
- assertValidScope(scope);
293
- if (scope === StorageScope.Memory) {
294
- return memoryStore.size;
295
- }
296
- 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
+ });
297
387
  },
298
388
  setAccessControl: level => {
299
- secureDefaultAccessControl = level;
300
- getStorageModule().setSecureAccessControl(level);
389
+ measureOperation("storage:setAccessControl", StorageScope.Secure, () => {
390
+ secureDefaultAccessControl = level;
391
+ getStorageModule().setSecureAccessControl(level);
392
+ });
393
+ },
394
+ setSecureWritesAsync: enabled => {
395
+ measureOperation("storage:setSecureWritesAsync", StorageScope.Secure, () => {
396
+ getStorageModule().setSecureWritesAsync(enabled);
397
+ });
398
+ },
399
+ flushSecureWrites: () => {
400
+ measureOperation("storage:flushSecureWrites", StorageScope.Secure, () => {
401
+ flushSecureWrites();
402
+ });
301
403
  },
302
404
  setKeychainAccessGroup: group => {
303
- 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();
304
426
  }
305
427
  };
428
+ export function setWebSecureStorageBackend(_backend) {
429
+ // Native platforms do not use web secure backends.
430
+ }
431
+ export function getWebSecureStorageBackend() {
432
+ return undefined;
433
+ }
306
434
  function canUseRawBatchPath(item) {
307
435
  return item._hasExpiration === false && item._hasValidation === false && item._isBiometric !== true && item._secureAccessControl === undefined;
308
436
  }
437
+ function canUseSecureRawBatchPath(item) {
438
+ return item._hasExpiration === false && item._hasValidation === false && item._isBiometric !== true;
439
+ }
309
440
  function defaultSerialize(value) {
310
441
  return serializeWithPrimitiveFastPath(value);
311
442
  }
@@ -317,7 +448,8 @@ export function createStorageItem(config) {
317
448
  const serialize = config.serialize ?? defaultSerialize;
318
449
  const deserialize = config.deserialize ?? defaultDeserialize;
319
450
  const isMemory = config.scope === StorageScope.Memory;
320
- 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;
321
453
  const secureAccessControl = config.accessControl;
322
454
  const validate = config.validate;
323
455
  const onValidationError = config.onValidationError;
@@ -326,7 +458,8 @@ export function createStorageItem(config) {
326
458
  const expirationTtlMs = expiration?.ttlMs;
327
459
  const memoryExpiration = expiration && isMemory ? new Map() : null;
328
460
  const readCache = !isMemory && config.readCache === true;
329
- const coalesceSecureWrites = config.scope === StorageScope.Secure && config.coalesceSecureWrites === true && !isBiometric && secureAccessControl === undefined;
461
+ const coalesceSecureWrites = config.scope === StorageScope.Secure && config.coalesceSecureWrites === true && !isBiometric;
462
+ const defaultValue = config.defaultValue;
330
463
  const nonMemoryScope = config.scope === StorageScope.Disk ? StorageScope.Disk : config.scope === StorageScope.Secure ? StorageScope.Secure : null;
331
464
  if (expiration && expiration.ttlMs <= 0) {
332
465
  throw new Error("expiration.ttlMs must be greater than 0.");
@@ -336,10 +469,12 @@ export function createStorageItem(config) {
336
469
  let lastRaw = undefined;
337
470
  let lastValue;
338
471
  let hasLastValue = false;
472
+ let lastExpiresAt = undefined;
339
473
  const invalidateParsedCache = () => {
340
474
  lastRaw = undefined;
341
475
  lastValue = undefined;
342
476
  hasLastValue = false;
477
+ lastExpiresAt = undefined;
343
478
  };
344
479
  const ensureSubscription = () => {
345
480
  if (unsubscribe) {
@@ -387,12 +522,12 @@ export function createStorageItem(config) {
387
522
  };
388
523
  const writeStoredRaw = rawValue => {
389
524
  if (isBiometric) {
390
- getStorageModule().setSecureBiometric(storageKey, rawValue);
525
+ getStorageModule().setSecureBiometricWithLevel(storageKey, rawValue, resolvedBiometricLevel);
391
526
  return;
392
527
  }
393
528
  cacheRawValue(nonMemoryScope, storageKey, rawValue);
394
529
  if (coalesceSecureWrites) {
395
- scheduleSecureWrite(storageKey, rawValue);
530
+ scheduleSecureWrite(storageKey, rawValue, secureAccessControl ?? secureDefaultAccessControl);
396
531
  return;
397
532
  }
398
533
  if (nonMemoryScope === StorageScope.Secure) {
@@ -408,7 +543,7 @@ export function createStorageItem(config) {
408
543
  }
409
544
  cacheRawValue(nonMemoryScope, storageKey, undefined);
410
545
  if (coalesceSecureWrites) {
411
- scheduleSecureWrite(storageKey, undefined);
546
+ scheduleSecureWrite(storageKey, undefined, secureAccessControl ?? secureDefaultAccessControl);
412
547
  return;
413
548
  }
414
549
  if (nonMemoryScope === StorageScope.Secure) {
@@ -441,7 +576,7 @@ export function createStorageItem(config) {
441
576
  if (onValidationError) {
442
577
  return onValidationError(invalidValue);
443
578
  }
444
- return config.defaultValue;
579
+ return defaultValue;
445
580
  };
446
581
  const ensureValidatedValue = (candidate, hadStoredValue) => {
447
582
  if (!validate || validate(candidate)) {
@@ -449,40 +584,62 @@ export function createStorageItem(config) {
449
584
  }
450
585
  const resolved = resolveInvalidValue(candidate);
451
586
  if (validate && !validate(resolved)) {
452
- return config.defaultValue;
587
+ return defaultValue;
453
588
  }
454
589
  if (hadStoredValue) {
455
590
  writeValueWithoutValidation(resolved);
456
591
  }
457
592
  return resolved;
458
593
  };
459
- const get = () => {
594
+ const getInternal = () => {
460
595
  const raw = readStoredRaw();
461
- const canUseCachedValue = !expiration && !memoryExpiration;
462
- if (canUseCachedValue && raw === lastRaw && hasLastValue) {
463
- return lastValue;
596
+ if (!memoryExpiration && raw === lastRaw && hasLastValue) {
597
+ if (!expiration || lastExpiresAt === null) {
598
+ return lastValue;
599
+ }
600
+ if (typeof lastExpiresAt === "number") {
601
+ if (lastExpiresAt > Date.now()) {
602
+ return lastValue;
603
+ }
604
+ removeStoredRaw();
605
+ invalidateParsedCache();
606
+ onExpired?.(storageKey);
607
+ lastValue = ensureValidatedValue(defaultValue, false);
608
+ hasLastValue = true;
609
+ return lastValue;
610
+ }
464
611
  }
465
612
  lastRaw = raw;
466
613
  if (raw === undefined) {
467
- lastValue = ensureValidatedValue(config.defaultValue, false);
614
+ lastExpiresAt = undefined;
615
+ lastValue = ensureValidatedValue(defaultValue, false);
468
616
  hasLastValue = true;
469
617
  return lastValue;
470
618
  }
471
619
  if (isMemory) {
620
+ lastExpiresAt = undefined;
472
621
  lastValue = ensureValidatedValue(raw, true);
473
622
  hasLastValue = true;
474
623
  return lastValue;
475
624
  }
625
+ if (typeof raw !== "string") {
626
+ lastExpiresAt = undefined;
627
+ lastValue = ensureValidatedValue(defaultValue, false);
628
+ hasLastValue = true;
629
+ return lastValue;
630
+ }
476
631
  let deserializableRaw = raw;
477
632
  if (expiration) {
633
+ let envelopeExpiresAt = null;
478
634
  try {
479
635
  const parsed = JSON.parse(raw);
480
636
  if (isStoredEnvelope(parsed)) {
637
+ envelopeExpiresAt = parsed.expiresAt;
481
638
  if (parsed.expiresAt <= Date.now()) {
482
639
  removeStoredRaw();
483
640
  invalidateParsedCache();
484
641
  onExpired?.(storageKey);
485
- lastValue = ensureValidatedValue(config.defaultValue, false);
642
+ lastValue = ensureValidatedValue(defaultValue, false);
486
643
  hasLastValue = true;
487
644
  return lastValue;
488
645
  }
@@ -491,37 +648,60 @@ export function createStorageItem(config) {
491
648
  } catch {
492
649
  // Keep backward compatibility with legacy raw values.
493
650
  }
651
+ lastExpiresAt = envelopeExpiresAt;
652
+ } else {
653
+ lastExpiresAt = undefined;
494
654
  }
495
655
  lastValue = ensureValidatedValue(deserialize(deserializableRaw), true);
496
656
  hasLastValue = true;
497
657
  return lastValue;
498
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
+ }));
499
668
  const set = valueOrFn => {
500
- const currentValue = get();
501
- const newValue = typeof valueOrFn === "function" ? valueOrFn(currentValue) : valueOrFn;
502
- invalidateParsedCache();
503
- if (validate && !validate(newValue)) {
504
- throw new Error(`Validation failed for key "${storageKey}" in scope "${StorageScope[config.scope]}".`);
505
- }
506
- 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
+ });
507
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
+ });
508
686
  const deleteItem = () => {
509
- invalidateParsedCache();
510
- if (isMemory) {
511
- if (memoryExpiration) {
512
- 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;
513
696
  }
514
- memoryStore.delete(storageKey);
515
- notifyKeyListeners(memoryListeners, storageKey);
516
- return;
517
- }
518
- removeStoredRaw();
697
+ removeStoredRaw();
698
+ });
519
699
  };
520
- const hasItem = () => {
700
+ const hasItem = () => measureOperation("item:has", config.scope, () => {
521
701
  if (isMemory) return memoryStore.has(storageKey);
522
702
  if (isBiometric) return getStorageModule().hasSecureBiometric(storageKey);
523
703
  return getStorageModule().has(storageKey, config.scope);
524
- };
704
+ });
525
705
  const subscribe = callback => {
526
706
  ensureSubscription();
527
707
  listeners.add(callback);
@@ -538,7 +718,9 @@ export function createStorageItem(config) {
538
718
  };
539
719
  const storageItem = {
540
720
  get,
721
+ getWithVersion,
541
722
  set,
723
+ setIfVersion,
542
724
  delete: deleteItem,
543
725
  has: hasItem,
544
726
  subscribe,
@@ -552,124 +734,152 @@ export function createStorageItem(config) {
552
734
  _hasExpiration: expiration !== undefined,
553
735
  _readCacheEnabled: readCache,
554
736
  _isBiometric: isBiometric,
555
- _secureAccessControl: secureAccessControl,
737
+ _defaultValue: defaultValue,
738
+ ...(secureAccessControl !== undefined ? {
739
+ _secureAccessControl: secureAccessControl
740
+ } : {}),
556
741
  scope: config.scope,
557
742
  key: storageKey
558
743
  };
559
744
  return storageItem;
560
745
  }
561
- export function useStorage(item) {
562
- const value = useSyncExternalStore(item.subscribe, item.get, item.get);
563
- return [value, item.set];
564
- }
565
- export function useStorageSelector(item, selector, isEqual = Object.is) {
566
- const selectedRef = useRef({
567
- hasValue: false
568
- });
569
- const getSelectedSnapshot = () => {
570
- const nextSelected = selector(item.get());
571
- const current = selectedRef.current;
572
- if (current.hasValue && isEqual(current.value, nextSelected)) {
573
- return current.value;
574
- }
575
- selectedRef.current = {
576
- hasValue: true,
577
- value: nextSelected
578
- };
579
- return nextSelected;
580
- };
581
- const selectedValue = useSyncExternalStore(item.subscribe, getSelectedSnapshot, getSelectedSnapshot);
582
- return [selectedValue, item.set];
583
- }
584
- export function useSetStorage(item) {
585
- return item.set;
586
- }
746
+ export { useStorage, useStorageSelector, useSetStorage } from "./storage-hooks";
587
747
  export function getBatch(items, scope) {
588
- assertBatchScope(items, scope);
589
- if (scope === StorageScope.Memory) {
590
- return items.map(item => item.get());
591
- }
592
- const useRawBatchPath = items.every(item => canUseRawBatchPath(item));
593
- if (!useRawBatchPath) {
594
- return items.map(item => item.get());
595
- }
596
- const useBatchCache = items.every(item => item._readCacheEnabled === true);
597
- const rawValues = new Array(items.length);
598
- const keysToFetch = [];
599
- const keyIndexes = [];
600
- items.forEach((item, index) => {
601
- if (scope === StorageScope.Secure) {
602
- if (hasPendingSecureWrite(item.key)) {
603
- rawValues[index] = readPendingSecureWrite(item.key);
604
- return;
605
- }
748
+ return measureOperation("batch:get", scope, () => {
749
+ assertBatchScope(items, scope);
750
+ if (scope === StorageScope.Memory) {
751
+ return items.map(item => item.get());
606
752
  }
607
- if (useBatchCache) {
608
- if (hasCachedRawValue(scope, item.key)) {
609
- rawValues[index] = readCachedRawValue(scope, item.key);
610
- return;
611
- }
753
+ const useRawBatchPath = items.every(item => scope === StorageScope.Secure ? canUseSecureRawBatchPath(item) : canUseRawBatchPath(item));
754
+ if (!useRawBatchPath) {
755
+ return items.map(item => item.get());
612
756
  }
613
- keysToFetch.push(item.key);
614
- keyIndexes.push(index);
615
- });
616
- if (keysToFetch.length > 0) {
617
- const fetchedValues = getStorageModule().getBatch(keysToFetch, scope).map(value => decodeNativeBatchValue(value));
618
- fetchedValues.forEach((value, index) => {
619
- const key = keysToFetch[index];
620
- const targetIndex = keyIndexes[index];
621
- rawValues[targetIndex] = value;
622
- cacheRawValue(scope, key, value);
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
+ }
766
+ }
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);
623
775
  });
624
- }
625
- return items.map((item, index) => {
626
- const raw = rawValues[index];
627
- if (raw === undefined) {
628
- 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
+ });
629
787
  }
630
- return item.deserialize(raw);
631
- });
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);
632
796
  }
633
797
  export function setBatch(items, scope) {
634
- assertBatchScope(items.map(batchEntry => batchEntry.item), scope);
635
- if (scope === StorageScope.Memory) {
636
- items.forEach(({
637
- item,
638
- value
639
- }) => item.set(value));
640
- return;
641
- }
642
- const useRawBatchPath = items.every(({
643
- item
644
- }) => canUseRawBatchPath(asInternal(item)));
645
- if (!useRawBatchPath) {
646
- items.forEach(({
647
- item,
648
- value
649
- }) => item.set(value));
650
- return;
651
- }
652
- const keys = items.map(entry => entry.item.key);
653
- const values = items.map(entry => entry.item.serialize(entry.value));
654
- if (scope === StorageScope.Secure) {
655
- flushSecureWrites();
656
- getStorageModule().setSecureAccessControl(secureDefaultAccessControl);
657
- }
658
- getStorageModule().setBatch(keys, values, scope);
659
- keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
798
+ measureOperation("batch:set", scope, () => {
799
+ assertBatchScope(items.map(batchEntry => batchEntry.item), scope);
800
+ if (scope === StorageScope.Memory) {
801
+ items.forEach(({
802
+ item,
803
+ value
804
+ }) => item.set(value));
805
+ return;
806
+ }
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;
825
+ }
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);
660
868
  }
661
869
  export function removeBatch(items, scope) {
662
- assertBatchScope(items, scope);
663
- if (scope === StorageScope.Memory) {
664
- items.forEach(item => item.delete());
665
- return;
666
- }
667
- const keys = items.map(item => item.key);
668
- if (scope === StorageScope.Secure) {
669
- flushSecureWrites();
670
- }
671
- getStorageModule().removeBatch(keys, scope);
672
- 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);
673
883
  }
674
884
  export function registerMigration(version, migration) {
675
885
  if (!Number.isInteger(version) || version <= 0) {
@@ -681,93 +891,133 @@ export function registerMigration(version, migration) {
681
891
  registeredMigrations.set(version, migration);
682
892
  }
683
893
  export function migrateToLatest(scope = StorageScope.Disk) {
684
- assertValidScope(scope);
685
- const currentVersion = readMigrationVersion(scope);
686
- const versions = Array.from(registeredMigrations.keys()).filter(version => version > currentVersion).sort((a, b) => a - b);
687
- let appliedVersion = currentVersion;
688
- const context = {
689
- scope,
690
- getRaw: key => getRawValue(key, scope),
691
- setRaw: (key, value) => setRawValue(key, value, scope),
692
- removeRaw: key => removeRawValue(key, scope)
693
- };
694
- versions.forEach(version => {
695
- const migration = registeredMigrations.get(version);
696
- if (!migration) {
697
- return;
698
- }
699
- migration(context);
700
- writeMigrationVersion(scope, version);
701
- 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;
702
915
  });
703
- return appliedVersion;
704
916
  }
705
917
  export function runTransaction(scope, transaction) {
706
- assertValidScope(scope);
707
- if (scope === StorageScope.Secure) {
708
- flushSecureWrites();
709
- }
710
- const rollback = new Map();
711
- const rememberRollback = key => {
712
- if (rollback.has(key)) {
713
- return;
714
- }
715
- rollback.set(key, getRawValue(key, scope));
716
- };
717
- const tx = {
718
- scope,
719
- getRaw: key => getRawValue(key, scope),
720
- setRaw: (key, value) => {
721
- rememberRollback(key);
722
- setRawValue(key, value, scope);
723
- },
724
- removeRaw: key => {
725
- rememberRollback(key);
726
- removeRawValue(key, scope);
727
- },
728
- getItem: item => {
729
- assertBatchScope([item], scope);
730
- return item.get();
731
- },
732
- setItem: (item, value) => {
733
- assertBatchScope([item], scope);
734
- rememberRollback(item.key);
735
- item.set(value);
736
- },
737
- removeItem: item => {
738
- assertBatchScope([item], scope);
739
- rememberRollback(item.key);
740
- item.delete();
918
+ return measureOperation("transaction:run", scope, () => {
919
+ assertValidScope(scope);
920
+ if (scope === StorageScope.Secure) {
921
+ flushSecureWrites();
741
922
  }
742
- };
743
- try {
744
- return transaction(tx);
745
- } catch (error) {
746
- Array.from(rollback.entries()).reverse().forEach(([key, previousValue]) => {
747
- 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);
748
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
+ });
749
968
  } else {
750
- 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
+ }
751
991
  }
752
- });
753
- throw error;
754
- }
992
+ throw error;
993
+ }
994
+ });
755
995
  }
756
996
  export function createSecureAuthStorage(config, options) {
757
997
  const ns = options?.namespace ?? "auth";
758
998
  const result = {};
759
- for (const key of Object.keys(config)) {
999
+ for (const key of typedKeys(config)) {
760
1000
  const itemConfig = config[key];
1001
+ const expirationConfig = itemConfig.ttlMs !== undefined ? {
1002
+ ttlMs: itemConfig.ttlMs
1003
+ } : undefined;
761
1004
  result[key] = createStorageItem({
762
1005
  key,
763
1006
  scope: StorageScope.Secure,
764
1007
  defaultValue: "",
765
1008
  namespace: ns,
766
- biometric: itemConfig.biometric,
767
- accessControl: itemConfig.accessControl,
768
- expiration: itemConfig.ttlMs ? {
769
- ttlMs: itemConfig.ttlMs
770
- } : undefined
1009
+ ...(itemConfig.biometric !== undefined ? {
1010
+ biometric: itemConfig.biometric
1011
+ } : {}),
1012
+ ...(itemConfig.biometricLevel !== undefined ? {
1013
+ biometricLevel: itemConfig.biometricLevel
1014
+ } : {}),
1015
+ ...(itemConfig.accessControl !== undefined ? {
1016
+ accessControl: itemConfig.accessControl
1017
+ } : {}),
1018
+ ...(expirationConfig !== undefined ? {
1019
+ expiration: expirationConfig
1020
+ } : {})
771
1021
  });
772
1022
  }
773
1023
  return result;