react-native-nitro-storage 0.3.1 → 0.3.2

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 (37) hide show
  1. package/README.md +199 -10
  2. package/android/CMakeLists.txt +2 -0
  3. package/android/src/main/cpp/AndroidStorageAdapterCpp.cpp +4 -0
  4. package/android/src/main/cpp/AndroidStorageAdapterCpp.hpp +1 -0
  5. package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +36 -13
  6. package/cpp/bindings/HybridStorage.cpp +55 -9
  7. package/cpp/bindings/HybridStorage.hpp +19 -2
  8. package/cpp/core/NativeStorageAdapter.hpp +1 -0
  9. package/ios/IOSStorageAdapterCpp.hpp +1 -0
  10. package/ios/IOSStorageAdapterCpp.mm +7 -1
  11. package/lib/commonjs/index.js +139 -63
  12. package/lib/commonjs/index.js.map +1 -1
  13. package/lib/commonjs/index.web.js +236 -89
  14. package/lib/commonjs/index.web.js.map +1 -1
  15. package/lib/commonjs/storage-hooks.js +36 -0
  16. package/lib/commonjs/storage-hooks.js.map +1 -0
  17. package/lib/module/index.js +121 -60
  18. package/lib/module/index.js.map +1 -1
  19. package/lib/module/index.web.js +219 -87
  20. package/lib/module/index.web.js.map +1 -1
  21. package/lib/module/storage-hooks.js +30 -0
  22. package/lib/module/storage-hooks.js.map +1 -0
  23. package/lib/typescript/Storage.nitro.d.ts +2 -0
  24. package/lib/typescript/Storage.nitro.d.ts.map +1 -1
  25. package/lib/typescript/index.d.ts +3 -3
  26. package/lib/typescript/index.d.ts.map +1 -1
  27. package/lib/typescript/index.web.d.ts +5 -3
  28. package/lib/typescript/index.web.d.ts.map +1 -1
  29. package/lib/typescript/storage-hooks.d.ts +10 -0
  30. package/lib/typescript/storage-hooks.d.ts.map +1 -0
  31. package/nitrogen/generated/shared/c++/HybridStorageSpec.cpp +2 -0
  32. package/nitrogen/generated/shared/c++/HybridStorageSpec.hpp +2 -0
  33. package/package.json +5 -3
  34. package/src/Storage.nitro.ts +2 -0
  35. package/src/index.ts +143 -83
  36. package/src/index.web.ts +255 -112
  37. package/src/storage-hooks.ts +48 -0
@@ -1,13 +1,18 @@
1
1
  "use strict";
2
2
 
3
- import { useRef, useSyncExternalStore } from "react";
4
- import { StorageScope } from "./Storage.types";
3
+ import { StorageScope, AccessControl } from "./Storage.types";
5
4
  import { MIGRATION_VERSION_KEY, isStoredEnvelope, assertBatchScope, assertValidScope, serializeWithPrimitiveFastPath, deserializeWithPrimitiveFastPath, prefixKey, isNamespaced } from "./internal";
