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.
- package/README.md +90 -7
- package/android/build.gradle +5 -5
- package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +12 -25
- package/app.plugin.js +114 -9
- package/docs/api-reference.md +39 -36
- package/docs/batch-transactions-migrations.md +1 -1
- package/docs/recipes.md +1 -1
- package/docs/secure-storage.md +15 -4
- package/docs/web-backends.md +5 -0
- package/lib/commonjs/index.js +49 -9
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +71 -11
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/indexeddb-backend.js +28 -0
- package/lib/commonjs/indexeddb-backend.js.map +1 -1
- package/lib/commonjs/storage-hooks.js.map +1 -1
- package/lib/commonjs/web-storage-backend.js.map +1 -1
- package/lib/module/index.js +49 -9
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +71 -11
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/indexeddb-backend.js +28 -0
- package/lib/module/indexeddb-backend.js.map +1 -1
- package/lib/module/storage-hooks.js.map +1 -1
- package/lib/module/web-storage-backend.js.map +1 -1
- package/lib/typescript/index.d.ts +21 -9
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/index.web.d.ts +10 -3
- package/lib/typescript/index.web.d.ts.map +1 -1
- package/lib/typescript/indexeddb-backend.d.ts.map +1 -1
- package/lib/typescript/storage-hooks.d.ts +5 -4
- package/lib/typescript/storage-hooks.d.ts.map +1 -1
- package/lib/typescript/web-storage-backend.d.ts +1 -0
- package/lib/typescript/web-storage-backend.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/index.ts +96 -19
- package/src/index.web.ts +107 -11
- package/src/indexeddb-backend.ts +30 -0
- package/src/storage-hooks.ts +6 -6
- 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: (
|
|
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 =
|
|
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 =
|
|
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: (
|
|
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 =
|
|
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 =
|
|
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 =
|
|
2811
|
+
const oldValues = shouldReadPreviousEventValues(scope)
|
|
2716
2812
|
? WebStorage.getBatch(keys, scope)
|
|
2717
2813
|
: [];
|
|
2718
2814
|
WebStorage.removeBatch(keys, scope);
|
package/src/indexeddb-backend.ts
CHANGED
|
@@ -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;
|
package/src/storage-hooks.ts
CHANGED
|
@@ -2,13 +2,13 @@ import { useRef, useSyncExternalStore } from "react";
|
|
|
2
2
|
|
|
3
3
|
type HookStorageItem<T> = {
|
|
4
4
|
get: () => T;
|
|
5
|
-
set:
|
|
5
|
+
set: StorageSetter<T>;
|
|
6
6
|
subscribe: (callback: () => void) => () => void;
|
|
7
7
|
};
|
|
8
8
|
|
|
9
|
-
export
|
|
10
|
-
|
|
11
|
-
|
|
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,
|
|
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
|
}
|