react-native-nitro-storage 0.5.4 → 0.5.6

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 (40) hide show
  1. package/README.md +90 -7
  2. package/android/build.gradle +5 -5
  3. package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +12 -25
  4. package/app.plugin.js +114 -9
  5. package/docs/api-reference.md +39 -36
  6. package/docs/batch-transactions-migrations.md +1 -1
  7. package/docs/recipes.md +1 -1
  8. package/docs/secure-storage.md +15 -4
  9. package/docs/web-backends.md +5 -0
  10. package/lib/commonjs/index.js +49 -9
  11. package/lib/commonjs/index.js.map +1 -1
  12. package/lib/commonjs/index.web.js +71 -11
  13. package/lib/commonjs/index.web.js.map +1 -1
  14. package/lib/commonjs/indexeddb-backend.js +28 -0
  15. package/lib/commonjs/indexeddb-backend.js.map +1 -1
  16. package/lib/commonjs/storage-hooks.js.map +1 -1
  17. package/lib/commonjs/web-storage-backend.js.map +1 -1
  18. package/lib/module/index.js +49 -9
  19. package/lib/module/index.js.map +1 -1
  20. package/lib/module/index.web.js +71 -11
  21. package/lib/module/index.web.js.map +1 -1
  22. package/lib/module/indexeddb-backend.js +28 -0
  23. package/lib/module/indexeddb-backend.js.map +1 -1
  24. package/lib/module/storage-hooks.js.map +1 -1
  25. package/lib/module/web-storage-backend.js.map +1 -1
  26. package/lib/typescript/index.d.ts +21 -9
  27. package/lib/typescript/index.d.ts.map +1 -1
  28. package/lib/typescript/index.web.d.ts +10 -3
  29. package/lib/typescript/index.web.d.ts.map +1 -1
  30. package/lib/typescript/indexeddb-backend.d.ts.map +1 -1
  31. package/lib/typescript/storage-hooks.d.ts +5 -4
  32. package/lib/typescript/storage-hooks.d.ts.map +1 -1
  33. package/lib/typescript/web-storage-backend.d.ts +1 -0
  34. package/lib/typescript/web-storage-backend.d.ts.map +1 -1
  35. package/package.json +4 -4
  36. package/src/index.ts +96 -19
  37. package/src/index.web.ts +107 -11
  38. package/src/indexeddb-backend.ts +30 -0
  39. package/src/storage-hooks.ts +6 -6
  40. package/src/web-storage-backend.ts +1 -0
package/src/index.web.ts CHANGED
@@ -54,6 +54,8 @@ export type {
54
54
  StorageKeyChangeEvent,
55
55
  } from "./storage-events";