6
5
  export { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
7
6
  export { migrateFromMMKV } from "./migration";
8
7
  function asInternal(item) {
9
8
  return item;
10
9
  }
10
+ function isUpdater(valueOrFn) {
11
+ return typeof valueOrFn === "function";
12
+ }
13
+ function typedKeys(record) {
14
+ return Object.keys(record);
15
+ }
11
16
  const registeredMigrations = new Map();
12
17
  const runMicrotask = typeof queueMicrotask === "function" ? queueMicrotask : task => {
13
18
  Promise.resolve().then(task);
@@ -16,6 +21,8 @@ const memoryStore = new Map();
16
21
  const memoryListeners = new Map();
17
22
  const webScopeListeners = new Map([[StorageScope.Disk, new Map()], [StorageScope.Secure, new Map()]]);
18
23
  const scopedRawCache = new Map([[StorageScope.Disk, new Map()], [StorageScope.Secure, new Map()]]);
24
+ const webScopeKeyIndex = new Map([[StorageScope.Disk, new Set()], [StorageScope.Secure, new Set()]]);
25
+ const hydratedWebScopeKeyIndex = new Set();
19
26
  const pendingSecureWrites = new Map();
20
27
  let secureFlushScheduled = false;
21
28
  const SECURE_WEB_PREFIX = "__secure_";
@@ -42,6 +49,43 @@ function toBiometricStorageKey(key) {
42
49
  function fromBiometricStorageKey(key) {
43
50
  return key.slice(BIOMETRIC_WEB_PREFIX.length);
44
51
  }
52
+ function getWebScopeKeyIndex(scope) {
53
+ return webScopeKeyIndex.get(scope);
54
+ }
55
+ function hydrateWebScopeKeyIndex(scope) {
56
+ if (hydratedWebScopeKeyIndex.has(scope)) {
57
+ return;
58
+ }
59
+ const storage = getBrowserStorage(scope);
60
+ const keyIndex = getWebScopeKeyIndex(scope);
61
+ keyIndex.clear();
62
+ if (storage) {
63
+ for (let index = 0; index < storage.length; index += 1) {
64
+ const key = storage.key(index);
65
+ if (!key) {
66
+ continue;
67
+ }
68
+ if (scope === StorageScope.Disk) {
69
+ if (!key.startsWith(SECURE_WEB_PREFIX) && !key.startsWith(BIOMETRIC_WEB_PREFIX)) {
70
+ keyIndex.add(key);
71
+ }
72
+ continue;
73
+ }
74
+ if (key.startsWith(SECURE_WEB_PREFIX)) {
75
+ keyIndex.add(fromSecureStorageKey(key));
76
+ continue;
77
+ }
78
+ if (key.startsWith(BIOMETRIC_WEB_PREFIX)) {
79
+ keyIndex.add(fromBiometricStorageKey(key));
80
+ }
81
+ }
82
+ }
83
+ hydratedWebScopeKeyIndex.add(scope);
84
+ }
85
+ function ensureWebScopeKeyIndex(scope) {
86
+ hydrateWebScopeKeyIndex(scope);
87
+ return getWebScopeKeyIndex(scope);
88
+ }
45
89
  function getScopedListeners(scope) {
46
90
  return webScopeListeners.get(scope);
47
91
  }
@@ -146,6 +190,7 @@ const WebStorage = {
146
190
  const storageKey = scope === StorageScope.Secure ? toSecureStorageKey(key) : key;
147
191
  storage.setItem(storageKey, value);
148
192
  if (scope === StorageScope.Disk || scope === StorageScope.Secure) {
193
+ ensureWebScopeKeyIndex(scope).add(key);
149
194
  notifyKeyListeners(getScopedListeners(scope), key);
150
195
  }
151
196
  },
@@ -166,6 +211,7 @@ const WebStorage = {
166
211
  storage.removeItem(key);
167
212
  }
168
213
  if (scope === StorageScope.Disk || scope === StorageScope.Secure) {
214
+ ensureWebScopeKeyIndex(scope).delete(key);
169
215
  notifyKeyListeners(getScopedListeners(scope), key);
170
216
  }
171
217
  },
@@ -196,6 +242,7 @@ const WebStorage = {
196
242
  storage.clear();
197
243
  }
198
244
  if (scope === StorageScope.Disk || scope === StorageScope.Secure) {
245
+ ensureWebScopeKeyIndex(scope).clear();
199
246
  notifyAllListeners(getScopedListeners(scope));
200
247
  }
201
248
  },
@@ -205,10 +252,16 @@ const WebStorage = {
205
252
  return;
206
253
  }
207
254
  keys.forEach((key, index) => {
255
+ const value = values[index];
256
+ if (value === undefined) {
257
+ return;
258
+ }
208
259
  const storageKey = scope === StorageScope.Secure ? toSecureStorageKey(key) : key;
209
- storage.setItem(storageKey, values[index]);
260
+ storage.setItem(storageKey, value);
210
261
  });
211
262
  if (scope === StorageScope.Disk || scope === StorageScope.Secure) {
263
+ const keyIndex = ensureWebScopeKeyIndex(scope);
264
+ keys.forEach(key => keyIndex.add(key));
212
265
  const listeners = getScopedListeners(scope);
213
266
  keys.forEach(key => notifyKeyListeners(listeners, key));
214
267
  }
@@ -221,9 +274,37 @@ const WebStorage = {
221
274
  });
