react-native-nitro-storage 0.5.9 → 0.6.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 (50) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +171 -7
  3. package/cpp/bindings/HybridStorage.cpp +25 -9
  4. package/cpp/bindings/HybridStorage.hpp +5 -0
  5. package/lib/commonjs/index.js +19 -2
  6. package/lib/commonjs/index.js.map +1 -1
  7. package/lib/commonjs/index.web.js +19 -2
  8. package/lib/commonjs/index.web.js.map +1 -1
  9. package/lib/commonjs/shared.js.map +1 -1
  10. package/lib/commonjs/storage-core.js +306 -10
  11. package/lib/commonjs/storage-core.js.map +1 -1
  12. package/lib/commonjs/storage-events.js.map +1 -1
  13. package/lib/commonjs/storage-hooks.js +16 -1
  14. package/lib/commonjs/storage-hooks.js.map +1 -1
  15. package/lib/commonjs/testing.js +267 -0
  16. package/lib/commonjs/testing.js.map +1 -0
  17. package/lib/module/index.js +5 -1
  18. package/lib/module/index.js.map +1 -1
  19. package/lib/module/index.web.js +5 -1
  20. package/lib/module/index.web.js.map +1 -1
  21. package/lib/module/shared.js.map +1 -1
  22. package/lib/module/storage-core.js +307 -11
  23. package/lib/module/storage-core.js.map +1 -1
  24. package/lib/module/storage-events.js.map +1 -1
  25. package/lib/module/storage-hooks.js +15 -2
  26. package/lib/module/storage-hooks.js.map +1 -1
  27. package/lib/module/testing.js +188 -0
  28. package/lib/module/testing.js.map +1 -0
  29. package/lib/typescript/index.d.ts +22 -4
  30. package/lib/typescript/index.d.ts.map +1 -1
  31. package/lib/typescript/index.web.d.ts +22 -4
  32. package/lib/typescript/index.web.d.ts.map +1 -1
  33. package/lib/typescript/shared.d.ts +1 -0
  34. package/lib/typescript/shared.d.ts.map +1 -1
  35. package/lib/typescript/storage-core.d.ts +51 -2
  36. package/lib/typescript/storage-core.d.ts.map +1 -1
  37. package/lib/typescript/storage-events.d.ts +1 -1
  38. package/lib/typescript/storage-events.d.ts.map +1 -1
  39. package/lib/typescript/storage-hooks.d.ts +18 -1
  40. package/lib/typescript/storage-hooks.d.ts.map +1 -1
  41. package/lib/typescript/testing.d.ts +148 -0
  42. package/lib/typescript/testing.d.ts.map +1 -0
  43. package/package.json +7 -1
  44. package/src/index.ts +16 -2
  45. package/src/index.web.ts +16 -2
  46. package/src/shared.ts +1 -0
  47. package/src/storage-core.ts +446 -11
  48. package/src/storage-events.ts +2 -0
  49. package/src/storage-hooks.ts +42 -3
  50. package/src/testing.ts +270 -0
package/src/index.web.ts CHANGED
@@ -57,7 +57,7 @@ export type {
57
57
  StorageEventListener,
58
58
  StorageKeyChangeEvent,
59
59
  } from "./storage-events";