56
56
  export type {
57
+ WebDiskStorageBackend,
58
+ WebSecureStorageBackend,
57
59
  WebStorageBackend,
58
60
  WebStorageChangeEvent,
59
61
  WebStorageScope,
@@ -75,6 +77,12 @@ export type StorageMetricsEvent = {
75
77
  keysCount: number;
76
78
  };
77
79
  export type StorageMetricsObserver = (event: StorageMetricsEvent) => void;
80
+ export type StorageEventObserverOptions = {
81
+ redactSecureValues?: boolean;
82
+ };
83
+ export type StorageExportOptions = {
84
+ includeSecureValues?: boolean;
85
+ };
78
86
  export type StorageMetricSummary = {
79
87
  count: number;
80
88
  totalDurationMs: number;
@@ -254,6 +262,7 @@ let hasWarnedAboutWebBiometricFallback = false;
254
262
  let hasWindowStorageEventSubscription = false;
255
263
  let metricsObserver: StorageMetricsObserver | undefined;
256
264
  let eventObserver: StorageEventListener | undefined;
265
+ let eventObserverRedactSecureValues = true;
257
266
  const metricsCounters = new Map<
258
267
  string,
259
268
  { count: number; totalDurationMs: number; maxDurationMs: number }
@@ -552,6 +561,21 @@ function resetBackendChangeSubscription(scope: NonMemoryScope): void {
552
561
  externalSyncUnsubscribers.delete(scope);
553
562
  }
554
563
 
564
+ function closeWebBackend(
565
+ scope: NonMemoryScope,
566
+ backend: WebStorageBackend | undefined,
567
+ ): void {
568
+ if (!backend?.close) {
569
+ return;
570
+ }
571
+
572
+ try {
573
+ backend.close();
574
+ } catch (error) {
575
+ throw createWebStorageError(scope, "close", error, backend);
576
+ }
577
+ }
578
+
555
579
  function ensureExternalSyncSubscriptions(): void {
556
580
  if (
557
581
  !hasWindowStorageEventSubscription &&
@@ -675,6 +699,49 @@ function hasStorageChangeObservers(scope: StorageScope): boolean {
675
699
  return storageEvents.hasListeners(scope) || eventObserver !== undefined;
676
700
  }
677
701
 
702
+ function shouldReadPreviousEventValues(scope: StorageScope): boolean {
703
+ if (storageEvents.hasListeners(scope)) {
704
+ return true;
705
+ }
706
+ if (!eventObserver) {
707
+ return false;
708
+ }
709
+ return scope !== StorageScope.Secure || !eventObserverRedactSecureValues;
710
+ }
711
+
712
+ const SECURE_EVENT_REDACTED_VALUE = "[secure]";
713
+
714
+ function redactSecureKeyChange(
715
+ event: StorageKeyChangeEvent,
716
+ ): StorageKeyChangeEvent {
717
+ if (event.scope !== StorageScope.Secure) {
718
+ return event;
719
+ }
720
+
721
+ return {
722
+ ...event,
723
+ oldValue:
724
+ event.oldValue === undefined ? undefined : SECURE_EVENT_REDACTED_VALUE,
725
+ newValue:
726
+ event.newValue === undefined ? undefined : SECURE_EVENT_REDACTED_VALUE,
727
+ };
728
+ }
729
+
730
+ function eventForGlobalObserver(event: StorageChangeEvent): StorageChangeEvent {
731
+ if (!eventObserverRedactSecureValues || event.scope !== StorageScope.Secure) {
732
+ return event;
733
+ }
734
+
735
+ if (event.type === "key") {
736
+ return redactSecureKeyChange(event);
737
+ }
738
+
739
+ return {
740
+ ...event,
741
+ changes: event.changes.map(redactSecureKeyChange),
742
+ };
743
+ }
744
+
678
745
  function emitKeyChange(
679
746
  scope: StorageScope,
680
747
  key: string,
@@ -692,7 +759,7 @@ function emitKeyChange(
692
759
  source,
693
760
  );
694
761
  storageEvents.emitKey(event);
695
- eventObserver?.(event);
762
+ eventObserver?.(eventForGlobalObserver(event));
696
763
  }
697
764
 
698
765
  function emitBatchChange(
@@ -713,7 +780,7 @@ function emitBatchChange(
713
780
  changes,
714
781
  };
715
782
  storageEvents.emitBatch(event);
716
- eventObserver?.(event);
783
+ eventObserver?.(eventForGlobalObserver(event));
717
784
  }
718
785
 
719
786
  function readPendingSecureWrite(key: string): string | undefined {
@@ -1293,15 +1360,19 @@ export const storage = {
1293
1360
  ): (() => void) => {
1294
1361
  return storage.subscribePrefix(scope, prefixKey(namespace, ""), listener);
1295
1362
  },
1296
- setEventObserver: (observer?: StorageEventListener) => {
1363
+ setEventObserver: (
1364
+ observer?: StorageEventListener,
1365
+ options: StorageEventObserverOptions = {},
1366
+ ) => {
1297
1367
  eventObserver = observer;
1368
+ eventObserverRedactSecureValues = options.redactSecureValues !== false;
1298
1369
  if (observer) {
1299
1370
  ensureExternalSyncSubscriptions();
1300
1371
  }
1301
1372
  },
1302
1373
  clear: (scope: StorageScope) => {
1303
1374
  measureOperation("storage:clear", scope, () => {
1304
- const previousValues = hasStorageChangeObservers(scope)
1375
+ const previousValues = shouldReadPreviousEventValues(scope)
1305
1376
  ? storage.getAll(scope)
1306
1377
  : {};
1307
1378
  if (scope === StorageScope.Memory) {
@@ -1405,7 +1476,7 @@ export const storage = {
1405
1476
  }
1406
1477
 
1407
1478
  const keyPrefix = prefixKey(namespace, "");
1408
- const previousValues = hasStorageChangeObservers(scope)
1479
+ const previousValues = shouldReadPreviousEventValues(scope)
1409
1480
  ? storage.getByPrefix(keyPrefix, scope)
1410
1481
  : {};
1411
1482
  if (scope === StorageScope.Disk) {
@@ -1551,11 +1622,26 @@ export const storage = {
1551
1622
  return result;
1552
1623
  });
1553
1624
  },
1554
- export: (scope: StorageScope): Record<string, string> => {
1625
+ export: (
1626
+ scope: StorageScope,
1627
+ options: StorageExportOptions = {},
1628
+ ): Record<string, string> => {
1629
+ if (scope === StorageScope.Secure && options.includeSecureValues !== true) {
1630
+ throw new Error(
1631
+ "NitroStorage: exporting Secure scope exposes raw secret values. Pass { includeSecureValues: true } or use exportSecureUnsafe().",
1632
+ );
1633
+ }
1555
1634
  return measureOperation("storage:export", scope, () =>
1556
1635
  storage.getAll(scope),
1557
1636
  );
1558
1637
  },
1638
+ exportSecureUnsafe: (): Record<string, string> => {
1639
+ return measureOperation(
1640
+ "storage:exportSecureUnsafe",
1641
+ StorageScope.Secure,
1642
+ () => storage.getAll(StorageScope.Secure),
1643
+ );
1644
+ },
1559
1645
  size: (scope: StorageScope): number => {
1560
1646
  return measureOperation("storage:size", scope, () => {
1561
1647
  assertValidScope(scope);
@@ -1759,12 +1845,17 @@ export const storage = {
1759
1845
  export function setWebSecureStorageBackend(
1760
1846
  backend?: WebSecureStorageBackend,
1761
1847
  ): void {
1848
+ const previousBackend = webSecureStorageBackend;
1849
+ const nextBackend = backend ?? createDefaultSecureBackend();
1762
1850
  pendingSecureWrites.clear();
1763
- webSecureStorageBackend = backend ?? createDefaultSecureBackend();
1764
1851
  resetBackendChangeSubscription(StorageScope.Secure);
1852
+ webSecureStorageBackend = nextBackend;
1765
1853
  hydratedWebScopeKeyIndex.delete(StorageScope.Secure);
1766
1854
  clearScopeRawCache(StorageScope.Secure);
1767
1855
  ensureExternalSyncSubscriptions();
1856
+ if (previousBackend !== nextBackend) {
1857
+ closeWebBackend(StorageScope.Secure, previousBackend);
1858
+ }
1768
1859
  }
1769
1860
 
1770
1861
  export function getWebSecureStorageBackend():
@@ -1776,12 +1867,17 @@ export function getWebSecureStorageBackend():
1776
1867
  export function setWebDiskStorageBackend(
1777
1868
  backend?: WebDiskStorageBackend,
1778
1869
  ): void {
1870
+ const previousBackend = webDiskStorageBackend;
1871
+ const nextBackend = backend ?? createDefaultDiskBackend();
1779
1872
  pendingDiskWrites.clear();
1780
- webDiskStorageBackend = backend ?? createDefaultDiskBackend();
1781
1873
  resetBackendChangeSubscription(StorageScope.Disk);
1874
+ webDiskStorageBackend = nextBackend;
1782
1875
  hydratedWebScopeKeyIndex.delete(StorageScope.Disk);
1783
1876
  clearScopeRawCache(StorageScope.Disk);
1784
1877
  ensureExternalSyncSubscriptions();
1878
+ if (previousBackend !== nextBackend) {
1879
+ closeWebBackend(StorageScope.Disk, previousBackend);
1880
+ }
1785
1881
  }
1786
1882
 
1787
1883
  export function getWebDiskStorageBackend(): WebDiskStorageBackend | undefined {
@@ -2597,7 +2693,7 @@ export function setBatch<T>(
2597
2693
 
2598
2694
  flushSecureWrites();
2599
2695
  const keys = secureEntries.map(({ item }) => item.key);
2600
- const oldValues = hasStorageChangeObservers(scope)
2696
+ const oldValues = shouldReadPreviousEventValues(scope)
2601
2697
  ? WebStorage.getBatch(keys, scope)
2602
2698
  : [];
2603
2699
  const groupedByAccessControl = new Map<
@@ -2654,7 +2750,7 @@ export function setBatch<T>(
2654
2750
 
2655
2751
  const keys = items.map((entry) => entry.item.key);
2656
2752
  const values = items.map((entry) => entry.item.serialize(entry.value));
2657
- const oldValues = hasStorageChangeObservers(scope)
2753
+ const oldValues = shouldReadPreviousEventValues(scope)
2658
2754
  ? WebStorage.getBatch(keys, scope)
2659
2755
  : [];
2660
2756
  WebStorage.setBatch(keys, values, scope);
@@ -2712,7 +2808,7 @@ export function removeBatch(
2712
2808
  if (scope === StorageScope.Secure) {
2713
2809
  flushSecureWrites();
2714
2810
  }
2715
- const oldValues = hasStorageChangeObservers(scope)
2811
+ const oldValues = shouldReadPreviousEventValues(scope)
2716
2812
  ? WebStorage.getBatch(keys, scope)
2717
2813
  : [];
2718
2814
  WebStorage.removeBatch(keys, scope);
@@ -74,6 +74,7 @@ export async function createIndexedDBBackend(
74
74
  const pendingWrites = new Set<Promise<void>>();
75
75
  const pendingErrors: Error[] = [];
76
76
  const subscribers = new Set<(event: WebStorageChangeEvent) => void>();
77
+ let closed = false;
77
78
  const sourceId = `nitro-storage-${Math.random().toString(36).slice(2)}`;
78
79
  const channelName =
79
80
  options.channelName ?? `nitro-storage:${dbName}:${storeName}`;
@@ -82,6 +83,12 @@ export async function createIndexedDBBackend(
82
83
  ? new BroadcastChannel(channelName)
83
84
  : null;
84
85
 
86
+ function assertOpen(): void {
87
+ if (closed) {
88
+ throw new Error(`IndexedDB backend "${dbName}/${storeName}" is closed.`);
89
+ }
90
+ }
91
+
85
92
  function emitExternal(event: WebStorageChangeEvent): void {
86
93
  subscribers.forEach((subscriber) => {
87
94
  subscriber(event);
@@ -98,6 +105,10 @@ export async function createIndexedDBBackend(
98
105
  }
99
106
 
100
107
  channel?.addEventListener("message", (event: MessageEvent) => {
108
+ if (closed) {
109
+ return;
110
+ }
111
+
101
112
  const data = event.data as
102
113
  | (WebStorageChangeEvent & { sourceId?: string })
103
114
  | undefined;
@@ -209,34 +220,41 @@ export async function createIndexedDBBackend(
209
220
  const backend: WebSecureStorageBackend = {
210
221
  name: `indexeddb:${dbName}/${storeName}`,
211
222
  getItem(key: string): string | null {
223
+ assertOpen();
212
224
  return cache.get(key) ?? null;
213
225
  },
214
226
 
215
227
  setItem(key: string, value: string): void {
228
+ assertOpen();
216
229
  cache.set(key, value);
217
230
  persistSet(key, value);
218
231
  publish({ key, newValue: value });
219
232
  },
220
233
 
221
234
  removeItem(key: string): void {
235
+ assertOpen();
222
236
  cache.delete(key);
223
237
  persistDelete(key);
224
238
  publish({ key, newValue: null });
225
239
  },
226
240
 
227
241
  clear(): void {
242
+ assertOpen();
228
243
  cache.clear();
229
244
  persistClear();
230
245
  publish({ key: null, newValue: null });
231
246
  },
232
247
 
233
248
  getAllKeys(): string[] {
249
+ assertOpen();
234
250
  return Array.from(cache.keys());
235
251
  },
236
252
  getMany(keys: string[]): (string | null)[] {
253
+ assertOpen();
237
254
  return keys.map((key) => cache.get(key) ?? null);
238
255
  },
239
256
  setMany(entries): void {
257
+ assertOpen();
240
258
  entries.forEach(([key, value]) => {
241
259
  cache.set(key, value);
242
260
  });
@@ -253,6 +271,7 @@ export async function createIndexedDBBackend(
253
271
  }
254
272
  },
255
273
  removeMany(keys: string[]): void {
274
+ assertOpen();
256
275
  keys.forEach((key) => {
257
276
  cache.delete(key);
258
277
  });
@@ -269,9 +288,11 @@ export async function createIndexedDBBackend(
269
288
  }
270
289
  },
271
290
  size(): number {
291
+ assertOpen();
272
292
  return cache.size;
273
293
  },
274
294
  subscribe(listener): () => void {
295
+ assertOpen();
275
296
  subscribers.add(listener);
276
297
  return () => {
277
298
  subscribers.delete(listener);
@@ -286,6 +307,15 @@ export async function createIndexedDBBackend(
286
307
  const [error] = pendingErrors.splice(0);
287
308
  throw error;
288
309
  },
310
+ close(): void {
311
+ if (closed) {
312
+ return;
313
+ }
314
+ closed = true;
315
+ subscribers.clear();
316
+ channel?.close();
317
+ db.close();
318
+ },
289
319
  };
290
320
 
291
321
  return backend;
@@ -2,13 +2,13 @@ import { useRef, useSyncExternalStore } from "react";
2
2
 
3
3
  type HookStorageItem<T> = {
4
4
  get: () => T;
5
- set: (value: T | ((prev: T) => T)) => void;
5
+ set: StorageSetter<T>;
6
6
  subscribe: (callback: () => void) => () => void;
7
7
  };
8
8
 
9
- export function useStorage<T>(
10
- item: HookStorageItem<T>,
11
- ): [T, (value: T | ((prev: T) => T)) => void] {
9
+ export type StorageSetter<T> = (value: T | ((prev: T) => T)) => void;
10
+
11
+ export function useStorage<T>(item: HookStorageItem<T>): [T, StorageSetter<T>] {
12
12
  const value = useSyncExternalStore(item.subscribe, item.get, item.get);
13
13
  return [value, item.set];
14
14
  }
@@ -17,7 +17,7 @@ export function useStorageSelector<T, TSelected>(
17
17
  item: HookStorageItem<T>,
18
18
  selector: (value: T) => TSelected,
19
19
  isEqual: (prev: TSelected, next: TSelected) => boolean = Object.is,
20
- ): [TSelected, (value: T | ((prev: T) => T)) => void] {
20
+ ): [TSelected, StorageSetter<T>] {
21
21
  const selectedRef = useRef<
22
22
  { hasValue: false } | { hasValue: true; value: TSelected }
23
23
  >({
@@ -43,6 +43,6 @@ export function useStorageSelector<T, TSelected>(
43
43
  return [selectedValue, item.set];
44
44
  }
45
45
 
46
- export function useSetStorage<T>(item: HookStorageItem<T>) {
46
+ export function useSetStorage<T>(item: HookStorageItem<T>): StorageSetter<T> {
47
47
  return item.set;
48
48
  }
@@ -21,6 +21,7 @@ export type WebStorageBackend = {
21
21
  size?: () => number;
22
22
  subscribe?: (listener: (event: WebStorageChangeEvent) => void) => () => void;
23
23
  flush?: () => Promise<void>;
24
+ close?: () => void;
24
25
  name?: string;
25
26
  };
26
27