222
275
  },
223
276
  removeBatch: (keys, scope) => {
224
- keys.forEach(key => {
225
- WebStorage.remove(key, scope);
226
- });
277
+ const storage = getBrowserStorage(scope);
278
+ if (!storage) {
279
+ return;
280
+ }
281
+ if (scope === StorageScope.Secure) {
282
+ keys.forEach(key => {
283
+ storage.removeItem(toSecureStorageKey(key));
284
+ storage.removeItem(toBiometricStorageKey(key));
285
+ });
286
+ } else {
287
+ keys.forEach(key => {
288
+ storage.removeItem(key);
289
+ });
290
+ }
291
+ if (scope === StorageScope.Disk || scope === StorageScope.Secure) {
292
+ const keyIndex = ensureWebScopeKeyIndex(scope);
293
+ keys.forEach(key => keyIndex.delete(key));
294
+ const listeners = getScopedListeners(scope);
295
+ keys.forEach(key => notifyKeyListeners(listeners, key));
296
+ }
297
+ },
298
+ removeByPrefix: (prefix, scope) => {
299
+ if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
300
+ return;
301
+ }
302
+ const keyIndex = ensureWebScopeKeyIndex(scope);
303
+ const keys = Array.from(keyIndex).filter(key => key.startsWith(prefix));
304
+ if (keys.length === 0) {
305
+ return;
306
+ }
307
+ WebStorage.removeBatch(keys, scope);
227
308
  },
228
309
  addOnChange: (_scope, _callback) => {
229
310
  return () => {};
@@ -236,33 +317,19 @@ const WebStorage = {
236
317
  return storage?.getItem(key) !== null;
237
318
  },
238
319
  getAllKeys: scope => {
239
- const storage = getBrowserStorage(scope);
240
- if (!storage) return [];
241
- const keys = new Set();
242
- for (let i = 0; i < storage.length; i++) {
243
- const k = storage.key(i);
244
- if (!k) {
245
- continue;
246
- }
247
- if (scope === StorageScope.Secure) {
248
- if (k.startsWith(SECURE_WEB_PREFIX)) {
249
- keys.add(fromSecureStorageKey(k));
250
- } else if (k.startsWith(BIOMETRIC_WEB_PREFIX)) {
251
- keys.add(fromBiometricStorageKey(k));
252
- }
253
- continue;
254
- }
255
- if (k.startsWith(SECURE_WEB_PREFIX) || k.startsWith(BIOMETRIC_WEB_PREFIX)) {
256
- continue;
257
- }
258
- keys.add(k);
320
+ if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
321
+ return [];
259
322
  }
260
- return Array.from(keys);
323
+ return Array.from(ensureWebScopeKeyIndex(scope));
261
324
  },
262
325
  size: scope => {
263
- return WebStorage.getAllKeys(scope).length;
326
+ if (scope === StorageScope.Disk || scope === StorageScope.Secure) {
327
+ return ensureWebScopeKeyIndex(scope).size;
328
+ }
329
+ return 0;
264
330
  },
265
331
  setSecureAccessControl: () => {},
332
+ setSecureWritesAsync: _enabled => {},
266
333
  setKeychainAccessGroup: () => {},
267
334
  setSecureBiometric: (key, value) => {
268
335
  if (typeof __DEV__ !== "undefined" && __DEV__ && !hasWarnedAboutWebBiometricFallback) {
@@ -270,13 +337,18 @@ const WebStorage = {
270
337
  console.warn("[NitroStorage] Biometric storage is not supported on web. Using localStorage.");
271
338
  }
272
339
  globalThis.localStorage?.setItem(toBiometricStorageKey(key), value);
340
+ ensureWebScopeKeyIndex(StorageScope.Secure).add(key);
273
341
  notifyKeyListeners(getScopedListeners(StorageScope.Secure), key);
274
342
  },
275
343
  getSecureBiometric: key => {
276
344
  return globalThis.localStorage?.getItem(toBiometricStorageKey(key)) ?? undefined;
277
345
  },
278
346
  deleteSecureBiometric: key => {
279
- globalThis.localStorage?.removeItem(toBiometricStorageKey(key));
347
+ const storage = globalThis.localStorage;
348
+ storage?.removeItem(toBiometricStorageKey(key));
349
+ if (storage?.getItem(toSecureStorageKey(key)) === null) {
350
+ ensureWebScopeKeyIndex(StorageScope.Secure).delete(key);
351
+ }
280
352
  notifyKeyListeners(getScopedListeners(StorageScope.Secure), key);
281
353
  },
282
354
  hasSecureBiometric: key => {
@@ -295,6 +367,12 @@ const WebStorage = {
295
367
  }
296
368
  }
297
369
  toRemove.forEach(k => storage.removeItem(k));
370
+ const keyIndex = ensureWebScopeKeyIndex(StorageScope.Secure);
371
+ keysToNotify.forEach(key => {
372
+ if (storage.getItem(toSecureStorageKey(key)) === null) {
373
+ keyIndex.delete(key);
374
+ }
375
+ });
298
376
  const listeners = getScopedListeners(StorageScope.Secure);
299
377
  keysToNotify.forEach(key => notifyKeyListeners(listeners, key));
300
378
  }
@@ -362,9 +440,6 @@ export const storage = {
362
440
  }
363
441
  clearScopeRawCache(scope);
364
442
  WebStorage.clear(scope);
365
- if (scope === StorageScope.Secure) {
366
- WebStorage.clearSecureBiometric();
367
- }
368
443
  },
