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,17 +36,39 @@ 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 _Storage = require("./Storage.types");
44
60
  var _internal = require("./internal");
45
61
  var _migration = require("./migration");
62
+ var _storageHooks = require("./storage-hooks");
46
63
  function asInternal(item) {
47
64
  return item;
48
65
  }
66
+ function isUpdater(valueOrFn) {
67
+ return typeof valueOrFn === "function";
68
+ }
69
+ function typedKeys(record) {
70
+ return Object.keys(record);
71
+ }
49
72
  const registeredMigrations = new Map();
50
73
  const runMicrotask = typeof queueMicrotask === "function" ? queueMicrotask : task => {
51
74
  Promise.resolve().then(task);
@@ -54,17 +77,83 @@ const memoryStore = new Map();
54
77
  const memoryListeners = new Map();
55
78
  const webScopeListeners = new Map([[_Storage.StorageScope.Disk, new Map()], [_Storage.StorageScope.Secure, new Map()]]);
56
79
  const scopedRawCache = new Map([[_Storage.StorageScope.Disk, new Map()], [_Storage.StorageScope.Secure, new Map()]]);
80
+ const webScopeKeyIndex = new Map([[_Storage.StorageScope.Disk, new Set()], [_Storage.StorageScope.Secure, new Set()]]);
81
+ const hydratedWebScopeKeyIndex = new Set();
57
82
  const pendingSecureWrites = new Map();
58
83
  let secureFlushScheduled = false;
59
84
  const SECURE_WEB_PREFIX = "__secure_";
60
85
  const BIOMETRIC_WEB_PREFIX = "__bio_";
61
86
  let hasWarnedAboutWebBiometricFallback = false;
87
+ let hasWebStorageEventSubscription = false;
88
+ let metricsObserver;
89
+ const metricsCounters = new Map();
90
+ function recordMetric(operation, scope, durationMs, keysCount = 1) {
91
+ const existing = metricsCounters.get(operation);
92
+ if (!existing) {
93
+ metricsCounters.set(operation, {
94
+ count: 1,
95
+ totalDurationMs: durationMs,
96
+ maxDurationMs: durationMs
97
+ });
98
+ } else {
99
+ existing.count += 1;
100
+ existing.totalDurationMs += durationMs;
101
+ existing.maxDurationMs = Math.max(existing.maxDurationMs, durationMs);
102
+ }
103
+ metricsObserver?.({
104
+ operation,
105
+ scope,
106
+ durationMs,
107
+ keysCount
108
+ });
109
+ }
110
+ function measureOperation(operation, scope, fn, keysCount = 1) {
111
+ const start = Date.now();
112
+ try {
113
+ return fn();
114
+ } finally {
115
+ recordMetric(operation, scope, Date.now() - start, keysCount);
116
+ }
117
+ }
118
+ function createLocalStorageWebSecureBackend() {
119
+ return {
120
+ getItem: key => globalThis.localStorage?.getItem(key) ?? null,
121
+ setItem: (key, value) => globalThis.localStorage?.setItem(key, value),
122
+ removeItem: key => globalThis.localStorage?.removeItem(key),
123
+ clear: () => globalThis.localStorage?.clear(),
124
+ getAllKeys: () => {
125
+ const storage = globalThis.localStorage;
126
+ if (!storage) return [];
127
+ const keys = [];
128
+ for (let index = 0; index < storage.length; index += 1) {
129
+ const key = storage.key(index);
130
+ if (key) {
131
+ keys.push(key);
132
+ }
133
+ }
134
+ return keys;
135
+ }
136
+ };
137
+ }
138
+ let webSecureStorageBackend = createLocalStorageWebSecureBackend();
62
139
  function getBrowserStorage(scope) {
63
140
  if (scope === _Storage.StorageScope.Disk) {
64
141
  return globalThis.localStorage;
65
142
  }
66
143
  if (scope === _Storage.StorageScope.Secure) {
67
- return globalThis.localStorage;
144
+ if (!webSecureStorageBackend) {
145
+ return undefined;
146
+ }
147
+ return {
148
+ setItem: (key, value) => webSecureStorageBackend?.setItem(key, value),
149
+ getItem: key => webSecureStorageBackend?.getItem(key) ?? null,
150
+ removeItem: key => webSecureStorageBackend?.removeItem(key),
151
+ clear: () => webSecureStorageBackend?.clear(),
152
+ key: index => webSecureStorageBackend?.getAllKeys()[index] ?? null,
153
+ get length() {
154
+ return webSecureStorageBackend?.getAllKeys().length ?? 0;
155
+ }
156
+ };
68
157
  }
69
158
  return undefined;
70
159
  }
@@ -80,6 +169,99 @@ function toBiometricStorageKey(key) {
80
169
  function fromBiometricStorageKey(key) {
81
170
  return key.slice(BIOMETRIC_WEB_PREFIX.length);
82
171
  }
172
+ function getWebScopeKeyIndex(scope) {
173
+ return webScopeKeyIndex.get(scope);
174
+ }
175
+ function hydrateWebScopeKeyIndex(scope) {
176
+ if (hydratedWebScopeKeyIndex.has(scope)) {
177
+ return;
178
+ }
179
+ const storage = getBrowserStorage(scope);
180
+ const keyIndex = getWebScopeKeyIndex(scope);
181
+ keyIndex.clear();
182
+ if (storage) {
183
+ for (let index = 0; index < storage.length; index += 1) {
184
+ const key = storage.key(index);
185
+ if (!key) {
186
+ continue;
187
+ }
188
+ if (scope === _Storage.StorageScope.Disk) {
189
+ if (!key.startsWith(SECURE_WEB_PREFIX) && !key.startsWith(BIOMETRIC_WEB_PREFIX)) {
190
+ keyIndex.add(key);
191
+ }
192
+ continue;
193
+ }
194
+ if (key.startsWith(SECURE_WEB_PREFIX)) {
195
+ keyIndex.add(fromSecureStorageKey(key));
196
+ continue;
197
+ }
198
+ if (key.startsWith(BIOMETRIC_WEB_PREFIX)) {
199
+ keyIndex.add(fromBiometricStorageKey(key));
200
+ }
201
+ }
202
+ }
203
+ hydratedWebScopeKeyIndex.add(scope);
204
+ }
205
+ function ensureWebScopeKeyIndex(scope) {
206
+ hydrateWebScopeKeyIndex(scope);
207
+ return getWebScopeKeyIndex(scope);
208
+ }
209
+ function handleWebStorageEvent(event) {
210
+ const key = event.key;
211
+ if (key === null) {
212
+ clearScopeRawCache(_Storage.StorageScope.Disk);
213
+ clearScopeRawCache(_Storage.StorageScope.Secure);
214
+ ensureWebScopeKeyIndex(_Storage.StorageScope.Disk).clear();
215
+ ensureWebScopeKeyIndex(_Storage.StorageScope.Secure).clear();
216
+ notifyAllListeners(getScopedListeners(_Storage.StorageScope.Disk));
217
+ notifyAllListeners(getScopedListeners(_Storage.StorageScope.Secure));
218
+ return;
219
+ }
220
+ if (key.startsWith(SECURE_WEB_PREFIX)) {
221
+ const plainKey = fromSecureStorageKey(key);
222
+ if (event.newValue === null) {
223
+ ensureWebScopeKeyIndex(_Storage.StorageScope.Secure).delete(plainKey);
224
+ cacheRawValue(_Storage.StorageScope.Secure, plainKey, undefined);
225
+ } else {
226
+ ensureWebScopeKeyIndex(_Storage.StorageScope.Secure).add(plainKey);
227
+ cacheRawValue(_Storage.StorageScope.Secure, plainKey, event.newValue);
228
+ }
229
+ notifyKeyListeners(getScopedListeners(_Storage.StorageScope.Secure), plainKey);
230
+ return;
231
+ }
232
+ if (key.startsWith(BIOMETRIC_WEB_PREFIX)) {
233
+ const plainKey = fromBiometricStorageKey(key);
234
+ if (event.newValue === null) {
235
+ if (getBrowserStorage(_Storage.StorageScope.Secure)?.getItem(toSecureStorageKey(plainKey)) === null) {
236
+ ensureWebScopeKeyIndex(_Storage.StorageScope.Secure).delete(plainKey);
237
+ }
238
+ cacheRawValue(_Storage.StorageScope.Secure, plainKey, undefined);
239
+ } else {
240
+ ensureWebScopeKeyIndex(_Storage.StorageScope.Secure).add(plainKey);
241
+ cacheRawValue(_Storage.StorageScope.Secure, plainKey, event.newValue);
242
+ }
243
+ notifyKeyListeners(getScopedListeners(_Storage.StorageScope.Secure), plainKey);
244
+ return;
245
+ }
246
+ if (event.newValue === null) {
247
+ ensureWebScopeKeyIndex(_Storage.StorageScope.Disk).delete(key);
248
+ cacheRawValue(_Storage.StorageScope.Disk, key, undefined);
249
+ } else {
250
+ ensureWebScopeKeyIndex(_Storage.StorageScope.Disk).add(key);
251
+ cacheRawValue(_Storage.StorageScope.Disk, key, event.newValue);
252
+ }
253
+ notifyKeyListeners(getScopedListeners(_Storage.StorageScope.Disk), key);
254
+ }
255
+ function ensureWebStorageEventSubscription() {
256
+ if (hasWebStorageEventSubscription) {
257
+ return;
258
+ }
259
+ if (typeof window === "undefined" || typeof window.addEventListener !== "function") {
260
+ return;
261
+ }
262
+ window.addEventListener("storage", handleWebStorageEvent);
263
+ hasWebStorageEventSubscription = true;
264
+ }
83
265
  function getScopedListeners(scope) {
84
266
  return webScopeListeners.get(scope);
85
267
  }
@@ -140,32 +322,46 @@ function flushSecureWrites() {
140
322
  }
141
323
  const writes = Array.from(pendingSecureWrites.values());
142
324
  pendingSecureWrites.clear();
143
- const keysToSet = [];
144
- const valuesToSet = [];
325
+ const groupedSetWrites = new Map();
145
326
  const keysToRemove = [];
146
327
  writes.forEach(({
147
328
  key,
148
- value
329
+ value,
330
+ accessControl
149
331
  }) => {
150
332
  if (value === undefined) {
151
333
  keysToRemove.push(key);
152
334
  } else {
153
- keysToSet.push(key);
154
- valuesToSet.push(value);
335
+ const resolvedAccessControl = accessControl ?? _Storage.AccessControl.WhenUnlocked;
336
+ const existingGroup = groupedSetWrites.get(resolvedAccessControl);
337
+ const group = existingGroup ?? {
338
+ keys: [],
339
+ values: []
340
+ };
341
+ group.keys.push(key);
342
+ group.values.push(value);
343
+ if (!existingGroup) {
344
+ groupedSetWrites.set(resolvedAccessControl, group);
345
+ }
155
346
  }
156
347
  });
157
- if (keysToSet.length > 0) {
158
- WebStorage.setBatch(keysToSet, valuesToSet, _Storage.StorageScope.Secure);
159
- }
348
+ groupedSetWrites.forEach((group, accessControl) => {
349
+ WebStorage.setSecureAccessControl(accessControl);
350
+ WebStorage.setBatch(group.keys, group.values, _Storage.StorageScope.Secure);
351
+ });
160
352
  if (keysToRemove.length > 0) {
161
353
  WebStorage.removeBatch(keysToRemove, _Storage.StorageScope.Secure);
162
354
  }
163
355
  }
164
- function scheduleSecureWrite(key, value) {
165
- pendingSecureWrites.set(key, {
356
+ function scheduleSecureWrite(key, value, accessControl) {
357
+ const pendingWrite = {
166
358
  key,
167
359
  value
168
- });
360
+ };
361
+ if (accessControl !== undefined) {
362
+ pendingWrite.accessControl = accessControl;
363
+ }
364
+ pendingSecureWrites.set(key, pendingWrite);
169
365
  if (secureFlushScheduled) {
170
366
  return;
171
367
  }
@@ -184,6 +380,7 @@ const WebStorage = {
184
380
  const storageKey = scope === _Storage.StorageScope.Secure ? toSecureStorageKey(key) : key;
185
381
  storage.setItem(storageKey, value);
186
382
  if (scope === _Storage.StorageScope.Disk || scope === _Storage.StorageScope.Secure) {
383
+ ensureWebScopeKeyIndex(scope).add(key);
187
384
  notifyKeyListeners(getScopedListeners(scope), key);
188
385
  }
189
386
  },
@@ -204,6 +401,7 @@ const WebStorage = {
204
401
  storage.removeItem(key);
205
402
  }
206
403
  if (scope === _Storage.StorageScope.Disk || scope === _Storage.StorageScope.Secure) {
404
+ ensureWebScopeKeyIndex(scope).delete(key);
207
405
  notifyKeyListeners(getScopedListeners(scope), key);
208
406
  }
209
407
  },
@@ -234,6 +432,7 @@ const WebStorage = {
234
432
  storage.clear();
235
433
  }
236
434
  if (scope === _Storage.StorageScope.Disk || scope === _Storage.StorageScope.Secure) {
435
+ ensureWebScopeKeyIndex(scope).clear();
237
436
  notifyAllListeners(getScopedListeners(scope));
238
437
  }
239
438
  },
@@ -243,10 +442,16 @@ const WebStorage = {
243
442
  return;
244
443
  }
245
444
  keys.forEach((key, index) => {
445
+ const value = values[index];
446
+ if (value === undefined) {
447
+ return;
448
+ }
246
449
  const storageKey = scope === _Storage.StorageScope.Secure ? toSecureStorageKey(key) : key;
247
- storage.setItem(storageKey, values[index]);
450
+ storage.setItem(storageKey, value);
248
451
  });
249
452
  if (scope === _Storage.StorageScope.Disk || scope === _Storage.StorageScope.Secure) {
453
+ const keyIndex = ensureWebScopeKeyIndex(scope);
454
+ keys.forEach(key => keyIndex.add(key));
250
455
  const listeners = getScopedListeners(scope);
251
456
  keys.forEach(key => notifyKeyListeners(listeners, key));
252
457
  }
@@ -259,9 +464,37 @@ const WebStorage = {
259
464
  });
