react-native-nitro-storage 0.5.3 → 0.5.5
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/.watchmanconfig +6 -0
- package/README.md +45 -5
- 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 +129 -27
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +169 -32
- 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/web-storage-backend.js.map +1 -1
- package/lib/module/index.js +129 -27
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +169 -32
- 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/web-storage-backend.js.map +1 -1
- package/lib/typescript/index.d.ts +10 -3
- 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/web-storage-backend.d.ts +1 -0
- package/lib/typescript/web-storage-backend.d.ts.map +1 -1
- package/package.json +5 -3
- package/src/index.ts +197 -32
- package/src/index.web.ts +250 -37
- package/src/indexeddb-backend.ts +30 -0
- 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;
|
|
@@ -118,8 +126,24 @@ type RawBatchPathItem = {
|
|
|
118
126
|
_hasValidation?: boolean;
|
|
119
127
|
_hasExpiration?: boolean;
|
|
120
128
|
_isBiometric?: boolean;
|
|
129
|
+
_biometricLevel?: BiometricLevel;
|
|
121
130
|
_secureAccessControl?: AccessControl;
|
|
122
131
|
};
|
|
132
|
+
type RollbackRecord =
|
|
133
|
+
| {
|
|
134
|
+
kind: "memory";
|
|
135
|
+
value: unknown;
|
|
136
|
+
}
|
|
137
|
+
| {
|
|
138
|
+
kind: "raw";
|
|
139
|
+
value: string | undefined;
|
|
140
|
+
accessControl?: AccessControl;
|
|
141
|
+
}
|
|
142
|
+
| {
|
|
143
|
+
kind: "biometric";
|
|
144
|
+
value: string | undefined;
|
|
145
|
+
level: BiometricLevel;
|
|
146
|
+
};
|
|
123
147
|
|
|
124
148
|
function asInternal<T>(item: StorageItem<T>): StorageItemInternal<T> {
|
|
125
149
|
return item as StorageItemInternal<T>;
|
|
@@ -134,6 +158,27 @@ function isUpdater<T>(
|
|
|
134
158
|
function typedKeys<K extends string, V>(record: Record<K, V>): K[] {
|
|
135
159
|
return Object.keys(record) as K[];
|
|
136
160
|
}
|
|
161
|
+
function assertEnumInteger(
|
|
162
|
+
value: number,
|
|
163
|
+
min: number,
|
|
164
|
+
max: number,
|
|
165
|
+
label: string,
|
|
166
|
+
): void {
|
|
167
|
+
if (!Number.isFinite(value) || value < min || value > max) {
|
|
168
|
+
throw new Error(`NitroStorage: Invalid ${label}`);
|
|
169
|
+
}
|
|
170
|
+
if (value !== Math.trunc(value)) {
|
|
171
|
+
throw new Error(`NitroStorage: Invalid ${label}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function assertAccessControlLevel(level: number): void {
|
|
176
|
+
assertEnumInteger(level, 0, 4, "access control level");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function assertBiometricLevel(level: number): void {
|
|
180
|
+
assertEnumInteger(level, 0, 2, "biometric level");
|
|
181
|
+
}
|
|
137
182
|
type NonMemoryScope = StorageScope.Disk | StorageScope.Secure;
|
|
138
183
|
type PendingDiskWrite = {
|
|
139
184
|
key: string;
|
|
@@ -217,6 +262,7 @@ let hasWarnedAboutWebBiometricFallback = false;
|
|
|
217
262
|
let hasWindowStorageEventSubscription = false;
|
|
218
263
|
let metricsObserver: StorageMetricsObserver | undefined;
|
|
219
264
|
let eventObserver: StorageEventListener | undefined;
|
|
265
|
+
let eventObserverRedactSecureValues = true;
|
|
220
266
|
const metricsCounters = new Map<
|
|
221
267
|
string,
|
|
222
268
|
{ count: number; totalDurationMs: number; maxDurationMs: number }
|
|
@@ -515,6 +561,21 @@ function resetBackendChangeSubscription(scope: NonMemoryScope): void {
|
|
|
515
561
|
externalSyncUnsubscribers.delete(scope);
|
|
516
562
|
}
|
|
517
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
|
+
|
|
518
579
|
function ensureExternalSyncSubscriptions(): void {
|
|
519
580
|
if (
|
|
520
581
|
!hasWindowStorageEventSubscription &&
|
|
@@ -638,6 +699,49 @@ function hasStorageChangeObservers(scope: StorageScope): boolean {
|
|
|
638
699
|
return storageEvents.hasListeners(scope) || eventObserver !== undefined;
|
|
639
700
|
}
|
|
640
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
|
+
|
|
641
745
|
function emitKeyChange(
|
|
642
746
|
scope: StorageScope,
|
|
643
747
|
key: string,
|
|
@@ -655,7 +759,7 @@ function emitKeyChange(
|
|
|
655
759
|
source,
|
|
656
760
|
);
|
|
657
761
|
storageEvents.emitKey(event);
|
|
658
|
-
eventObserver?.(event);
|
|
762
|
+
eventObserver?.(eventForGlobalObserver(event));
|
|
659
763
|
}
|
|
660
764
|
|
|
661
765
|
function emitBatchChange(
|
|
@@ -676,7 +780,7 @@ function emitBatchChange(
|
|
|
676
780
|
changes,
|
|
677
781
|
};
|
|
678
782
|
storageEvents.emitBatch(event);
|
|
679
|
-
eventObserver?.(event);
|
|
783
|
+
eventObserver?.(eventForGlobalObserver(event));
|
|
680
784
|
}
|
|
681
785
|
|
|
682
786
|
function readPendingSecureWrite(key: string): string | undefined {
|
|
@@ -866,6 +970,11 @@ const WebStorage: Storage = {
|
|
|
866
970
|
if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
|
|
867
971
|
return;
|
|
868
972
|
}
|
|
973
|
+
if (keys.length !== values.length) {
|
|
974
|
+
throw new Error(
|
|
975
|
+
"NitroStorage: Keys and values size mismatch in setBatch",
|
|
976
|
+
);
|
|
977
|
+
}
|
|
869
978
|
|
|
870
979
|
const entries: (readonly [string, string])[] = [];
|
|
871
980
|
keys.forEach((key, index) => {
|
|
@@ -888,7 +997,13 @@ const WebStorage: Storage = {
|
|
|
888
997
|
});
|
|
889
998
|
});
|
|
890
999
|
const keyIndex = ensureWebScopeKeyIndex(scope);
|
|
891
|
-
|
|
1000
|
+
entries.forEach(([storageKey]) =>
|
|
1001
|
+
keyIndex.add(
|
|
1002
|
+
scope === StorageScope.Secure
|
|
1003
|
+
? storageKey.slice(SECURE_WEB_PREFIX.length)
|
|
1004
|
+
: storageKey,
|
|
1005
|
+
),
|
|
1006
|
+
);
|
|
892
1007
|
const listeners = getScopedListeners(scope);
|
|
893
1008
|
keys.forEach((key) => notifyKeyListeners(listeners, key));
|
|
894
1009
|
},
|
|
@@ -988,7 +1103,9 @@ const WebStorage: Storage = {
|
|
|
988
1103
|
}
|
|
989
1104
|
return 0;
|
|
990
1105
|
},
|
|
991
|
-
setSecureAccessControl: () => {
|
|
1106
|
+
setSecureAccessControl: (level: number) => {
|
|
1107
|
+
assertAccessControlLevel(level);
|
|
1108
|
+
},
|
|
992
1109
|
setSecureWritesAsync: (_enabled: boolean) => {},
|
|
993
1110
|
setKeychainAccessGroup: () => {},
|
|
994
1111
|
setSecureBiometric: (key: string, value: string) => {
|
|
@@ -998,7 +1115,17 @@ const WebStorage: Storage = {
|
|
|
998
1115
|
BiometricLevel.BiometryOnly,
|
|
999
1116
|
);
|
|
1000
1117
|
},
|
|
1001
|
-
setSecureBiometricWithLevel: (key: string, value: string,
|
|
1118
|
+
setSecureBiometricWithLevel: (key: string, value: string, level: number) => {
|
|
1119
|
+
assertBiometricLevel(level);
|
|
1120
|
+
if (level === BiometricLevel.None) {
|
|
1121
|
+
withWebBackendOperation(StorageScope.Secure, "setSecure", (backend) => {
|
|
1122
|
+
backend.removeItem(toBiometricStorageKey(key));
|
|
1123
|
+
backend.setItem(toSecureStorageKey(key), value);
|
|
1124
|
+
});
|
|
1125
|
+
ensureWebScopeKeyIndex(StorageScope.Secure).add(key);
|
|
1126
|
+
notifyKeyListeners(getScopedListeners(StorageScope.Secure), key);
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
1002
1129
|
if (
|
|
1003
1130
|
typeof __DEV__ !== "undefined" &&
|
|
1004
1131
|
__DEV__ &&
|
|
@@ -1233,15 +1360,19 @@ export const storage = {
|
|
|
1233
1360
|
): (() => void) => {
|
|
1234
1361
|
return storage.subscribePrefix(scope, prefixKey(namespace, ""), listener);
|
|
1235
1362
|
},
|
|
1236
|
-
setEventObserver: (
|
|
1363
|
+
setEventObserver: (
|
|
1364
|
+
observer?: StorageEventListener,
|
|
1365
|
+
options: StorageEventObserverOptions = {},
|
|
1366
|
+
) => {
|
|
1237
1367
|
eventObserver = observer;
|
|
1368
|
+
eventObserverRedactSecureValues = options.redactSecureValues !== false;
|
|
1238
1369
|
if (observer) {
|
|
1239
1370
|
ensureExternalSyncSubscriptions();
|
|
1240
1371
|
}
|
|
1241
1372
|
},
|
|
1242
1373
|
clear: (scope: StorageScope) => {
|
|
1243
1374
|
measureOperation("storage:clear", scope, () => {
|
|
1244
|
-
const previousValues =
|
|
1375
|
+
const previousValues = shouldReadPreviousEventValues(scope)
|
|
1245
1376
|
? storage.getAll(scope)
|
|
1246
1377
|
: {};
|
|
1247
1378
|
if (scope === StorageScope.Memory) {
|
|
@@ -1345,7 +1476,7 @@ export const storage = {
|
|
|
1345
1476
|
}
|
|
1346
1477
|
|
|
1347
1478
|
const keyPrefix = prefixKey(namespace, "");
|
|
1348
|
-
const previousValues =
|
|
1479
|
+
const previousValues = shouldReadPreviousEventValues(scope)
|
|
1349
1480
|
? storage.getByPrefix(keyPrefix, scope)
|
|
1350
1481
|
: {};
|
|
1351
1482
|
if (scope === StorageScope.Disk) {
|
|
@@ -1491,11 +1622,26 @@ export const storage = {
|
|
|
1491
1622
|
return result;
|
|
1492
1623
|
});
|
|
1493
1624
|
},
|
|
1494
|
-
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
|
+
}
|
|
1495
1634
|
return measureOperation("storage:export", scope, () =>
|
|
1496
1635
|
storage.getAll(scope),
|
|
1497
1636
|
);
|
|
1498
1637
|
},
|
|
1638
|
+
exportSecureUnsafe: (): Record<string, string> => {
|
|
1639
|
+
return measureOperation(
|
|
1640
|
+
"storage:exportSecureUnsafe",
|
|
1641
|
+
StorageScope.Secure,
|
|
1642
|
+
() => storage.getAll(StorageScope.Secure),
|
|
1643
|
+
);
|
|
1644
|
+
},
|
|
1499
1645
|
size: (scope: StorageScope): number => {
|
|
1500
1646
|
return measureOperation("storage:size", scope, () => {
|
|
1501
1647
|
assertValidScope(scope);
|
|
@@ -1510,6 +1656,7 @@ export const storage = {
|
|
|
1510
1656
|
});
|
|
1511
1657
|
},
|
|
1512
1658
|
setAccessControl: (level: AccessControl) => {
|
|
1659
|
+
assertAccessControlLevel(level);
|
|
1513
1660
|
secureDefaultAccessControl = level;
|
|
1514
1661
|
recordMetric("storage:setAccessControl", StorageScope.Secure, 0);
|
|
1515
1662
|
},
|
|
@@ -1698,12 +1845,17 @@ export const storage = {
|
|
|
1698
1845
|
export function setWebSecureStorageBackend(
|
|
1699
1846
|
backend?: WebSecureStorageBackend,
|
|
1700
1847
|
): void {
|
|
1848
|
+
const previousBackend = webSecureStorageBackend;
|
|
1849
|
+
const nextBackend = backend ?? createDefaultSecureBackend();
|
|
1701
1850
|
pendingSecureWrites.clear();
|
|
1702
|
-
webSecureStorageBackend = backend ?? createDefaultSecureBackend();
|
|
1703
1851
|
resetBackendChangeSubscription(StorageScope.Secure);
|
|
1852
|
+
webSecureStorageBackend = nextBackend;
|
|
1704
1853
|
hydratedWebScopeKeyIndex.delete(StorageScope.Secure);
|
|
1705
1854
|
clearScopeRawCache(StorageScope.Secure);
|
|
1706
1855
|
ensureExternalSyncSubscriptions();
|
|
1856
|
+
if (previousBackend !== nextBackend) {
|
|
1857
|
+
closeWebBackend(StorageScope.Secure, previousBackend);
|
|
1858
|
+
}
|
|
1707
1859
|
}
|
|
1708
1860
|
|
|
1709
1861
|
export function getWebSecureStorageBackend():
|
|
@@ -1715,12 +1867,17 @@ export function getWebSecureStorageBackend():
|
|
|
1715
1867
|
export function setWebDiskStorageBackend(
|
|
1716
1868
|
backend?: WebDiskStorageBackend,
|
|
1717
1869
|
): void {
|
|
1870
|
+
const previousBackend = webDiskStorageBackend;
|
|
1871
|
+
const nextBackend = backend ?? createDefaultDiskBackend();
|
|
1718
1872
|
pendingDiskWrites.clear();
|
|
1719
|
-
webDiskStorageBackend = backend ?? createDefaultDiskBackend();
|
|
1720
1873
|
resetBackendChangeSubscription(StorageScope.Disk);
|
|
1874
|
+
webDiskStorageBackend = nextBackend;
|
|
1721
1875
|
hydratedWebScopeKeyIndex.delete(StorageScope.Disk);
|
|
1722
1876
|
clearScopeRawCache(StorageScope.Disk);
|
|
1723
1877
|
ensureExternalSyncSubscriptions();
|
|
1878
|
+
if (previousBackend !== nextBackend) {
|
|
1879
|
+
closeWebBackend(StorageScope.Disk, previousBackend);
|
|
1880
|
+
}
|
|
1724
1881
|
}
|
|
1725
1882
|
|
|
1726
1883
|
export function getWebDiskStorageBackend(): WebDiskStorageBackend | undefined {
|
|
@@ -1793,6 +1950,7 @@ type StorageItemInternal<T> = StorageItem<T> & {
|
|
|
1793
1950
|
_hasExpiration: boolean;
|
|
1794
1951
|
_readCacheEnabled: boolean;
|
|
1795
1952
|
_isBiometric: boolean;
|
|
1953
|
+
_biometricLevel: BiometricLevel;
|
|
1796
1954
|
_defaultValue: T;
|
|
1797
1955
|
_secureAccessControl?: AccessControl;
|
|
1798
1956
|
};
|
|
@@ -1863,6 +2021,12 @@ export function createStorageItem<T = undefined>(
|
|
|
1863
2021
|
if (expiration && expiration.ttlMs <= 0) {
|
|
1864
2022
|
throw new Error("expiration.ttlMs must be greater than 0.");
|
|
1865
2023
|
}
|
|
2024
|
+
if (config.scope === StorageScope.Secure) {
|
|
2025
|
+
assertBiometricLevel(resolvedBiometricLevel);
|
|
2026
|
+
if (secureAccessControl !== undefined) {
|
|
2027
|
+
assertAccessControlLevel(secureAccessControl);
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
1866
2030
|
|
|
1867
2031
|
const listeners = new Set<() => void>();
|
|
1868
2032
|
let unsubscribe: (() => void) | null = null;
|
|
@@ -2350,6 +2514,7 @@ export function createStorageItem<T = undefined>(
|
|
|
2350
2514
|
_hasExpiration: expiration !== undefined,
|
|
2351
2515
|
_readCacheEnabled: readCache,
|
|
2352
2516
|
_isBiometric: isBiometric,
|
|
2517
|
+
_biometricLevel: resolvedBiometricLevel,
|
|
2353
2518
|
_defaultValue: defaultValue,
|
|
2354
2519
|
...(secureAccessControl !== undefined
|
|
2355
2520
|
? { _secureAccessControl: secureAccessControl }
|
|
@@ -2528,7 +2693,7 @@ export function setBatch<T>(
|
|
|
2528
2693
|
|
|
2529
2694
|
flushSecureWrites();
|
|
2530
2695
|
const keys = secureEntries.map(({ item }) => item.key);
|
|
2531
|
-
const oldValues =
|
|
2696
|
+
const oldValues = shouldReadPreviousEventValues(scope)
|
|
2532
2697
|
? WebStorage.getBatch(keys, scope)
|
|
2533
2698
|
: [];
|
|
2534
2699
|
const groupedByAccessControl = new Map<
|
|
@@ -2585,7 +2750,7 @@ export function setBatch<T>(
|
|
|
2585
2750
|
|
|
2586
2751
|
const keys = items.map((entry) => entry.item.key);
|
|
2587
2752
|
const values = items.map((entry) => entry.item.serialize(entry.value));
|
|
2588
|
-
const oldValues =
|
|
2753
|
+
const oldValues = shouldReadPreviousEventValues(scope)
|
|
2589
2754
|
? WebStorage.getBatch(keys, scope)
|
|
2590
2755
|
: [];
|
|
2591
2756
|
WebStorage.setBatch(keys, values, scope);
|
|
@@ -2643,7 +2808,7 @@ export function removeBatch(
|
|
|
2643
2808
|
if (scope === StorageScope.Secure) {
|
|
2644
2809
|
flushSecureWrites();
|
|
2645
2810
|
}
|
|
2646
|
-
const oldValues =
|
|
2811
|
+
const oldValues = shouldReadPreviousEventValues(scope)
|
|
2647
2812
|
? WebStorage.getBatch(keys, scope)
|
|
2648
2813
|
: [];
|
|
2649
2814
|
WebStorage.removeBatch(keys, scope);
|
|
@@ -2729,19 +2894,40 @@ export function runTransaction<T>(
|
|
|
2729
2894
|
}
|
|
2730
2895
|
|
|
2731
2896
|
const NOT_SET = Symbol();
|
|
2732
|
-
const rollback = new Map<string,
|
|
2897
|
+
const rollback = new Map<string, RollbackRecord>();
|
|
2733
2898
|
|
|
2734
|
-
const rememberRollback = (
|
|
2899
|
+
const rememberRollback = (
|
|
2900
|
+
key: string,
|
|
2901
|
+
item?: Pick<StorageItem<unknown>, "key" | "scope">,
|
|
2902
|
+
) => {
|
|
2735
2903
|
if (rollback.has(key)) {
|
|
2736
2904
|
return;
|
|
2737
2905
|
}
|
|
2738
2906
|
if (scope === StorageScope.Memory) {
|
|
2739
|
-
rollback.set(
|
|
2740
|
-
|
|
2741
|
-
memoryStore.has(key) ? memoryStore.get(key) : NOT_SET,
|
|
2742
|
-
);
|
|
2907
|
+
rollback.set(key, {
|
|
2908
|
+
kind: "memory",
|
|
2909
|
+
value: memoryStore.has(key) ? memoryStore.get(key) : NOT_SET,
|
|
2910
|
+
});
|
|
2743
2911
|
} else {
|
|
2744
|
-
|
|
2912
|
+
const internal = item
|
|
2913
|
+
? (item as StorageItemInternal<unknown>)
|
|
2914
|
+
: undefined;
|
|
2915
|
+
if (scope === StorageScope.Secure && internal?._isBiometric === true) {
|
|
2916
|
+
rollback.set(key, {
|
|
2917
|
+
kind: "biometric",
|
|
2918
|
+
value: WebStorage.getSecureBiometric(key),
|
|
2919
|
+
level: internal._biometricLevel,
|
|
2920
|
+
});
|
|
2921
|
+
return;
|
|
2922
|
+
}
|
|
2923
|
+
rollback.set(key, {
|
|
2924
|
+
kind: "raw",
|
|
2925
|
+
value: getRawValue(key, scope),
|
|
2926
|
+
...(scope === StorageScope.Secure &&
|
|
2927
|
+
internal?._secureAccessControl !== undefined
|
|
2928
|
+
? { accessControl: internal._secureAccessControl }
|
|
2929
|
+
: {}),
|
|
2930
|
+
});
|
|
2745
2931
|
}
|
|
2746
2932
|
};
|
|
2747
2933
|
|
|
@@ -2762,12 +2948,12 @@ export function runTransaction<T>(
|
|
|
2762
2948
|
},
|
|
2763
2949
|
setItem: (item, value) => {
|
|
2764
2950
|
assertBatchScope([item], scope);
|
|
2765
|
-
rememberRollback(item.key);
|
|
2951
|
+
rememberRollback(item.key, item);
|
|
2766
2952
|
item.set(value);
|
|
2767
2953
|
},
|
|
2768
2954
|
removeItem: (item) => {
|
|
2769
2955
|
assertBatchScope([item], scope);
|
|
2770
|
-
rememberRollback(item.key);
|
|
2956
|
+
rememberRollback(item.key, item);
|
|
2771
2957
|
item.delete();
|
|
2772
2958
|
},
|
|
2773
2959
|
};
|
|
@@ -2777,25 +2963,49 @@ export function runTransaction<T>(
|
|
|
2777
2963
|
} catch (error) {
|
|
2778
2964
|
const rollbackEntries = Array.from(rollback.entries()).reverse();
|
|
2779
2965
|
if (scope === StorageScope.Memory) {
|
|
2780
|
-
rollbackEntries.forEach(([key,
|
|
2781
|
-
if (
|
|
2966
|
+
rollbackEntries.forEach(([key, record]) => {
|
|
2967
|
+
if (record.value === NOT_SET) {
|
|
2782
2968
|
memoryStore.delete(key);
|
|
2783
2969
|
} else {
|
|
2784
|
-
memoryStore.set(key,
|
|
2970
|
+
memoryStore.set(key, record.value);
|
|
2785
2971
|
}
|
|
2786
2972
|
notifyKeyListeners(memoryListeners, key);
|
|
2787
2973
|
});
|
|
2788
2974
|
} else {
|
|
2789
|
-
const
|
|
2790
|
-
|
|
2975
|
+
const groupedKeysToSet = new Map<
|
|
2976
|
+
AccessControl,
|
|
2977
|
+
{ keys: string[]; values: string[] }
|
|
2978
|
+
>();
|
|
2791
2979
|
const keysToRemove: string[] = [];
|
|
2792
2980
|
|
|
2793
|
-
rollbackEntries.forEach(([key,
|
|
2794
|
-
if (
|
|
2981
|
+
rollbackEntries.forEach(([key, record]) => {
|
|
2982
|
+
if (record.kind === "biometric") {
|
|
2983
|
+
if (record.value === undefined) {
|
|
2984
|
+
WebStorage.deleteSecureBiometric(key);
|
|
2985
|
+
} else {
|
|
2986
|
+
WebStorage.setSecureBiometricWithLevel(
|
|
2987
|
+
key,
|
|
2988
|
+
record.value,
|
|
2989
|
+
record.level,
|
|
2990
|
+
);
|
|
2991
|
+
}
|
|
2992
|
+
return;
|
|
2993
|
+
}
|
|
2994
|
+
if (record.kind !== "raw") {
|
|
2995
|
+
return;
|
|
2996
|
+
}
|
|
2997
|
+
if (record.value === undefined) {
|
|
2795
2998
|
keysToRemove.push(key);
|
|
2796
2999
|
} else {
|
|
2797
|
-
|
|
2798
|
-
|
|
3000
|
+
const accessControl =
|
|
3001
|
+
record.accessControl ?? secureDefaultAccessControl;
|
|
3002
|
+
const existingGroup = groupedKeysToSet.get(accessControl);
|
|
3003
|
+
const group = existingGroup ?? { keys: [], values: [] };
|
|
3004
|
+
group.keys.push(key);
|
|
3005
|
+
group.values.push(record.value);
|
|
3006
|
+
if (!existingGroup) {
|
|
3007
|
+
groupedKeysToSet.set(accessControl, group);
|
|
3008
|
+
}
|
|
2799
3009
|
}
|
|
2800
3010
|
});
|
|
2801
3011
|
|
|
@@ -2805,12 +3015,15 @@ export function runTransaction<T>(
|
|
|
2805
3015
|
if (scope === StorageScope.Secure) {
|
|
2806
3016
|
flushSecureWrites();
|
|
2807
3017
|
}
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
3018
|
+
groupedKeysToSet.forEach((group, accessControl) => {
|
|
3019
|
+
if (scope === StorageScope.Secure) {
|
|
3020
|
+
WebStorage.setSecureAccessControl(accessControl);
|
|
3021
|
+
}
|
|
3022
|
+
WebStorage.setBatch(group.keys, group.values, scope);
|
|
3023
|
+
group.keys.forEach((key, index) =>
|
|
3024
|
+
cacheRawValue(scope, key, group.values[index]),
|
|
2812
3025
|
);
|
|
2813
|
-
}
|
|
3026
|
+
});
|
|
2814
3027
|
if (keysToRemove.length > 0) {
|
|
2815
3028
|
WebStorage.removeBatch(keysToRemove, scope);
|
|
2816
3029
|
keysToRemove.forEach((key) => cacheRawValue(scope, key, undefined));
|
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;
|