369
444
  clearAll: () => {
370
445
  storage.clear(StorageScope.Memory);
@@ -382,18 +457,12 @@ export const storage = {
382
457
  notifyAllListeners(memoryListeners);
383
458
  return;
384
459
  }
460
+ const keyPrefix = prefixKey(namespace, "");
385
461
  if (scope === StorageScope.Secure) {
386
462
  flushSecureWrites();
387
463
  }
388
- const keys = WebStorage.getAllKeys(scope);
389
- const namespacedKeys = keys.filter(k => isNamespaced(k, namespace));
390
- if (namespacedKeys.length > 0) {
391
- WebStorage.removeBatch(namespacedKeys, scope);
392
- namespacedKeys.forEach(k => cacheRawValue(scope, k, undefined));
393
- if (scope === StorageScope.Secure) {
394
- namespacedKeys.forEach(k => clearPendingSecureWrite(k));
395
- }
396
- }
464
+ clearScopeRawCache(scope);
465
+ WebStorage.removeByPrefix(keyPrefix, scope);
397
466
  },
398
467
  clearBiometric: () => {
399
468
  WebStorage.clearSecureBiometric();
@@ -430,11 +499,18 @@ export const storage = {
430
499
  return WebStorage.size(scope);
431
500
  },
432
501
  setAccessControl: _level => {},
502
+ setSecureWritesAsync: _enabled => {},
503
+ flushSecureWrites: () => {
504
+ flushSecureWrites();
505
+ },
433
506
  setKeychainAccessGroup: _group => {}
434
507
  };
435
508
  function canUseRawBatchPath(item) {
436
509
  return item._hasExpiration === false && item._hasValidation === false && item._isBiometric !== true && item._secureAccessControl === undefined;
437
510
  }
511
+ function canUseSecureRawBatchPath(item) {
512
+ return item._hasExpiration === false && item._hasValidation === false && item._isBiometric !== true;
513
+ }
438
514
  function defaultSerialize(value) {
439
515
  return serializeWithPrimitiveFastPath(value);
440
516
  }