60
- export type { StorageSetter } from "./storage-hooks";
60
+ export type { StorageActions, StorageSetter } from "./storage-hooks";
61
61
  export type {
62
62
  WebDiskStorageBackend,
63
63
  WebSecureStorageBackend,
@@ -66,9 +66,13 @@ export type {
66
66
  WebStorageScope,
67
67
  } from "./web-storage-backend";
68
68
  export type {
69
+ SetItemConfig,
70
+ SetStorageItem,
69
71
  StorageBatchSetItem,
72
+ StorageClearOptions,
70
73
  StorageItem,
71
74
  StorageItemConfig,
75
+ StorageKeyRef,
72
76
  TransactionContext,
73
77
  } from "./storage-core";
74
78
 
@@ -818,6 +822,10 @@ export const storage = {
818
822
  };
819
823
 
820
824
  export const createStorageItem = core.createStorageItem;
825
+ export const memoryItem = core.memoryItem;
826
+ export const diskItem = core.diskItem;
827
+ export const secureItem = core.secureItem;
828
+ export const createSetItem = core.createSetItem;
821
829
  export const getBatch = core.getBatch;
822
830
  export const setBatch = core.setBatch;
823
831
  export const removeBatch = core.removeBatch;
@@ -886,5 +894,11 @@ export async function flushWebStorageBackends(): Promise<void> {
886
894
  await Promise.all(flushes);
887
895
  }
888
896
 
889
- export { useStorage, useStorageSelector, useSetStorage } from "./storage-hooks";
897
+ export {
898
+ useSetStorage,
899
+ useStorage,
900
+ useStorageActions,
901
+ useStorageSelector,
902
+ useStorageValue,
903
+ } from "./storage-hooks";
890
904
  export { createIndexedDBBackend } from "./indexeddb-backend";
package/src/shared.ts CHANGED
@@ -241,6 +241,7 @@ export type SecureAuthStorageConfig<K extends string = string> = Record<
241
241
  biometric?: boolean;
242
242
  biometricLevel?: BiometricLevel;
243
243
  accessControl?: AccessControl;
244
+ renameFrom?: string | readonly string[];
244
245
  }
245
246
  >;
246
247
 