260
465
  },
261
466
  removeBatch: (keys, scope) => {
262
- keys.forEach(key => {
263
- WebStorage.remove(key, scope);
264
- });
467
+ const storage = getBrowserStorage(scope);
468
+ if (!storage) {
469
+ return;
470
+ }
471
+ if (scope === _Storage.StorageScope.Secure) {
472
+ keys.forEach(key => {
473
+ storage.removeItem(toSecureStorageKey(key));
474
+ storage.removeItem(toBiometricStorageKey(key));
475
+ });
476
+ } else {
477
+ keys.forEach(key => {
478
+ storage.removeItem(key);
479
+ });
480
+ }
481
+ if (scope === _Storage.StorageScope.Disk || scope === _Storage.StorageScope.Secure) {
482
+ const keyIndex = ensureWebScopeKeyIndex(scope);
483
+ keys.forEach(key => keyIndex.delete(key));
484
+ const listeners = getScopedListeners(scope);
485
+ keys.forEach(key => notifyKeyListeners(listeners, key));
486
+ }
487
+ },
488
+ removeByPrefix: (prefix, scope) => {
489
+ if (scope !== _Storage.StorageScope.Disk && scope !== _Storage.StorageScope.Secure) {
490
+ return;
491
+ }
492
+ const keyIndex = ensureWebScopeKeyIndex(scope);
493
+ const keys = Array.from(keyIndex).filter(key => key.startsWith(prefix));
494
+ if (keys.length === 0) {
495
+ return;
496
+ }
497
+ WebStorage.removeBatch(keys, scope);
265
498
  },
