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
@@ -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,48 @@ export type StorageBatchSetItem<T> = {
149
158
  value: T;
150
159
  };
151
160
 
161
+ type StorageBatchSetCandidate = {
162
+ get: () => unknown;
163
+ };
164
+
165
+ type StorageBatchSetEntries<
166
+ TItems extends readonly StorageBatchSetCandidate[],
167
+ > = {
168
+ [Index in keyof TItems]: {
169
+ item: TItems[Index] & StorageItem<ReturnType<TItems[Index]["get"]>>;
170
+ value: ReturnType<TItems[Index]["get"]>;
171
+ };
172
+ };
173
+
174
+ export type StorageKeyRef = string | { readonly key: string };
175
+
176
+ export type StorageClearOptions = {
177
+ except?: readonly StorageKeyRef[];
178
+ };
179
+
180
+ export type SetItemConfig<TMember extends string = string> = Omit<
181
+ StorageItemConfig<Record<string, true>>,
182
+ "defaultValue" | "serialize" | "deserialize"
183
+ > & {
184
+ defaultValue?: readonly TMember[];
185
+ };
186
+
187
+ export type SetStorageItem<TMember extends string = string> = {
188
+ get: () => Record<string, true>;
189
+ has: (id: TMember) => boolean;
190
+ add: (id: TMember) => void;
191
+ delete: (id: TMember) => void;
192
+ toggle: (id: TMember) => boolean;
193
+ values: () => TMember[];
194
+ size: () => number;
195
+ clear: () => void;
196
+ reset: () => void;
197
+ subscribe: (callback: () => void) => () => void;
198
+ scope: StorageScope;
199
+ key: string;
200
+ item: StorageItem<Record<string, true>>;
201
+ };
202
+
152
203
  export type StorageCoreBackend = {
153
204
  get(key: string, scope: StorageScope): string | undefined;
154
205
  set(key: string, value: string, scope: StorageScope): void;
@@ -234,6 +285,8 @@ export type StorageCoreInternals = {
234
285
  setSecureDefaultAccessControl(level: AccessControl): void;
235
286
  };
236
287
 
288
+ const EMPTY_KEYS: readonly string[] = Object.freeze([]);
289
+
237
290
  function asInternal<T>(item: StorageItem<T>): StorageItemInternal<T> {
238
291
  return item as StorageItemInternal<T>;
239
292
  }
@@ -242,6 +295,8 @@ export function createStorageCore(
242
295
  buildAdapter: (internals: StorageCoreInternals) => StorageCoreAdapter,
243
296
  ) {
244
297
  const registeredMigrations = new Map<number, Migration>();
298
+ const itemGroups = new Map<string, Set<StorageItemInternal<unknown>>>();
299
+ const registeredKeyCounts = new Map<string, number>();
245
300
  const memoryStore = new Map<string, unknown>();
246
301
  const memoryListeners: KeyListenerRegistry = new Map();
247
302
  const scopedListeners = new Map<NonMemoryScope, KeyListenerRegistry>([
@@ -723,6 +778,84 @@ export function createStorageCore(
723
778
 
724
779
  const adapter = buildAdapter(internals);
725
780
 
781
+ function resolveKeyRef(ref: StorageKeyRef): string {
782
+ return typeof ref === "string" ? ref : ref.key;
783
+ }
784
+
785
+ function clearScopeExcept(
786
+ scope: StorageScope,
787
+ exceptRefs: readonly StorageKeyRef[],
788
+ ): void {
789
+ measureOperation("storage:clearExcept", scope, () => {
790
+ assertValidScope(scope);
791
+ const keep = new Set(exceptRefs.map(resolveKeyRef));
792
+
793
+ if (scope === StorageScope.Memory) {
794
+ const removeKeys = Array.from(memoryStore.keys()).filter(
795
+ (key) => !keep.has(key),
796
+ );
797
+ if (removeKeys.length === 0) {
798
+ return;
799
+ }
800
+ const previousValues = shouldReadPreviousEventValues(scope)
801
+ ? removeKeys.map((key) => getEventRawValue(scope, key))
802
+ : [];
803
+ removeKeys.forEach((key) => memoryStore.delete(key));
804
+ removeKeys.forEach((key) => notifyKeyListeners(memoryListeners, key));
805
+ emitBatchChange(
806
+ scope,
807
+ "clear",
808
+ "memory",
809
+ removeKeys.map((key, index) =>
810
+ createKeyChange(
811
+ scope,
812
+ key,
813
+ previousValues[index],
814
+ undefined,
815
+ "clear",
816
+ "memory",
817
+ ),
818
+ ),
819
+ );
820
+ return;
821
+ }
822
+
823
+ if (scope === StorageScope.Disk) {
824
+ flushDiskWrites();
825
+ }
826
+ if (scope === StorageScope.Secure) {
827
+ flushSecureWrites();
828
+ }
829
+
830
+ const removeKeys = adapter.backend
831
+ .getAllKeys(scope)
832
+ .filter((key) => !keep.has(key));
833
+ if (removeKeys.length === 0) {
834
+ return;
835
+ }
836
+ const previousValues = shouldReadPreviousEventValues(scope)
837
+ ? adapter.backend.getBatch(removeKeys, scope)
838
+ : [];
839
+ adapter.backend.removeBatch(removeKeys, scope);
840
+ removeKeys.forEach((key) => cacheRawValue(scope, key, undefined));
841
+ emitBatchChange(
842
+ scope,
843
+ "clear",
844
+ adapter.changeSource,
845
+ removeKeys.map((key, index) =>
846
+ createKeyChange(
847
+ scope,
848
+ key,
849
+ previousValues[index],
850
+ undefined,
851
+ "clear",
852
+ adapter.changeSource,
853
+ ),
854
+ ),
855
+ );
856
+ });
857
+ }
858
+
726
859
  const storage = {
727
860
  subscribe: (
728
861
  scope: StorageScope,
@@ -796,7 +929,11 @@ export function createStorageCore(
796
929
  adapter.maybeCleanupScopeSubscription(StorageScope.Disk);
797
930
  adapter.maybeCleanupScopeSubscription(StorageScope.Secure);
798
931
  },
799
- clear: (scope: StorageScope) => {
932
+ clear: (scope: StorageScope, options?: StorageClearOptions) => {
933
+ if (options?.except && options.except.length > 0) {
934
+ clearScopeExcept(scope, options.except);
935
+ return;
936
+ }
800
937
  measureOperation("storage:clear", scope, () => {
801
938
  const previousValues = shouldReadPreviousEventValues(scope)
802
939
  ? storage.getAll(scope)
@@ -863,6 +1000,87 @@ export function createStorageCore(
863
1000
  3,
864
1001
  );
865
1002
  },
1003
+ clearGroup: (group: string) => {
1004
+ const items = itemGroups.get(group);
1005
+ if (!items || items.size === 0) {
1006
+ return;
1007
+ }
1008
+ measureOperation(
1009
+ "storage:clearGroup",
1010
+ StorageScope.Memory,
1011
+ () => {
1012
+ const byScope = new Map<
1013
+ StorageScope,
1014
+ StorageItemInternal<unknown>[]
1015
+ >();
1016
+ for (const item of items) {
1017
+ const scoped = byScope.get(item.scope);
1018
+ if (scoped) {
1019
+ scoped.push(item);
1020
+ } else {
1021
+ byScope.set(item.scope, [item]);
1022
+ }
1023
+ }
1024
+ byScope.forEach((groupItems, scope) => {
1025
+ removeBatch(groupItems, scope);
1026
+ });
1027
+ },
1028
+ items.size,
1029
+ );
1030
+ },
1031
+ getGroupItems: (group: string): StorageItem<unknown>[] => {
1032
+ const items = itemGroups.get(group);
1033
+ return items ? Array.from(items) : [];
1034
+ },
1035
+ subscribeExpired: (
1036
+ scope: StorageScope,
1037
+ listener: (event: StorageKeyChangeEvent) => void,
1038
+ ): (() => void) => {
1039
+ return storage.subscribe(scope, (event) => {
1040
+ if (event.type === "key") {
1041
+ if (event.operation === "expire") {
1042
+ listener(event);
1043
+ }
1044
+ return;
1045
+ }
1046
+ event.changes.forEach((change) => {
1047
+ if (change.operation === "expire") {
1048
+ listener(change);
1049
+ }
1050
+ });
1051
+ });
1052
+ },
1053
+ findDuplicateKeys: (): {
1054
+ key: string;
1055
+ scope: StorageScope;
1056
+ count: number;
1057
+ }[] => {
1058
+ const duplicates: { key: string; scope: StorageScope; count: number }[] =
1059
+ [];
1060
+ registeredKeyCounts.forEach((count, registryKey) => {
1061
+ if (count <= 1) {
1062
+ return;
1063
+ }
1064
+ const separatorIndex = registryKey.indexOf(":");
1065
+ duplicates.push({
1066
+ scope: Number(registryKey.slice(0, separatorIndex)),
1067
+ key: registryKey.slice(separatorIndex + 1),
1068
+ count,
1069
+ });
1070
+ });
1071
+ return duplicates;
1072
+ },
1073
+ getRegisteredKeys: (): { key: string; scope: StorageScope }[] => {
1074
+ const result: { key: string; scope: StorageScope }[] = [];
1075
+ registeredKeyCounts.forEach((_count, registryKey) => {
1076
+ const separatorIndex = registryKey.indexOf(":");
1077
+ result.push({
1078
+ scope: Number(registryKey.slice(0, separatorIndex)),
1079
+ key: registryKey.slice(separatorIndex + 1),
1080
+ });
1081
+ });
1082
+ return result;
1083
+ },
866
1084
  clearNamespace: (namespace: string, scope: StorageScope) => {
867
1085
  measureOperation("storage:clearNamespace", scope, () => {
868
1086
  assertValidScope(scope);
@@ -1269,6 +1487,16 @@ export function createStorageCore(
1269
1487
  : config.scope === StorageScope.Secure
1270
1488
  ? StorageScope.Secure
1271
1489
  : null;
1490
+ const renameFromKeys: readonly string[] =
1491
+ config.renameFrom === undefined
1492
+ ? EMPTY_KEYS
1493
+ : typeof config.renameFrom === "string"
1494
+ ? [config.renameFrom]
1495
+ : config.renameFrom;
1496
+ const fallbackToCacheOnReadError =
1497
+ config.fallbackToCacheOnReadError === true;
1498
+ const onReadError = config.onReadError;
1499
+ let renamesMigrated = isMemory || renameFromKeys.length === 0;
1272
1500
 
1273
1501
  if (expiration && expiration.ttlMs <= 0) {
1274
1502
  throw new Error("expiration.ttlMs must be greater than 0.");
@@ -1322,9 +1550,18 @@ export function createStorageCore(
1322
1550
  if (memoryExpiration) {
1323
1551
  const expiresAt = memoryExpiration.get(storageKey);
1324
1552
  if (expiresAt !== undefined && expiresAt <= Date.now()) {
1553
+ const expiredRaw = memoryStore.get(storageKey);
1325
1554
  memoryExpiration.delete(storageKey);
1326
1555
  memoryStore.delete(storageKey);
1327
1556
  notifyKeyListeners(memoryListeners, storageKey);
1557
+ emitKeyChange(
1558
+ config.scope,
1559
+ storageKey,
1560
+ typeof expiredRaw === "string" ? expiredRaw : undefined,
1561
+ undefined,
1562
+ "expire",
1563
+ "memory",
1564
+ );
1328
1565
  onExpired?.(storageKey);
1329
1566
  return undefined;
1330
1567
  }
@@ -1332,6 +1569,8 @@ export function createStorageCore(
1332
1569
  return memoryStore.get(storageKey);
1333
1570
  }
1334
1571
 
1572
+ migrateRenamesIfNeeded();
1573
+
1335
1574
  if (nonMemoryScope === StorageScope.Disk) {
1336
1575
  const pending = pendingDiskWrites.get(storageKey);
1337
1576
  if (pending !== undefined) {
@@ -1355,14 +1594,36 @@ export function createStorageCore(
1355
1594
  }
1356
1595
 
1357
1596
  if (isBiometric) {
1358
- return adapter.backend.getSecureBiometric(storageKey);
1597
+ return readBackendRaw(() =>
1598
+ adapter.backend.getSecureBiometric(storageKey),
1599
+ );
1359
1600
  }
1360
1601
 
1361
- const raw = adapter.backend.get(storageKey, config.scope);
1602
+ const raw = readBackendRaw(() =>
1603
+ adapter.backend.get(storageKey, config.scope),
1604
+ );
1362
1605
  cacheRawValue(nonMemoryScope!, storageKey, raw);
1363
1606
  return raw;
1364
1607
  };
1365
1608
 
1609
+ const readBackendRaw = (
1610
+ read: () => string | undefined,
1611
+ ): string | undefined => {
1612
+ try {
1613
+ return read();
1614
+ } catch (error) {
1615
+ onReadError?.(error);
1616
+ if (fallbackToCacheOnReadError) {
1617
+ const cached = getScopeRawCache(nonMemoryScope!).get(storageKey);
1618
+ if (cached !== undefined) {
1619
+ return cached;
1620
+ }
1621
+ return typeof lastRaw === "string" ? lastRaw : undefined;
1622
+ }
1623
+ throw error;
1624
+ }
1625
+ };
1626
+
1366
1627
  const writeStoredRaw = (rawValue: string): void => {
1367
1628
  const oldValue = undefined;
1368
1629
  if (isBiometric) {
@@ -1438,7 +1699,60 @@ export function createStorageCore(
1438
1699
  );
1439
1700
  };
1440
1701
 
1441
- const removeStoredRaw = (): void => {
1702
+ const migrateRenamesIfNeeded = (): void => {
1703
+ if (renamesMigrated) {
1704
+ return;
1705
+ }
1706
+
1707
+ try {
1708
+ const hasCurrent = isBiometric
1709
+ ? adapter.backend.hasSecureBiometric(storageKey)
1710
+ : adapter.backend.has(storageKey, config.scope);
1711
+
1712
+ if (hasCurrent) {
1713
+ for (const legacyKey of renameFromKeys) {
1714
+ if (isBiometric) {
1715
+ if (adapter.backend.hasSecureBiometric(legacyKey)) {
1716
+ adapter.backend.deleteSecureBiometric(legacyKey);
1717
+ }
1718
+ } else if (adapter.backend.has(legacyKey, config.scope)) {
1719
+ adapter.backend.remove(legacyKey, config.scope);
1720
+ }
1721
+ }
1722
+ renamesMigrated = true;
1723
+ return;
1724
+ }
1725
+
1726
+ for (const legacyKey of renameFromKeys) {
1727
+ const legacyRaw = isBiometric
1728
+ ? adapter.backend.getSecureBiometric(legacyKey)
1729
+ : adapter.backend.get(legacyKey, config.scope);
1730
+ if (legacyRaw === undefined) {
1731
+ continue;
1732
+ }
1733
+
1734
+ writeStoredRaw(legacyRaw);
1735
+ if (isBiometric) {
1736
+ adapter.backend.deleteSecureBiometric(legacyKey);
1737
+ } else {
1738
+ adapter.backend.remove(legacyKey, config.scope);
1739
+ }
1740
+ break;
1741
+ }
1742
+
1743
+ renamesMigrated = true;
1744
+ } catch (error) {
1745
+ if (isKeychainLockedError(error)) {
1746
+ onReadError?.(error);
1747
+ return;
1748
+ }
1749
+ throw error;
1750
+ }
1751
+ };
1752
+
1753
+ const removeStoredRaw = (
1754
+ operation: StorageChangeOperation = "remove",
1755
+ ): void => {
1442
1756
  const oldValue = getEventRawValue(config.scope, storageKey);
1443
1757
  if (isBiometric) {
1444
1758
  adapter.backend.deleteSecureBiometric(storageKey);
@@ -1447,7 +1761,7 @@ export function createStorageCore(
1447
1761
  storageKey,
1448
1762
  oldValue,
1449
1763
  undefined,
1450
- "remove",
1764
+ operation,
1451
1765
  adapter.changeSource,
1452
1766
  );
1453
1767
  return;
@@ -1463,7 +1777,7 @@ export function createStorageCore(
1463
1777
  storageKey,
1464
1778
  oldValue,
1465
1779
  undefined,
1466
- "remove",
1780
+ operation,
1467
1781
  adapter.changeSource,
1468
1782
  );
1469
1783
  return;
@@ -1483,7 +1797,7 @@ export function createStorageCore(
1483
1797
  storageKey,
1484
1798
  oldValue,
1485
1799
  undefined,
1486
- "remove",
1800
+ operation,
1487
1801
  adapter.changeSource,
1488
1802
  );
1489
1803
  return;
@@ -1499,7 +1813,7 @@ export function createStorageCore(
1499
1813
  storageKey,
1500
1814
  oldValue,
1501
1815
  undefined,
1502
- "remove",
1816
+ operation,
1503
1817
  adapter.changeSource,
1504
1818
  );
1505
1819
  };
@@ -1576,7 +1890,7 @@ export function createStorageCore(
1576
1890
  return lastValue as T;
1577
1891
  }
1578
1892
 
1579
- removeStoredRaw();
1893
+ removeStoredRaw("expire");
1580
1894
  invalidateParsedCache();
1581
1895
  onExpired?.(storageKey);
1582
1896
  lastValue = ensureValidatedValue(defaultValue, false);
@@ -1618,7 +1932,7 @@ export function createStorageCore(
1618
1932
  if (isStoredEnvelope(parsed)) {
1619
1933
  envelopeExpiresAt = parsed.expiresAt;
1620
1934
  if (parsed.expiresAt <= Date.now()) {
1621
- removeStoredRaw();
1935
+ removeStoredRaw("expire");
1622
1936
  invalidateParsedCache();
1623
1937
  onExpired?.(storageKey);
1624
1938
  lastValue = ensureValidatedValue(defaultValue, false);
@@ -1712,6 +2026,27 @@ export function createStorageCore(
1712
2026
  });
1713
2027
  };
1714
2028
 
2029
+ const merge = (partial: Partial<T>): void => {
2030
+ set((prev) => {
2031
+ if (prev === null || typeof prev !== "object") {
2032
+ return partial as T;
2033
+ }
2034
+ return { ...(prev as object), ...(partial as object) } as T;
2035
+ });
2036
+ };
2037
+
2038
+ const reset = (): void => {
2039
+ deleteItem();
2040
+ };
2041
+
2042
+ const setOrDelete = (value: T | null | undefined): void => {
2043
+ if (value === null || value === undefined) {
2044
+ deleteItem();
2045
+ return;
2046
+ }
2047
+ set(value);
2048
+ };
2049
+
1715
2050
  const hasItem = (): boolean =>
1716
2051
  measureOperation("item:has", config.scope, () => {
1717
2052
  if (isMemory) return memoryStore.has(storageKey);
@@ -1775,6 +2110,9 @@ export function createStorageCore(
1775
2110
  getWithVersion,
1776
2111
  set,
1777
2112
  setIfVersion,
2113
+ merge,
2114
+ reset,
2115
+ setOrDelete,
1778
2116
  delete: deleteItem,
1779
2117
  has: hasItem,
1780
2118
  subscribe,
@@ -1797,10 +2135,26 @@ export function createStorageCore(
1797
2135
  ...(secureAccessControl !== undefined
1798
2136
  ? { _secureAccessControl: secureAccessControl }
1799
2137
  : {}),
2138
+ ...(config.group !== undefined ? { _group: config.group } : {}),
1800
2139
  scope: config.scope,
1801
2140
  key: storageKey,
1802
2141
  };
1803
2142
 
2143
+ if (config.group !== undefined) {
2144
+ let groupSet = itemGroups.get(config.group);
2145
+ if (!groupSet) {
2146
+ groupSet = new Set();
2147
+ itemGroups.set(config.group, groupSet);
2148
+ }
2149
+ groupSet.add(storageItem as StorageItemInternal<unknown>);
2150
+ }
2151
+
2152
+ const registryKey = `${config.scope}:${storageKey}`;
2153
+ registeredKeyCounts.set(
2154
+ registryKey,
2155
+ (registeredKeyCounts.get(registryKey) ?? 0) + 1,
2156
+ );
2157
+
1804
2158
  return storageItem;
1805
2159
  }
1806
2160
 
@@ -1886,6 +2240,10 @@ export function createStorageCore(
1886
2240
  ) as BatchValues<TItems>;
1887
2241
  }
1888
2242
 
2243
+ function setBatch<const TItems extends readonly StorageBatchSetCandidate[]>(
2244
+ items: StorageBatchSetEntries<TItems>,
2245
+ scope: StorageScope,
2246
+ ): void;
1889
2247
  function setBatch<T>(
1890
2248
  items: readonly StorageBatchSetItem<T>[],
1891
2249
  scope: StorageScope,
@@ -2298,7 +2656,11 @@ export function createStorageCore(
2298
2656
 
2299
2657
  function createSecureAuthStorage<K extends string>(
2300
2658
  config: SecureAuthStorageConfig<K>,
2301
- options?: { namespace?: string },
2659
+ options?: {
2660
+ namespace?: string;
2661
+ group?: string;
2662
+ fallbackToCacheOnReadError?: boolean;
2663
+ },
2302
2664
  ): Record<K, StorageItem<string>> {
2303
2665
  const ns = options?.namespace ?? "auth";
2304
2666
  const result: Partial<Record<K, StorageItem<string>>> = {};
@@ -2326,15 +2688,105 @@ export function createStorageCore(
2326
2688
  ...(expirationConfig !== undefined
2327
2689
  ? { expiration: expirationConfig }
2328
2690
  : {}),
2691
+ ...(itemConfig.renameFrom !== undefined
2692
+ ? { renameFrom: itemConfig.renameFrom }
2693
+ : {}),
2694
+ ...(options?.group !== undefined ? { group: options.group } : {}),
2695
+ ...(options?.fallbackToCacheOnReadError !== undefined
2696
+ ? { fallbackToCacheOnReadError: options.fallbackToCacheOnReadError }
2697
+ : {}),
2329
2698
  });
2330
2699
  }
2331
2700
 
2332
2701
  return result as Record<K, StorageItem<string>>;
2333
2702
  }
2334
2703
 
2704
+ function memoryItem<T = undefined>(
2705
+ config: Omit<StorageItemConfig<T>, "scope">,
2706
+ ): StorageItem<T> {
2707
+ return createStorageItem<T>({ ...config, scope: StorageScope.Memory });
2708
+ }
2709
+
2710
+ function diskItem<T = undefined>(
2711
+ config: Omit<StorageItemConfig<T>, "scope">,
2712
+ ): StorageItem<T> {
2713
+ return createStorageItem<T>({ ...config, scope: StorageScope.Disk });
2714
+ }
2715
+
2716
+ function secureItem<T = undefined>(
2717
+ config: Omit<StorageItemConfig<T>, "scope">,
2718
+ ): StorageItem<T> {
2719
+ return createStorageItem<T>({ ...config, scope: StorageScope.Secure });
2720
+ }
2721
+
2722
+ function createSetItem<TMember extends string = string>(
2723
+ config: SetItemConfig<TMember>,
2724
+ ): SetStorageItem<TMember> {
2725
+ const { defaultValue, ...rest } = config;
2726
+ const initial: Record<string, true> = {};
2727
+ if (defaultValue) {
2728
+ for (const id of defaultValue) {
2729
+ initial[id] = true;
2730
+ }
2731
+ }
2732
+
2733
+ const item = createStorageItem<Record<string, true>>({
2734
+ ...rest,
2735
+ defaultValue: initial,
2736
+ });
2737
+
2738
+ const has = (id: TMember): boolean => item.get()[id] === true;
2739
+
2740
+ const add = (id: TMember): void => {
2741
+ if (item.get()[id] === true) {
2742
+ return;
2743
+ }
2744
+ item.merge({ [id]: true });
2745
+ };
2746
+
2747
+ const deleteId = (id: TMember): void => {
2748
+ const current = item.get();
2749
+ if (current[id] !== true) {
2750
+ return;
2751
+ }
2752
+ const next = { ...current };
2753
+ delete next[id];
2754
+ item.set(next);
2755
+ };
2756
+
2757
+ const toggle = (id: TMember): boolean => {
2758
+ if (has(id)) {
2759
+ deleteId(id);
2760
+ return false;
2761
+ }
2762
+ add(id);
2763
+ return true;
2764
+ };
2765
+
2766
+ return {
2767
+ get: item.get,
2768
+ has,
2769
+ add,
2770
+ delete: deleteId,
2771
+ toggle,
2772
+ values: () => Object.keys(item.get()) as TMember[],
2773
+ size: () => Object.keys(item.get()).length,
2774
+ clear: () => item.set({}),
2775
+ reset: item.reset,
2776
+ subscribe: item.subscribe,
2777
+ scope: item.scope,
2778
+ key: item.key,
2779
+ item,
2780
+ };
2781
+ }
2782
+
2335
2783
  return {
2336
2784
  storage,
2337
2785
  createStorageItem,
2786
+ memoryItem,
2787
+ diskItem,
2788
+ secureItem,
2789
+ createSetItem,
2338
2790
  getBatch,
2339
2791
  setBatch,
2340
2792
  removeBatch,
@@ -5,9 +5,11 @@ export type StorageChangeOperation =
5
5
  | "remove"
6
6
  | "clear"
7
7
  | "clearNamespace"
8
+ | "clearGroup"
8
9
  | "setBatch"
9
10
  | "removeBatch"
10
11
  | "import"
12
+ | "expire"
11
13
  | "external";
12
14
 
13
15
  export type StorageChangeSource = "memory" | "native" | "web" | "external";
@@ -31,8 +33,7 @@ export type StorageBatchChangeEvent = {
31
33
  };
32
34
 
33
35
  export type StorageChangeEvent =
34
- | StorageKeyChangeEvent
35
- | StorageBatchChangeEvent;
36
+ StorageKeyChangeEvent | StorageBatchChangeEvent;
36
37
 
37
38
  export type StorageEventListener = (event: StorageChangeEvent) => void;
38
39