react-native-nitro-storage 0.5.9 → 0.7.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 (60) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +193 -16
  3. package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +225 -76
  4. package/app.plugin.js +2 -0
  5. package/cpp/bindings/HybridStorage.cpp +29 -21
  6. package/cpp/bindings/HybridStorage.hpp +5 -0
  7. package/docs/secure-storage.md +4 -0
  8. package/lib/commonjs/index.js +19 -2
  9. package/lib/commonjs/index.js.map +1 -1
  10. package/lib/commonjs/index.web.js +19 -2
  11. package/lib/commonjs/index.web.js.map +1 -1
  12. package/lib/commonjs/indexeddb-backend.js.map +1 -1
  13. package/lib/commonjs/internal.js.map +1 -1
  14. package/lib/commonjs/shared.js.map +1 -1
  15. package/lib/commonjs/storage-core.js +306 -10
  16. package/lib/commonjs/storage-core.js.map +1 -1
  17. package/lib/commonjs/storage-events.js.map +1 -1
  18. package/lib/commonjs/storage-hooks.js +16 -1
  19. package/lib/commonjs/storage-hooks.js.map +1 -1
  20. package/lib/commonjs/testing.js +267 -0
  21. package/lib/commonjs/testing.js.map +1 -0
  22. package/lib/module/index.js +5 -1
  23. package/lib/module/index.js.map +1 -1
  24. package/lib/module/index.web.js +5 -1
  25. package/lib/module/index.web.js.map +1 -1
  26. package/lib/module/indexeddb-backend.js.map +1 -1
  27. package/lib/module/internal.js.map +1 -1
  28. package/lib/module/shared.js.map +1 -1
  29. package/lib/module/storage-core.js +307 -11
  30. package/lib/module/storage-core.js.map +1 -1
  31. package/lib/module/storage-events.js.map +1 -1
  32. package/lib/module/storage-hooks.js +15 -2
  33. package/lib/module/storage-hooks.js.map +1 -1
  34. package/lib/module/testing.js +188 -0
  35. package/lib/module/testing.js.map +1 -0
  36. package/lib/typescript/index.d.ts +28 -5
  37. package/lib/typescript/index.d.ts.map +1 -1
  38. package/lib/typescript/index.web.d.ts +28 -5
  39. package/lib/typescript/index.web.d.ts.map +1 -1
  40. package/lib/typescript/indexeddb-backend.d.ts.map +1 -1
  41. package/lib/typescript/shared.d.ts +1 -0
  42. package/lib/typescript/shared.d.ts.map +1 -1
  43. package/lib/typescript/storage-core.d.ts +62 -3
  44. package/lib/typescript/storage-core.d.ts.map +1 -1
  45. package/lib/typescript/storage-events.d.ts +1 -1
  46. package/lib/typescript/storage-events.d.ts.map +1 -1
  47. package/lib/typescript/storage-hooks.d.ts +18 -1
  48. package/lib/typescript/storage-hooks.d.ts.map +1 -1
  49. package/lib/typescript/testing.d.ts +158 -0
  50. package/lib/typescript/testing.d.ts.map +1 -0
  51. package/package.json +11 -6
  52. package/src/index.ts +17 -4
  53. package/src/index.web.ts +17 -4
  54. package/src/indexeddb-backend.ts +1 -2
  55. package/src/internal.ts +1 -1
  56. package/src/shared.ts +1 -0
  57. package/src/storage-core.ts +463 -11
  58. package/src/storage-events.ts +3 -2
  59. package/src/storage-hooks.ts +42 -3
  60. package/src/testing.ts +270 -0
@@ -1,14 +1,17 @@
1
1
  "use strict";
2
2
 