266
499
  addOnChange: (_scope, _callback) => {
267
500
  return () => {};
@@ -274,54 +507,54 @@ const WebStorage = {
274
507
  return storage?.getItem(key) !== null;
275
508
  },
276
509
  getAllKeys: scope => {
277
- const storage = getBrowserStorage(scope);
278
- if (!storage) return [];
279
- const keys = new Set();
280
- for (let i = 0; i < storage.length; i++) {
281
- const k = storage.key(i);
282
- if (!k) {
283
- continue;
284
- }
285
- if (scope === _Storage.StorageScope.Secure) {
286
- if (k.startsWith(SECURE_WEB_PREFIX)) {
287
- keys.add(fromSecureStorageKey(k));
288
- } else if (k.startsWith(BIOMETRIC_WEB_PREFIX)) {
289
- keys.add(fromBiometricStorageKey(k));
290
- }
291
- continue;
292
- }
293
- if (k.startsWith(SECURE_WEB_PREFIX) || k.startsWith(BIOMETRIC_WEB_PREFIX)) {
294
- continue;
295
- }
296
- keys.add(k);
510
+ if (scope !== _Storage.StorageScope.Disk && scope !== _Storage.StorageScope.Secure) {
511
+ return [];
512
+ }
513
+ return Array.from(ensureWebScopeKeyIndex(scope));
514
+ },
515
+ getKeysByPrefix: (prefix, scope) => {
516
+ if (scope !== _Storage.StorageScope.Disk && scope !== _Storage.StorageScope.Secure) {
517
+ return [];
297
518
  }
298
- return Array.from(keys);
519
+ return Array.from(ensureWebScopeKeyIndex(scope)).filter(key => key.startsWith(prefix));
299
520
  },
300
521
  size: scope => {
301
- return WebStorage.getAllKeys(scope).length;
522
+ if (scope === _Storage.StorageScope.Disk || scope === _Storage.StorageScope.Secure) {
523
+ return ensureWebScopeKeyIndex(scope).size;
524
+ }
525
+ return 0;
302
526
  },
303
527
  setSecureAccessControl: () => {},
528
+ setSecureWritesAsync: _enabled => {},
304
529
  setKeychainAccessGroup: () => {},
305
530
  setSecureBiometric: (key, value) => {
531
+ WebStorage.setSecureBiometricWithLevel(key, value, _Storage.BiometricLevel.BiometryOnly);
532
+ },
533
+ setSecureBiometricWithLevel: (key, value, _level) => {
306
534
  if (typeof __DEV__ !== "undefined" && __DEV__ && !hasWarnedAboutWebBiometricFallback) {
307
535
  hasWarnedAboutWebBiometricFallback = true;
308
536
  console.warn("[NitroStorage] Biometric storage is not supported on web. Using localStorage.");
309
537
  }
310
- globalThis.localStorage?.setItem(toBiometricStorageKey(key), value);
538
+ getBrowserStorage(_Storage.StorageScope.Secure)?.setItem(toBiometricStorageKey(key), value);
539
+ ensureWebScopeKeyIndex(_Storage.StorageScope.Secure).add(key);
311
540
  notifyKeyListeners(getScopedListeners(_Storage.StorageScope.Secure), key);
312
541
  },
313
542
  getSecureBiometric: key => {
314
- return globalThis.localStorage?.getItem(toBiometricStorageKey(key)) ?? undefined;
543
+ return getBrowserStorage(_Storage.StorageScope.Secure)?.getItem(toBiometricStorageKey(key)) ?? undefined;
315
544
  },
316
545
  deleteSecureBiometric: key => {
317
- globalThis.localStorage?.removeItem(toBiometricStorageKey(key));
546
+ const storage = getBrowserStorage(_Storage.StorageScope.Secure);
547
+ storage?.removeItem(toBiometricStorageKey(key));
548
+ if (storage?.getItem(toSecureStorageKey(key)) === null) {
549
+ ensureWebScopeKeyIndex(_Storage.StorageScope.Secure).delete(key);
550
+ }
318
551
  notifyKeyListeners(getScopedListeners(_Storage.StorageScope.Secure), key);
319
552
  },
320
553
  hasSecureBiometric: key => {
321
- return globalThis.localStorage?.getItem(toBiometricStorageKey(key)) !== null;
554
+ return getBrowserStorage(_Storage.StorageScope.Secure)?.getItem(toBiometricStorageKey(key)) !== null;
322
555
  },
323
556
  clearSecureBiometric: () => {
324
- const storage = globalThis.localStorage;
557
+ const storage = getBrowserStorage(_Storage.StorageScope.Secure);
325
558
  if (!storage) return;
326
559
  const keysToNotify = [];
327
560
  const toRemove = [];
@@ -333,6 +566,12 @@ const WebStorage = {
333
566
  }
334
567
  }
335
568
  toRemove.forEach(k => storage.removeItem(k));
569
+ const keyIndex = ensureWebScopeKeyIndex(_Storage.StorageScope.Secure);
570
+ keysToNotify.forEach(key => {
571
+ if (storage.getItem(toSecureStorageKey(key)) === null) {
572
+ keyIndex.delete(key);
573
+ }
574
+ });
336
575
  const listeners = getScopedListeners(_Storage.StorageScope.Secure);
337
576
  keysToNotify.forEach(key => notifyKeyListeners(listeners, key));
338
577
  }
@@ -389,90 +628,173 @@ function writeMigrationVersion(scope, version) {
389
628
  }
390
629
  const storage = exports.storage = {
391
630
  clear: scope => {
392
- if (scope === _Storage.StorageScope.Memory) {
393
- memoryStore.clear();
394
- notifyAllListeners(memoryListeners);
395
- return;
396
- }
397
- if (scope === _Storage.StorageScope.Secure) {
398
- flushSecureWrites();
399
- pendingSecureWrites.clear();
400
- }
401
- clearScopeRawCache(scope);
402
- WebStorage.clear(scope);
403
- if (scope === _Storage.StorageScope.Secure) {
404
- WebStorage.clearSecureBiometric();
405
- }
631
+ measureOperation("storage:clear", scope, () => {
632
+ if (scope === _Storage.StorageScope.Memory) {
633
+ memoryStore.clear();
634
+ notifyAllListeners(memoryListeners);
635
+ return;
636
+ }
637
+ if (scope === _Storage.StorageScope.Secure) {
638
+ flushSecureWrites();
639
+ pendingSecureWrites.clear();
640
+ }
641
+ clearScopeRawCache(scope);
642
+ WebStorage.clear(scope);
643
+ });
406
644
  },
407
645
  clearAll: () => {
408
- storage.clear(_Storage.StorageScope.Memory);
409
- storage.clear(_Storage.StorageScope.Disk);
410
- storage.clear(_Storage.StorageScope.Secure);
646
+ measureOperation("storage:clearAll", _Storage.StorageScope.Memory, () => {
647
+ storage.clear(_Storage.StorageScope.Memory);
648
+ storage.clear(_Storage.StorageScope.Disk);
649
+ storage.clear(_Storage.StorageScope.Secure);
650
+ }, 3);
411
651
  },
412
652
  clearNamespace: (namespace, scope) => {
413
- (0, _internal.assertValidScope)(scope);
414
- if (scope === _Storage.StorageScope.Memory) {
415
- for (const key of memoryStore.keys()) {
416
- if ((0, _internal.isNamespaced)(key, namespace)) {
417
- memoryStore.delete(key);
653
+ measureOperation("storage:clearNamespace", scope, () => {
654
+ (0, _internal.assertValidScope)(scope);
655
+ if (scope === _Storage.StorageScope.Memory) {
656
+ for (const key of memoryStore.keys()) {
657
+ if ((0, _internal.isNamespaced)(key, namespace)) {
658
+ memoryStore.delete(key);
659
+ }
418
660
  }
661
+ notifyAllListeners(memoryListeners);
662
+ return;
419
663
  }
420
- notifyAllListeners(memoryListeners);
421
- return;
422
- }
423
- if (scope === _Storage.StorageScope.Secure) {
424
- flushSecureWrites();
425
- }
426
- const keys = WebStorage.getAllKeys(scope);
427
- const namespacedKeys = keys.filter(k => (0, _internal.isNamespaced)(k, namespace));
428
- if (namespacedKeys.length > 0) {
429
- WebStorage.removeBatch(namespacedKeys, scope);
430
- namespacedKeys.forEach(k => cacheRawValue(scope, k, undefined));
664
+ const keyPrefix = (0, _internal.prefixKey)(namespace, "");
431
665
  if (scope === _Storage.StorageScope.Secure) {
432
- namespacedKeys.forEach(k => clearPendingSecureWrite(k));
666
+ flushSecureWrites();
433
667
  }
434
- }
668
+ clearScopeRawCache(scope);
669
+ WebStorage.removeByPrefix(keyPrefix, scope);
670
+ });
435
671
  },
436
672
  clearBiometric: () => {
437
- WebStorage.clearSecureBiometric();
673
+ measureOperation("storage:clearBiometric", _Storage.StorageScope.Secure, () => {
674
+ WebStorage.clearSecureBiometric();
675
+ });
438
676
  },
439
677
  has: (key, scope) => {
440
- (0, _internal.assertValidScope)(scope);
441
- if (scope === _Storage.StorageScope.Memory) return memoryStore.has(key);
442
- return WebStorage.has(key, scope);
678
+ return measureOperation("storage:has", scope, () => {
679
+ (0, _internal.assertValidScope)(scope);
680
+ if (scope === _Storage.StorageScope.Memory) return memoryStore.has(key);
681
+ return WebStorage.has(key, scope);
682
+ });
443
683
  },
444
684
  getAllKeys: scope => {
445
- (0, _internal.assertValidScope)(scope);
446
- if (scope === _Storage.StorageScope.Memory) return Array.from(memoryStore.keys());
447
- return WebStorage.getAllKeys(scope);
685
+ return measureOperation("storage:getAllKeys", scope, () => {
686
+ (0, _internal.assertValidScope)(scope);
687
+ if (scope === _Storage.StorageScope.Memory) return Array.from(memoryStore.keys());
688
+ return WebStorage.getAllKeys(scope);
689
+ });
690
+ },
691
+ getKeysByPrefix: (prefix, scope) => {
692
+ return measureOperation("storage:getKeysByPrefix", scope, () => {
693
+ (0, _internal.assertValidScope)(scope);
694
+ if (scope === _Storage.StorageScope.Memory) {
695
+ return Array.from(memoryStore.keys()).filter(key => key.startsWith(prefix));
696
+ }
697
+ return WebStorage.getKeysByPrefix(prefix, scope);
698
+ });
699
+ },
700
+ getByPrefix: (prefix, scope) => {
701
+ return measureOperation("storage:getByPrefix", scope, () => {
702
+ const result = {};
703
+ const keys = storage.getKeysByPrefix(prefix, scope);
704
+ if (keys.length === 0) {
705
+ return result;
706
+ }
707
+ if (scope === _Storage.StorageScope.Memory) {
708
+ keys.forEach(key => {
709
+ const value = memoryStore.get(key);
710
+ if (typeof value === "string") {
711
+ result[key] = value;
712
+ }
713
+ });
714
+ return result;
715
+ }
716
+ const values = WebStorage.getBatch(keys, scope);
717
+ keys.forEach((key, index) => {
718
+ const value = values[index];
719
+ if (value !== undefined) {
720
+ result[key] = value;
721
+ }
722
+ });
723
+ return result;
724
+ });
448
725
  },
449
726
  getAll: scope => {
450
- (0, _internal.assertValidScope)(scope);
451
- const result = {};
452
- if (scope === _Storage.StorageScope.Memory) {
453
- memoryStore.forEach((value, key) => {
454
- if (typeof value === "string") result[key] = value;
727
+ return measureOperation("storage:getAll", scope, () => {
728
+ (0, _internal.assertValidScope)(scope);
729
+ const result = {};
730
+ if (scope === _Storage.StorageScope.Memory) {
731
+ memoryStore.forEach((value, key) => {
732
+ if (typeof value === "string") result[key] = value;
733
+ });
734
+ return result;
735
+ }
736
+ const keys = WebStorage.getAllKeys(scope);
737
+ keys.forEach(key => {
738
+ const val = WebStorage.get(key, scope);
739
+ if (val !== undefined) result[key] = val;
455
740
  });
456
741
  return result;
457
- }
458
- const keys = WebStorage.getAllKeys(scope);
459
- keys.forEach(key => {
460
- const val = WebStorage.get(key, scope);
461
- if (val !== undefined) result[key] = val;
462
742
  });
463
- return result;
464
743
  },
465
744
  size: scope => {
466
- (0, _internal.assertValidScope)(scope);
467
- if (scope === _Storage.StorageScope.Memory) return memoryStore.size;
468
- return WebStorage.size(scope);
745
+ return measureOperation("storage:size", scope, () => {
746
+ (0, _internal.assertValidScope)(scope);
747
+ if (scope === _Storage.StorageScope.Memory) return memoryStore.size;
748
+ return WebStorage.size(scope);
749
+ });
750
+ },
751
+ setAccessControl: _level => {
752
+ recordMetric("storage:setAccessControl", _Storage.StorageScope.Secure, 0);
753
+ },
754
+ setSecureWritesAsync: _enabled => {
755
+ recordMetric("storage:setSecureWritesAsync", _Storage.StorageScope.Secure, 0);
469
756
  },
470
- setAccessControl: _level => {},
471
- setKeychainAccessGroup: _group => {}
757
+ flushSecureWrites: () => {
758
+ measureOperation("storage:flushSecureWrites", _Storage.StorageScope.Secure, () => {
759
+ flushSecureWrites();
760
+ });
761
+ },
762
+ setKeychainAccessGroup: _group => {
763
+ recordMetric("storage:setKeychainAccessGroup", _Storage.StorageScope.Secure, 0);
764
+ },
765
+ setMetricsObserver: observer => {
766
+ metricsObserver = observer;
767
+ },
768
+ getMetricsSnapshot: () => {
769
+ const snapshot = {};
770
+ metricsCounters.forEach((value, key) => {
771
+ snapshot[key] = {
772
+ count: value.count,
773
+ totalDurationMs: value.totalDurationMs,
774
+ avgDurationMs: value.count === 0 ? 0 : value.totalDurationMs / value.count,
775
+ maxDurationMs: value.maxDurationMs
776
+ };
777
+ });
778
+ return snapshot;
779
+ },
780
+ resetMetrics: () => {
781
+ metricsCounters.clear();
782
+ }
472
783
  };
784
+ function setWebSecureStorageBackend(backend) {
785
+ webSecureStorageBackend = backend ?? createLocalStorageWebSecureBackend();
786
+ hydratedWebScopeKeyIndex.delete(_Storage.StorageScope.Secure);
787
+ clearScopeRawCache(_Storage.StorageScope.Secure);
788
+ }
789
+ function getWebSecureStorageBackend() {
790
+ return webSecureStorageBackend;
791
+ }
473
792
  function canUseRawBatchPath(item) {
474
793
  return item._hasExpiration === false && item._hasValidation === false && item._isBiometric !== true && item._secureAccessControl === undefined;
475
794
  }
795
+ function canUseSecureRawBatchPath(item) {
796
+ return item._hasExpiration === false && item._hasValidation === false && item._isBiometric !== true;
797
+ }
476
798
  function defaultSerialize(value) {
477
799
  return (0, _internal.serializeWithPrimitiveFastPath)(value);
478
800
  }
@@ -484,7 +806,8 @@ function createStorageItem(config) {
484
806
  const serialize = config.serialize ?? defaultSerialize;
485
807
  const deserialize = config.deserialize ?? defaultDeserialize;
486
808
  const isMemory = config.scope === _Storage.StorageScope.Memory;
487
- const isBiometric = config.biometric === true && config.scope === _Storage.StorageScope.Secure;
809
+ const resolvedBiometricLevel = config.scope === _Storage.StorageScope.Secure ? config.biometricLevel ?? (config.biometric === true ? _Storage.BiometricLevel.BiometryOnly : _Storage.BiometricLevel.None) : _Storage.BiometricLevel.None;
810
+ const isBiometric = resolvedBiometricLevel !== _Storage.BiometricLevel.None;
488
811
  const secureAccessControl = config.accessControl;
489
812
  const validate = config.validate;
490
813
  const onValidationError = config.onValidationError;
@@ -493,7 +816,8 @@ function createStorageItem(config) {
493
816
  const expirationTtlMs = expiration?.ttlMs;
494
817
  const memoryExpiration = expiration && isMemory ? new Map() : null;
495
818
  const readCache = !isMemory && config.readCache === true;
496
- const coalesceSecureWrites = config.scope === _Storage.StorageScope.Secure && config.coalesceSecureWrites === true && !isBiometric && secureAccessControl === undefined;
819
+ const coalesceSecureWrites = config.scope === _Storage.StorageScope.Secure && config.coalesceSecureWrites === true && !isBiometric;
820
+ const defaultValue = config.defaultValue;
497
821
  const nonMemoryScope = config.scope === _Storage.StorageScope.Disk ? _Storage.StorageScope.Disk : config.scope === _Storage.StorageScope.Secure ? _Storage.StorageScope.Secure : null;
498
822
  if (expiration && expiration.ttlMs <= 0) {
499
823
  throw new Error("expiration.ttlMs must be greater than 0.");
@@ -503,10 +827,12 @@ function createStorageItem(config) {
503
827
  let lastRaw = undefined;
504
828
  let lastValue;
505
829
  let hasLastValue = false;
830
+ let lastExpiresAt = undefined;
506
831
  const invalidateParsedCache = () => {
507
832
  lastRaw = undefined;
508
833
  lastValue = undefined;
509
834
  hasLastValue = false;
835
+ lastExpiresAt = undefined;
510
836
  };
511
837
  const ensureSubscription = () => {
512
838
  if (unsubscribe) {
@@ -520,6 +846,7 @@ function createStorageItem(config) {
520
846
  unsubscribe = addKeyListener(memoryListeners, storageKey, listener);
521
847
  return;
522
848
  }
849
+ ensureWebStorageEventSubscription();
523
850
  unsubscribe = addKeyListener(getScopedListeners(nonMemoryScope), storageKey, listener);
524
851
  };
525
852
  const readStoredRaw = () => {
@@ -553,12 +880,12 @@ function createStorageItem(config) {
553
880
  };
554
881
  const writeStoredRaw = rawValue => {
555
882
  if (isBiometric) {
556
- WebStorage.setSecureBiometric(storageKey, rawValue);
883
+ WebStorage.setSecureBiometricWithLevel(storageKey, rawValue, resolvedBiometricLevel);
557
884
  return;
558
885
  }
559
886
  cacheRawValue(nonMemoryScope, storageKey, rawValue);
560
887
  if (coalesceSecureWrites) {
561
- scheduleSecureWrite(storageKey, rawValue);
888
+ scheduleSecureWrite(storageKey, rawValue, secureAccessControl ?? _Storage.AccessControl.WhenUnlocked);
562
889
  return;
563
890
  }
564
891
  if (nonMemoryScope === _Storage.StorageScope.Secure) {
@@ -573,7 +900,7 @@ function createStorageItem(config) {
573
900
  }
574
901
  cacheRawValue(nonMemoryScope, storageKey, undefined);
575
902
  if (coalesceSecureWrites) {
576
- scheduleSecureWrite(storageKey, undefined);
903
+ scheduleSecureWrite(storageKey, undefined, secureAccessControl ?? _Storage.AccessControl.WhenUnlocked);
577
904
  return;
578
905
  }
579
906
  if (nonMemoryScope === _Storage.StorageScope.Secure) {
@@ -606,7 +933,7 @@ function createStorageItem(config) {
606
933
  if (onValidationError) {
607
934
  return onValidationError(invalidValue);
608
935
  }
609
- return config.defaultValue;
936
+ return defaultValue;
610
937
  };
611
938
  const ensureValidatedValue = (candidate, hadStoredValue) => {
612
939
  if (!validate || validate(candidate)) {
@@ -614,40 +941,62 @@ function createStorageItem(config) {
614
941
  }
615
942
  const resolved = resolveInvalidValue(candidate);
616
943
  if (validate && !validate(resolved)) {
617
- return config.defaultValue;
944
+ return defaultValue;
618
945
  }
619
946
  if (hadStoredValue) {
620
947
  writeValueWithoutValidation(resolved);
621
948
  }
622
949
  return resolved;
623
950
  };
624
- const get = () => {
951
+ const getInternal = () => {
625
952
  const raw = readStoredRaw();
626
- const canUseCachedValue = !expiration && !memoryExpiration;
627
- if (canUseCachedValue && raw === lastRaw && hasLastValue) {
628
- return lastValue;
953
+ if (!memoryExpiration && raw === lastRaw && hasLastValue) {
954
+ if (!expiration || lastExpiresAt === null) {
955
+ return lastValue;
956
+ }
957
+ if (typeof lastExpiresAt === "number") {
958
+ if (lastExpiresAt > Date.now()) {
959
+ return lastValue;
960
+ }
961
+ removeStoredRaw();
962
+ invalidateParsedCache();
963
+ onExpired?.(storageKey);
964
+ lastValue = ensureValidatedValue(defaultValue, false);
965
+ hasLastValue = true;
966
+ return lastValue;
967
+ }
629
968
  }
630
969
  lastRaw = raw;
631
970
  if (raw === undefined) {
632
- lastValue = ensureValidatedValue(config.defaultValue, false);
971
+ lastExpiresAt = undefined;
972
+ lastValue = ensureValidatedValue(defaultValue, false);
633
973
  hasLastValue = true;
634
974
  return lastValue;
635
975
  }
636
976
  if (isMemory) {
977
+ lastExpiresAt = undefined;
637
978
  lastValue = ensureValidatedValue(raw, true);
638
979
  hasLastValue = true;
639
980
  return lastValue;
640
981
  }
982
+ if (typeof raw !== "string") {
983
+ lastExpiresAt = undefined;
984
+ lastValue = ensureValidatedValue(defaultValue, false);
985
+ hasLastValue = true;
986
+ return lastValue;
987
+ }
641
988
  let deserializableRaw = raw;
642
989
  if (expiration) {
990
+ let envelopeExpiresAt = null;
643
991
  try {
644
992
  const parsed = JSON.parse(raw);
645
993
  if ((0, _internal.isStoredEnvelope)(parsed)) {
994
+ envelopeExpiresAt = parsed.expiresAt;
646
995
  if (parsed.expiresAt <= Date.now()) {
647
996
  removeStoredRaw();
648
997
  invalidateParsedCache();
649
998
  onExpired?.(storageKey);
650
- lastValue = ensureValidatedValue(config.defaultValue, false);
999
+ lastValue = ensureValidatedValue(defaultValue, false);
651
1000
  hasLastValue = true;
652
1001
  return lastValue;
653
1002
  }
@@ -656,37 +1005,60 @@ function createStorageItem(config) {
656
1005
  } catch {
657
1006
  // Keep backward compatibility with legacy raw values.
658
1007
  }
1008
+ lastExpiresAt = envelopeExpiresAt;
1009
+ } else {
1010
+ lastExpiresAt = undefined;
659
1011
  }
660
1012
  lastValue = ensureValidatedValue(deserialize(deserializableRaw), true);
661
1013
  hasLastValue = true;
662
1014
  return lastValue;
663
1015
  };
1016
+ const getCurrentVersion = () => {
1017
+ const raw = readStoredRaw();
1018
+ return (0, _internal.toVersionToken)(raw);
1019
+ };
1020
+ const get = () => measureOperation("item:get", config.scope, () => getInternal());
1021
+ const getWithVersion = () => measureOperation("item:getWithVersion", config.scope, () => ({
1022
+ value: getInternal(),
1023
+ version: getCurrentVersion()
1024
+ }));
664
1025
  const set = valueOrFn => {
665
- const currentValue = get();
666
- const newValue = typeof valueOrFn === "function" ? valueOrFn(currentValue) : valueOrFn;
667
- invalidateParsedCache();
668
- if (validate && !validate(newValue)) {
669
- throw new Error(`Validation failed for key "${storageKey}" in scope "${_Storage.StorageScope[config.scope]}".`);
670
- }
671
- writeValueWithoutValidation(newValue);
1026
+ measureOperation("item:set", config.scope, () => {
1027
+ const newValue = isUpdater(valueOrFn) ? valueOrFn(getInternal()) : valueOrFn;
1028
+ invalidateParsedCache();
1029
+ if (validate && !validate(newValue)) {
1030
+ throw new Error(`Validation failed for key "${storageKey}" in scope "${_Storage.StorageScope[config.scope]}".`);
1031
+ }
1032
+ writeValueWithoutValidation(newValue);
1033
+ });
672
1034
  };
1035
+ const setIfVersion = (version, valueOrFn) => measureOperation("item:setIfVersion", config.scope, () => {
1036
+ const currentVersion = getCurrentVersion();
1037
+ if (currentVersion !== version) {
1038
+ return false;
1039
+ }
1040
+ set(valueOrFn);
1041
+ return true;
1042
+ });
673
1043
  const deleteItem = () => {
674
- invalidateParsedCache();
675
- if (isMemory) {
676
- if (memoryExpiration) {
677
- memoryExpiration.delete(storageKey);
1044
+ measureOperation("item:delete", config.scope, () => {
1045
+ invalidateParsedCache();
1046
+ if (isMemory) {
1047
+ if (memoryExpiration) {
1048
+ memoryExpiration.delete(storageKey);
1049
+ }
1050
+ memoryStore.delete(storageKey);
1051
+ notifyKeyListeners(memoryListeners, storageKey);
1052
+ return;
678
1053
  }
679
- memoryStore.delete(storageKey);
680
- notifyKeyListeners(memoryListeners, storageKey);
681
- return;
682
- }
683
- removeStoredRaw();
1054
+ removeStoredRaw();
1055
+ });
684
1056
  };
685
- const hasItem = () => {
1057
+ const hasItem = () => measureOperation("item:has", config.scope, () => {
686
1058
  if (isMemory) return memoryStore.has(storageKey);
687
1059
  if (isBiometric) return WebStorage.hasSecureBiometric(storageKey);
688
1060
  return WebStorage.has(storageKey, config.scope);
689
- };
1061
+ });
690
1062
  const subscribe = callback => {
691
1063
  ensureSubscription();
692
1064
  listeners.add(callback);
@@ -700,7 +1072,9 @@ function createStorageItem(config) {
700
1072
  };
701
1073
  const storageItem = {
702
1074
  get,
1075
+ getWithVersion,
703
1076
  set,
1077
+ setIfVersion,
704
1078
  delete: deleteItem,
705
1079
  has: hasItem,
706
1080
  subscribe,
@@ -714,123 +1088,150 @@ function createStorageItem(config) {
714
1088
  _hasExpiration: expiration !== undefined,
715
1089
  _readCacheEnabled: readCache,
716
1090
  _isBiometric: isBiometric,
717
- _secureAccessControl: secureAccessControl,
1091
+ _defaultValue: defaultValue,
1092
+ ...(secureAccessControl !== undefined ? {
1093
+ _secureAccessControl: secureAccessControl
1094
+ } : {}),
718
1095
  scope: config.scope,
719
1096
  key: storageKey
720
1097
  };
721
1098
  return storageItem;
722
1099
  }
723
- function useStorage(item) {
724
- const value = (0, _react.useSyncExternalStore)(item.subscribe, item.get, item.get);
725
- return [value, item.set];
726
- }
727
- function useStorageSelector(item, selector, isEqual = Object.is) {
728
- const selectedRef = (0, _react.useRef)({
729
- hasValue: false
730
- });
731
- const getSelectedSnapshot = () => {
732
- const nextSelected = selector(item.get());
733
- const current = selectedRef.current;
734
- if (current.hasValue && isEqual(current.value, nextSelected)) {
735
- return current.value;
736
- }
737
- selectedRef.current = {
738
- hasValue: true,
739
- value: nextSelected
740
- };
741
- return nextSelected;
742
- };
743
- const selectedValue = (0, _react.useSyncExternalStore)(item.subscribe, getSelectedSnapshot, getSelectedSnapshot);
744
- return [selectedValue, item.set];
745
- }
746
- function useSetStorage(item) {
747
- return item.set;
748
- }
749
1100
  function getBatch(items, scope) {
750
- (0, _internal.assertBatchScope)(items, scope);
751
- if (scope === _Storage.StorageScope.Memory) {
752
- return items.map(item => item.get());
753
- }
754
- const useRawBatchPath = items.every(item => canUseRawBatchPath(item));
755
- if (!useRawBatchPath) {
756
- return items.map(item => item.get());
757
- }
758
- const useBatchCache = items.every(item => item._readCacheEnabled === true);
759
- const rawValues = new Array(items.length);
760
- const keysToFetch = [];
761
- const keyIndexes = [];
762
- items.forEach((item, index) => {
763
- if (scope === _Storage.StorageScope.Secure) {
764
- if (hasPendingSecureWrite(item.key)) {
765
- rawValues[index] = readPendingSecureWrite(item.key);
766
- return;
767
- }
1101
+ return measureOperation("batch:get", scope, () => {
1102
+ (0, _internal.assertBatchScope)(items, scope);
1103
+ if (scope === _Storage.StorageScope.Memory) {
1104
+ return items.map(item => item.get());
768
1105
  }
769
- if (useBatchCache) {
770
- if (hasCachedRawValue(scope, item.key)) {
771
- rawValues[index] = readCachedRawValue(scope, item.key);
772
- return;
773
- }
1106
+ const useRawBatchPath = items.every(item => scope === _Storage.StorageScope.Secure ? canUseSecureRawBatchPath(item) : canUseRawBatchPath(item));
1107
+ if (!useRawBatchPath) {
1108
+ return items.map(item => item.get());
774
1109
  }
775
- keysToFetch.push(item.key);
776
- keyIndexes.push(index);
777
- });
778
- if (keysToFetch.length > 0) {
779
- const fetchedValues = WebStorage.getBatch(keysToFetch, scope);
780
- fetchedValues.forEach((value, index) => {
781
- const key = keysToFetch[index];
782
- const targetIndex = keyIndexes[index];
783
- rawValues[targetIndex] = value;
784
- cacheRawValue(scope, key, value);
1110
+ const rawValues = new Array(items.length);
1111
+ const keysToFetch = [];
1112
+ const keyIndexes = [];
1113
+ items.forEach((item, index) => {
1114
+ if (scope === _Storage.StorageScope.Secure) {
1115
+ if (hasPendingSecureWrite(item.key)) {
1116
+ rawValues[index] = readPendingSecureWrite(item.key);
1117
+ return;
1118
+ }
1119
+ }
1120
+ if (item._readCacheEnabled === true) {
1121
+ if (hasCachedRawValue(scope, item.key)) {
1122
+ rawValues[index] = readCachedRawValue(scope, item.key);
1123
+ return;
1124
+ }
1125
+ }
1126
+ keysToFetch.push(item.key);
1127
+ keyIndexes.push(index);
785
1128
  });
786
- }
787
- return items.map((item, index) => {
788
- const raw = rawValues[index];
789
- if (raw === undefined) {
790
- return item.get();
1129
+ if (keysToFetch.length > 0) {
1130
+ const fetchedValues = WebStorage.getBatch(keysToFetch, scope);
1131
+ fetchedValues.forEach((value, index) => {
1132
+ const key = keysToFetch[index];
1133
+ const targetIndex = keyIndexes[index];
1134
+ if (key === undefined || targetIndex === undefined) {
1135
+ return;
1136
+ }
1137
+ rawValues[targetIndex] = value;
1138
+ cacheRawValue(scope, key, value);
1139
+ });
791
1140
  }
792
- return item.deserialize(raw);
793
- });
1141
+ return items.map((item, index) => {
1142
+ const raw = rawValues[index];
1143
+ if (raw === undefined) {
1144
+ return asInternal(item)._defaultValue;
1145
+ }
1146
+ return item.deserialize(raw);
1147
+ });
1148
+ }, items.length);
794
1149
  }
795
1150
  function setBatch(items, scope) {
796
- (0, _internal.assertBatchScope)(items.map(batchEntry => batchEntry.item), scope);
797
- if (scope === _Storage.StorageScope.Memory) {
798
- items.forEach(({
799
- item,
800
- value
801
- }) => item.set(value));
802
- return;
803
- }
804
- const useRawBatchPath = items.every(({
805
- item
806
- }) => canUseRawBatchPath(asInternal(item)));
807
- if (!useRawBatchPath) {
808
- items.forEach(({
809
- item,
810
- value
811
- }) => item.set(value));
812
- return;
813
- }
814
- const keys = items.map(entry => entry.item.key);
815
- const values = items.map(entry => entry.item.serialize(entry.value));
816
- if (scope === _Storage.StorageScope.Secure) {
817
- flushSecureWrites();
818
- }
819
- WebStorage.setBatch(keys, values, scope);
820
- keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
1151
+ measureOperation("batch:set", scope, () => {
1152
+ (0, _internal.assertBatchScope)(items.map(batchEntry => batchEntry.item), scope);
1153
+ if (scope === _Storage.StorageScope.Memory) {
1154
+ items.forEach(({
1155
+ item,
1156
+ value
1157
+ }) => item.set(value));
1158
+ return;
1159
+ }
1160
+ if (scope === _Storage.StorageScope.Secure) {
1161
+ const secureEntries = items.map(({
1162
+ item,
1163
+ value
1164
+ }) => ({
1165
+ item,
1166
+ value,
1167
+ internal: asInternal(item)
1168
+ }));
1169
+ const canUseSecureBatchPath = secureEntries.every(({
1170
+ internal
1171
+ }) => canUseSecureRawBatchPath(internal));
1172
+ if (!canUseSecureBatchPath) {
1173
+ items.forEach(({
1174
+ item,
1175
+ value
1176
+ }) => item.set(value));
1177
+ return;
1178
+ }
1179
+ flushSecureWrites();
1180
+ const groupedByAccessControl = new Map();
1181
+ secureEntries.forEach(({
1182
+ item,
1183
+ value,
1184
+ internal
1185
+ }) => {
1186
+ const accessControl = internal._secureAccessControl ?? _Storage.AccessControl.WhenUnlocked;
1187
+ const existingGroup = groupedByAccessControl.get(accessControl);
1188
+ const group = existingGroup ?? {
1189
+ keys: [],
1190
+ values: []
1191
+ };
1192
+ group.keys.push(item.key);
1193
+ group.values.push(item.serialize(value));
1194
+ if (!existingGroup) {
1195
+ groupedByAccessControl.set(accessControl, group);
1196
+ }
1197
+ });
1198
+ groupedByAccessControl.forEach((group, accessControl) => {
1199
+ WebStorage.setSecureAccessControl(accessControl);
1200
+ WebStorage.setBatch(group.keys, group.values, scope);
1201
+ group.keys.forEach((key, index) => cacheRawValue(scope, key, group.values[index]));
1202
+ });
1203
+ return;
1204
+ }
1205
+ const useRawBatchPath = items.every(({
1206
+ item
1207
+ }) => canUseRawBatchPath(asInternal(item)));
1208
+ if (!useRawBatchPath) {
1209
+ items.forEach(({
1210
+ item,
1211
+ value
1212
+ }) => item.set(value));
1213
+ return;
1214
+ }
1215
+ const keys = items.map(entry => entry.item.key);
1216
+ const values = items.map(entry => entry.item.serialize(entry.value));
1217
+ WebStorage.setBatch(keys, values, scope);
1218
+ keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
1219
+ }, items.length);
821
1220
  }
822
1221
  function removeBatch(items, scope) {
823
- (0, _internal.assertBatchScope)(items, scope);
824
- if (scope === _Storage.StorageScope.Memory) {
825
- items.forEach(item => item.delete());
826
- return;
827
- }
828
- const keys = items.map(item => item.key);
829
- if (scope === _Storage.StorageScope.Secure) {
830
- flushSecureWrites();
831
- }
832
- WebStorage.removeBatch(keys, scope);
833
- keys.forEach(key => cacheRawValue(scope, key, undefined));
1222
+ measureOperation("batch:remove", scope, () => {
1223
+ (0, _internal.assertBatchScope)(items, scope);
1224
+ if (scope === _Storage.StorageScope.Memory) {
1225
+ items.forEach(item => item.delete());
1226
+ return;
1227
+ }
1228
+ const keys = items.map(item => item.key);
1229
+ if (scope === _Storage.StorageScope.Secure) {
1230
+ flushSecureWrites();
1231
+ }
1232
+ WebStorage.removeBatch(keys, scope);
1233
+ keys.forEach(key => cacheRawValue(scope, key, undefined));
1234
+ }, items.length);
834
1235
  }
835
1236
  function registerMigration(version, migration) {
836
1237
  if (!Number.isInteger(version) || version <= 0) {
@@ -842,93 +1243,133 @@ function registerMigration(version, migration) {
842
1243
  registeredMigrations.set(version, migration);
843
1244
  }
844
1245
  function migrateToLatest(scope = _Storage.StorageScope.Disk) {
845
- (0, _internal.assertValidScope)(scope);
846
- const currentVersion = readMigrationVersion(scope);
847
- const versions = Array.from(registeredMigrations.keys()).filter(version => version > currentVersion).sort((a, b) => a - b);
848
- let appliedVersion = currentVersion;
849
- const context = {
850
- scope,
851
- getRaw: key => getRawValue(key, scope),
852
- setRaw: (key, value) => setRawValue(key, value, scope),
853
- removeRaw: key => removeRawValue(key, scope)
854
- };
855
- versions.forEach(version => {
856
- const migration = registeredMigrations.get(version);
857
- if (!migration) {
858
- return;
859
- }
860
- migration(context);
861
- writeMigrationVersion(scope, version);
862
- appliedVersion = version;
1246
+ return measureOperation("migration:run", scope, () => {
1247
+ (0, _internal.assertValidScope)(scope);
1248
+ const currentVersion = readMigrationVersion(scope);
1249
+ const versions = Array.from(registeredMigrations.keys()).filter(version => version > currentVersion).sort((a, b) => a - b);
1250
+ let appliedVersion = currentVersion;
1251
+ const context = {
1252
+ scope,
1253
+ getRaw: key => getRawValue(key, scope),
1254
+ setRaw: (key, value) => setRawValue(key, value, scope),
1255
+ removeRaw: key => removeRawValue(key, scope)
1256
+ };
1257
+ versions.forEach(version => {
1258
+ const migration = registeredMigrations.get(version);
1259
+ if (!migration) {
1260
+ return;
1261
+ }
1262
+ migration(context);
1263
+ writeMigrationVersion(scope, version);
1264
+ appliedVersion = version;
1265
+ });
1266
+ return appliedVersion;
863
1267
  });
864
- return appliedVersion;
865
1268
  }
866
1269
  function runTransaction(scope, transaction) {
867
- (0, _internal.assertValidScope)(scope);
868
- if (scope === _Storage.StorageScope.Secure) {
869
- flushSecureWrites();
870
- }
871
- const rollback = new Map();
872
- const rememberRollback = key => {
873
- if (rollback.has(key)) {
874
- return;
875
- }
876
- rollback.set(key, getRawValue(key, scope));
877
- };
878
- const tx = {
879
- scope,
880
- getRaw: key => getRawValue(key, scope),
881
- setRaw: (key, value) => {
882
- rememberRollback(key);
883
- setRawValue(key, value, scope);
884
- },
885
- removeRaw: key => {
886
- rememberRollback(key);
887
- removeRawValue(key, scope);
888
- },
889
- getItem: item => {
890
- (0, _internal.assertBatchScope)([item], scope);
891
- return item.get();
892
- },
893
- setItem: (item, value) => {
894
- (0, _internal.assertBatchScope)([item], scope);
895
- rememberRollback(item.key);
896
- item.set(value);
897
- },
898
- removeItem: item => {
899
- (0, _internal.assertBatchScope)([item], scope);
900
- rememberRollback(item.key);
901
- item.delete();
1270
+ return measureOperation("transaction:run", scope, () => {
1271
+ (0, _internal.assertValidScope)(scope);
1272
+ if (scope === _Storage.StorageScope.Secure) {
1273
+ flushSecureWrites();
902
1274
  }
903
- };
904
- try {
905
- return transaction(tx);
906
- } catch (error) {
907
- Array.from(rollback.entries()).reverse().forEach(([key, previousValue]) => {
908
- if (previousValue === undefined) {
1275
+ const rollback = new Map();
1276
+ const rememberRollback = key => {
1277
+ if (rollback.has(key)) {
1278
+ return;
1279
+ }
1280
+ rollback.set(key, getRawValue(key, scope));
1281
+ };
1282
+ const tx = {
1283
+ scope,
1284
+ getRaw: key => getRawValue(key, scope),
1285
+ setRaw: (key, value) => {
1286
+ rememberRollback(key);
1287
+ setRawValue(key, value, scope);
1288
+ },
1289
+ removeRaw: key => {
1290
+ rememberRollback(key);
909
1291
  removeRawValue(key, scope);
1292
+ },
1293
+ getItem: item => {
1294
+ (0, _internal.assertBatchScope)([item], scope);
1295
+ return item.get();
1296
+ },
1297
+ setItem: (item, value) => {
1298
+ (0, _internal.assertBatchScope)([item], scope);
1299
+ rememberRollback(item.key);
1300
+ item.set(value);
1301
+ },
1302
+ removeItem: item => {
1303
+ (0, _internal.assertBatchScope)([item], scope);
1304
+ rememberRollback(item.key);
1305
+ item.delete();
1306
+ }
1307
+ };
1308
+ try {
1309
+ return transaction(tx);
1310
+ } catch (error) {
1311
+ const rollbackEntries = Array.from(rollback.entries()).reverse();
1312
+ if (scope === _Storage.StorageScope.Memory) {
1313
+ rollbackEntries.forEach(([key, previousValue]) => {
1314
+ if (previousValue === undefined) {
1315
+ removeRawValue(key, scope);
1316
+ } else {
1317
+ setRawValue(key, previousValue, scope);
1318
+ }
1319
+ });
910
1320
  } else {
911
- setRawValue(key, previousValue, scope);
1321
+ const keysToSet = [];
1322
+ const valuesToSet = [];
1323
+ const keysToRemove = [];
1324
+ rollbackEntries.forEach(([key, previousValue]) => {
1325
+ if (previousValue === undefined) {
1326
+ keysToRemove.push(key);
1327
+ } else {
1328
+ keysToSet.push(key);
1329
+ valuesToSet.push(previousValue);
1330
+ }
1331
+ });
1332
+ if (scope === _Storage.StorageScope.Secure) {
1333
+ flushSecureWrites();
1334
+ }
1335
+ if (keysToSet.length > 0) {
1336
+ WebStorage.setBatch(keysToSet, valuesToSet, scope);
1337
+ keysToSet.forEach((key, index) => cacheRawValue(scope, key, valuesToSet[index]));
1338
+ }
1339
+ if (keysToRemove.length > 0) {
1340
+ WebStorage.removeBatch(keysToRemove, scope);
1341
+ keysToRemove.forEach(key => cacheRawValue(scope, key, undefined));
1342
+ }
912
1343
  }
913
- });
914
- throw error;
915
- }
1344
+ throw error;
1345
+ }
1346
+ });
916
1347
  }
917
1348
  function createSecureAuthStorage(config, options) {
918
1349
  const ns = options?.namespace ?? "auth";
919
1350
  const result = {};
920
- for (const key of Object.keys(config)) {
1351
+ for (const key of typedKeys(config)) {
921
1352
  const itemConfig = config[key];
1353
+ const expirationConfig = itemConfig.ttlMs !== undefined ? {
1354
+ ttlMs: itemConfig.ttlMs
1355
+ } : undefined;
922
1356
  result[key] = createStorageItem({
923
1357
  key,
924
1358
  scope: _Storage.StorageScope.Secure,
925
1359
  defaultValue: "",
926
1360
  namespace: ns,
927
- biometric: itemConfig.biometric,
928
- accessControl: itemConfig.accessControl,
929
- expiration: itemConfig.ttlMs ? {
930
- ttlMs: itemConfig.ttlMs
931
- } : undefined
1361
+ ...(itemConfig.biometric !== undefined ? {
1362
+ biometric: itemConfig.biometric
1363
+ } : {}),
1364
+ ...(itemConfig.biometricLevel !== undefined ? {
1365
+ biometricLevel: itemConfig.biometricLevel
1366
+ } : {}),
1367
+ ...(itemConfig.accessControl !== undefined ? {
1368
+ accessControl: itemConfig.accessControl
1369
+ } : {}),
1370
+ ...(expirationConfig !== undefined ? {
1371
+ expiration: expirationConfig
1372
+ } : {})
932
1373
  });
933
1374
  }
934
1375
  return result;