@@ -6,6 +6,7 @@ import {
6
6
  createKeyChange,
7
7
  defaultDeserialize,
8
8
  defaultSerialize,
9
+ isKeychainLockedError,
9
10
  isUpdater,
10
11
  notifyAllListeners,
11
12
  notifyKeyListeners,
@@ -87,6 +88,10 @@ export type StorageItemConfig<T> = {
87
88
  biometric?: boolean;
88
89
  biometricLevel?: BiometricLevel;
89
90
  accessControl?: AccessControl;
91
+ group?: string;
92
+ renameFrom?: string | readonly string[];
93
+ fallbackToCacheOnReadError?: boolean;
94
+ onReadError?: (error: unknown) => void;
90
95
  };
91
96
 
92
97
  export type StorageItem<T> = {
@@ -97,6 +102,9 @@ export type StorageItem<T> = {
97
102
  version: StorageVersion,
98
103
  value: T | ((prev: T) => T),
99
104
  ) => boolean;
105
+ merge: (partial: Partial<T>) => void;
106
+ reset: () => void;
107
+ setOrDelete: (value: T | null | undefined) => void;
100
108
  delete: () => void;
101
109
  has: () => boolean;
102
110
  subscribe: (callback: () => void) => () => void;
@@ -121,6 +129,7 @@ type StorageItemInternal<T> = StorageItem<T> & {
121
129
  _biometricLevel: BiometricLevel;
122
130
  _defaultValue: T;
123
131
  _secureAccessControl?: AccessControl;
132
+ _group?: string;
124
133
  };
125
134
 
126
135
  export type BatchReadItem<T> = Pick<
@@ -149,6 +158,35 @@ export type StorageBatchSetItem<T> = {
149
158
  value: T;
150
159
  };
151
160
 
161
+ export type StorageKeyRef = string | { readonly key: string };
162
+
163
+ export type StorageClearOptions = {
164
+ except?: readonly StorageKeyRef[];
165
+ };
166
+
167
+ export type SetItemConfig<TMember extends string = string> = Omit<
168
+ StorageItemConfig<Record<string, true>>,
169
+ "defaultValue" | "serialize" | "deserialize"
170
+ > & {
171
+ defaultValue?: readonly TMember[];
172
+ };
173
+
174
+ export type SetStorageItem<TMember extends string = string> = {
175
+ get: () => Record<string, true>;
176
+ has: (id: TMember) => boolean;
177
+ add: (id: TMember) => void;
178
+ delete: (id: TMember) => void;
179
+ toggle: (id: TMember) => boolean;
180
+ values: () => TMember[];
181
+ size: () => number;
182
+ clear: () => void;
183
+ reset: () => void;
184
+ subscribe: (callback: () => void) => () => void;
185
+ scope: StorageScope;
186
+ key: string;
187
+ item: StorageItem<Record<string, true>>;
188
+ };
189
+
152
190
  export type StorageCoreBackend = {
153
191
  get(key: string, scope: StorageScope): string | undefined;
154
192
  set(key: string, value: string, scope: StorageScope): void;
@@ -234,6 +272,8 @@ export type StorageCoreInternals = {
234
272
  setSecureDefaultAccessControl(level: AccessControl): void;
235
273
  };
236
274
 
275
+ const EMPTY_KEYS: readonly string[] = Object.freeze([]);
276
+
237
277
  function asInternal<T>(item: StorageItem<T>): StorageItemInternal<T> {
238
278
  return item as StorageItemInternal<T>;
239
279
  }
@@ -242,6 +282,8 @@ export function createStorageCore(
242
282
  buildAdapter: (internals: StorageCoreInternals) => StorageCoreAdapter,
243
283
  ) {
244
284
  const registeredMigrations = new Map<number, Migration>();
285
+ const itemGroups = new Map<string, Set<StorageItemInternal<unknown>>>();
286
+ const registeredKeyCounts = new Map<string, number>();
245
287
  const memoryStore = new Map<string, unknown>();
246
288
  const memoryListeners: KeyListenerRegistry = new Map();
247
289
  const scopedListeners = new Map<NonMemoryScope, KeyListenerRegistry>([
@@ -723,6 +765,84 @@ export function createStorageCore(
723
765
 
724
766
  const adapter = buildAdapter(internals);
725
767
 
768
+ function resolveKeyRef(ref: StorageKeyRef): string {
769
+ return typeof ref === "string" ? ref : ref.key;
770
+ }
771
+
772
+ function clearScopeExcept(
773
+ scope: StorageScope,
774
+ exceptRefs: readonly StorageKeyRef[],
775
+ ): void {
776
+ measureOperation("storage:clearExcept", scope, () => {
777
+ assertValidScope(scope);
778
+ const keep = new Set(exceptRefs.map(resolveKeyRef));
779
+
780
+ if (scope === StorageScope.Memory) {
781
+ const removeKeys = Array.from(memoryStore.keys()).filter(
782
+ (key) => !keep.has(key),
783
+ );
784
+ if (removeKeys.length === 0) {
785
+ return;
786
+ }
787
+ const previousValues = shouldReadPreviousEventValues(scope)
788
+ ? removeKeys.map((key) => getEventRawValue(scope, key))
789
+ : [];
790
+ removeKeys.forEach((key) => memoryStore.delete(key));
791
+ removeKeys.forEach((key) => notifyKeyListeners(memoryListeners, key));
792
+ emitBatchChange(
793
+ scope,
794
+ "clear",
795
+ "memory",
796
+ removeKeys.map((key, index) =>
797
+ createKeyChange(
798
+ scope,
799
+ key,
800
+ previousValues[index],
801
+ undefined,
802
+ "clear",
803
+ "memory",
804
+ ),
805
+ ),
806
+ );
807
+ return;
808
+ }
809
+
810
+ if (scope === StorageScope.Disk) {
811
+ flushDiskWrites();
812
+ }
813
+ if (scope === StorageScope.Secure) {
814
+ flushSecureWrites();
815
+ }
816
+
817
+ const removeKeys = adapter.backend
818
+ .getAllKeys(scope)
819
+ .filter((key) => !keep.has(key));
820
+ if (removeKeys.length === 0) {
821
+ return;
822
+ }
823
+ const previousValues = shouldReadPreviousEventValues(scope)
824
+ ? adapter.backend.getBatch(removeKeys, scope)
825
+ : [];
826
+ adapter.backend.removeBatch(removeKeys, scope);
827
+ removeKeys.forEach((key) => cacheRawValue(scope, key, undefined));
828
+ emitBatchChange(
829
+ scope,
830
+ "clear",
831
+ adapter.changeSource,
832
+ removeKeys.map((key, index) =>
833
+ createKeyChange(
834
+ scope,
835
+ key,
836
+ previousValues[index],
837
+ undefined,
838
+ "clear",
839
+ adapter.changeSource,
840
+ ),
841
+ ),
842
+ );
843
+ });
844
+ }
845
+
726
846
  const storage = {
727
847
  subscribe: (
728
848
  scope: StorageScope,
@@ -796,7 +916,11 @@ export function createStorageCore(
796
916
  adapter.maybeCleanupScopeSubscription(StorageScope.Disk);
797
917
  adapter.maybeCleanupScopeSubscription(StorageScope.Secure);
798
918
  },
799
- clear: (scope: StorageScope) => {
919
+ clear: (scope: StorageScope, options?: StorageClearOptions) => {
920
+ if (options?.except && options.except.length > 0) {
921
+ clearScopeExcept(scope, options.except);
922
+ return;
923
+ }
800
924
  measureOperation("storage:clear", scope, () => {
801
925
  const previousValues = shouldReadPreviousEventValues(scope)
802
926
  ? storage.getAll(scope)
@@ -863,6 +987,87 @@ export function createStorageCore(
863
987
  3,
864
988
  );
865
989
  },
990
+ clearGroup: (group: string) => {
991
+ const items = itemGroups.get(group);
992
+ if (!items || items.size === 0) {
993
+ return;
994
+ }
995
+ measureOperation(
996
+ "storage:clearGroup",
997
+ StorageScope.Memory,
998
+ () => {
999
+ const byScope = new Map<
1000
+ StorageScope,
1001
+ StorageItemInternal<unknown>[]
1002
+ >();
1003
+ for (const item of items) {
1004
+ const scoped = byScope.get(item.scope);
1005
+ if (scoped) {
1006
+ scoped.push(item);
1007
+ } else {
1008
+ byScope.set(item.scope, [item]);
1009
+ }
1010
+ }
1011
+ byScope.forEach((groupItems, scope) => {
1012
+ removeBatch(groupItems, scope);
1013
+ });
1014
+ },
1015
+ items.size,
1016
+ );
1017
+ },
1018
+ getGroupItems: (group: string): StorageItem<unknown>[] => {
1019
+ const items = itemGroups.get(group);
1020
+ return items ? Array.from(items) : [];
1021
+ },
1022
+ subscribeExpired: (
1023
+ scope: StorageScope,
1024
+ listener: (event: StorageKeyChangeEvent) => void,
1025
+ ): (() => void) => {
1026
+ return storage.subscribe(scope, (event) => {
1027
+ if (event.type === "key") {
1028
+ if (event.operation === "expire") {
1029
+ listener(event);
1030
+ }
1031
+ return;
1032
+ }
1033
+ event.changes.forEach((change) => {
1034
+ if (change.operation === "expire") {
1035
+ listener(change);
1036
+ }
1037
+ });
1038
+ });
1039
+ },
1040
+ findDuplicateKeys: (): {
1041
+ key: string;
1042
+ scope: StorageScope;
1043
+ count: number;
1044
+ }[] => {
1045
+ const duplicates: { key: string; scope: StorageScope; count: number }[] =
1046
+ [];
1047
+ registeredKeyCounts.forEach((count, registryKey) => {
1048
+ if (count <= 1) {
1049
+ return;
1050
+ }
1051
+ const separatorIndex = registryKey.indexOf(":");
1052
+ duplicates.push({
1053
+ scope: Number(registryKey.slice(0, separatorIndex)) as StorageScope,
1054
+ key: registryKey.slice(separatorIndex + 1),
1055
+ count,
1056
+ });
1057
+ });
1058
+ return duplicates;
1059
+ },
1060
+ getRegisteredKeys: (): { key: string; scope: StorageScope }[] => {
1061
+ const result: { key: string; scope: StorageScope }[] = [];
1062
+ registeredKeyCounts.forEach((_count, registryKey) => {
1063
+ const separatorIndex = registryKey.indexOf(":");
1064
+ result.push({
1065
+ scope: Number(registryKey.slice(0, separatorIndex)) as StorageScope,
1066
+ key: registryKey.slice(separatorIndex + 1),
1067
+ });
1068
+ });
1069
+ return result;
1070
+ },
866
1071
  clearNamespace: (namespace: string, scope: StorageScope) => {
867
1072
  measureOperation("storage:clearNamespace", scope, () => {
868
1073
  assertValidScope(scope);
@@ -1269,6 +1474,16 @@ export function createStorageCore(
1269
1474
  : config.scope === StorageScope.Secure
1270
1475
  ? StorageScope.Secure
1271
1476
  : null;
1477
+ const renameFromKeys: readonly string[] =
1478
+ config.renameFrom === undefined
1479
+ ? EMPTY_KEYS
1480
+ : typeof config.renameFrom === "string"
1481
+ ? [config.renameFrom]
1482
+ : config.renameFrom;
1483
+ const fallbackToCacheOnReadError =
1484
+ config.fallbackToCacheOnReadError === true;
1485
+ const onReadError = config.onReadError;
1486
+ let renamesMigrated = isMemory || renameFromKeys.length === 0;
1272
1487
 
1273
1488
  if (expiration && expiration.ttlMs <= 0) {
1274
1489
  throw new Error("expiration.ttlMs must be greater than 0.");
@@ -1322,9 +1537,18 @@ export function createStorageCore(
1322
1537
  if (memoryExpiration) {
1323
1538
  const expiresAt = memoryExpiration.get(storageKey);
1324
1539
  if (expiresAt !== undefined && expiresAt <= Date.now()) {
1540
+ const expiredRaw = memoryStore.get(storageKey);
1325
1541
  memoryExpiration.delete(storageKey);
1326
1542
  memoryStore.delete(storageKey);
1327
1543
  notifyKeyListeners(memoryListeners, storageKey);
1544
+ emitKeyChange(
1545
+ config.scope,
1546
+ storageKey,
1547
+ typeof expiredRaw === "string" ? expiredRaw : undefined,
1548
+ undefined,
1549
+ "expire",
1550
+ "memory",
1551
+ );
1328
1552
  onExpired?.(storageKey);
1329
1553
  return undefined;
1330
1554
  }
@@ -1332,6 +1556,8 @@ export function createStorageCore(
1332
1556
  return memoryStore.get(storageKey);
1333
1557
  }
1334
1558
 
1559
+ migrateRenamesIfNeeded();
1560
+
1335
1561
  if (nonMemoryScope === StorageScope.Disk) {
1336
1562
  const pending = pendingDiskWrites.get(storageKey);
1337
1563
  if (pending !== undefined) {
@@ -1355,14 +1581,36 @@ export function createStorageCore(
1355
1581
  }
1356
1582
 
1357
1583
  if (isBiometric) {
1358
- return adapter.backend.getSecureBiometric(storageKey);
1584
+ return readBackendRaw(() =>
1585
+ adapter.backend.getSecureBiometric(storageKey),
1586
+ );
1359
1587
  }
1360
1588
 
1361
- const raw = adapter.backend.get(storageKey, config.scope);
1589
+ const raw = readBackendRaw(() =>
1590
+ adapter.backend.get(storageKey, config.scope),
1591
+ );
1362
1592
  cacheRawValue(nonMemoryScope!, storageKey, raw);
1363
1593
  return raw;
1364
1594
  };
1365
1595
 
1596
+ const readBackendRaw = (
1597
+ read: () => string | undefined,
1598
+ ): string | undefined => {
1599
+ try {
1600
+ return read();
1601
+ } catch (error) {
1602
+ onReadError?.(error);
1603
+ if (fallbackToCacheOnReadError) {
1604
+ const cached = getScopeRawCache(nonMemoryScope!).get(storageKey);
1605
+ if (cached !== undefined) {
1606
+ return cached;
1607
+ }
1608
+ return typeof lastRaw === "string" ? lastRaw : undefined;
1609
+ }
1610
+ throw error;
1611
+ }
1612
+ };
1613
+
1366
1614
  const writeStoredRaw = (rawValue: string): void => {
1367
1615
  const oldValue = undefined;
1368
1616
  if (isBiometric) {
@@ -1438,7 +1686,60 @@ export function createStorageCore(
1438
1686
  );
1439
1687
  };
1440
1688
 
1441
- const removeStoredRaw = (): void => {
1689
+ const migrateRenamesIfNeeded = (): void => {
1690
+ if (renamesMigrated) {
1691
+ return;
1692
+ }
1693
+
1694
+ try {
1695
+ const hasCurrent = isBiometric
1696
+ ? adapter.backend.hasSecureBiometric(storageKey)
1697
+ : adapter.backend.has(storageKey, config.scope);
1698
+
1699
+ if (hasCurrent) {
1700
+ for (const legacyKey of renameFromKeys) {
1701
+ if (isBiometric) {
1702
+ if (adapter.backend.hasSecureBiometric(legacyKey)) {
1703
+ adapter.backend.deleteSecureBiometric(legacyKey);
1704
+ }
1705
+ } else if (adapter.backend.has(legacyKey, config.scope)) {
1706
+ adapter.backend.remove(legacyKey, config.scope);
1707
+ }
1708
+ }
1709
+ renamesMigrated = true;
1710
+ return;
1711
+ }
1712
+
1713
+ for (const legacyKey of renameFromKeys) {
1714
+ const legacyRaw = isBiometric
1715
+ ? adapter.backend.getSecureBiometric(legacyKey)
1716
+ : adapter.backend.get(legacyKey, config.scope);
1717
+ if (legacyRaw === undefined) {
1718
+ continue;
1719
+ }
1720
+
1721
+ writeStoredRaw(legacyRaw);
1722
+ if (isBiometric) {
1723
+ adapter.backend.deleteSecureBiometric(legacyKey);
1724
+ } else {
1725
+ adapter.backend.remove(legacyKey, config.scope);
1726
+ }
1727
+ break;
1728
+ }
1729
+
1730
+ renamesMigrated = true;
1731
+ } catch (error) {
1732
+ if (isKeychainLockedError(error)) {
1733
+ onReadError?.(error);
1734
+ return;
1735
+ }
1736
+ throw error;
1737
+ }
1738
+ };
1739
+
1740
+ const removeStoredRaw = (
1741
+ operation: StorageChangeOperation = "remove",
1742
+ ): void => {
1442
1743
  const oldValue = getEventRawValue(config.scope, storageKey);
1443
1744
  if (isBiometric) {
1444
1745
  adapter.backend.deleteSecureBiometric(storageKey);
@@ -1447,7 +1748,7 @@ export function createStorageCore(
1447
1748
  storageKey,
1448
1749
  oldValue,
1449
1750
  undefined,
1450
- "remove",
1751
+ operation,
1451
1752
  adapter.changeSource,
1452
1753
  );
1453
1754
  return;
@@ -1463,7 +1764,7 @@ export function createStorageCore(
1463
1764
  storageKey,
1464
1765
  oldValue,
1465
1766
  undefined,
1466
- "remove",
1767
+ operation,
1467
1768
  adapter.changeSource,
1468
1769
  );
1469
1770
  return;
@@ -1483,7 +1784,7 @@ export function createStorageCore(
1483
1784
  storageKey,
1484
1785
  oldValue,
1485
1786
  undefined,
1486
- "remove",
1787
+ operation,
1487
1788
  adapter.changeSource,
1488
1789
  );
1489
1790
  return;
@@ -1499,7 +1800,7 @@ export function createStorageCore(
1499
1800
  storageKey,
1500
1801
  oldValue,
1501
1802
  undefined,
1502
- "remove",
1803
+ operation,
1503
1804
  adapter.changeSource,
1504
1805
  );
1505
1806
  };
@@ -1576,7 +1877,7 @@ export function createStorageCore(
1576
1877
  return lastValue as T;
1577
1878
  }
1578
1879
 
1579
- removeStoredRaw();
1880
+ removeStoredRaw("expire");
1580
1881
  invalidateParsedCache();
1581
1882
  onExpired?.(storageKey);
1582
1883
  lastValue = ensureValidatedValue(defaultValue, false);
@@ -1618,7 +1919,7 @@ export function createStorageCore(
1618
1919
  if (isStoredEnvelope(parsed)) {
1619
1920
  envelopeExpiresAt = parsed.expiresAt;
1620
1921
  if (parsed.expiresAt <= Date.now()) {
1621
- removeStoredRaw();
1922
+ removeStoredRaw("expire");
1622
1923
  invalidateParsedCache();
1623
1924
  onExpired?.(storageKey);
1624
1925
  lastValue = ensureValidatedValue(defaultValue, false);
@@ -1712,6 +2013,27 @@ export function createStorageCore(
1712
2013
  });
1713
2014
  };
1714
2015
 
2016
+ const merge = (partial: Partial<T>): void => {
2017
+ set((prev) => {
2018
+ if (prev === null || typeof prev !== "object") {
2019
+ return partial as T;
2020
+ }
2021
+ return { ...(prev as object), ...(partial as object) } as T;
2022
+ });
2023
+ };
2024
+
2025
+ const reset = (): void => {
2026
+ deleteItem();
2027
+ };
2028
+
2029
+ const setOrDelete = (value: T | null | undefined): void => {
2030
+ if (value === null || value === undefined) {
2031
+ deleteItem();
2032
+ return;
2033
+ }
2034
+ set(value);
2035
+ };
2036
+
1715
2037
  const hasItem = (): boolean =>
1716
2038
  measureOperation("item:has", config.scope, () => {
1717
2039
  if (isMemory) return memoryStore.has(storageKey);
@@ -1775,6 +2097,9 @@ export function createStorageCore(
1775
2097
  getWithVersion,
1776
2098
  set,
1777
2099
  setIfVersion,
2100
+ merge,
2101
+ reset,
2102
+ setOrDelete,
1778
2103
  delete: deleteItem,
1779
2104
  has: hasItem,
1780
2105
  subscribe,
@@ -1797,10 +2122,26 @@ export function createStorageCore(
1797
2122
  ...(secureAccessControl !== undefined
1798
2123
  ? { _secureAccessControl: secureAccessControl }
1799
2124
  : {}),
2125
+ ...(config.group !== undefined ? { _group: config.group } : {}),
1800
2126
  scope: config.scope,
1801
2127
  key: storageKey,
1802
2128
  };
1803
2129
 
2130
+ if (config.group !== undefined) {
2131
+ let groupSet = itemGroups.get(config.group);
2132
+ if (!groupSet) {
2133
+ groupSet = new Set();
2134
+ itemGroups.set(config.group, groupSet);
2135
+ }
2136
+ groupSet.add(storageItem as StorageItemInternal<unknown>);
2137
+ }
2138
+
2139
+ const registryKey = `${config.scope}:${storageKey}`;
2140
+ registeredKeyCounts.set(
2141
+ registryKey,
2142
+ (registeredKeyCounts.get(registryKey) ?? 0) + 1,
2143
+ );
2144
+
1804
2145
  return storageItem;
1805
2146
  }
1806
2147
 
@@ -2298,7 +2639,11 @@ export function createStorageCore(
2298
2639
 
2299
2640
  function createSecureAuthStorage<K extends string>(
2300
2641
  config: SecureAuthStorageConfig<K>,
2301
- options?: { namespace?: string },
2642
+ options?: {
2643
+ namespace?: string;
2644
+ group?: string;
2645
+ fallbackToCacheOnReadError?: boolean;
2646
+ },
2302
2647
  ): Record<K, StorageItem<string>> {
2303
2648
  const ns = options?.namespace ?? "auth";
2304
2649
  const result: Partial<Record<K, StorageItem<string>>> = {};
@@ -2326,15 +2671,105 @@ export function createStorageCore(
2326
2671
  ...(expirationConfig !== undefined
2327
2672
  ? { expiration: expirationConfig }
2328
2673
  : {}),
2674
+ ...(itemConfig.renameFrom !== undefined
2675
+ ? { renameFrom: itemConfig.renameFrom }
2676
+ : {}),
2677
+ ...(options?.group !== undefined ? { group: options.group } : {}),
2678
+ ...(options?.fallbackToCacheOnReadError !== undefined
2679
+ ? { fallbackToCacheOnReadError: options.fallbackToCacheOnReadError }
2680
+ : {}),
2329
2681
  });
2330
2682
  }
2331
2683
 
2332
2684
  return result as Record<K, StorageItem<string>>;
2333
2685
  }
2334
2686
 
2687
+ function memoryItem<T = undefined>(
2688
+ config: Omit<StorageItemConfig<T>, "scope">,
2689
+ ): StorageItem<T> {
2690
+ return createStorageItem<T>({ ...config, scope: StorageScope.Memory });
2691
+ }
2692
+
2693
+ function diskItem<T = undefined>(
2694
+ config: Omit<StorageItemConfig<T>, "scope">,
2695
+ ): StorageItem<T> {
2696
+ return createStorageItem<T>({ ...config, scope: StorageScope.Disk });
2697
+ }
2698
+
2699
+ function secureItem<T = undefined>(
2700
+ config: Omit<StorageItemConfig<T>, "scope">,
2701
+ ): StorageItem<T> {
2702
+ return createStorageItem<T>({ ...config, scope: StorageScope.Secure });
2703
+ }
2704
+
2705
+ function createSetItem<TMember extends string = string>(
2706
+ config: SetItemConfig<TMember>,
2707
+ ): SetStorageItem<TMember> {
2708
+ const { defaultValue, ...rest } = config;
2709
+ const initial: Record<string, true> = {};
2710
+ if (defaultValue) {
2711
+ for (const id of defaultValue) {
2712
+ initial[id] = true;
2713
+ }
2714
+ }
2715
+
2716
+ const item = createStorageItem<Record<string, true>>({
2717
+ ...rest,
2718
+ defaultValue: initial,
2719
+ });
2720
+
2721
+ const has = (id: TMember): boolean => item.get()[id] === true;
2722
+
2723
+ const add = (id: TMember): void => {
2724
+ if (item.get()[id] === true) {
2725
+ return;
2726
+ }
2727
+ item.merge({ [id]: true });
2728
+ };
2729
+
2730
+ const deleteId = (id: TMember): void => {
2731
+ const current = item.get();
2732
+ if (current[id] !== true) {
2733
+ return;
2734
+ }
2735
+ const next = { ...current };
2736
+ delete next[id];
2737
+ item.set(next);
2738
+ };
2739
+
2740
+ const toggle = (id: TMember): boolean => {
2741
+ if (has(id)) {
2742
+ deleteId(id);
2743
+ return false;
2744
+ }
2745
+ add(id);
2746
+ return true;
2747
+ };
2748
+
2749
+ return {
2750
+ get: item.get,
2751
+ has,
2752
+ add,
2753
+ delete: deleteId,
2754
+ toggle,
2755
+ values: () => Object.keys(item.get()) as TMember[],
2756
+ size: () => Object.keys(item.get()).length,
2757
+ clear: () => item.set({}),
2758
+ reset: item.reset,
2759
+ subscribe: item.subscribe,
2760
+ scope: item.scope,
2761
+ key: item.key,
2762
+ item,
2763
+ };
2764
+ }
2765
+
2335
2766
  return {
2336
2767
  storage,
2337
2768
  createStorageItem,
2769
+ memoryItem,
2770
+ diskItem,
2771
+ secureItem,
2772
+ createSetItem,
2338
2773
  getBatch,
2339
2774
  setBatch,
2340
2775
  removeBatch,