@@ -456,6 +532,7 @@ export function createStorageItem(config) {
456
532
  const memoryExpiration = expiration && isMemory ? new Map() : null;
457
533
  const readCache = !isMemory && config.readCache === true;
458
534
  const coalesceSecureWrites = config.scope === StorageScope.Secure && config.coalesceSecureWrites === true && !isBiometric && secureAccessControl === undefined;
535
+ const defaultValue = config.defaultValue;
459
536
  const nonMemoryScope = config.scope === StorageScope.Disk ? StorageScope.Disk : config.scope === StorageScope.Secure ? StorageScope.Secure : null;
460
537
  if (expiration && expiration.ttlMs <= 0) {
461
538
  throw new Error("expiration.ttlMs must be greater than 0.");
@@ -465,10 +542,12 @@ export function createStorageItem(config) {
465
542
  let lastRaw = undefined;
466
543
  let lastValue;
467
544
  let hasLastValue = false;
545
+ let lastExpiresAt = undefined;
468
546
  const invalidateParsedCache = () => {
469
547
  lastRaw = undefined;
470
548
  lastValue = undefined;
471
549
  hasLastValue = false;
550
+ lastExpiresAt = undefined;
472
551
  };
473
552
  const ensureSubscription = () => {
474
553
  if (unsubscribe) {
@@ -568,7 +647,7 @@ export function createStorageItem(config) {
568
647
  if (onValidationError) {
569
648
  return onValidationError(invalidValue);
570
649
  }
571
- return config.defaultValue;
650
+ return defaultValue;
572
651
  };
573
652
  const ensureValidatedValue = (candidate, hadStoredValue) => {
574
653
  if (!validate || validate(candidate)) {
@@ -576,7 +655,7 @@ export function createStorageItem(config) {
576
655
  }
577
656
  const resolved = resolveInvalidValue(candidate);
578
657
  if (validate && !validate(resolved)) {
579
- return config.defaultValue;
658
+ return defaultValue;
580
659
  }
581
660
  if (hadStoredValue) {
582
661
  writeValueWithoutValidation(resolved);
@@ -585,31 +664,53 @@ export function createStorageItem(config) {
585
664
  };
586
665
  const get = () => {
587
666
  const raw = readStoredRaw();
588
- const canUseCachedValue = !expiration && !memoryExpiration;
589
- if (canUseCachedValue && raw === lastRaw && hasLastValue) {
590
- return lastValue;
667
+ if (!memoryExpiration && raw === lastRaw && hasLastValue) {
668
+ if (!expiration || lastExpiresAt === null) {
669
+ return lastValue;
670
+ }
671
+ if (typeof lastExpiresAt === "number") {
672
+ if (lastExpiresAt > Date.now()) {
673
+ return lastValue;
674
+ }
675
+ removeStoredRaw();
676
+ invalidateParsedCache();
677
+ onExpired?.(storageKey);
678
+ lastValue = ensureValidatedValue(defaultValue, false);
679
+ hasLastValue = true;
680
+ return lastValue;
681
+ }
591
682
  }
592
683
  lastRaw = raw;
593
684
  if (raw === undefined) {
594
- lastValue = ensureValidatedValue(config.defaultValue, false);
685
+ lastExpiresAt = undefined;
686
+ lastValue = ensureValidatedValue(defaultValue, false);
595
687
  hasLastValue = true;
596
688
  return lastValue;
597
689
  }
598
690
  if (isMemory) {
691
+ lastExpiresAt = undefined;
599
692
  lastValue = ensureValidatedValue(raw, true);
600
693
  hasLastValue = true;
601
694
  return lastValue;
602
695
  }
696
+ if (typeof raw !== "string") {
697
+ lastExpiresAt = undefined;
698
+ lastValue = ensureValidatedValue(defaultValue, false);
699
+ hasLastValue = true;
700
+ return lastValue;
701
+ }
603
702
  let deserializableRaw = raw;
604
703
  if (expiration) {
704
+ let envelopeExpiresAt = null;
605
705
  try {
606
706
  const parsed = JSON.parse(raw);
607
707
  if (isStoredEnvelope(parsed)) {
708
+ envelopeExpiresAt = parsed.expiresAt;
608
709
  if (parsed.expiresAt <= Date.now()) {
609
710
  removeStoredRaw();
610
711
  invalidateParsedCache();
611
712
  onExpired?.(storageKey);
612
- lastValue = ensureValidatedValue(config.defaultValue, false);
713
+ lastValue = ensureValidatedValue(defaultValue, false);
613
714
  hasLastValue = true;
614
715
  return lastValue;
615
716
  }
@@ -618,14 +719,16 @@ export function createStorageItem(config) {
618
719
  } catch {
619
720
  // Keep backward compatibility with legacy raw values.
620
721
  }
722
+ lastExpiresAt = envelopeExpiresAt;
723
+ } else {
724
+ lastExpiresAt = undefined;
621
725
  }
622
726
  lastValue = ensureValidatedValue(deserialize(deserializableRaw), true);
623
727
  hasLastValue = true;
624
728
  return lastValue;
625
729
  };
626
730
  const set = valueOrFn => {
627
- const currentValue = get();
628
- const newValue = typeof valueOrFn === "function" ? valueOrFn(currentValue) : valueOrFn;
731
+ const newValue = isUpdater(valueOrFn) ? valueOrFn(get()) : valueOrFn;
629
732
  invalidateParsedCache();
630
733
  if (validate && !validate(newValue)) {
631
734
  throw new Error(`Validation failed for key "${storageKey}" in scope "${StorageScope[config.scope]}".`);
@@ -676,44 +779,21 @@ export function createStorageItem(config) {
676
779
  _hasExpiration: expiration !== undefined,
677
780
  _readCacheEnabled: readCache,
678
781
  _isBiometric: isBiometric,
679
- _secureAccessControl: secureAccessControl,
782
+ ...(secureAccessControl !== undefined ? {
783
+ _secureAccessControl: secureAccessControl
784
+ } : {}),
680
785
  scope: config.scope,
681
786
  key: storageKey
682
787
  };
683
788
  return storageItem;
684
789
  }
685
- export function useStorage(item) {
686
- const value = useSyncExternalStore(item.subscribe, item.get, item.get);
687
- return [value, item.set];
688
- }
689
- export function useStorageSelector(item, selector, isEqual = Object.is) {
690
- const selectedRef = useRef({
691
- hasValue: false
692
- });
693
- const getSelectedSnapshot = () => {
694
- const nextSelected = selector(item.get());
695
- const current = selectedRef.current;
696
- if (current.hasValue && isEqual(current.value, nextSelected)) {
697
- return current.value;
698
- }
699
- selectedRef.current = {
700
- hasValue: true,
701
- value: nextSelected
702
- };
703
- return nextSelected;
704
- };
705
- const selectedValue = useSyncExternalStore(item.subscribe, getSelectedSnapshot, getSelectedSnapshot);
706
- return [selectedValue, item.set];
707
- }
708
- export function useSetStorage(item) {
709
- return item.set;
710
- }
790
+ export { useStorage, useStorageSelector, useSetStorage } from "./storage-hooks";
711
791
  export function getBatch(items, scope) {
712
792
  assertBatchScope(items, scope);
713
793
  if (scope === StorageScope.Memory) {
714
794
  return items.map(item => item.get());
715
795
  }
716
- const useRawBatchPath = items.every(item => canUseRawBatchPath(item));
796
+ const useRawBatchPath = items.every(item => scope === StorageScope.Secure ? canUseSecureRawBatchPath(item) : canUseRawBatchPath(item));
717
797
  if (!useRawBatchPath) {
718
798
  return items.map(item => item.get());
719
799
  }
@@ -742,6 +822,9 @@ export function getBatch(items, scope) {
742
822
  fetchedValues.forEach((value, index) => {
743
823
  const key = keysToFetch[index];
744
824
  const targetIndex = keyIndexes[index];
825
+ if (key === undefined || targetIndex === undefined) {
826
+ return;
827
+ }
745
828
  rawValues[targetIndex] = value;
746
829
  cacheRawValue(scope, key, value);
747
830
  });
@@ -763,6 +846,51 @@ export function setBatch(items, scope) {
763
846
  }) => item.set(value));
764
847
  return;
765
848
  }
849
+ if (scope === StorageScope.Secure) {
850
+ const secureEntries = items.map(({
851
+ item,
852
+ value
853
+ }) => ({
854
+ item,
855
+ value,
856
+ internal: asInternal(item)
857
+ }));
858
+ const canUseSecureBatchPath = secureEntries.every(({
859
+ internal
860
+ }) => canUseSecureRawBatchPath(internal));
861
+ if (!canUseSecureBatchPath) {
862
+ items.forEach(({
863
+ item,
864
+ value
865
+ }) => item.set(value));
866
+ return;
867
+ }
868
+ flushSecureWrites();
869
+ const groupedByAccessControl = new Map();
870
+ secureEntries.forEach(({
871
+ item,
872
+ value,
873
+ internal
874
+ }) => {
875
+ const accessControl = internal._secureAccessControl ?? AccessControl.WhenUnlocked;
876
+ const existingGroup = groupedByAccessControl.get(accessControl);
877
+ const group = existingGroup ?? {
878
+ keys: [],
879
+ values: []
880
+ };
881
+ group.keys.push(item.key);
882
+ group.values.push(item.serialize(value));
883
+ if (!existingGroup) {
884
+ groupedByAccessControl.set(accessControl, group);
885
+ }
886
+ });
887
+ groupedByAccessControl.forEach((group, accessControl) => {
888
+ WebStorage.setSecureAccessControl(accessControl);
889
+ WebStorage.setBatch(group.keys, group.values, scope);
890
+ group.keys.forEach((key, index) => cacheRawValue(scope, key, group.values[index]));
891
+ });
892
+ return;
893
+ }
766
894
  const useRawBatchPath = items.every(({
767
895
  item
768
896
  }) => canUseRawBatchPath(asInternal(item)));
@@ -775,9 +903,6 @@ export function setBatch(items, scope) {
775
903
  }
776
904
  const keys = items.map(entry => entry.item.key);
777
905
  const values = items.map(entry => entry.item.serialize(entry.value));
778
- if (scope === StorageScope.Secure) {
779
- flushSecureWrites();
780
- }
781
906
  WebStorage.setBatch(keys, values, scope);
782
907
  keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
783
908
  }
@@ -879,18 +1004,25 @@ export function runTransaction(scope, transaction) {
879
1004
  export function createSecureAuthStorage(config, options) {
880
1005
  const ns = options?.namespace ?? "auth";
881
1006
  const result = {};
882
- for (const key of Object.keys(config)) {
1007
+ for (const key of typedKeys(config)) {
883
1008
  const itemConfig = config[key];
1009
+ const expirationConfig = itemConfig.ttlMs !== undefined ? {
1010
+ ttlMs: itemConfig.ttlMs
1011
+ } : undefined;
884
1012
  result[key] = createStorageItem({
885
1013
  key,
886
1014
  scope: StorageScope.Secure,
887
1015
  defaultValue: "",
888
1016
  namespace: ns,
889
- biometric: itemConfig.biometric,
890
- accessControl: itemConfig.accessControl,
891
- expiration: itemConfig.ttlMs ? {
892
- ttlMs: itemConfig.ttlMs
893
- } : undefined
1017
+ ...(itemConfig.biometric !== undefined ? {
1018
+ biometric: itemConfig.biometric
1019
+ } : {}),
1020
+ ...(itemConfig.accessControl !== undefined ? {
1021
+ accessControl: itemConfig.accessControl
1022
+ } : {}),
1023
+ ...(expirationConfig !== undefined ? {
1024
+ expiration: expirationConfig
1025
+ } : {})
894
1026
  });
895
1027
  }
896
1028
  return result;