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