3
- import { assertAccessControlLevel, assertBiometricLevel, canUseRawBatchPath, canUseSecureRawBatchPath, createKeyChange, defaultDeserialize, defaultSerialize, isUpdater, notifyAllListeners, notifyKeyListeners, now, redactSecureKeyChange, runMicrotask, typedKeys } from "./shared";
3
+ import { assertAccessControlLevel, assertBiometricLevel, canUseRawBatchPath, canUseSecureRawBatchPath, createKeyChange, defaultDeserialize, defaultSerialize, isKeychainLockedError, isUpdater, notifyAllListeners, notifyKeyListeners, now, redactSecureKeyChange, runMicrotask, typedKeys } from "./shared";
4
4
  import { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
5
5
  import { MIGRATION_VERSION_KEY, isStoredEnvelope, assertBatchScope, assertValidScope, toVersionToken, prefixKey, isNamespaced } from "./internal";
6
6
  import { StorageEventRegistry } from "./storage-events";
7
+ const EMPTY_KEYS = Object.freeze([]);
7
8
  function asInternal(item) {
8
9
  return item;
9
10
  }
10
11
  export function createStorageCore(buildAdapter) {
11
12
  const registeredMigrations = new Map();
13
+ const itemGroups = new Map();
14
+ const registeredKeyCounts = new Map();
12
15
  const memoryStore = new Map();
13
16
  const memoryListeners = new Map();
14
17
  const scopedListeners = new Map([[StorageScope.Disk, new Map()], [StorageScope.Secure, new Map()]]);
@@ -355,6 +358,40 @@ export function createStorageCore(buildAdapter) {
355
358
  }
356
359
  };
357
360
  const adapter = buildAdapter(internals);
361
+ function resolveKeyRef(ref) {
362
+ return typeof ref === "string" ? ref : ref.key;
363
+ }
364
+ function clearScopeExcept(scope, exceptRefs) {
365
+ measureOperation("storage:clearExcept", scope, () => {
366
+ assertValidScope(scope);
367
+ const keep = new Set(exceptRefs.map(resolveKeyRef));
368
+ if (scope === StorageScope.Memory) {
369
+ const removeKeys = Array.from(memoryStore.keys()).filter(key => !keep.has(key));
370
+ if (removeKeys.length === 0) {
371
+ return;
372
+ }
373
+ const previousValues = shouldReadPreviousEventValues(scope) ? removeKeys.map(key => getEventRawValue(scope, key)) : [];
374
+ removeKeys.forEach(key => memoryStore.delete(key));
375
+ removeKeys.forEach(key => notifyKeyListeners(memoryListeners, key));
376
+ emitBatchChange(scope, "clear", "memory", removeKeys.map((key, index) => createKeyChange(scope, key, previousValues[index], undefined, "clear", "memory")));
377
+ return;
378
+ }
379
+ if (scope === StorageScope.Disk) {
380
+ flushDiskWrites();
381
+ }
382
+ if (scope === StorageScope.Secure) {
383
+ flushSecureWrites();
384
+ }
385
+ const removeKeys = adapter.backend.getAllKeys(scope).filter(key => !keep.has(key));
386
+ if (removeKeys.length === 0) {
387
+ return;
388
+ }
389
+ const previousValues = shouldReadPreviousEventValues(scope) ? adapter.backend.getBatch(removeKeys, scope) : [];
390
+ adapter.backend.removeBatch(removeKeys, scope);
391
+ removeKeys.forEach(key => cacheRawValue(scope, key, undefined));
392
+ emitBatchChange(scope, "clear", adapter.changeSource, removeKeys.map((key, index) => createKeyChange(scope, key, previousValues[index], undefined, "clear", adapter.changeSource)));
393
+ });
394
+ }
358
395
  const storage = {
359
396
  subscribe: (scope, listener) => {
360
397
  assertValidScope(scope);
@@ -406,7 +443,11 @@ export function createStorageCore(buildAdapter) {
406
443
  adapter.maybeCleanupScopeSubscription(StorageScope.Disk);
407
444
  adapter.maybeCleanupScopeSubscription(StorageScope.Secure);
408
445
  },
409
- clear: scope => {
446
+ clear: (scope, options) => {
447
+ if (options?.except && options.except.length > 0) {
448
+ clearScopeExcept(scope, options.except);
449
+ return;
450
+ }
410
451
  measureOperation("storage:clear", scope, () => {
411
452
  const previousValues = shouldReadPreviousEventValues(scope) ? storage.getAll(scope) : {};
412
453
  if (scope === StorageScope.Memory) {
@@ -435,6 +476,71 @@ export function createStorageCore(buildAdapter) {
435
476
  storage.clear(StorageScope.Secure);
436
477
  }, 3);
437
478
  },
479
+ clearGroup: group => {
480
+ const items = itemGroups.get(group);
481
+ if (!items || items.size === 0) {
482
+ return;
483
+ }
484
+ measureOperation("storage:clearGroup", StorageScope.Memory, () => {
485
+ const byScope = new Map();
486
+ for (const item of items) {
487
+ const scoped = byScope.get(item.scope);
488
+ if (scoped) {
489
+ scoped.push(item);
490
+ } else {
491
+ byScope.set(item.scope, [item]);
492
+ }
493
+ }
494
+ byScope.forEach((groupItems, scope) => {
495
+ removeBatch(groupItems, scope);
496
+ });
497
+ }, items.size);
498
+ },
499
+ getGroupItems: group => {
500
+ const items = itemGroups.get(group);
501
+ return items ? Array.from(items) : [];
502
+ },
503
+ subscribeExpired: (scope, listener) => {
504
+ return storage.subscribe(scope, event => {
505
+ if (event.type === "key") {
506
+ if (event.operation === "expire") {
507
+ listener(event);
508
+ }
509
+ return;
510
+ }
511
+ event.changes.forEach(change => {
512
+ if (change.operation === "expire") {
513
+ listener(change);
514
+ }
515
+ });
516
+ });
517
+ },
518
+ findDuplicateKeys: () => {
519
+ const duplicates = [];
520
+ registeredKeyCounts.forEach((count, registryKey) => {
521
+ if (count <= 1) {
522
+ return;
523
+ }
524
+ const separatorIndex = registryKey.indexOf(":");
525
+ duplicates.push({
526
+ scope: Number(registryKey.slice(0, separatorIndex)),
527
+ key: registryKey.slice(separatorIndex + 1),
528
+ count
529
+ });
530
+ });
531
+ return duplicates;
532
+ },
533
+ getRegisteredKeys: () => {
534
+ const result = [];
535
+ registeredKeyCounts.forEach((_count, registryKey) => {
536
+ const separatorIndex = registryKey.indexOf(":");
537
+ result.push({
538
+ scope: Number(registryKey.slice(0, separatorIndex)),
539
+ key: registryKey.slice(separatorIndex + 1)
540
+ });
541
+ });
542
+ return result;
543
+ },
438
544
  clearNamespace: (namespace, scope) => {
439
545
  measureOperation("storage:clearNamespace", scope, () => {
440
546
  assertValidScope(scope);
@@ -734,6 +840,10 @@ export function createStorageCore(buildAdapter) {
734
840
  const coalesceSecureWrites = config.scope === StorageScope.Secure && config.coalesceSecureWrites === true && !isBiometric;
735
841
  const defaultValue = config.defaultValue;
736
842
  const nonMemoryScope = config.scope === StorageScope.Disk ? StorageScope.Disk : config.scope === StorageScope.Secure ? StorageScope.Secure : null;
843
+ const renameFromKeys = config.renameFrom === undefined ? EMPTY_KEYS : typeof config.renameFrom === "string" ? [config.renameFrom] : config.renameFrom;
844
+ const fallbackToCacheOnReadError = config.fallbackToCacheOnReadError === true;
845
+ const onReadError = config.onReadError;
846
+ let renamesMigrated = isMemory || renameFromKeys.length === 0;
737
847
  if (expiration && expiration.ttlMs <= 0) {
738
848
  throw new Error("expiration.ttlMs must be greater than 0.");
739
849
  }
@@ -775,15 +885,18 @@ export function createStorageCore(buildAdapter) {
775
885
  if (memoryExpiration) {
776
886
  const expiresAt = memoryExpiration.get(storageKey);
777
887
  if (expiresAt !== undefined && expiresAt <= Date.now()) {
888
+ const expiredRaw = memoryStore.get(storageKey);
778
889
  memoryExpiration.delete(storageKey);
779
890
  memoryStore.delete(storageKey);
780
891
  notifyKeyListeners(memoryListeners, storageKey);
892
+ emitKeyChange(config.scope, storageKey, typeof expiredRaw === "string" ? expiredRaw : undefined, undefined, "expire", "memory");
781
893
  onExpired?.(storageKey);
782
894
  return undefined;
783
895
  }
784
896
  }
785
897
  return memoryStore.get(storageKey);
786
898
  }
899
+ migrateRenamesIfNeeded();
787
900
  if (nonMemoryScope === StorageScope.Disk) {
788
901
  const pending = pendingDiskWrites.get(storageKey);
789
902
  if (pending !== undefined) {
@@ -804,12 +917,27 @@ export function createStorageCore(buildAdapter) {
804
917
  }
805
918
  }
806
919
  if (isBiometric) {
807
- return adapter.backend.getSecureBiometric(storageKey);
920
+ return readBackendRaw(() => adapter.backend.getSecureBiometric(storageKey));
808
921
  }
809
- const raw = adapter.backend.get(storageKey, config.scope);
922
+ const raw = readBackendRaw(() => adapter.backend.get(storageKey, config.scope));
810
923
  cacheRawValue(nonMemoryScope, storageKey, raw);
811
924
  return raw;
812
925
  };
926
+ const readBackendRaw = read => {
927
+ try {
928
+ return read();
929
+ } catch (error) {
930
+ onReadError?.(error);
931
+ if (fallbackToCacheOnReadError) {
932
+ const cached = getScopeRawCache(nonMemoryScope).get(storageKey);
933
+ if (cached !== undefined) {
934
+ return cached;
935
+ }
936
+ return typeof lastRaw === "string" ? lastRaw : undefined;
937
+ }
938
+ throw error;
939
+ }
940
+ };
813
941
  const writeStoredRaw = rawValue => {
814
942
  const oldValue = undefined;
815
943
  if (isBiometric) {
@@ -840,32 +968,73 @@ export function createStorageCore(buildAdapter) {
840
968
  adapter.backend.set(storageKey, rawValue, config.scope);
841
969
  emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", adapter.changeSource);
842
970
  };
843
- const removeStoredRaw = () => {
971
+ const migrateRenamesIfNeeded = () => {
972
+ if (renamesMigrated) {
973
+ return;
974
+ }
975
+ try {
976
+ const hasCurrent = isBiometric ? adapter.backend.hasSecureBiometric(storageKey) : adapter.backend.has(storageKey, config.scope);
977
+ if (hasCurrent) {
978
+ for (const legacyKey of renameFromKeys) {
979
+ if (isBiometric) {
980
+ if (adapter.backend.hasSecureBiometric(legacyKey)) {
981
+ adapter.backend.deleteSecureBiometric(legacyKey);
982
+ }
983
+ } else if (adapter.backend.has(legacyKey, config.scope)) {
984
+ adapter.backend.remove(legacyKey, config.scope);
985
+ }
986
+ }
987
+ renamesMigrated = true;
988
+ return;
989
+ }
990
+ for (const legacyKey of renameFromKeys) {
991
+ const legacyRaw = isBiometric ? adapter.backend.getSecureBiometric(legacyKey) : adapter.backend.get(legacyKey, config.scope);
992
+ if (legacyRaw === undefined) {
993
+ continue;
994
+ }
995
+ writeStoredRaw(legacyRaw);
996
+ if (isBiometric) {
997
+ adapter.backend.deleteSecureBiometric(legacyKey);
998
+ } else {
999
+ adapter.backend.remove(legacyKey, config.scope);
1000
+ }
1001
+ break;
1002
+ }
1003
+ renamesMigrated = true;
1004
+ } catch (error) {
1005
+ if (isKeychainLockedError(error)) {
1006
+ onReadError?.(error);
1007
+ return;
1008
+ }
1009
+ throw error;
1010
+ }
1011
+ };
1012
+ const removeStoredRaw = (operation = "remove") => {
844
1013
  const oldValue = getEventRawValue(config.scope, storageKey);
845
1014
  if (isBiometric) {
846
1015
  adapter.backend.deleteSecureBiometric(storageKey);
847
- emitKeyChange(config.scope, storageKey, oldValue, undefined, "remove", adapter.changeSource);
1016
+ emitKeyChange(config.scope, storageKey, oldValue, undefined, operation, adapter.changeSource);
848
1017
  return;
849
1018
  }
850
1019
  cacheRawValue(nonMemoryScope, storageKey, undefined);
851
1020
  if (nonMemoryScope === StorageScope.Disk) {
852
1021
  if (coalesceDiskWrites || diskWritesAsync) {
853
1022
  scheduleDiskWrite(storageKey, undefined);
854
- emitKeyChange(config.scope, storageKey, oldValue, undefined, "remove", adapter.changeSource);
1023
+ emitKeyChange(config.scope, storageKey, oldValue, undefined, operation, adapter.changeSource);
855
1024
  return;
856
1025
  }
857
1026
  clearPendingDiskWrite(storageKey);
858
1027
  }
859
1028
  if (coalesceSecureWrites) {
860
1029
  scheduleSecureWrite(storageKey, undefined, secureAccessControl ?? secureDefaultAccessControl);
861
- emitKeyChange(config.scope, storageKey, oldValue, undefined, "remove", adapter.changeSource);
1030
+ emitKeyChange(config.scope, storageKey, oldValue, undefined, operation, adapter.changeSource);
862
1031
  return;
863
1032
  }
864
1033
  if (nonMemoryScope === StorageScope.Secure) {
865
1034
  clearPendingSecureWrite(storageKey);
866
1035
  }
867
1036
  adapter.backend.remove(storageKey, config.scope);
868
- emitKeyChange(config.scope, storageKey, oldValue, undefined, "remove", adapter.changeSource);
1037
+ emitKeyChange(config.scope, storageKey, oldValue, undefined, operation, adapter.changeSource);
869
1038
  };
870
1039
  const writeValueWithoutValidation = value => {
871
1040
  if (isMemory) {
@@ -919,7 +1088,7 @@ export function createStorageCore(buildAdapter) {
919
1088
  if (lastExpiresAt > Date.now()) {
920
1089
  return lastValue;
921
1090
  }
922
- removeStoredRaw();
1091
+ removeStoredRaw("expire");
923
1092
  invalidateParsedCache();
924
1093
  onExpired?.(storageKey);
925
1094
  lastValue = ensureValidatedValue(defaultValue, false);
@@ -955,7 +1124,7 @@ export function createStorageCore(buildAdapter) {
955
1124
  if (isStoredEnvelope(parsed)) {
956
1125
  envelopeExpiresAt = parsed.expiresAt;
957
1126
  if (parsed.expiresAt <= Date.now()) {
958
- removeStoredRaw();
1127
+ removeStoredRaw("expire");
959
1128
  invalidateParsedCache();
960
1129
  onExpired?.(storageKey);
961
1130
  lastValue = ensureValidatedValue(defaultValue, false);
@@ -1019,6 +1188,27 @@ export function createStorageCore(buildAdapter) {
1019
1188
  removeStoredRaw();
1020
1189
  });
1021
1190
  };
1191
+ const merge = partial => {
1192
+ set(prev => {
1193
+ if (prev === null || typeof prev !== "object") {
1194
+ return partial;
1195
+ }
1196
+ return {
1197
+ ...prev,
1198
+ ...partial
1199
+ };
1200
+ });
1201
+ };
1202
+ const reset = () => {
1203
+ deleteItem();
1204
+ };
1205
+ const setOrDelete = value => {
1206
+ if (value === null || value === undefined) {
1207
+ deleteItem();
1208
+ return;
1209
+ }
1210
+ set(value);
1211
+ };
1022
1212
  const hasItem = () => measureOperation("item:has", config.scope, () => {
1023
1213
  if (isMemory) return memoryStore.has(storageKey);
1024
1214
  if (isBiometric) return adapter.backend.hasSecureBiometric(storageKey);
@@ -1071,6 +1261,9 @@ export function createStorageCore(buildAdapter) {
1071
1261
  getWithVersion,
1072
1262
  set,
1073
1263
  setIfVersion,
1264
+ merge,
1265
+ reset,
1266
+ setOrDelete,
1074
1267
  delete: deleteItem,
1075
1268
  has: hasItem,
1076
1269
  subscribe,
@@ -1093,9 +1286,22 @@ export function createStorageCore(buildAdapter) {
1093
1286
  ...(secureAccessControl !== undefined ? {
1094
1287
  _secureAccessControl: secureAccessControl
1095
1288
  } : {}),
1289
+ ...(config.group !== undefined ? {
1290
+ _group: config.group
1291
+ } : {}),
1096
1292
  scope: config.scope,
1097
1293
  key: storageKey
1098
1294
  };
1295
+ if (config.group !== undefined) {
1296
+ let groupSet = itemGroups.get(config.group);
1297
+ if (!groupSet) {
1298
+ groupSet = new Set();
1299
+ itemGroups.set(config.group, groupSet);
1300
+ }
1301
+ groupSet.add(storageItem);
1302
+ }
1303
+ const registryKey = `${config.scope}:${storageKey}`;
1304
+ registeredKeyCounts.set(registryKey, (registeredKeyCounts.get(registryKey) ?? 0) + 1);
1099
1305
  return storageItem;
1100
1306
  }
1101
1307
  function getBatch(items, scope) {
@@ -1480,14 +1686,104 @@ export function createStorageCore(buildAdapter) {
1480
1686
  } : {}),
1481
1687
  ...(expirationConfig !== undefined ? {
1482
1688
  expiration: expirationConfig
1689
+ } : {}),
1690
+ ...(itemConfig.renameFrom !== undefined ? {
1691
+ renameFrom: itemConfig.renameFrom
1692
+ } : {}),
1693
+ ...(options?.group !== undefined ? {
1694
+ group: options.group
1695
+ } : {}),
1696
+ ...(options?.fallbackToCacheOnReadError !== undefined ? {
1697
+ fallbackToCacheOnReadError: options.fallbackToCacheOnReadError
1483
1698
  } : {})
1484
1699
  });
1485
1700
  }
1486
1701
  return result;
1487
1702
  }
1703
+ function memoryItem(config) {
1704
+ return createStorageItem({
1705
+ ...config,
1706
+ scope: StorageScope.Memory
1707
+ });
1708
+ }
1709
+ function diskItem(config) {
1710
+ return createStorageItem({
1711
+ ...config,
1712
+ scope: StorageScope.Disk
1713
+ });
1714
+ }
1715
+ function secureItem(config) {
1716
+ return createStorageItem({
1717
+ ...config,
1718
+ scope: StorageScope.Secure
1719
+ });
1720
+ }
1721
+ function createSetItem(config) {
1722
+ const {
1723
+ defaultValue,
1724
+ ...rest
1725
+ } = config;
1726
+ const initial = {};
1727
+ if (defaultValue) {
1728
+ for (const id of defaultValue) {
1729
+ initial[id] = true;
1730
+ }
1731
+ }
1732
+ const item = createStorageItem({
1733
+ ...rest,
1734
+ defaultValue: initial
1735
+ });
1736
+ const has = id => item.get()[id] === true;
1737
+ const add = id => {
1738
+ if (item.get()[id] === true) {
1739
+ return;
1740
+ }
1741
+ item.merge({
1742
+ [id]: true
1743
+ });
1744
+ };
1745
+ const deleteId = id => {
1746
+ const current = item.get();
1747
+ if (current[id] !== true) {
1748
+ return;
1749
+ }
1750
+ const next = {
1751
+ ...current
1752
+ };
1753
+ delete next[id];
1754
+ item.set(next);
1755
+ };
1756
+ const toggle = id => {
1757
+ if (has(id)) {
1758
+ deleteId(id);
1759
+ return false;
1760
+ }
1761
+ add(id);
1762
+ return true;
1763
+ };
1764
+ return {
1765
+ get: item.get,
1766
+ has,
1767
+ add,
1768
+ delete: deleteId,
1769
+ toggle,
1770
+ values: () => Object.keys(item.get()),
1771
+ size: () => Object.keys(item.get()).length,
1772
+ clear: () => item.set({}),
1773
+ reset: item.reset,
1774
+ subscribe: item.subscribe,
1775
+ scope: item.scope,
1776
+ key: item.key,
1777
+ item
1778
+ };
1779
+ }
1488
1780
  return {
1489
1781
  storage,
1490
1782
  createStorageItem,
1783
+ memoryItem,
1784
+ diskItem,
1785
+ secureItem,
1786
+ createSetItem,
1491
1787
  getBatch,
1492
1788
  setBatch,
1493
1789
  removeBatch,