react-native-nitro-storage 0.5.6 → 0.5.8
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 +267 -362
- package/ios/IOSStorageAdapterCpp.mm +126 -80
- package/lib/commonjs/index.js +119 -1599
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +83 -1550
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/shared.js +102 -0
- package/lib/commonjs/shared.js.map +1 -0
- package/lib/commonjs/storage-core.js +1505 -0
- package/lib/commonjs/storage-core.js.map +1 -0
- package/lib/module/index.js +111 -1591
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +69 -1536
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/shared.js +82 -0
- package/lib/module/shared.js.map +1 -0
- package/lib/module/storage-core.js +1501 -0
- package/lib/module/storage-core.js.map +1 -0
- package/lib/typescript/index.d.ts +30 -133
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/index.web.d.ts +31 -129
- package/lib/typescript/index.web.d.ts.map +1 -1
- package/lib/typescript/shared.d.ts +96 -0
- package/lib/typescript/shared.d.ts.map +1 -0
- package/lib/typescript/storage-core.d.ts +157 -0
- package/lib/typescript/storage-core.d.ts.map +1 -0
- package/package.json +7 -3
- package/react-native-nitro-storage.podspec +4 -0
- package/src/index.ts +260 -2519
- package/src/index.web.ts +133 -2326
- package/src/shared.ts +249 -0
- package/src/storage-core.ts +2349 -0
package/src/index.web.ts
CHANGED
|
@@ -1,16 +1,11 @@
|
|
|
1
|
-
import { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
|
|
2
1
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
toVersionToken,
|
|
11
|
-
prefixKey,
|
|
12
|
-
isNamespaced,
|
|
13
|
-
} from "./internal";
|
|
2
|
+
assertAccessControlLevel,
|
|
3
|
+
assertBiometricLevel,
|
|
4
|
+
notifyAllListeners,
|
|
5
|
+
notifyKeyListeners,
|
|
6
|
+
type NonMemoryScope,
|
|
7
|
+
} from "./shared";
|
|
8
|
+
import { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
|
|
14
9
|
import {
|
|
15
10
|
createLocalStorageWebBackend,
|
|
16
11
|
type WebDiskStorageBackend,
|
|
@@ -18,23 +13,32 @@ import {
|
|
|
18
13
|
type WebStorageBackend,
|
|
19
14
|
type WebStorageChangeEvent,
|
|
20
15
|
} from "./web-storage-backend";
|
|
21
|
-
import {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
type SecureStorageMetadata,
|
|
25
|
-
type SecurityCapabilities,
|
|
26
|
-
type StorageCapabilities,
|
|
27
|
-
type StorageErrorCode,
|
|
16
|
+
import type {
|
|
17
|
+
SecurityCapabilities,
|
|
18
|
+
StorageCapabilities,
|
|
28
19
|
} from "./storage-runtime";
|
|
29
20
|
import {
|
|
30
|
-
|
|
31
|
-
type
|
|
32
|
-
type
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
21
|
+
createStorageCore,
|
|
22
|
+
type StorageCoreAdapter,
|
|
23
|
+
type StorageCoreInternals,
|
|
24
|
+
} from "./storage-core";
|
|
25
|
+
export type {
|
|
26
|
+
ExpirationConfig,
|
|
27
|
+
Migration,
|
|
28
|
+
MigrationContext,
|
|
29
|
+
SecureAuthStorageConfig,
|
|
30
|
+
StorageEventObserverOptions,
|
|
31
|
+
StorageExportOptions,
|
|
32
|
+
StorageMetricSummary,
|
|
33
|
+
StorageMetricsEvent,
|
|
34
|
+
StorageMetricsObserver,
|
|
35
|
+
StorageSelectorListener,
|
|
36
|
+
StorageSelectorSubscribeOptions,
|
|
37
|
+
StorageVersion,
|
|
38
|
+
Validator,
|
|
39
|
+
VersionedValue,
|
|
40
|
+
} from "./shared";
|
|
41
|
+
export { isKeychainLockedError } from "./shared";
|
|
38
42
|
|
|
39
43
|
export { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
|
|
40
44
|
export { migrateFromMMKV } from "./migration";
|
|
@@ -53,6 +57,7 @@ export type {
|
|
|
53
57
|
StorageEventListener,
|
|
54
58
|
StorageKeyChangeEvent,
|
|
55
59
|
} from "./storage-events";
|
|
60
|
+
export type { StorageSetter } from "./storage-hooks";
|
|
56
61
|
export type {
|
|
57
62
|
WebDiskStorageBackend,
|
|
58
63
|
WebSecureStorageBackend,
|
|
@@ -60,147 +65,12 @@ export type {
|
|
|
60
65
|
WebStorageChangeEvent,
|
|
61
66
|
WebStorageScope,
|
|
62
67
|
} from "./web-storage-backend";
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
export type VersionedValue<T> = {
|
|
70
|
-
value: T;
|
|
71
|
-
version: StorageVersion;
|
|
72
|
-
};
|
|
73
|
-
export type StorageMetricsEvent = {
|
|
74
|
-
operation: string;
|
|
75
|
-
scope: StorageScope;
|
|
76
|
-
durationMs: number;
|
|
77
|
-
keysCount: number;
|
|
78
|
-
};
|
|
79
|
-
export type StorageMetricsObserver = (event: StorageMetricsEvent) => void;
|
|
80
|
-
export type StorageEventObserverOptions = {
|
|
81
|
-
redactSecureValues?: boolean;
|
|
82
|
-
};
|
|
83
|
-
export type StorageExportOptions = {
|
|
84
|
-
includeSecureValues?: boolean;
|
|
85
|
-
};
|
|
86
|
-
export type StorageMetricSummary = {
|
|
87
|
-
count: number;
|
|
88
|
-
totalDurationMs: number;
|
|
89
|
-
avgDurationMs: number;
|
|
90
|
-
maxDurationMs: number;
|
|
91
|
-
};
|
|
92
|
-
export type StorageSelectorListener<TSelected> = (
|
|
93
|
-
value: TSelected,
|
|
94
|
-
previousValue: TSelected,
|
|
95
|
-
) => void;
|
|
96
|
-
export type StorageSelectorSubscribeOptions<TSelected> = {
|
|
97
|
-
isEqual?: (previousValue: TSelected, nextValue: TSelected) => boolean;
|
|
98
|
-
fireImmediately?: boolean;
|
|
99
|
-
};
|
|
100
|
-
export type MigrationContext = {
|
|
101
|
-
scope: StorageScope;
|
|
102
|
-
getRaw: (key: string) => string | undefined;
|
|
103
|
-
setRaw: (key: string, value: string) => void;
|
|
104
|
-
removeRaw: (key: string) => void;
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
export type Migration = (context: MigrationContext) => void;
|
|
108
|
-
|
|
109
|
-
export type TransactionContext = {
|
|
110
|
-
scope: StorageScope;
|
|
111
|
-
getRaw: (key: string) => string | undefined;
|
|
112
|
-
setRaw: (key: string, value: string) => void;
|
|
113
|
-
removeRaw: (key: string) => void;
|
|
114
|
-
getItem: <T>(item: Pick<StorageItem<T>, "scope" | "key" | "get">) => T;
|
|
115
|
-
setItem: <T>(
|
|
116
|
-
item: Pick<StorageItem<T>, "scope" | "key" | "set">,
|
|
117
|
-
value: T,
|
|
118
|
-
) => void;
|
|
119
|
-
removeItem: (
|
|
120
|
-
item: Pick<StorageItem<unknown>, "scope" | "key" | "delete">,
|
|
121
|
-
) => void;
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
type KeyListenerRegistry = Map<string, Set<() => void>>;
|
|
125
|
-
type RawBatchPathItem = {
|
|
126
|
-
_hasValidation?: boolean;
|
|
127
|
-
_hasExpiration?: boolean;
|
|
128
|
-
_isBiometric?: boolean;
|
|
129
|
-
_biometricLevel?: BiometricLevel;
|
|
130
|
-
_secureAccessControl?: AccessControl;
|
|
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
|
-
};
|
|
147
|
-
|
|
148
|
-
function asInternal<T>(item: StorageItem<T>): StorageItemInternal<T> {
|
|
149
|
-
return item as StorageItemInternal<T>;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function isUpdater<T>(
|
|
153
|
-
valueOrFn: T | ((prev: T) => T),
|
|
154
|
-
): valueOrFn is (prev: T) => T {
|
|
155
|
-
return typeof valueOrFn === "function";
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function typedKeys<K extends string, V>(record: Record<K, V>): K[] {
|
|
159
|
-
return Object.keys(record) as K[];
|
|
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
|
-
}
|
|
182
|
-
type NonMemoryScope = StorageScope.Disk | StorageScope.Secure;
|
|
183
|
-
type PendingDiskWrite = {
|
|
184
|
-
key: string;
|
|
185
|
-
value: string | undefined;
|
|
186
|
-
};
|
|
187
|
-
type PendingSecureWrite = {
|
|
188
|
-
key: string;
|
|
189
|
-
value: string | undefined;
|
|
190
|
-
accessControl?: AccessControl;
|
|
191
|
-
};
|
|
192
|
-
|
|
193
|
-
const registeredMigrations = new Map<number, Migration>();
|
|
194
|
-
const runMicrotask =
|
|
195
|
-
typeof queueMicrotask === "function"
|
|
196
|
-
? queueMicrotask
|
|
197
|
-
: (task: () => void) => {
|
|
198
|
-
Promise.resolve().then(task);
|
|
199
|
-
};
|
|
200
|
-
const now =
|
|
201
|
-
typeof performance !== "undefined" && typeof performance.now === "function"
|
|
202
|
-
? () => performance.now()
|
|
203
|
-
: () => Date.now();
|
|
68
|
+
export type {
|
|
69
|
+
StorageBatchSetItem,
|
|
70
|
+
StorageItem,
|
|
71
|
+
StorageItemConfig,
|
|
72
|
+
TransactionContext,
|
|
73
|
+
} from "./storage-core";
|
|
204
74
|
|
|
205
75
|
export interface Storage {
|
|
206
76
|
name: string;
|
|
@@ -233,79 +103,17 @@ export interface Storage {
|
|
|
233
103
|
clearSecureBiometric(): void;
|
|
234
104
|
}
|
|
235
105
|
|
|
236
|
-
const memoryStore = new Map<string, unknown>();
|
|
237
|
-
const memoryListeners: KeyListenerRegistry = new Map();
|
|
238
|
-
const webScopeListeners = new Map<NonMemoryScope, KeyListenerRegistry>([
|
|
239
|
-
[StorageScope.Disk, new Map()],
|
|
240
|
-
[StorageScope.Secure, new Map()],
|
|
241
|
-
]);
|
|
242
|
-
const scopedRawCache = new Map<NonMemoryScope, Map<string, string | undefined>>(
|
|
243
|
-
[
|
|
244
|
-
[StorageScope.Disk, new Map()],
|
|
245
|
-
[StorageScope.Secure, new Map()],
|
|
246
|
-
],
|
|
247
|
-
);
|
|
248
106
|
const webScopeKeyIndex = new Map<NonMemoryScope, Set<string>>([
|
|
249
107
|
[StorageScope.Disk, new Set()],
|
|
250
108
|
[StorageScope.Secure, new Set()],
|
|
251
109
|
]);
|
|
252
110
|
const hydratedWebScopeKeyIndex = new Set<NonMemoryScope>();
|
|
253
|
-
const pendingDiskWrites = new Map<string, PendingDiskWrite>();
|
|
254
|
-
let diskFlushScheduled = false;
|
|
255
|
-
let diskWritesAsync = false;
|
|
256
|
-
const pendingSecureWrites = new Map<string, PendingSecureWrite>();
|
|
257
|
-
let secureFlushScheduled = false;
|
|
258
|
-
let secureDefaultAccessControl: AccessControl = AccessControl.WhenUnlocked;
|
|
259
111
|
const SECURE_WEB_PREFIX = "__secure_";
|
|
260
112
|
const BIOMETRIC_WEB_PREFIX = "__bio_";
|
|
261
113
|
let hasWarnedAboutWebBiometricFallback = false;
|
|
262
114
|
let hasWindowStorageEventSubscription = false;
|
|
263
|
-
let metricsObserver: StorageMetricsObserver | undefined;
|
|
264
|
-
let eventObserver: StorageEventListener | undefined;
|
|
265
|
-
let eventObserverRedactSecureValues = true;
|
|
266
|
-
const metricsCounters = new Map<
|
|
267
|
-
string,
|
|
268
|
-
{ count: number; totalDurationMs: number; maxDurationMs: number }
|
|
269
|
-
>();
|
|
270
|
-
const storageEvents = new StorageEventRegistry();
|
|
271
|
-
|
|
272
|
-
function recordMetric(
|
|
273
|
-
operation: string,
|
|
274
|
-
scope: StorageScope,
|
|
275
|
-
durationMs: number,
|
|
276
|
-
keysCount = 1,
|
|
277
|
-
): void {
|
|
278
|
-
const existing = metricsCounters.get(operation);
|
|
279
|
-
if (!existing) {
|
|
280
|
-
metricsCounters.set(operation, {
|
|
281
|
-
count: 1,
|
|
282
|
-
totalDurationMs: durationMs,
|
|
283
|
-
maxDurationMs: durationMs,
|
|
284
|
-
});
|
|
285
|
-
} else {
|
|
286
|
-
existing.count += 1;
|
|
287
|
-
existing.totalDurationMs += durationMs;
|
|
288
|
-
existing.maxDurationMs = Math.max(existing.maxDurationMs, durationMs);
|
|
289
|
-
}
|
|
290
|
-
metricsObserver?.({ operation, scope, durationMs, keysCount });
|
|
291
|
-
}
|
|
292
115
|
|
|
293
|
-
|
|
294
|
-
operation: string,
|
|
295
|
-
scope: StorageScope,
|
|
296
|
-
fn: () => T,
|
|
297
|
-
keysCount = 1,
|
|
298
|
-
): T {
|
|
299
|
-
if (!metricsObserver) {
|
|
300
|
-
return fn();
|
|
301
|
-
}
|
|
302
|
-
const start = now();
|
|
303
|
-
try {
|
|
304
|
-
return fn();
|
|
305
|
-
} finally {
|
|
306
|
-
recordMetric(operation, scope, now() - start, keysCount);
|
|
307
|
-
}
|
|
308
|
-
}
|
|
116
|
+
let internals!: StorageCoreInternals;
|
|
309
117
|
|
|
310
118
|
function createDefaultDiskBackend(): WebDiskStorageBackend {
|
|
311
119
|
return createLocalStorageWebBackend({
|
|
@@ -444,24 +252,30 @@ function applyExternalChangeEvent(
|
|
|
444
252
|
newValue: string | null,
|
|
445
253
|
): void {
|
|
446
254
|
if (key === null) {
|
|
447
|
-
clearScopeRawCache(scope);
|
|
255
|
+
internals.clearScopeRawCache(scope);
|
|
448
256
|
ensureWebScopeKeyIndex(scope).clear();
|
|
449
|
-
notifyAllListeners(getScopedListeners(scope));
|
|
257
|
+
notifyAllListeners(internals.getScopedListeners(scope));
|
|
450
258
|
return;
|
|
451
259
|
}
|
|
452
260
|
|
|
453
261
|
if (scope === StorageScope.Secure && key.startsWith(SECURE_WEB_PREFIX)) {
|
|
454
262
|
const plainKey = fromSecureStorageKey(key);
|
|
455
|
-
const oldValue = readCachedRawValue(
|
|
263
|
+
const oldValue = internals.readCachedRawValue(
|
|
264
|
+
StorageScope.Secure,
|
|
265
|
+
plainKey,
|
|
266
|
+
);
|
|
456
267
|
if (newValue === null) {
|
|
457
268
|
ensureWebScopeKeyIndex(StorageScope.Secure).delete(plainKey);
|
|
458
|
-
cacheRawValue(StorageScope.Secure, plainKey, undefined);
|
|
269
|
+
internals.cacheRawValue(StorageScope.Secure, plainKey, undefined);
|
|
459
270
|
} else {
|
|
460
271
|
ensureWebScopeKeyIndex(StorageScope.Secure).add(plainKey);
|
|
461
|
-
cacheRawValue(StorageScope.Secure, plainKey, newValue);
|
|
272
|
+
internals.cacheRawValue(StorageScope.Secure, plainKey, newValue);
|
|
462
273
|
}
|
|
463
|
-
notifyKeyListeners(
|
|
464
|
-
|
|
274
|
+
notifyKeyListeners(
|
|
275
|
+
internals.getScopedListeners(StorageScope.Secure),
|
|
276
|
+
plainKey,
|
|
277
|
+
);
|
|
278
|
+
internals.emitKeyChange(
|
|
465
279
|
StorageScope.Secure,
|
|
466
280
|
plainKey,
|
|
467
281
|
oldValue,
|
|
@@ -474,7 +288,10 @@ function applyExternalChangeEvent(
|
|
|
474
288
|
|
|
475
289
|
if (scope === StorageScope.Secure && key.startsWith(BIOMETRIC_WEB_PREFIX)) {
|
|
476
290
|
const plainKey = fromBiometricStorageKey(key);
|
|
477
|
-
const oldValue = readCachedRawValue(
|
|
291
|
+
const oldValue = internals.readCachedRawValue(
|
|
292
|
+
StorageScope.Secure,
|
|
293
|
+
plainKey,
|
|
294
|
+
);
|
|
478
295
|
if (newValue === null) {
|
|
479
296
|
if (
|
|
480
297
|
withWebBackendOperation(
|
|
@@ -485,13 +302,16 @@ function applyExternalChangeEvent(
|
|
|
485
302
|
) {
|
|
486
303
|
ensureWebScopeKeyIndex(StorageScope.Secure).delete(plainKey);
|
|
487
304
|
}
|
|
488
|
-
cacheRawValue(StorageScope.Secure, plainKey, undefined);
|
|
305
|
+
internals.cacheRawValue(StorageScope.Secure, plainKey, undefined);
|
|
489
306
|
} else {
|
|
490
307
|
ensureWebScopeKeyIndex(StorageScope.Secure).add(plainKey);
|
|
491
|
-
cacheRawValue(StorageScope.Secure, plainKey, newValue);
|
|
308
|
+
internals.cacheRawValue(StorageScope.Secure, plainKey, newValue);
|
|
492
309
|
}
|
|
493
|
-
notifyKeyListeners(
|
|
494
|
-
|
|
310
|
+
notifyKeyListeners(
|
|
311
|
+
internals.getScopedListeners(StorageScope.Secure),
|
|
312
|
+
plainKey,
|
|
313
|
+
);
|
|
314
|
+
internals.emitKeyChange(
|
|
495
315
|
StorageScope.Secure,
|
|
496
316
|
plainKey,
|
|
497
317
|
oldValue,
|
|
@@ -502,16 +322,16 @@ function applyExternalChangeEvent(
|
|
|
502
322
|
return;
|
|
503
323
|
}
|
|
504
324
|
|
|
505
|
-
const oldValue = readCachedRawValue(scope, key);
|
|
325
|
+
const oldValue = internals.readCachedRawValue(scope, key);
|
|
506
326
|
if (newValue === null) {
|
|
507
327
|
ensureWebScopeKeyIndex(scope).delete(key);
|
|
508
|
-
cacheRawValue(scope, key, undefined);
|
|
328
|
+
internals.cacheRawValue(scope, key, undefined);
|
|
509
329
|
} else {
|
|
510
330
|
ensureWebScopeKeyIndex(scope).add(key);
|
|
511
|
-
cacheRawValue(scope, key, newValue);
|
|
331
|
+
internals.cacheRawValue(scope, key, newValue);
|
|
512
332
|
}
|
|
513
|
-
notifyKeyListeners(getScopedListeners(scope), key);
|
|
514
|
-
emitKeyChange(
|
|
333
|
+
notifyKeyListeners(internals.getScopedListeners(scope), key);
|
|
334
|
+
internals.emitKeyChange(
|
|
515
335
|
scope,
|
|
516
336
|
key,
|
|
517
337
|
oldValue,
|
|
@@ -590,321 +410,6 @@ function ensureExternalSyncSubscriptions(): void {
|
|
|
590
410
|
subscribeToBackendChanges(StorageScope.Secure);
|
|
591
411
|
}
|
|
592
412
|
|
|
593
|
-
function getScopedListeners(scope: NonMemoryScope): KeyListenerRegistry {
|
|
594
|
-
return webScopeListeners.get(scope)!;
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
function getScopeRawCache(
|
|
598
|
-
scope: NonMemoryScope,
|
|
599
|
-
): Map<string, string | undefined> {
|
|
600
|
-
return scopedRawCache.get(scope)!;
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
function cacheRawValue(
|
|
604
|
-
scope: NonMemoryScope,
|
|
605
|
-
key: string,
|
|
606
|
-
value: string | undefined,
|
|
607
|
-
): void {
|
|
608
|
-
getScopeRawCache(scope).set(key, value);
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
function readCachedRawValue(
|
|
612
|
-
scope: NonMemoryScope,
|
|
613
|
-
key: string,
|
|
614
|
-
): string | undefined {
|
|
615
|
-
return getScopeRawCache(scope).get(key);
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
function hasCachedRawValue(scope: NonMemoryScope, key: string): boolean {
|
|
619
|
-
return getScopeRawCache(scope).has(key);
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
function clearScopeRawCache(scope: NonMemoryScope): void {
|
|
623
|
-
getScopeRawCache(scope).clear();
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
function notifyKeyListeners(registry: KeyListenerRegistry, key: string): void {
|
|
627
|
-
const listeners = registry.get(key);
|
|
628
|
-
if (listeners) {
|
|
629
|
-
for (const listener of listeners) {
|
|
630
|
-
listener();
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
function notifyAllListeners(registry: KeyListenerRegistry): void {
|
|
636
|
-
for (const listeners of registry.values()) {
|
|
637
|
-
for (const listener of listeners) {
|
|
638
|
-
listener();
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
function addKeyListener(
|
|
644
|
-
registry: KeyListenerRegistry,
|
|
645
|
-
key: string,
|
|
646
|
-
listener: () => void,
|
|
647
|
-
): () => void {
|
|
648
|
-
let listeners = registry.get(key);
|
|
649
|
-
if (!listeners) {
|
|
650
|
-
listeners = new Set();
|
|
651
|
-
registry.set(key, listeners);
|
|
652
|
-
}
|
|
653
|
-
listeners.add(listener);
|
|
654
|
-
|
|
655
|
-
return () => {
|
|
656
|
-
const scopedListeners = registry.get(key);
|
|
657
|
-
if (!scopedListeners) {
|
|
658
|
-
return;
|
|
659
|
-
}
|
|
660
|
-
scopedListeners.delete(listener);
|
|
661
|
-
if (scopedListeners.size === 0) {
|
|
662
|
-
registry.delete(key);
|
|
663
|
-
}
|
|
664
|
-
};
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
function getEventRawValue(
|
|
668
|
-
scope: StorageScope,
|
|
669
|
-
key: string,
|
|
670
|
-
): string | undefined {
|
|
671
|
-
if (scope === StorageScope.Memory) {
|
|
672
|
-
const value = memoryStore.get(key);
|
|
673
|
-
return typeof value === "string" ? value : undefined;
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
return getRawValue(key, scope);
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
function createKeyChange(
|
|
680
|
-
scope: StorageScope,
|
|
681
|
-
key: string,
|
|
682
|
-
oldValue: string | undefined,
|
|
683
|
-
newValue: string | undefined,
|
|
684
|
-
operation: StorageChangeOperation,
|
|
685
|
-
source: StorageChangeSource,
|
|
686
|
-
): StorageKeyChangeEvent {
|
|
687
|
-
return {
|
|
688
|
-
type: "key",
|
|
689
|
-
scope,
|
|
690
|
-
key,
|
|
691
|
-
oldValue,
|
|
692
|
-
newValue,
|
|
693
|
-
operation,
|
|
694
|
-
source,
|
|
695
|
-
};
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
function hasStorageChangeObservers(scope: StorageScope): boolean {
|
|
699
|
-
return storageEvents.hasListeners(scope) || eventObserver !== undefined;
|
|
700
|
-
}
|
|
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
|
-
|
|
745
|
-
function emitKeyChange(
|
|
746
|
-
scope: StorageScope,
|
|
747
|
-
key: string,
|
|
748
|
-
oldValue: string | undefined,
|
|
749
|
-
newValue: string | undefined,
|
|
750
|
-
operation: StorageChangeOperation,
|
|
751
|
-
source: StorageChangeSource,
|
|
752
|
-
): void {
|
|
753
|
-
const event = createKeyChange(
|
|
754
|
-
scope,
|
|
755
|
-
key,
|
|
756
|
-
oldValue,
|
|
757
|
-
newValue,
|
|
758
|
-
operation,
|
|
759
|
-
source,
|
|
760
|
-
);
|
|
761
|
-
storageEvents.emitKey(event);
|
|
762
|
-
eventObserver?.(eventForGlobalObserver(event));
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
function emitBatchChange(
|
|
766
|
-
scope: StorageScope,
|
|
767
|
-
operation: StorageChangeOperation,
|
|
768
|
-
source: StorageChangeSource,
|
|
769
|
-
changes: StorageKeyChangeEvent[],
|
|
770
|
-
): void {
|
|
771
|
-
if (changes.length === 0) {
|
|
772
|
-
return;
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
const event: StorageBatchChangeEvent = {
|
|
776
|
-
type: "batch",
|
|
777
|
-
scope,
|
|
778
|
-
operation,
|
|
779
|
-
source,
|
|
780
|
-
changes,
|
|
781
|
-
};
|
|
782
|
-
storageEvents.emitBatch(event);
|
|
783
|
-
eventObserver?.(eventForGlobalObserver(event));
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
function readPendingSecureWrite(key: string): string | undefined {
|
|
787
|
-
return pendingSecureWrites.get(key)?.value;
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
function readPendingDiskWrite(key: string): string | undefined {
|
|
791
|
-
return pendingDiskWrites.get(key)?.value;
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
function hasPendingDiskWrite(key: string): boolean {
|
|
795
|
-
return pendingDiskWrites.has(key);
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
function hasPendingSecureWrite(key: string): boolean {
|
|
799
|
-
return pendingSecureWrites.has(key);
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
function clearPendingDiskWrite(key: string): void {
|
|
803
|
-
pendingDiskWrites.delete(key);
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
function clearPendingSecureWrite(key: string): void {
|
|
807
|
-
pendingSecureWrites.delete(key);
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
function flushDiskWrites(): void {
|
|
811
|
-
diskFlushScheduled = false;
|
|
812
|
-
|
|
813
|
-
if (pendingDiskWrites.size === 0) {
|
|
814
|
-
return;
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
const writes = Array.from(pendingDiskWrites.values());
|
|
818
|
-
pendingDiskWrites.clear();
|
|
819
|
-
|
|
820
|
-
const keysToSet: string[] = [];
|
|
821
|
-
const valuesToSet: string[] = [];
|
|
822
|
-
const keysToRemove: string[] = [];
|
|
823
|
-
|
|
824
|
-
writes.forEach(({ key, value }) => {
|
|
825
|
-
if (value === undefined) {
|
|
826
|
-
keysToRemove.push(key);
|
|
827
|
-
return;
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
keysToSet.push(key);
|
|
831
|
-
valuesToSet.push(value);
|
|
832
|
-
});
|
|
833
|
-
|
|
834
|
-
if (keysToSet.length > 0) {
|
|
835
|
-
WebStorage.setBatch(keysToSet, valuesToSet, StorageScope.Disk);
|
|
836
|
-
}
|
|
837
|
-
if (keysToRemove.length > 0) {
|
|
838
|
-
WebStorage.removeBatch(keysToRemove, StorageScope.Disk);
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
function flushSecureWrites(): void {
|
|
843
|
-
secureFlushScheduled = false;
|
|
844
|
-
|
|
845
|
-
if (pendingSecureWrites.size === 0) {
|
|
846
|
-
return;
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
const writes = Array.from(pendingSecureWrites.values());
|
|
850
|
-
pendingSecureWrites.clear();
|
|
851
|
-
|
|
852
|
-
const groupedSetWrites = new Map<
|
|
853
|
-
AccessControl,
|
|
854
|
-
{ keys: string[]; values: string[] }
|
|
855
|
-
>();
|
|
856
|
-
const keysToRemove: string[] = [];
|
|
857
|
-
|
|
858
|
-
writes.forEach(({ key, value, accessControl }) => {
|
|
859
|
-
if (value === undefined) {
|
|
860
|
-
keysToRemove.push(key);
|
|
861
|
-
} else {
|
|
862
|
-
const resolvedAccessControl = accessControl ?? secureDefaultAccessControl;
|
|
863
|
-
const existingGroup = groupedSetWrites.get(resolvedAccessControl);
|
|
864
|
-
const group = existingGroup ?? { keys: [], values: [] };
|
|
865
|
-
group.keys.push(key);
|
|
866
|
-
group.values.push(value);
|
|
867
|
-
if (!existingGroup) {
|
|
868
|
-
groupedSetWrites.set(resolvedAccessControl, group);
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
});
|
|
872
|
-
|
|
873
|
-
groupedSetWrites.forEach((group, accessControl) => {
|
|
874
|
-
WebStorage.setSecureAccessControl(accessControl);
|
|
875
|
-
WebStorage.setBatch(group.keys, group.values, StorageScope.Secure);
|
|
876
|
-
});
|
|
877
|
-
if (keysToRemove.length > 0) {
|
|
878
|
-
WebStorage.removeBatch(keysToRemove, StorageScope.Secure);
|
|
879
|
-
}
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
function scheduleDiskWrite(key: string, value: string | undefined): void {
|
|
883
|
-
pendingDiskWrites.set(key, { key, value });
|
|
884
|
-
if (diskFlushScheduled) {
|
|
885
|
-
return;
|
|
886
|
-
}
|
|
887
|
-
diskFlushScheduled = true;
|
|
888
|
-
runMicrotask(flushDiskWrites);
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
function scheduleSecureWrite(
|
|
892
|
-
key: string,
|
|
893
|
-
value: string | undefined,
|
|
894
|
-
accessControl?: AccessControl,
|
|
895
|
-
): void {
|
|
896
|
-
const pendingWrite: PendingSecureWrite = { key, value };
|
|
897
|
-
if (accessControl !== undefined) {
|
|
898
|
-
pendingWrite.accessControl = accessControl;
|
|
899
|
-
}
|
|
900
|
-
pendingSecureWrites.set(key, pendingWrite);
|
|
901
|
-
if (secureFlushScheduled) {
|
|
902
|
-
return;
|
|
903
|
-
}
|
|
904
|
-
secureFlushScheduled = true;
|
|
905
|
-
runMicrotask(flushSecureWrites);
|
|
906
|
-
}
|
|
907
|
-
|
|
908
413
|
const WebStorage: Storage = {
|
|
909
414
|
name: "Storage",
|
|
910
415
|
equals: (other) => other === WebStorage,
|
|
@@ -919,7 +424,7 @@ const WebStorage: Storage = {
|
|
|
919
424
|
backend.setItem(storageKey, value);
|
|
920
425
|
});
|
|
921
426
|
ensureWebScopeKeyIndex(scope).add(key);
|
|
922
|
-
notifyKeyListeners(getScopedListeners(scope), key);
|
|
427
|
+
notifyKeyListeners(internals.getScopedListeners(scope), key);
|
|
923
428
|
},
|
|
924
429
|
get: (key: string, scope: number) => {
|
|
925
430
|
if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
|
|
@@ -954,7 +459,7 @@ const WebStorage: Storage = {
|
|
|
954
459
|
});
|
|
955
460
|
}
|
|
956
461
|
ensureWebScopeKeyIndex(scope).delete(key);
|
|
957
|
-
notifyKeyListeners(getScopedListeners(scope), key);
|
|
462
|
+
notifyKeyListeners(internals.getScopedListeners(scope), key);
|
|
958
463
|
},
|
|
959
464
|
clear: (scope: number) => {
|
|
960
465
|
if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
|
|
@@ -964,7 +469,7 @@ const WebStorage: Storage = {
|
|
|
964
469
|
backend.clear();
|
|
965
470
|
});
|
|
966
471
|
ensureWebScopeKeyIndex(scope).clear();
|
|
967
|
-
notifyAllListeners(getScopedListeners(scope));
|
|
472
|
+
notifyAllListeners(internals.getScopedListeners(scope));
|
|
968
473
|
},
|
|
969
474
|
setBatch: (keys: string[], values: string[], scope: number) => {
|
|
970
475
|
if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
|
|
@@ -1004,7 +509,7 @@ const WebStorage: Storage = {
|
|
|
1004
509
|
: storageKey,
|
|
1005
510
|
),
|
|
1006
511
|
);
|
|
1007
|
-
const listeners = getScopedListeners(scope);
|
|
512
|
+
const listeners = internals.getScopedListeners(scope);
|
|
1008
513
|
keys.forEach((key) => notifyKeyListeners(listeners, key));
|
|
1009
514
|
},
|
|
1010
515
|
getBatch: (keys: string[], scope: number) => {
|
|
@@ -1055,7 +560,7 @@ const WebStorage: Storage = {
|
|
|
1055
560
|
|
|
1056
561
|
const keyIndex = ensureWebScopeKeyIndex(scope);
|
|
1057
562
|
keys.forEach((key) => keyIndex.delete(key));
|
|
1058
|
-
const listeners = getScopedListeners(scope);
|
|
563
|
+
const listeners = internals.getScopedListeners(scope);
|
|
1059
564
|
keys.forEach((key) => notifyKeyListeners(listeners, key));
|
|
1060
565
|
},
|
|
1061
566
|
removeByPrefix: (prefix: string, scope: number) => {
|
|
@@ -1123,7 +628,10 @@ const WebStorage: Storage = {
|
|
|
1123
628
|
backend.setItem(toSecureStorageKey(key), value);
|
|
1124
629
|
});
|
|
1125
630
|
ensureWebScopeKeyIndex(StorageScope.Secure).add(key);
|
|
1126
|
-
notifyKeyListeners(
|
|
631
|
+
notifyKeyListeners(
|
|
632
|
+
internals.getScopedListeners(StorageScope.Secure),
|
|
633
|
+
key,
|
|
634
|
+
);
|
|
1127
635
|
return;
|
|
1128
636
|
}
|
|
1129
637
|
if (
|
|
@@ -1142,7 +650,7 @@ const WebStorage: Storage = {
|
|
|
1142
650
|
(backend) => backend.setItem(toBiometricStorageKey(key), value),
|
|
1143
651
|
);
|
|
1144
652
|
ensureWebScopeKeyIndex(StorageScope.Secure).add(key);
|
|
1145
|
-
notifyKeyListeners(getScopedListeners(StorageScope.Secure), key);
|
|
653
|
+
notifyKeyListeners(internals.getScopedListeners(StorageScope.Secure), key);
|
|
1146
654
|
},
|
|
1147
655
|
getSecureBiometric: (key: string) => {
|
|
1148
656
|
const value = withWebBackendOperation(
|
|
@@ -1167,7 +675,7 @@ const WebStorage: Storage = {
|
|
|
1167
675
|
) {
|
|
1168
676
|
ensureWebScopeKeyIndex(StorageScope.Secure).delete(key);
|
|
1169
677
|
}
|
|
1170
|
-
notifyKeyListeners(getScopedListeners(StorageScope.Secure), key);
|
|
678
|
+
notifyKeyListeners(internals.getScopedListeners(StorageScope.Secure), key);
|
|
1171
679
|
},
|
|
1172
680
|
hasSecureBiometric: (key: string) => {
|
|
1173
681
|
return (
|
|
@@ -1218,490 +726,55 @@ const WebStorage: Storage = {
|
|
|
1218
726
|
keyIndex.delete(key);
|
|
1219
727
|
}
|
|
1220
728
|
});
|
|
1221
|
-
const listeners = getScopedListeners(StorageScope.Secure);
|
|
729
|
+
const listeners = internals.getScopedListeners(StorageScope.Secure);
|
|
1222
730
|
keysToNotify.forEach((key) => notifyKeyListeners(listeners, key));
|
|
1223
731
|
},
|
|
1224
732
|
};
|
|
1225
733
|
|
|
1226
|
-
function
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
const oldValue =
|
|
1247
|
-
scope === StorageScope.Memory ? getEventRawValue(scope, key) : undefined;
|
|
1248
|
-
if (scope === StorageScope.Memory) {
|
|
1249
|
-
memoryStore.set(key, value);
|
|
1250
|
-
notifyKeyListeners(memoryListeners, key);
|
|
1251
|
-
emitKeyChange(scope, key, oldValue, value, "set", "memory");
|
|
1252
|
-
return;
|
|
1253
|
-
}
|
|
1254
|
-
|
|
1255
|
-
if (scope === StorageScope.Disk) {
|
|
1256
|
-
cacheRawValue(scope, key, value);
|
|
1257
|
-
if (diskWritesAsync) {
|
|
1258
|
-
scheduleDiskWrite(key, value);
|
|
1259
|
-
emitKeyChange(scope, key, oldValue, value, "set", "web");
|
|
1260
|
-
return;
|
|
1261
|
-
}
|
|
1262
|
-
|
|
1263
|
-
flushDiskWrites();
|
|
1264
|
-
clearPendingDiskWrite(key);
|
|
1265
|
-
}
|
|
1266
|
-
|
|
1267
|
-
if (scope === StorageScope.Secure) {
|
|
1268
|
-
flushSecureWrites();
|
|
1269
|
-
clearPendingSecureWrite(key);
|
|
1270
|
-
}
|
|
1271
|
-
|
|
1272
|
-
WebStorage.set(key, value, scope);
|
|
1273
|
-
cacheRawValue(scope, key, value);
|
|
1274
|
-
emitKeyChange(scope, key, oldValue, value, "set", "web");
|
|
1275
|
-
}
|
|
1276
|
-
|
|
1277
|
-
function removeRawValue(key: string, scope: StorageScope): void {
|
|
1278
|
-
assertValidScope(scope);
|
|
1279
|
-
const oldValue = getEventRawValue(scope, key);
|
|
1280
|
-
if (scope === StorageScope.Memory) {
|
|
1281
|
-
memoryStore.delete(key);
|
|
1282
|
-
notifyKeyListeners(memoryListeners, key);
|
|
1283
|
-
emitKeyChange(scope, key, oldValue, undefined, "remove", "memory");
|
|
1284
|
-
return;
|
|
1285
|
-
}
|
|
1286
|
-
|
|
1287
|
-
if (scope === StorageScope.Disk) {
|
|
1288
|
-
cacheRawValue(scope, key, undefined);
|
|
1289
|
-
if (diskWritesAsync) {
|
|
1290
|
-
scheduleDiskWrite(key, undefined);
|
|
1291
|
-
emitKeyChange(scope, key, oldValue, undefined, "remove", "web");
|
|
1292
|
-
return;
|
|
1293
|
-
}
|
|
1294
|
-
|
|
1295
|
-
flushDiskWrites();
|
|
1296
|
-
clearPendingDiskWrite(key);
|
|
1297
|
-
}
|
|
1298
|
-
|
|
1299
|
-
if (scope === StorageScope.Secure) {
|
|
1300
|
-
flushSecureWrites();
|
|
1301
|
-
clearPendingSecureWrite(key);
|
|
1302
|
-
}
|
|
1303
|
-
|
|
1304
|
-
WebStorage.remove(key, scope);
|
|
1305
|
-
cacheRawValue(scope, key, undefined);
|
|
1306
|
-
emitKeyChange(scope, key, oldValue, undefined, "remove", "web");
|
|
1307
|
-
}
|
|
1308
|
-
|
|
1309
|
-
function readMigrationVersion(scope: StorageScope): number {
|
|
1310
|
-
const raw = getRawValue(MIGRATION_VERSION_KEY, scope);
|
|
1311
|
-
if (raw === undefined) {
|
|
1312
|
-
return 0;
|
|
1313
|
-
}
|
|
1314
|
-
|
|
1315
|
-
const parsed = Number.parseInt(raw, 10);
|
|
1316
|
-
return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
|
|
734
|
+
function buildWebAdapter(
|
|
735
|
+
coreInternals: StorageCoreInternals,
|
|
736
|
+
): StorageCoreAdapter {
|
|
737
|
+
internals = coreInternals;
|
|
738
|
+
return {
|
|
739
|
+
backend: WebStorage,
|
|
740
|
+
changeSource: "web",
|
|
741
|
+
applyAccessControlOnSecureRawWrite: false,
|
|
742
|
+
flushDiskWritesOnImport: true,
|
|
743
|
+
ensureScopeSubscription: () => {
|
|
744
|
+
ensureExternalSyncSubscriptions();
|
|
745
|
+
},
|
|
746
|
+
maybeCleanupScopeSubscription: () => {},
|
|
747
|
+
onWillEmitChanges: () => {},
|
|
748
|
+
getSecureMetadataProfile: () => ({
|
|
749
|
+
backend: getBackendName(StorageScope.Secure, webSecureStorageBackend),
|
|
750
|
+
encrypted: getWebSecureEncryptionStatus(webSecureStorageBackend),
|
|
751
|
+
hardwareBacked: "unavailable",
|
|
752
|
+
}),
|
|
753
|
+
};
|
|
1317
754
|
}
|
|
1318
755
|
|
|
1319
|
-
|
|
1320
|
-
setRawValue(MIGRATION_VERSION_KEY, String(version), scope);
|
|
1321
|
-
}
|
|
756
|
+
const core = createStorageCore(buildWebAdapter);
|
|
1322
757
|
|
|
1323
758
|
export const storage = {
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
if (scope !== StorageScope.Memory) {
|
|
1330
|
-
ensureExternalSyncSubscriptions();
|
|
1331
|
-
}
|
|
1332
|
-
return storageEvents.subscribe(scope, listener);
|
|
759
|
+
...core.storage,
|
|
760
|
+
setAccessControl: (level: AccessControl) => {
|
|
761
|
+
assertAccessControlLevel(level);
|
|
762
|
+
internals.setSecureDefaultAccessControl(level);
|
|
763
|
+
internals.recordMetric("storage:setAccessControl", StorageScope.Secure, 0);
|
|
1333
764
|
},
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
if (scope !== StorageScope.Memory) {
|
|
1341
|
-
ensureExternalSyncSubscriptions();
|
|
1342
|
-
}
|
|
1343
|
-
return storageEvents.subscribeKey(scope, key, listener);
|
|
765
|
+
setSecureWritesAsync: (_enabled: boolean) => {
|
|
766
|
+
internals.recordMetric(
|
|
767
|
+
"storage:setSecureWritesAsync",
|
|
768
|
+
StorageScope.Secure,
|
|
769
|
+
0,
|
|
770
|
+
);
|
|
1344
771
|
},
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
if (scope !== StorageScope.Memory) {
|
|
1352
|
-
ensureExternalSyncSubscriptions();
|
|
1353
|
-
}
|
|
1354
|
-
return storageEvents.subscribePrefix(scope, prefix, listener);
|
|
1355
|
-
},
|
|
1356
|
-
subscribeNamespace: (
|
|
1357
|
-
namespace: string,
|
|
1358
|
-
scope: StorageScope,
|
|
1359
|
-
listener: StorageEventListener,
|
|
1360
|
-
): (() => void) => {
|
|
1361
|
-
return storage.subscribePrefix(scope, prefixKey(namespace, ""), listener);
|
|
1362
|
-
},
|
|
1363
|
-
setEventObserver: (
|
|
1364
|
-
observer?: StorageEventListener,
|
|
1365
|
-
options: StorageEventObserverOptions = {},
|
|
1366
|
-
) => {
|
|
1367
|
-
eventObserver = observer;
|
|
1368
|
-
eventObserverRedactSecureValues = options.redactSecureValues !== false;
|
|
1369
|
-
if (observer) {
|
|
1370
|
-
ensureExternalSyncSubscriptions();
|
|
1371
|
-
}
|
|
1372
|
-
},
|
|
1373
|
-
clear: (scope: StorageScope) => {
|
|
1374
|
-
measureOperation("storage:clear", scope, () => {
|
|
1375
|
-
const previousValues = shouldReadPreviousEventValues(scope)
|
|
1376
|
-
? storage.getAll(scope)
|
|
1377
|
-
: {};
|
|
1378
|
-
if (scope === StorageScope.Memory) {
|
|
1379
|
-
memoryStore.clear();
|
|
1380
|
-
notifyAllListeners(memoryListeners);
|
|
1381
|
-
emitBatchChange(
|
|
1382
|
-
scope,
|
|
1383
|
-
"clear",
|
|
1384
|
-
"memory",
|
|
1385
|
-
Object.keys(previousValues).map((key) =>
|
|
1386
|
-
createKeyChange(
|
|
1387
|
-
scope,
|
|
1388
|
-
key,
|
|
1389
|
-
previousValues[key],
|
|
1390
|
-
undefined,
|
|
1391
|
-
"clear",
|
|
1392
|
-
"memory",
|
|
1393
|
-
),
|
|
1394
|
-
),
|
|
1395
|
-
);
|
|
1396
|
-
return;
|
|
1397
|
-
}
|
|
1398
|
-
|
|
1399
|
-
if (scope === StorageScope.Disk) {
|
|
1400
|
-
flushDiskWrites();
|
|
1401
|
-
pendingDiskWrites.clear();
|
|
1402
|
-
}
|
|
1403
|
-
|
|
1404
|
-
if (scope === StorageScope.Secure) {
|
|
1405
|
-
flushSecureWrites();
|
|
1406
|
-
pendingSecureWrites.clear();
|
|
1407
|
-
}
|
|
1408
|
-
|
|
1409
|
-
clearScopeRawCache(scope);
|
|
1410
|
-
WebStorage.clear(scope);
|
|
1411
|
-
emitBatchChange(
|
|
1412
|
-
scope,
|
|
1413
|
-
"clear",
|
|
1414
|
-
"web",
|
|
1415
|
-
Object.keys(previousValues).map((key) =>
|
|
1416
|
-
createKeyChange(
|
|
1417
|
-
scope,
|
|
1418
|
-
key,
|
|
1419
|
-
previousValues[key],
|
|
1420
|
-
undefined,
|
|
1421
|
-
"clear",
|
|
1422
|
-
"web",
|
|
1423
|
-
),
|
|
1424
|
-
),
|
|
1425
|
-
);
|
|
1426
|
-
});
|
|
1427
|
-
},
|
|
1428
|
-
clearAll: () => {
|
|
1429
|
-
measureOperation(
|
|
1430
|
-
"storage:clearAll",
|
|
1431
|
-
StorageScope.Memory,
|
|
1432
|
-
() => {
|
|
1433
|
-
storage.clear(StorageScope.Memory);
|
|
1434
|
-
storage.clear(StorageScope.Disk);
|
|
1435
|
-
storage.clear(StorageScope.Secure);
|
|
1436
|
-
},
|
|
1437
|
-
3,
|
|
1438
|
-
);
|
|
1439
|
-
},
|
|
1440
|
-
clearNamespace: (namespace: string, scope: StorageScope) => {
|
|
1441
|
-
measureOperation("storage:clearNamespace", scope, () => {
|
|
1442
|
-
assertValidScope(scope);
|
|
1443
|
-
if (scope === StorageScope.Memory) {
|
|
1444
|
-
const affectedKeys = Array.from(memoryStore.keys()).filter((key) =>
|
|
1445
|
-
isNamespaced(key, namespace),
|
|
1446
|
-
);
|
|
1447
|
-
const previousValues = affectedKeys.map((key) => ({
|
|
1448
|
-
key,
|
|
1449
|
-
value: getEventRawValue(scope, key),
|
|
1450
|
-
}));
|
|
1451
|
-
|
|
1452
|
-
if (affectedKeys.length === 0) {
|
|
1453
|
-
return;
|
|
1454
|
-
}
|
|
1455
|
-
|
|
1456
|
-
affectedKeys.forEach((key) => {
|
|
1457
|
-
memoryStore.delete(key);
|
|
1458
|
-
});
|
|
1459
|
-
affectedKeys.forEach((key) => notifyKeyListeners(memoryListeners, key));
|
|
1460
|
-
emitBatchChange(
|
|
1461
|
-
scope,
|
|
1462
|
-
"clearNamespace",
|
|
1463
|
-
"memory",
|
|
1464
|
-
previousValues.map(({ key, value }) =>
|
|
1465
|
-
createKeyChange(
|
|
1466
|
-
scope,
|
|
1467
|
-
key,
|
|
1468
|
-
value,
|
|
1469
|
-
undefined,
|
|
1470
|
-
"clearNamespace",
|
|
1471
|
-
"memory",
|
|
1472
|
-
),
|
|
1473
|
-
),
|
|
1474
|
-
);
|
|
1475
|
-
return;
|
|
1476
|
-
}
|
|
1477
|
-
|
|
1478
|
-
const keyPrefix = prefixKey(namespace, "");
|
|
1479
|
-
const previousValues = shouldReadPreviousEventValues(scope)
|
|
1480
|
-
? storage.getByPrefix(keyPrefix, scope)
|
|
1481
|
-
: {};
|
|
1482
|
-
if (scope === StorageScope.Disk) {
|
|
1483
|
-
flushDiskWrites();
|
|
1484
|
-
}
|
|
1485
|
-
if (scope === StorageScope.Secure) {
|
|
1486
|
-
flushSecureWrites();
|
|
1487
|
-
}
|
|
1488
|
-
const scopeCache = getScopeRawCache(scope);
|
|
1489
|
-
for (const key of scopeCache.keys()) {
|
|
1490
|
-
if (isNamespaced(key, namespace)) {
|
|
1491
|
-
scopeCache.delete(key);
|
|
1492
|
-
}
|
|
1493
|
-
}
|
|
1494
|
-
WebStorage.removeByPrefix(keyPrefix, scope);
|
|
1495
|
-
emitBatchChange(
|
|
1496
|
-
scope,
|
|
1497
|
-
"clearNamespace",
|
|
1498
|
-
"web",
|
|
1499
|
-
Object.keys(previousValues).map((key) =>
|
|
1500
|
-
createKeyChange(
|
|
1501
|
-
scope,
|
|
1502
|
-
key,
|
|
1503
|
-
previousValues[key],
|
|
1504
|
-
undefined,
|
|
1505
|
-
"clearNamespace",
|
|
1506
|
-
"web",
|
|
1507
|
-
),
|
|
1508
|
-
),
|
|
1509
|
-
);
|
|
1510
|
-
});
|
|
1511
|
-
},
|
|
1512
|
-
clearBiometric: () => {
|
|
1513
|
-
measureOperation("storage:clearBiometric", StorageScope.Secure, () => {
|
|
1514
|
-
WebStorage.clearSecureBiometric();
|
|
1515
|
-
});
|
|
1516
|
-
},
|
|
1517
|
-
has: (key: string, scope: StorageScope): boolean => {
|
|
1518
|
-
return measureOperation("storage:has", scope, () => {
|
|
1519
|
-
assertValidScope(scope);
|
|
1520
|
-
if (scope === StorageScope.Memory) return memoryStore.has(key);
|
|
1521
|
-
if (scope === StorageScope.Disk) {
|
|
1522
|
-
flushDiskWrites();
|
|
1523
|
-
}
|
|
1524
|
-
if (scope === StorageScope.Secure) {
|
|
1525
|
-
flushSecureWrites();
|
|
1526
|
-
}
|
|
1527
|
-
return WebStorage.has(key, scope);
|
|
1528
|
-
});
|
|
1529
|
-
},
|
|
1530
|
-
getAllKeys: (scope: StorageScope): string[] => {
|
|
1531
|
-
return measureOperation("storage:getAllKeys", scope, () => {
|
|
1532
|
-
assertValidScope(scope);
|
|
1533
|
-
if (scope === StorageScope.Memory) return Array.from(memoryStore.keys());
|
|
1534
|
-
if (scope === StorageScope.Disk) {
|
|
1535
|
-
flushDiskWrites();
|
|
1536
|
-
}
|
|
1537
|
-
if (scope === StorageScope.Secure) {
|
|
1538
|
-
flushSecureWrites();
|
|
1539
|
-
}
|
|
1540
|
-
return WebStorage.getAllKeys(scope);
|
|
1541
|
-
});
|
|
1542
|
-
},
|
|
1543
|
-
getKeysByPrefix: (prefix: string, scope: StorageScope): string[] => {
|
|
1544
|
-
return measureOperation("storage:getKeysByPrefix", scope, () => {
|
|
1545
|
-
assertValidScope(scope);
|
|
1546
|
-
if (scope === StorageScope.Memory) {
|
|
1547
|
-
return Array.from(memoryStore.keys()).filter((key) =>
|
|
1548
|
-
key.startsWith(prefix),
|
|
1549
|
-
);
|
|
1550
|
-
}
|
|
1551
|
-
if (scope === StorageScope.Disk) {
|
|
1552
|
-
flushDiskWrites();
|
|
1553
|
-
}
|
|
1554
|
-
if (scope === StorageScope.Secure) {
|
|
1555
|
-
flushSecureWrites();
|
|
1556
|
-
}
|
|
1557
|
-
return WebStorage.getKeysByPrefix(prefix, scope);
|
|
1558
|
-
});
|
|
1559
|
-
},
|
|
1560
|
-
getByPrefix: (
|
|
1561
|
-
prefix: string,
|
|
1562
|
-
scope: StorageScope,
|
|
1563
|
-
): Record<string, string> => {
|
|
1564
|
-
return measureOperation("storage:getByPrefix", scope, () => {
|
|
1565
|
-
const result: Record<string, string> = {};
|
|
1566
|
-
const keys = storage.getKeysByPrefix(prefix, scope);
|
|
1567
|
-
if (keys.length === 0) {
|
|
1568
|
-
return result;
|
|
1569
|
-
}
|
|
1570
|
-
|
|
1571
|
-
if (scope === StorageScope.Memory) {
|
|
1572
|
-
keys.forEach((key) => {
|
|
1573
|
-
const value = memoryStore.get(key);
|
|
1574
|
-
if (typeof value === "string") {
|
|
1575
|
-
result[key] = value;
|
|
1576
|
-
}
|
|
1577
|
-
});
|
|
1578
|
-
return result;
|
|
1579
|
-
}
|
|
1580
|
-
|
|
1581
|
-
if (scope === StorageScope.Disk) {
|
|
1582
|
-
flushDiskWrites();
|
|
1583
|
-
}
|
|
1584
|
-
if (scope === StorageScope.Secure) {
|
|
1585
|
-
flushSecureWrites();
|
|
1586
|
-
}
|
|
1587
|
-
const values = WebStorage.getBatch(keys, scope);
|
|
1588
|
-
keys.forEach((key, index) => {
|
|
1589
|
-
const value = values[index];
|
|
1590
|
-
if (value !== undefined) {
|
|
1591
|
-
result[key] = value;
|
|
1592
|
-
}
|
|
1593
|
-
});
|
|
1594
|
-
return result;
|
|
1595
|
-
});
|
|
1596
|
-
},
|
|
1597
|
-
getAll: (scope: StorageScope): Record<string, string> => {
|
|
1598
|
-
return measureOperation("storage:getAll", scope, () => {
|
|
1599
|
-
assertValidScope(scope);
|
|
1600
|
-
const result: Record<string, string> = {};
|
|
1601
|
-
if (scope === StorageScope.Memory) {
|
|
1602
|
-
memoryStore.forEach((value, key) => {
|
|
1603
|
-
if (typeof value === "string") result[key] = value;
|
|
1604
|
-
});
|
|
1605
|
-
return result;
|
|
1606
|
-
}
|
|
1607
|
-
if (scope === StorageScope.Disk) {
|
|
1608
|
-
flushDiskWrites();
|
|
1609
|
-
}
|
|
1610
|
-
if (scope === StorageScope.Secure) {
|
|
1611
|
-
flushSecureWrites();
|
|
1612
|
-
}
|
|
1613
|
-
const keys = WebStorage.getAllKeys(scope);
|
|
1614
|
-
if (keys.length === 0) return {};
|
|
1615
|
-
const values = WebStorage.getBatch(keys, scope);
|
|
1616
|
-
keys.forEach((key, index) => {
|
|
1617
|
-
const val = values[index];
|
|
1618
|
-
if (val !== undefined && val !== null) {
|
|
1619
|
-
result[key] = val;
|
|
1620
|
-
}
|
|
1621
|
-
});
|
|
1622
|
-
return result;
|
|
1623
|
-
});
|
|
1624
|
-
},
|
|
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
|
-
}
|
|
1634
|
-
return measureOperation("storage:export", scope, () =>
|
|
1635
|
-
storage.getAll(scope),
|
|
1636
|
-
);
|
|
1637
|
-
},
|
|
1638
|
-
exportSecureUnsafe: (): Record<string, string> => {
|
|
1639
|
-
return measureOperation(
|
|
1640
|
-
"storage:exportSecureUnsafe",
|
|
1641
|
-
StorageScope.Secure,
|
|
1642
|
-
() => storage.getAll(StorageScope.Secure),
|
|
1643
|
-
);
|
|
1644
|
-
},
|
|
1645
|
-
size: (scope: StorageScope): number => {
|
|
1646
|
-
return measureOperation("storage:size", scope, () => {
|
|
1647
|
-
assertValidScope(scope);
|
|
1648
|
-
if (scope === StorageScope.Memory) return memoryStore.size;
|
|
1649
|
-
if (scope === StorageScope.Disk) {
|
|
1650
|
-
flushDiskWrites();
|
|
1651
|
-
}
|
|
1652
|
-
if (scope === StorageScope.Secure) {
|
|
1653
|
-
flushSecureWrites();
|
|
1654
|
-
}
|
|
1655
|
-
return WebStorage.size(scope);
|
|
1656
|
-
});
|
|
1657
|
-
},
|
|
1658
|
-
setAccessControl: (level: AccessControl) => {
|
|
1659
|
-
assertAccessControlLevel(level);
|
|
1660
|
-
secureDefaultAccessControl = level;
|
|
1661
|
-
recordMetric("storage:setAccessControl", StorageScope.Secure, 0);
|
|
1662
|
-
},
|
|
1663
|
-
setSecureWritesAsync: (_enabled: boolean) => {
|
|
1664
|
-
recordMetric("storage:setSecureWritesAsync", StorageScope.Secure, 0);
|
|
1665
|
-
},
|
|
1666
|
-
setDiskWritesAsync: (enabled: boolean) => {
|
|
1667
|
-
measureOperation("storage:setDiskWritesAsync", StorageScope.Disk, () => {
|
|
1668
|
-
diskWritesAsync = enabled;
|
|
1669
|
-
if (!enabled) {
|
|
1670
|
-
flushDiskWrites();
|
|
1671
|
-
}
|
|
1672
|
-
});
|
|
1673
|
-
},
|
|
1674
|
-
flushDiskWrites: () => {
|
|
1675
|
-
measureOperation("storage:flushDiskWrites", StorageScope.Disk, () => {
|
|
1676
|
-
flushDiskWrites();
|
|
1677
|
-
});
|
|
1678
|
-
},
|
|
1679
|
-
flushSecureWrites: () => {
|
|
1680
|
-
measureOperation("storage:flushSecureWrites", StorageScope.Secure, () => {
|
|
1681
|
-
flushSecureWrites();
|
|
1682
|
-
});
|
|
1683
|
-
},
|
|
1684
|
-
setKeychainAccessGroup: (_group: string) => {
|
|
1685
|
-
recordMetric("storage:setKeychainAccessGroup", StorageScope.Secure, 0);
|
|
1686
|
-
},
|
|
1687
|
-
setMetricsObserver: (observer?: StorageMetricsObserver) => {
|
|
1688
|
-
metricsObserver = observer;
|
|
1689
|
-
},
|
|
1690
|
-
getMetricsSnapshot: (): Record<string, StorageMetricSummary> => {
|
|
1691
|
-
const snapshot: Record<string, StorageMetricSummary> = {};
|
|
1692
|
-
metricsCounters.forEach((value, key) => {
|
|
1693
|
-
snapshot[key] = {
|
|
1694
|
-
count: value.count,
|
|
1695
|
-
totalDurationMs: value.totalDurationMs,
|
|
1696
|
-
avgDurationMs:
|
|
1697
|
-
value.count === 0 ? 0 : value.totalDurationMs / value.count,
|
|
1698
|
-
maxDurationMs: value.maxDurationMs,
|
|
1699
|
-
};
|
|
1700
|
-
});
|
|
1701
|
-
return snapshot;
|
|
1702
|
-
},
|
|
1703
|
-
resetMetrics: () => {
|
|
1704
|
-
metricsCounters.clear();
|
|
772
|
+
setKeychainAccessGroup: (_group: string) => {
|
|
773
|
+
internals.recordMetric(
|
|
774
|
+
"storage:setKeychainAccessGroup",
|
|
775
|
+
StorageScope.Secure,
|
|
776
|
+
0,
|
|
777
|
+
);
|
|
1705
778
|
},
|
|
1706
779
|
getCapabilities: (): StorageCapabilities => ({
|
|
1707
780
|
platform: "web",
|
|
@@ -1742,116 +815,27 @@ export const storage = {
|
|
|
1742
815
|
},
|
|
1743
816
|
};
|
|
1744
817
|
},
|
|
1745
|
-
getSecureMetadata: (key: string): SecureStorageMetadata => {
|
|
1746
|
-
return measureOperation(
|
|
1747
|
-
"storage:getSecureMetadata",
|
|
1748
|
-
StorageScope.Secure,
|
|
1749
|
-
() => {
|
|
1750
|
-
flushSecureWrites();
|
|
1751
|
-
const biometricProtected = WebStorage.hasSecureBiometric(key);
|
|
1752
|
-
const exists =
|
|
1753
|
-
biometricProtected || WebStorage.has(key, StorageScope.Secure);
|
|
1754
|
-
let kind: SecureStorageMetadata["kind"] = "missing";
|
|
1755
|
-
if (exists) {
|
|
1756
|
-
kind = biometricProtected ? "biometric" : "secure";
|
|
1757
|
-
}
|
|
1758
|
-
|
|
1759
|
-
return {
|
|
1760
|
-
key,
|
|
1761
|
-
exists,
|
|
1762
|
-
kind,
|
|
1763
|
-
backend: getBackendName(StorageScope.Secure, webSecureStorageBackend),
|
|
1764
|
-
encrypted: getWebSecureEncryptionStatus(webSecureStorageBackend),
|
|
1765
|
-
hardwareBacked: "unavailable",
|
|
1766
|
-
biometricProtected,
|
|
1767
|
-
valueExposed: false,
|
|
1768
|
-
};
|
|
1769
|
-
},
|
|
1770
|
-
);
|
|
1771
|
-
},
|
|
1772
|
-
getAllSecureMetadata: (): SecureStorageMetadata[] => {
|
|
1773
|
-
return measureOperation(
|
|
1774
|
-
"storage:getAllSecureMetadata",
|
|
1775
|
-
StorageScope.Secure,
|
|
1776
|
-
() => {
|
|
1777
|
-
flushSecureWrites();
|
|
1778
|
-
return WebStorage.getAllKeys(StorageScope.Secure).map((key) =>
|
|
1779
|
-
storage.getSecureMetadata(key),
|
|
1780
|
-
);
|
|
1781
|
-
},
|
|
1782
|
-
);
|
|
1783
|
-
},
|
|
1784
|
-
getString: (key: string, scope: StorageScope): string | undefined => {
|
|
1785
|
-
return measureOperation("storage:getString", scope, () => {
|
|
1786
|
-
return getRawValue(key, scope);
|
|
1787
|
-
});
|
|
1788
|
-
},
|
|
1789
|
-
setString: (key: string, value: string, scope: StorageScope): void => {
|
|
1790
|
-
measureOperation("storage:setString", scope, () => {
|
|
1791
|
-
setRawValue(key, value, scope);
|
|
1792
|
-
});
|
|
1793
|
-
},
|
|
1794
|
-
deleteString: (key: string, scope: StorageScope): void => {
|
|
1795
|
-
measureOperation("storage:deleteString", scope, () => {
|
|
1796
|
-
removeRawValue(key, scope);
|
|
1797
|
-
});
|
|
1798
|
-
},
|
|
1799
|
-
import: (data: Record<string, string>, scope: StorageScope): void => {
|
|
1800
|
-
const keys = Object.keys(data);
|
|
1801
|
-
measureOperation(
|
|
1802
|
-
"storage:import",
|
|
1803
|
-
scope,
|
|
1804
|
-
() => {
|
|
1805
|
-
assertValidScope(scope);
|
|
1806
|
-
if (keys.length === 0) return;
|
|
1807
|
-
const values = keys.map((k) => data[k]!);
|
|
1808
|
-
const changes = keys.map((key, index) =>
|
|
1809
|
-
createKeyChange(
|
|
1810
|
-
scope,
|
|
1811
|
-
key,
|
|
1812
|
-
getEventRawValue(scope, key),
|
|
1813
|
-
values[index],
|
|
1814
|
-
"import",
|
|
1815
|
-
scope === StorageScope.Memory ? "memory" : "web",
|
|
1816
|
-
),
|
|
1817
|
-
);
|
|
1818
|
-
|
|
1819
|
-
if (scope === StorageScope.Memory) {
|
|
1820
|
-
keys.forEach((key, index) => {
|
|
1821
|
-
memoryStore.set(key, values[index]);
|
|
1822
|
-
});
|
|
1823
|
-
keys.forEach((key) => notifyKeyListeners(memoryListeners, key));
|
|
1824
|
-
emitBatchChange(scope, "import", "memory", changes);
|
|
1825
|
-
return;
|
|
1826
|
-
}
|
|
1827
|
-
|
|
1828
|
-
if (scope === StorageScope.Secure) {
|
|
1829
|
-
flushSecureWrites();
|
|
1830
|
-
WebStorage.setSecureAccessControl(secureDefaultAccessControl);
|
|
1831
|
-
}
|
|
1832
|
-
if (scope === StorageScope.Disk) {
|
|
1833
|
-
flushDiskWrites();
|
|
1834
|
-
}
|
|
1835
|
-
|
|
1836
|
-
WebStorage.setBatch(keys, values, scope);
|
|
1837
|
-
keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
|
|
1838
|
-
emitBatchChange(scope, "import", "web", changes);
|
|
1839
|
-
},
|
|
1840
|
-
keys.length,
|
|
1841
|
-
);
|
|
1842
|
-
},
|
|
1843
818
|
};
|
|
1844
819
|
|
|
820
|
+
export const createStorageItem = core.createStorageItem;
|
|
821
|
+
export const getBatch = core.getBatch;
|
|
822
|
+
export const setBatch = core.setBatch;
|
|
823
|
+
export const removeBatch = core.removeBatch;
|
|
824
|
+
export const registerMigration = core.registerMigration;
|
|
825
|
+
export const migrateToLatest = core.migrateToLatest;
|
|
826
|
+
export const runTransaction = core.runTransaction;
|
|
827
|
+
export const createSecureAuthStorage = core.createSecureAuthStorage;
|
|
828
|
+
|
|
1845
829
|
export function setWebSecureStorageBackend(
|
|
1846
830
|
backend?: WebSecureStorageBackend,
|
|
1847
831
|
): void {
|
|
1848
832
|
const previousBackend = webSecureStorageBackend;
|
|
1849
833
|
const nextBackend = backend ?? createDefaultSecureBackend();
|
|
1850
|
-
|
|
834
|
+
internals.clearAllPendingSecureWrites();
|
|
1851
835
|
resetBackendChangeSubscription(StorageScope.Secure);
|
|
1852
836
|
webSecureStorageBackend = nextBackend;
|
|
1853
837
|
hydratedWebScopeKeyIndex.delete(StorageScope.Secure);
|
|
1854
|
-
clearScopeRawCache(StorageScope.Secure);
|
|
838
|
+
internals.clearScopeRawCache(StorageScope.Secure);
|
|
1855
839
|
ensureExternalSyncSubscriptions();
|
|
1856
840
|
if (previousBackend !== nextBackend) {
|
|
1857
841
|
closeWebBackend(StorageScope.Secure, previousBackend);
|
|
@@ -1869,11 +853,11 @@ export function setWebDiskStorageBackend(
|
|
|
1869
853
|
): void {
|
|
1870
854
|
const previousBackend = webDiskStorageBackend;
|
|
1871
855
|
const nextBackend = backend ?? createDefaultDiskBackend();
|
|
1872
|
-
|
|
856
|
+
internals.clearAllPendingDiskWrites();
|
|
1873
857
|
resetBackendChangeSubscription(StorageScope.Disk);
|
|
1874
858
|
webDiskStorageBackend = nextBackend;
|
|
1875
859
|
hydratedWebScopeKeyIndex.delete(StorageScope.Disk);
|
|
1876
|
-
clearScopeRawCache(StorageScope.Disk);
|
|
860
|
+
internals.clearScopeRawCache(StorageScope.Disk);
|
|
1877
861
|
ensureExternalSyncSubscriptions();
|
|
1878
862
|
if (previousBackend !== nextBackend) {
|
|
1879
863
|
closeWebBackend(StorageScope.Disk, previousBackend);
|
|
@@ -1885,8 +869,8 @@ export function getWebDiskStorageBackend(): WebDiskStorageBackend | undefined {
|
|
|
1885
869
|
}
|
|
1886
870
|
|
|
1887
871
|
export async function flushWebStorageBackends(): Promise<void> {
|
|
1888
|
-
flushDiskWrites();
|
|
1889
|
-
flushSecureWrites();
|
|
872
|
+
internals.flushDiskWrites();
|
|
873
|
+
internals.flushSecureWrites();
|
|
1890
874
|
|
|
1891
875
|
const flushes: Promise<void>[] = [];
|
|
1892
876
|
const diskFlush = webDiskStorageBackend?.flush;
|
|
@@ -1902,1182 +886,5 @@ export async function flushWebStorageBackends(): Promise<void> {
|
|
|
1902
886
|
await Promise.all(flushes);
|
|
1903
887
|
}
|
|
1904
888
|
|
|
1905
|
-
export interface StorageItemConfig<T> {
|
|
1906
|
-
key: string;
|
|
1907
|
-
scope: StorageScope;
|
|
1908
|
-
defaultValue?: T;
|
|
1909
|
-
serialize?: (value: T) => string;
|
|
1910
|
-
deserialize?: (value: string) => T;
|
|
1911
|
-
validate?: Validator<T>;
|
|
1912
|
-
onValidationError?: (invalidValue: unknown) => T;
|
|
1913
|
-
expiration?: ExpirationConfig;
|
|
1914
|
-
onExpired?: (key: string) => void;
|
|
1915
|
-
readCache?: boolean;
|
|
1916
|
-
coalesceDiskWrites?: boolean;
|
|
1917
|
-
coalesceSecureWrites?: boolean;
|
|
1918
|
-
namespace?: string;
|
|
1919
|
-
biometric?: boolean;
|
|
1920
|
-
biometricLevel?: BiometricLevel;
|
|
1921
|
-
accessControl?: AccessControl;
|
|
1922
|
-
}
|
|
1923
|
-
|
|
1924
|
-
export interface StorageItem<T> {
|
|
1925
|
-
get: () => T;
|
|
1926
|
-
getWithVersion: () => VersionedValue<T>;
|
|
1927
|
-
set: (value: T | ((prev: T) => T)) => void;
|
|
1928
|
-
setIfVersion: (
|
|
1929
|
-
version: StorageVersion,
|
|
1930
|
-
value: T | ((prev: T) => T),
|
|
1931
|
-
) => boolean;
|
|
1932
|
-
delete: () => void;
|
|
1933
|
-
has: () => boolean;
|
|
1934
|
-
subscribe: (callback: () => void) => () => void;
|
|
1935
|
-
subscribeSelector: <TSelected>(
|
|
1936
|
-
selector: (value: T) => TSelected,
|
|
1937
|
-
listener: StorageSelectorListener<TSelected>,
|
|
1938
|
-
options?: StorageSelectorSubscribeOptions<TSelected>,
|
|
1939
|
-
) => () => void;
|
|
1940
|
-
serialize: (value: T) => string;
|
|
1941
|
-
deserialize: (value: string) => T;
|
|
1942
|
-
scope: StorageScope;
|
|
1943
|
-
key: string;
|
|
1944
|
-
}
|
|
1945
|
-
|
|
1946
|
-
type StorageItemInternal<T> = StorageItem<T> & {
|
|
1947
|
-
_triggerListeners: () => void;
|
|
1948
|
-
_invalidateParsedCacheOnly: () => void;
|
|
1949
|
-
_hasValidation: boolean;
|
|
1950
|
-
_hasExpiration: boolean;
|
|
1951
|
-
_readCacheEnabled: boolean;
|
|
1952
|
-
_isBiometric: boolean;
|
|
1953
|
-
_biometricLevel: BiometricLevel;
|
|
1954
|
-
_defaultValue: T;
|
|
1955
|
-
_secureAccessControl?: AccessControl;
|
|
1956
|
-
};
|
|
1957
|
-
|
|
1958
|
-
function canUseRawBatchPath(item: RawBatchPathItem): boolean {
|
|
1959
|
-
return (
|
|
1960
|
-
item._hasExpiration === false &&
|
|
1961
|
-
item._hasValidation === false &&
|
|
1962
|
-
item._isBiometric !== true &&
|
|
1963
|
-
item._secureAccessControl === undefined
|
|
1964
|
-
);
|
|
1965
|
-
}
|
|
1966
|
-
|
|
1967
|
-
function canUseSecureRawBatchPath(item: RawBatchPathItem): boolean {
|
|
1968
|
-
return (
|
|
1969
|
-
item._hasExpiration === false &&
|
|
1970
|
-
item._hasValidation === false &&
|
|
1971
|
-
item._isBiometric !== true
|
|
1972
|
-
);
|
|
1973
|
-
}
|
|
1974
|
-
|
|
1975
|
-
function defaultSerialize<T>(value: T): string {
|
|
1976
|
-
return serializeWithPrimitiveFastPath(value);
|
|
1977
|
-
}
|
|
1978
|
-
|
|
1979
|
-
function defaultDeserialize<T>(value: string): T {
|
|
1980
|
-
return deserializeWithPrimitiveFastPath(value);
|
|
1981
|
-
}
|
|
1982
|
-
|
|
1983
|
-
export function createStorageItem<T = undefined>(
|
|
1984
|
-
config: StorageItemConfig<T>,
|
|
1985
|
-
): StorageItem<T> {
|
|
1986
|
-
const storageKey = prefixKey(config.namespace, config.key);
|
|
1987
|
-
const serialize = config.serialize ?? defaultSerialize;
|
|
1988
|
-
const deserialize = config.deserialize ?? defaultDeserialize;
|
|
1989
|
-
const isMemory = config.scope === StorageScope.Memory;
|
|
1990
|
-
const resolvedBiometricLevel =
|
|
1991
|
-
config.scope === StorageScope.Secure
|
|
1992
|
-
? (config.biometricLevel ??
|
|
1993
|
-
(config.biometric === true
|
|
1994
|
-
? BiometricLevel.BiometryOnly
|
|
1995
|
-
: BiometricLevel.None))
|
|
1996
|
-
: BiometricLevel.None;
|
|
1997
|
-
const isBiometric = resolvedBiometricLevel !== BiometricLevel.None;
|
|
1998
|
-
const secureAccessControl = config.accessControl;
|
|
1999
|
-
const validate = config.validate;
|
|
2000
|
-
const onValidationError = config.onValidationError;
|
|
2001
|
-
const expiration = config.expiration;
|
|
2002
|
-
const onExpired = config.onExpired;
|
|
2003
|
-
const expirationTtlMs = expiration?.ttlMs;
|
|
2004
|
-
const memoryExpiration =
|
|
2005
|
-
expiration && isMemory ? new Map<string, number>() : null;
|
|
2006
|
-
const readCache = !isMemory && config.readCache === true;
|
|
2007
|
-
const coalesceDiskWrites =
|
|
2008
|
-
config.scope === StorageScope.Disk && config.coalesceDiskWrites === true;
|
|
2009
|
-
const coalesceSecureWrites =
|
|
2010
|
-
config.scope === StorageScope.Secure &&
|
|
2011
|
-
config.coalesceSecureWrites === true &&
|
|
2012
|
-
!isBiometric;
|
|
2013
|
-
const defaultValue = config.defaultValue as T;
|
|
2014
|
-
const nonMemoryScope: NonMemoryScope | null =
|
|
2015
|
-
config.scope === StorageScope.Disk
|
|
2016
|
-
? StorageScope.Disk
|
|
2017
|
-
: config.scope === StorageScope.Secure
|
|
2018
|
-
? StorageScope.Secure
|
|
2019
|
-
: null;
|
|
2020
|
-
|
|
2021
|
-
if (expiration && expiration.ttlMs <= 0) {
|
|
2022
|
-
throw new Error("expiration.ttlMs must be greater than 0.");
|
|
2023
|
-
}
|
|
2024
|
-
if (config.scope === StorageScope.Secure) {
|
|
2025
|
-
assertBiometricLevel(resolvedBiometricLevel);
|
|
2026
|
-
if (secureAccessControl !== undefined) {
|
|
2027
|
-
assertAccessControlLevel(secureAccessControl);
|
|
2028
|
-
}
|
|
2029
|
-
}
|
|
2030
|
-
|
|
2031
|
-
const listeners = new Set<() => void>();
|
|
2032
|
-
let unsubscribe: (() => void) | null = null;
|
|
2033
|
-
let lastRaw: unknown = undefined;
|
|
2034
|
-
let lastValue: T | undefined;
|
|
2035
|
-
let hasLastValue = false;
|
|
2036
|
-
let lastExpiresAt: number | null | undefined = undefined;
|
|
2037
|
-
|
|
2038
|
-
const invalidateParsedCache = () => {
|
|
2039
|
-
lastRaw = undefined;
|
|
2040
|
-
lastValue = undefined;
|
|
2041
|
-
hasLastValue = false;
|
|
2042
|
-
lastExpiresAt = undefined;
|
|
2043
|
-
};
|
|
2044
|
-
|
|
2045
|
-
const ensureSubscription = () => {
|
|
2046
|
-
if (unsubscribe) {
|
|
2047
|
-
return;
|
|
2048
|
-
}
|
|
2049
|
-
|
|
2050
|
-
const listener = () => {
|
|
2051
|
-
invalidateParsedCache();
|
|
2052
|
-
listeners.forEach((callback) => callback());
|
|
2053
|
-
};
|
|
2054
|
-
|
|
2055
|
-
if (isMemory) {
|
|
2056
|
-
unsubscribe = addKeyListener(memoryListeners, storageKey, listener);
|
|
2057
|
-
return;
|
|
2058
|
-
}
|
|
2059
|
-
|
|
2060
|
-
ensureExternalSyncSubscriptions();
|
|
2061
|
-
unsubscribe = addKeyListener(
|
|
2062
|
-
getScopedListeners(nonMemoryScope!),
|
|
2063
|
-
storageKey,
|
|
2064
|
-
listener,
|
|
2065
|
-
);
|
|
2066
|
-
};
|
|
2067
|
-
|
|
2068
|
-
const readStoredRaw = (): unknown => {
|
|
2069
|
-
if (isMemory) {
|
|
2070
|
-
if (memoryExpiration) {
|
|
2071
|
-
const expiresAt = memoryExpiration.get(storageKey);
|
|
2072
|
-
if (expiresAt !== undefined && expiresAt <= Date.now()) {
|
|
2073
|
-
memoryExpiration.delete(storageKey);
|
|
2074
|
-
memoryStore.delete(storageKey);
|
|
2075
|
-
notifyKeyListeners(memoryListeners, storageKey);
|
|
2076
|
-
onExpired?.(storageKey);
|
|
2077
|
-
return undefined;
|
|
2078
|
-
}
|
|
2079
|
-
}
|
|
2080
|
-
return memoryStore.get(storageKey);
|
|
2081
|
-
}
|
|
2082
|
-
|
|
2083
|
-
if (nonMemoryScope === StorageScope.Disk) {
|
|
2084
|
-
const pending = pendingDiskWrites.get(storageKey);
|
|
2085
|
-
if (pending !== undefined) {
|
|
2086
|
-
return pending.value;
|
|
2087
|
-
}
|
|
2088
|
-
}
|
|
2089
|
-
|
|
2090
|
-
if (nonMemoryScope === StorageScope.Secure && !isBiometric) {
|
|
2091
|
-
const pending = pendingSecureWrites.get(storageKey);
|
|
2092
|
-
if (pending !== undefined) {
|
|
2093
|
-
return pending.value;
|
|
2094
|
-
}
|
|
2095
|
-
}
|
|
2096
|
-
|
|
2097
|
-
if (readCache) {
|
|
2098
|
-
const cache = getScopeRawCache(nonMemoryScope!);
|
|
2099
|
-
const cached = cache.get(storageKey);
|
|
2100
|
-
if (cached !== undefined || cache.has(storageKey)) {
|
|
2101
|
-
return cached;
|
|
2102
|
-
}
|
|
2103
|
-
}
|
|
2104
|
-
|
|
2105
|
-
if (isBiometric) {
|
|
2106
|
-
return WebStorage.getSecureBiometric(storageKey);
|
|
2107
|
-
}
|
|
2108
|
-
|
|
2109
|
-
const raw = WebStorage.get(storageKey, config.scope);
|
|
2110
|
-
cacheRawValue(nonMemoryScope!, storageKey, raw);
|
|
2111
|
-
return raw;
|
|
2112
|
-
};
|
|
2113
|
-
|
|
2114
|
-
const writeStoredRaw = (rawValue: string): void => {
|
|
2115
|
-
const oldValue =
|
|
2116
|
-
config.scope === StorageScope.Memory
|
|
2117
|
-
? getEventRawValue(config.scope, storageKey)
|
|
2118
|
-
: undefined;
|
|
2119
|
-
if (isBiometric) {
|
|
2120
|
-
WebStorage.setSecureBiometricWithLevel(
|
|
2121
|
-
storageKey,
|
|
2122
|
-
rawValue,
|
|
2123
|
-
resolvedBiometricLevel,
|
|
2124
|
-
);
|
|
2125
|
-
emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", "web");
|
|
2126
|
-
return;
|
|
2127
|
-
}
|
|
2128
|
-
|
|
2129
|
-
cacheRawValue(nonMemoryScope!, storageKey, rawValue);
|
|
2130
|
-
|
|
2131
|
-
if (nonMemoryScope === StorageScope.Disk) {
|
|
2132
|
-
if (coalesceDiskWrites || diskWritesAsync) {
|
|
2133
|
-
scheduleDiskWrite(storageKey, rawValue);
|
|
2134
|
-
emitKeyChange(
|
|
2135
|
-
config.scope,
|
|
2136
|
-
storageKey,
|
|
2137
|
-
oldValue,
|
|
2138
|
-
rawValue,
|
|
2139
|
-
"set",
|
|
2140
|
-
"web",
|
|
2141
|
-
);
|
|
2142
|
-
return;
|
|
2143
|
-
}
|
|
2144
|
-
|
|
2145
|
-
clearPendingDiskWrite(storageKey);
|
|
2146
|
-
}
|
|
2147
|
-
|
|
2148
|
-
if (coalesceSecureWrites) {
|
|
2149
|
-
scheduleSecureWrite(
|
|
2150
|
-
storageKey,
|
|
2151
|
-
rawValue,
|
|
2152
|
-
secureAccessControl ?? secureDefaultAccessControl,
|
|
2153
|
-
);
|
|
2154
|
-
emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", "web");
|
|
2155
|
-
return;
|
|
2156
|
-
}
|
|
2157
|
-
|
|
2158
|
-
if (nonMemoryScope === StorageScope.Secure) {
|
|
2159
|
-
clearPendingSecureWrite(storageKey);
|
|
2160
|
-
}
|
|
2161
|
-
|
|
2162
|
-
WebStorage.set(storageKey, rawValue, config.scope);
|
|
2163
|
-
emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", "web");
|
|
2164
|
-
};
|
|
2165
|
-
|
|
2166
|
-
const removeStoredRaw = (): void => {
|
|
2167
|
-
const oldValue = getEventRawValue(config.scope, storageKey);
|
|
2168
|
-
if (isBiometric) {
|
|
2169
|
-
WebStorage.deleteSecureBiometric(storageKey);
|
|
2170
|
-
emitKeyChange(
|
|
2171
|
-
config.scope,
|
|
2172
|
-
storageKey,
|
|
2173
|
-
oldValue,
|
|
2174
|
-
undefined,
|
|
2175
|
-
"remove",
|
|
2176
|
-
"web",
|
|
2177
|
-
);
|
|
2178
|
-
return;
|
|
2179
|
-
}
|
|
2180
|
-
|
|
2181
|
-
cacheRawValue(nonMemoryScope!, storageKey, undefined);
|
|
2182
|
-
|
|
2183
|
-
if (nonMemoryScope === StorageScope.Disk) {
|
|
2184
|
-
if (coalesceDiskWrites || diskWritesAsync) {
|
|
2185
|
-
scheduleDiskWrite(storageKey, undefined);
|
|
2186
|
-
emitKeyChange(
|
|
2187
|
-
config.scope,
|
|
2188
|
-
storageKey,
|
|
2189
|
-
oldValue,
|
|
2190
|
-
undefined,
|
|
2191
|
-
"remove",
|
|
2192
|
-
"web",
|
|
2193
|
-
);
|
|
2194
|
-
return;
|
|
2195
|
-
}
|
|
2196
|
-
|
|
2197
|
-
clearPendingDiskWrite(storageKey);
|
|
2198
|
-
}
|
|
2199
|
-
|
|
2200
|
-
if (coalesceSecureWrites) {
|
|
2201
|
-
scheduleSecureWrite(
|
|
2202
|
-
storageKey,
|
|
2203
|
-
undefined,
|
|
2204
|
-
secureAccessControl ?? secureDefaultAccessControl,
|
|
2205
|
-
);
|
|
2206
|
-
emitKeyChange(
|
|
2207
|
-
config.scope,
|
|
2208
|
-
storageKey,
|
|
2209
|
-
oldValue,
|
|
2210
|
-
undefined,
|
|
2211
|
-
"remove",
|
|
2212
|
-
"web",
|
|
2213
|
-
);
|
|
2214
|
-
return;
|
|
2215
|
-
}
|
|
2216
|
-
|
|
2217
|
-
if (nonMemoryScope === StorageScope.Secure) {
|
|
2218
|
-
clearPendingSecureWrite(storageKey);
|
|
2219
|
-
}
|
|
2220
|
-
|
|
2221
|
-
WebStorage.remove(storageKey, config.scope);
|
|
2222
|
-
emitKeyChange(
|
|
2223
|
-
config.scope,
|
|
2224
|
-
storageKey,
|
|
2225
|
-
oldValue,
|
|
2226
|
-
undefined,
|
|
2227
|
-
"remove",
|
|
2228
|
-
"web",
|
|
2229
|
-
);
|
|
2230
|
-
};
|
|
2231
|
-
|
|
2232
|
-
const writeValueWithoutValidation = (value: T): void => {
|
|
2233
|
-
if (isMemory) {
|
|
2234
|
-
const oldValue = getEventRawValue(config.scope, storageKey);
|
|
2235
|
-
if (memoryExpiration) {
|
|
2236
|
-
memoryExpiration.set(storageKey, Date.now() + (expirationTtlMs ?? 0));
|
|
2237
|
-
}
|
|
2238
|
-
memoryStore.set(storageKey, value);
|
|
2239
|
-
notifyKeyListeners(memoryListeners, storageKey);
|
|
2240
|
-
emitKeyChange(
|
|
2241
|
-
config.scope,
|
|
2242
|
-
storageKey,
|
|
2243
|
-
oldValue,
|
|
2244
|
-
typeof value === "string" ? value : undefined,
|
|
2245
|
-
"set",
|
|
2246
|
-
"memory",
|
|
2247
|
-
);
|
|
2248
|
-
return;
|
|
2249
|
-
}
|
|
2250
|
-
|
|
2251
|
-
const serialized = serialize(value);
|
|
2252
|
-
if (expiration) {
|
|
2253
|
-
const envelope: StoredEnvelope = {
|
|
2254
|
-
__nitroStorageEnvelope: true,
|
|
2255
|
-
expiresAt: Date.now() + expiration.ttlMs,
|
|
2256
|
-
payload: serialized,
|
|
2257
|
-
};
|
|
2258
|
-
writeStoredRaw(JSON.stringify(envelope));
|
|
2259
|
-
return;
|
|
2260
|
-
}
|
|
2261
|
-
|
|
2262
|
-
writeStoredRaw(serialized);
|
|
2263
|
-
};
|
|
2264
|
-
|
|
2265
|
-
const resolveInvalidValue = (invalidValue: unknown): T => {
|
|
2266
|
-
if (onValidationError) {
|
|
2267
|
-
return onValidationError(invalidValue);
|
|
2268
|
-
}
|
|
2269
|
-
|
|
2270
|
-
return defaultValue;
|
|
2271
|
-
};
|
|
2272
|
-
|
|
2273
|
-
const ensureValidatedValue = (
|
|
2274
|
-
candidate: unknown,
|
|
2275
|
-
hadStoredValue: boolean,
|
|
2276
|
-
): T => {
|
|
2277
|
-
if (!validate || validate(candidate)) {
|
|
2278
|
-
return candidate as T;
|
|
2279
|
-
}
|
|
2280
|
-
|
|
2281
|
-
const resolved = resolveInvalidValue(candidate);
|
|
2282
|
-
if (validate && !validate(resolved)) {
|
|
2283
|
-
return defaultValue;
|
|
2284
|
-
}
|
|
2285
|
-
if (hadStoredValue) {
|
|
2286
|
-
writeValueWithoutValidation(resolved);
|
|
2287
|
-
}
|
|
2288
|
-
return resolved;
|
|
2289
|
-
};
|
|
2290
|
-
|
|
2291
|
-
const getInternal = (): T => {
|
|
2292
|
-
const raw = readStoredRaw();
|
|
2293
|
-
|
|
2294
|
-
if (!memoryExpiration && raw === lastRaw && hasLastValue) {
|
|
2295
|
-
if (!expiration || lastExpiresAt === null) {
|
|
2296
|
-
return lastValue as T;
|
|
2297
|
-
}
|
|
2298
|
-
|
|
2299
|
-
if (typeof lastExpiresAt === "number") {
|
|
2300
|
-
if (lastExpiresAt > Date.now()) {
|
|
2301
|
-
return lastValue as T;
|
|
2302
|
-
}
|
|
2303
|
-
|
|
2304
|
-
removeStoredRaw();
|
|
2305
|
-
invalidateParsedCache();
|
|
2306
|
-
onExpired?.(storageKey);
|
|
2307
|
-
lastValue = ensureValidatedValue(defaultValue, false);
|
|
2308
|
-
hasLastValue = true;
|
|
2309
|
-
listeners.forEach((cb) => cb());
|
|
2310
|
-
return lastValue;
|
|
2311
|
-
}
|
|
2312
|
-
}
|
|
2313
|
-
|
|
2314
|
-
lastRaw = raw;
|
|
2315
|
-
|
|
2316
|
-
if (raw === undefined) {
|
|
2317
|
-
lastExpiresAt = undefined;
|
|
2318
|
-
lastValue = ensureValidatedValue(defaultValue, false);
|
|
2319
|
-
hasLastValue = true;
|
|
2320
|
-
return lastValue;
|
|
2321
|
-
}
|
|
2322
|
-
|
|
2323
|
-
if (isMemory) {
|
|
2324
|
-
lastExpiresAt = undefined;
|
|
2325
|
-
lastValue = ensureValidatedValue(raw, true);
|
|
2326
|
-
hasLastValue = true;
|
|
2327
|
-
return lastValue;
|
|
2328
|
-
}
|
|
2329
|
-
|
|
2330
|
-
if (typeof raw !== "string") {
|
|
2331
|
-
lastExpiresAt = undefined;
|
|
2332
|
-
lastValue = ensureValidatedValue(defaultValue, false);
|
|
2333
|
-
hasLastValue = true;
|
|
2334
|
-
return lastValue;
|
|
2335
|
-
}
|
|
2336
|
-
|
|
2337
|
-
let deserializableRaw = raw;
|
|
2338
|
-
|
|
2339
|
-
if (expiration) {
|
|
2340
|
-
let envelopeExpiresAt: number | null = null;
|
|
2341
|
-
try {
|
|
2342
|
-
const parsed = JSON.parse(raw) as unknown;
|
|
2343
|
-
if (isStoredEnvelope(parsed)) {
|
|
2344
|
-
envelopeExpiresAt = parsed.expiresAt;
|
|
2345
|
-
if (parsed.expiresAt <= Date.now()) {
|
|
2346
|
-
removeStoredRaw();
|
|
2347
|
-
invalidateParsedCache();
|
|
2348
|
-
onExpired?.(storageKey);
|
|
2349
|
-
lastValue = ensureValidatedValue(defaultValue, false);
|
|
2350
|
-
hasLastValue = true;
|
|
2351
|
-
listeners.forEach((cb) => cb());
|
|
2352
|
-
return lastValue;
|
|
2353
|
-
}
|
|
2354
|
-
|
|
2355
|
-
deserializableRaw = parsed.payload;
|
|
2356
|
-
}
|
|
2357
|
-
} catch {
|
|
2358
|
-
// Keep backward compatibility with legacy raw values.
|
|
2359
|
-
}
|
|
2360
|
-
lastExpiresAt = envelopeExpiresAt;
|
|
2361
|
-
} else {
|
|
2362
|
-
lastExpiresAt = undefined;
|
|
2363
|
-
}
|
|
2364
|
-
|
|
2365
|
-
lastValue = ensureValidatedValue(deserialize(deserializableRaw), true);
|
|
2366
|
-
hasLastValue = true;
|
|
2367
|
-
return lastValue;
|
|
2368
|
-
};
|
|
2369
|
-
|
|
2370
|
-
const getCurrentVersion = (): StorageVersion => {
|
|
2371
|
-
const raw = readStoredRaw();
|
|
2372
|
-
return toVersionToken(raw);
|
|
2373
|
-
};
|
|
2374
|
-
|
|
2375
|
-
const get = (): T =>
|
|
2376
|
-
measureOperation("item:get", config.scope, () => getInternal());
|
|
2377
|
-
|
|
2378
|
-
const getWithVersion = (): VersionedValue<T> =>
|
|
2379
|
-
measureOperation("item:getWithVersion", config.scope, () => ({
|
|
2380
|
-
value: getInternal(),
|
|
2381
|
-
version: getCurrentVersion(),
|
|
2382
|
-
}));
|
|
2383
|
-
|
|
2384
|
-
const set = (valueOrFn: T | ((prev: T) => T)): void => {
|
|
2385
|
-
measureOperation("item:set", config.scope, () => {
|
|
2386
|
-
const newValue = isUpdater(valueOrFn)
|
|
2387
|
-
? valueOrFn(getInternal())
|
|
2388
|
-
: valueOrFn;
|
|
2389
|
-
|
|
2390
|
-
if (validate && !validate(newValue)) {
|
|
2391
|
-
throw new Error(
|
|
2392
|
-
`Validation failed for key "${storageKey}" in scope "${StorageScope[config.scope]}".`,
|
|
2393
|
-
);
|
|
2394
|
-
}
|
|
2395
|
-
|
|
2396
|
-
invalidateParsedCache();
|
|
2397
|
-
writeValueWithoutValidation(newValue);
|
|
2398
|
-
});
|
|
2399
|
-
};
|
|
2400
|
-
|
|
2401
|
-
const setIfVersion = (
|
|
2402
|
-
version: StorageVersion,
|
|
2403
|
-
valueOrFn: T | ((prev: T) => T),
|
|
2404
|
-
): boolean =>
|
|
2405
|
-
measureOperation("item:setIfVersion", config.scope, () => {
|
|
2406
|
-
const currentVersion = getCurrentVersion();
|
|
2407
|
-
if (currentVersion !== version) {
|
|
2408
|
-
return false;
|
|
2409
|
-
}
|
|
2410
|
-
set(valueOrFn);
|
|
2411
|
-
return true;
|
|
2412
|
-
});
|
|
2413
|
-
|
|
2414
|
-
const deleteItem = (): void => {
|
|
2415
|
-
measureOperation("item:delete", config.scope, () => {
|
|
2416
|
-
invalidateParsedCache();
|
|
2417
|
-
|
|
2418
|
-
if (isMemory) {
|
|
2419
|
-
const oldValue = getEventRawValue(config.scope, storageKey);
|
|
2420
|
-
if (memoryExpiration) {
|
|
2421
|
-
memoryExpiration.delete(storageKey);
|
|
2422
|
-
}
|
|
2423
|
-
memoryStore.delete(storageKey);
|
|
2424
|
-
notifyKeyListeners(memoryListeners, storageKey);
|
|
2425
|
-
emitKeyChange(
|
|
2426
|
-
config.scope,
|
|
2427
|
-
storageKey,
|
|
2428
|
-
oldValue,
|
|
2429
|
-
undefined,
|
|
2430
|
-
"remove",
|
|
2431
|
-
"memory",
|
|
2432
|
-
);
|
|
2433
|
-
return;
|
|
2434
|
-
}
|
|
2435
|
-
|
|
2436
|
-
removeStoredRaw();
|
|
2437
|
-
});
|
|
2438
|
-
};
|
|
2439
|
-
|
|
2440
|
-
const hasItem = (): boolean =>
|
|
2441
|
-
measureOperation("item:has", config.scope, () => {
|
|
2442
|
-
if (isMemory) return memoryStore.has(storageKey);
|
|
2443
|
-
if (isBiometric) return WebStorage.hasSecureBiometric(storageKey);
|
|
2444
|
-
if (nonMemoryScope === StorageScope.Disk) {
|
|
2445
|
-
const pending = pendingDiskWrites.get(storageKey);
|
|
2446
|
-
if (pending !== undefined) {
|
|
2447
|
-
return pending.value !== undefined;
|
|
2448
|
-
}
|
|
2449
|
-
}
|
|
2450
|
-
if (nonMemoryScope === StorageScope.Secure) {
|
|
2451
|
-
const pending = pendingSecureWrites.get(storageKey);
|
|
2452
|
-
if (pending !== undefined) {
|
|
2453
|
-
return pending.value !== undefined;
|
|
2454
|
-
}
|
|
2455
|
-
}
|
|
2456
|
-
return WebStorage.has(storageKey, config.scope);
|
|
2457
|
-
});
|
|
2458
|
-
|
|
2459
|
-
const subscribe = (callback: () => void): (() => void) => {
|
|
2460
|
-
ensureSubscription();
|
|
2461
|
-
listeners.add(callback);
|
|
2462
|
-
return () => {
|
|
2463
|
-
listeners.delete(callback);
|
|
2464
|
-
if (listeners.size === 0 && unsubscribe) {
|
|
2465
|
-
unsubscribe();
|
|
2466
|
-
unsubscribe = null;
|
|
2467
|
-
}
|
|
2468
|
-
};
|
|
2469
|
-
};
|
|
2470
|
-
|
|
2471
|
-
const subscribeSelector = <TSelected>(
|
|
2472
|
-
selector: (value: T) => TSelected,
|
|
2473
|
-
listener: StorageSelectorListener<TSelected>,
|
|
2474
|
-
options: StorageSelectorSubscribeOptions<TSelected> = {},
|
|
2475
|
-
): (() => void) => {
|
|
2476
|
-
const isEqual = options.isEqual ?? Object.is;
|
|
2477
|
-
let currentValue = selector(getInternal());
|
|
2478
|
-
|
|
2479
|
-
if (options.fireImmediately === true) {
|
|
2480
|
-
listener(currentValue, currentValue);
|
|
2481
|
-
}
|
|
2482
|
-
|
|
2483
|
-
return subscribe(() => {
|
|
2484
|
-
const nextValue = selector(getInternal());
|
|
2485
|
-
if (isEqual(currentValue, nextValue)) {
|
|
2486
|
-
return;
|
|
2487
|
-
}
|
|
2488
|
-
|
|
2489
|
-
const previousValue = currentValue;
|
|
2490
|
-
currentValue = nextValue;
|
|
2491
|
-
listener(nextValue, previousValue);
|
|
2492
|
-
});
|
|
2493
|
-
};
|
|
2494
|
-
|
|
2495
|
-
const storageItem: StorageItemInternal<T> = {
|
|
2496
|
-
get,
|
|
2497
|
-
getWithVersion,
|
|
2498
|
-
set,
|
|
2499
|
-
setIfVersion,
|
|
2500
|
-
delete: deleteItem,
|
|
2501
|
-
has: hasItem,
|
|
2502
|
-
subscribe,
|
|
2503
|
-
subscribeSelector,
|
|
2504
|
-
serialize,
|
|
2505
|
-
deserialize,
|
|
2506
|
-
_triggerListeners: () => {
|
|
2507
|
-
invalidateParsedCache();
|
|
2508
|
-
listeners.forEach((listener) => listener());
|
|
2509
|
-
},
|
|
2510
|
-
_invalidateParsedCacheOnly: () => {
|
|
2511
|
-
invalidateParsedCache();
|
|
2512
|
-
},
|
|
2513
|
-
_hasValidation: validate !== undefined,
|
|
2514
|
-
_hasExpiration: expiration !== undefined,
|
|
2515
|
-
_readCacheEnabled: readCache,
|
|
2516
|
-
_isBiometric: isBiometric,
|
|
2517
|
-
_biometricLevel: resolvedBiometricLevel,
|
|
2518
|
-
_defaultValue: defaultValue,
|
|
2519
|
-
...(secureAccessControl !== undefined
|
|
2520
|
-
? { _secureAccessControl: secureAccessControl }
|
|
2521
|
-
: {}),
|
|
2522
|
-
scope: config.scope,
|
|
2523
|
-
key: storageKey,
|
|
2524
|
-
};
|
|
2525
|
-
|
|
2526
|
-
return storageItem;
|
|
2527
|
-
}
|
|
2528
|
-
|
|
2529
889
|
export { useStorage, useStorageSelector, useSetStorage } from "./storage-hooks";
|
|
2530
890
|
export { createIndexedDBBackend } from "./indexeddb-backend";
|
|
2531
|
-
|
|
2532
|
-
type BatchReadItem<T> = Pick<
|
|
2533
|
-
StorageItem<T>,
|
|
2534
|
-
"key" | "scope" | "get" | "deserialize"
|
|
2535
|
-
> & {
|
|
2536
|
-
_hasValidation?: boolean;
|
|
2537
|
-
_hasExpiration?: boolean;
|
|
2538
|
-
_readCacheEnabled?: boolean;
|
|
2539
|
-
_isBiometric?: boolean;
|
|
2540
|
-
_defaultValue?: unknown;
|
|
2541
|
-
_secureAccessControl?: AccessControl;
|
|
2542
|
-
};
|
|
2543
|
-
type BatchRemoveItem = Pick<StorageItem<unknown>, "key" | "scope" | "delete">;
|
|
2544
|
-
|
|
2545
|
-
export type StorageBatchSetItem<T> = {
|
|
2546
|
-
item: StorageItem<T>;
|
|
2547
|
-
value: T;
|
|
2548
|
-
};
|
|
2549
|
-
|
|
2550
|
-
export function getBatch(
|
|
2551
|
-
items: readonly BatchReadItem<unknown>[],
|
|
2552
|
-
scope: StorageScope,
|
|
2553
|
-
): unknown[] {
|
|
2554
|
-
return measureOperation(
|
|
2555
|
-
"batch:get",
|
|
2556
|
-
scope,
|
|
2557
|
-
() => {
|
|
2558
|
-
assertBatchScope(items, scope);
|
|
2559
|
-
|
|
2560
|
-
if (scope === StorageScope.Memory) {
|
|
2561
|
-
return items.map((item) => item.get());
|
|
2562
|
-
}
|
|
2563
|
-
|
|
2564
|
-
const useRawBatchPath = items.every((item) =>
|
|
2565
|
-
scope === StorageScope.Secure
|
|
2566
|
-
? canUseSecureRawBatchPath(item)
|
|
2567
|
-
: canUseRawBatchPath(item),
|
|
2568
|
-
);
|
|
2569
|
-
if (!useRawBatchPath) {
|
|
2570
|
-
return items.map((item) => item.get());
|
|
2571
|
-
}
|
|
2572
|
-
|
|
2573
|
-
const rawValues = new Array<string | undefined>(items.length);
|
|
2574
|
-
const keysToFetch: string[] = [];
|
|
2575
|
-
const keyIndexes: number[] = [];
|
|
2576
|
-
|
|
2577
|
-
items.forEach((item, index) => {
|
|
2578
|
-
if (scope === StorageScope.Disk) {
|
|
2579
|
-
const pending = pendingDiskWrites.get(item.key);
|
|
2580
|
-
if (pending !== undefined) {
|
|
2581
|
-
rawValues[index] = pending.value;
|
|
2582
|
-
return;
|
|
2583
|
-
}
|
|
2584
|
-
}
|
|
2585
|
-
|
|
2586
|
-
if (scope === StorageScope.Secure) {
|
|
2587
|
-
const pending = pendingSecureWrites.get(item.key);
|
|
2588
|
-
if (pending !== undefined) {
|
|
2589
|
-
rawValues[index] = pending.value;
|
|
2590
|
-
return;
|
|
2591
|
-
}
|
|
2592
|
-
}
|
|
2593
|
-
|
|
2594
|
-
if (item._readCacheEnabled === true) {
|
|
2595
|
-
const cache = getScopeRawCache(scope);
|
|
2596
|
-
const cached = cache.get(item.key);
|
|
2597
|
-
if (cached !== undefined || cache.has(item.key)) {
|
|
2598
|
-
rawValues[index] = cached;
|
|
2599
|
-
return;
|
|
2600
|
-
}
|
|
2601
|
-
}
|
|
2602
|
-
|
|
2603
|
-
keysToFetch.push(item.key);
|
|
2604
|
-
keyIndexes.push(index);
|
|
2605
|
-
});
|
|
2606
|
-
|
|
2607
|
-
if (keysToFetch.length > 0) {
|
|
2608
|
-
const fetchedValues = WebStorage.getBatch(keysToFetch, scope);
|
|
2609
|
-
fetchedValues.forEach((value, index) => {
|
|
2610
|
-
const key = keysToFetch[index];
|
|
2611
|
-
const targetIndex = keyIndexes[index];
|
|
2612
|
-
if (key === undefined || targetIndex === undefined) {
|
|
2613
|
-
return;
|
|
2614
|
-
}
|
|
2615
|
-
rawValues[targetIndex] = value;
|
|
2616
|
-
cacheRawValue(scope, key, value);
|
|
2617
|
-
});
|
|
2618
|
-
}
|
|
2619
|
-
|
|
2620
|
-
return items.map((item, index) => {
|
|
2621
|
-
const raw = rawValues[index];
|
|
2622
|
-
if (raw === undefined) {
|
|
2623
|
-
return asInternal(item as StorageItem<unknown>)._defaultValue;
|
|
2624
|
-
}
|
|
2625
|
-
return item.deserialize(raw);
|
|
2626
|
-
});
|
|
2627
|
-
},
|
|
2628
|
-
items.length,
|
|
2629
|
-
);
|
|
2630
|
-
}
|
|
2631
|
-
|
|
2632
|
-
export function setBatch<T>(
|
|
2633
|
-
items: readonly StorageBatchSetItem<T>[],
|
|
2634
|
-
scope: StorageScope,
|
|
2635
|
-
): void {
|
|
2636
|
-
measureOperation(
|
|
2637
|
-
"batch:set",
|
|
2638
|
-
scope,
|
|
2639
|
-
() => {
|
|
2640
|
-
assertBatchScope(
|
|
2641
|
-
items.map((batchEntry) => batchEntry.item),
|
|
2642
|
-
scope,
|
|
2643
|
-
);
|
|
2644
|
-
|
|
2645
|
-
if (scope === StorageScope.Memory) {
|
|
2646
|
-
// Determine if any item needs per-item handling (validation or TTL)
|
|
2647
|
-
const needsIndividualSets = items.some(({ item }) => {
|
|
2648
|
-
const internal = asInternal(item as StorageItem<unknown>);
|
|
2649
|
-
return internal._hasValidation || internal._hasExpiration;
|
|
2650
|
-
});
|
|
2651
|
-
|
|
2652
|
-
if (needsIndividualSets) {
|
|
2653
|
-
items.forEach(({ item, value }) => item.set(value));
|
|
2654
|
-
return;
|
|
2655
|
-
}
|
|
2656
|
-
|
|
2657
|
-
const changes = items.map(({ item, value }) =>
|
|
2658
|
-
createKeyChange(
|
|
2659
|
-
scope,
|
|
2660
|
-
item.key,
|
|
2661
|
-
getEventRawValue(scope, item.key),
|
|
2662
|
-
typeof value === "string" ? value : undefined,
|
|
2663
|
-
"setBatch",
|
|
2664
|
-
"memory",
|
|
2665
|
-
),
|
|
2666
|
-
);
|
|
2667
|
-
|
|
2668
|
-
// Atomic write: update all values in memoryStore, invalidate caches, then batch-notify
|
|
2669
|
-
items.forEach(({ item, value }) => {
|
|
2670
|
-
memoryStore.set(item.key, value);
|
|
2671
|
-
asInternal(item as StorageItem<unknown>)._invalidateParsedCacheOnly();
|
|
2672
|
-
});
|
|
2673
|
-
items.forEach(({ item }) =>
|
|
2674
|
-
notifyKeyListeners(memoryListeners, item.key),
|
|
2675
|
-
);
|
|
2676
|
-
emitBatchChange(scope, "setBatch", "memory", changes);
|
|
2677
|
-
return;
|
|
2678
|
-
}
|
|
2679
|
-
|
|
2680
|
-
if (scope === StorageScope.Secure) {
|
|
2681
|
-
const secureEntries = items.map(({ item, value }) => ({
|
|
2682
|
-
item,
|
|
2683
|
-
value,
|
|
2684
|
-
internal: asInternal(item),
|
|
2685
|
-
}));
|
|
2686
|
-
const canUseSecureBatchPath = secureEntries.every(({ internal }) =>
|
|
2687
|
-
canUseSecureRawBatchPath(internal),
|
|
2688
|
-
);
|
|
2689
|
-
if (!canUseSecureBatchPath) {
|
|
2690
|
-
items.forEach(({ item, value }) => item.set(value));
|
|
2691
|
-
return;
|
|
2692
|
-
}
|
|
2693
|
-
|
|
2694
|
-
flushSecureWrites();
|
|
2695
|
-
const keys = secureEntries.map(({ item }) => item.key);
|
|
2696
|
-
const oldValues = shouldReadPreviousEventValues(scope)
|
|
2697
|
-
? WebStorage.getBatch(keys, scope)
|
|
2698
|
-
: [];
|
|
2699
|
-
const groupedByAccessControl = new Map<
|
|
2700
|
-
number,
|
|
2701
|
-
{ keys: string[]; values: string[] }
|
|
2702
|
-
>();
|
|
2703
|
-
|
|
2704
|
-
secureEntries.forEach(({ item, value, internal }) => {
|
|
2705
|
-
const accessControl =
|
|
2706
|
-
internal._secureAccessControl ?? secureDefaultAccessControl;
|
|
2707
|
-
const existingGroup = groupedByAccessControl.get(accessControl);
|
|
2708
|
-
const group = existingGroup ?? { keys: [], values: [] };
|
|
2709
|
-
group.keys.push(item.key);
|
|
2710
|
-
group.values.push(item.serialize(value));
|
|
2711
|
-
if (!existingGroup) {
|
|
2712
|
-
groupedByAccessControl.set(accessControl, group);
|
|
2713
|
-
}
|
|
2714
|
-
});
|
|
2715
|
-
|
|
2716
|
-
groupedByAccessControl.forEach((group, accessControl) => {
|
|
2717
|
-
WebStorage.setSecureAccessControl(accessControl);
|
|
2718
|
-
WebStorage.setBatch(group.keys, group.values, scope);
|
|
2719
|
-
group.keys.forEach((key, index) =>
|
|
2720
|
-
cacheRawValue(scope, key, group.values[index]),
|
|
2721
|
-
);
|
|
2722
|
-
});
|
|
2723
|
-
emitBatchChange(
|
|
2724
|
-
scope,
|
|
2725
|
-
"setBatch",
|
|
2726
|
-
"web",
|
|
2727
|
-
secureEntries.map(({ item, value }, index) =>
|
|
2728
|
-
createKeyChange(
|
|
2729
|
-
scope,
|
|
2730
|
-
item.key,
|
|
2731
|
-
oldValues[index],
|
|
2732
|
-
item.serialize(value),
|
|
2733
|
-
"setBatch",
|
|
2734
|
-
"web",
|
|
2735
|
-
),
|
|
2736
|
-
),
|
|
2737
|
-
);
|
|
2738
|
-
return;
|
|
2739
|
-
}
|
|
2740
|
-
|
|
2741
|
-
flushDiskWrites();
|
|
2742
|
-
|
|
2743
|
-
const useRawBatchPath = items.every(({ item }) =>
|
|
2744
|
-
canUseRawBatchPath(asInternal(item)),
|
|
2745
|
-
);
|
|
2746
|
-
if (!useRawBatchPath) {
|
|
2747
|
-
items.forEach(({ item, value }) => item.set(value));
|
|
2748
|
-
return;
|
|
2749
|
-
}
|
|
2750
|
-
|
|
2751
|
-
const keys = items.map((entry) => entry.item.key);
|
|
2752
|
-
const values = items.map((entry) => entry.item.serialize(entry.value));
|
|
2753
|
-
const oldValues = shouldReadPreviousEventValues(scope)
|
|
2754
|
-
? WebStorage.getBatch(keys, scope)
|
|
2755
|
-
: [];
|
|
2756
|
-
WebStorage.setBatch(keys, values, scope);
|
|
2757
|
-
keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
|
|
2758
|
-
emitBatchChange(
|
|
2759
|
-
scope,
|
|
2760
|
-
"setBatch",
|
|
2761
|
-
"web",
|
|
2762
|
-
keys.map((key, index) =>
|
|
2763
|
-
createKeyChange(
|
|
2764
|
-
scope,
|
|
2765
|
-
key,
|
|
2766
|
-
oldValues[index],
|
|
2767
|
-
values[index],
|
|
2768
|
-
"setBatch",
|
|
2769
|
-
"web",
|
|
2770
|
-
),
|
|
2771
|
-
),
|
|
2772
|
-
);
|
|
2773
|
-
},
|
|
2774
|
-
items.length,
|
|
2775
|
-
);
|
|
2776
|
-
}
|
|
2777
|
-
|
|
2778
|
-
export function removeBatch(
|
|
2779
|
-
items: readonly BatchRemoveItem[],
|
|
2780
|
-
scope: StorageScope,
|
|
2781
|
-
): void {
|
|
2782
|
-
measureOperation(
|
|
2783
|
-
"batch:remove",
|
|
2784
|
-
scope,
|
|
2785
|
-
() => {
|
|
2786
|
-
assertBatchScope(items, scope);
|
|
2787
|
-
|
|
2788
|
-
if (scope === StorageScope.Memory) {
|
|
2789
|
-
const changes = items.map((item) =>
|
|
2790
|
-
createKeyChange(
|
|
2791
|
-
scope,
|
|
2792
|
-
item.key,
|
|
2793
|
-
getEventRawValue(scope, item.key),
|
|
2794
|
-
undefined,
|
|
2795
|
-
"removeBatch",
|
|
2796
|
-
"memory",
|
|
2797
|
-
),
|
|
2798
|
-
);
|
|
2799
|
-
items.forEach((item) => item.delete());
|
|
2800
|
-
emitBatchChange(scope, "removeBatch", "memory", changes);
|
|
2801
|
-
return;
|
|
2802
|
-
}
|
|
2803
|
-
|
|
2804
|
-
const keys = items.map((item) => item.key);
|
|
2805
|
-
if (scope === StorageScope.Disk) {
|
|
2806
|
-
flushDiskWrites();
|
|
2807
|
-
}
|
|
2808
|
-
if (scope === StorageScope.Secure) {
|
|
2809
|
-
flushSecureWrites();
|
|
2810
|
-
}
|
|
2811
|
-
const oldValues = shouldReadPreviousEventValues(scope)
|
|
2812
|
-
? WebStorage.getBatch(keys, scope)
|
|
2813
|
-
: [];
|
|
2814
|
-
WebStorage.removeBatch(keys, scope);
|
|
2815
|
-
keys.forEach((key) => cacheRawValue(scope, key, undefined));
|
|
2816
|
-
emitBatchChange(
|
|
2817
|
-
scope,
|
|
2818
|
-
"removeBatch",
|
|
2819
|
-
"web",
|
|
2820
|
-
keys.map((key, index) =>
|
|
2821
|
-
createKeyChange(
|
|
2822
|
-
scope,
|
|
2823
|
-
key,
|
|
2824
|
-
oldValues[index],
|
|
2825
|
-
undefined,
|
|
2826
|
-
"removeBatch",
|
|
2827
|
-
"web",
|
|
2828
|
-
),
|
|
2829
|
-
),
|
|
2830
|
-
);
|
|
2831
|
-
},
|
|
2832
|
-
items.length,
|
|
2833
|
-
);
|
|
2834
|
-
}
|
|
2835
|
-
|
|
2836
|
-
export function registerMigration(version: number, migration: Migration): void {
|
|
2837
|
-
if (!Number.isInteger(version) || version <= 0) {
|
|
2838
|
-
throw new Error("Migration version must be a positive integer.");
|
|
2839
|
-
}
|
|
2840
|
-
|
|
2841
|
-
if (registeredMigrations.has(version)) {
|
|
2842
|
-
throw new Error(`Migration version ${version} is already registered.`);
|
|
2843
|
-
}
|
|
2844
|
-
|
|
2845
|
-
registeredMigrations.set(version, migration);
|
|
2846
|
-
}
|
|
2847
|
-
|
|
2848
|
-
export function migrateToLatest(
|
|
2849
|
-
scope: StorageScope = StorageScope.Disk,
|
|
2850
|
-
): number {
|
|
2851
|
-
return measureOperation("migration:run", scope, () => {
|
|
2852
|
-
assertValidScope(scope);
|
|
2853
|
-
const currentVersion = readMigrationVersion(scope);
|
|
2854
|
-
const versions = Array.from(registeredMigrations.keys())
|
|
2855
|
-
.filter((version) => version > currentVersion)
|
|
2856
|
-
.sort((a, b) => a - b);
|
|
2857
|
-
|
|
2858
|
-
let appliedVersion = currentVersion;
|
|
2859
|
-
const context: MigrationContext = {
|
|
2860
|
-
scope,
|
|
2861
|
-
getRaw: (key) => getRawValue(key, scope),
|
|
2862
|
-
setRaw: (key, value) => setRawValue(key, value, scope),
|
|
2863
|
-
removeRaw: (key) => removeRawValue(key, scope),
|
|
2864
|
-
};
|
|
2865
|
-
|
|
2866
|
-
versions.forEach((version) => {
|
|
2867
|
-
const migration = registeredMigrations.get(version);
|
|
2868
|
-
if (!migration) {
|
|
2869
|
-
return;
|
|
2870
|
-
}
|
|
2871
|
-
migration(context);
|
|
2872
|
-
appliedVersion = version;
|
|
2873
|
-
});
|
|
2874
|
-
|
|
2875
|
-
if (appliedVersion !== currentVersion) {
|
|
2876
|
-
writeMigrationVersion(scope, appliedVersion);
|
|
2877
|
-
}
|
|
2878
|
-
|
|
2879
|
-
return appliedVersion;
|
|
2880
|
-
});
|
|
2881
|
-
}
|
|
2882
|
-
|
|
2883
|
-
export function runTransaction<T>(
|
|
2884
|
-
scope: StorageScope,
|
|
2885
|
-
transaction: (context: TransactionContext) => T,
|
|
2886
|
-
): T {
|
|
2887
|
-
return measureOperation("transaction:run", scope, () => {
|
|
2888
|
-
assertValidScope(scope);
|
|
2889
|
-
if (scope === StorageScope.Disk) {
|
|
2890
|
-
flushDiskWrites();
|
|
2891
|
-
}
|
|
2892
|
-
if (scope === StorageScope.Secure) {
|
|
2893
|
-
flushSecureWrites();
|
|
2894
|
-
}
|
|
2895
|
-
|
|
2896
|
-
const NOT_SET = Symbol();
|
|
2897
|
-
const rollback = new Map<string, RollbackRecord>();
|
|
2898
|
-
|
|
2899
|
-
const rememberRollback = (
|
|
2900
|
-
key: string,
|
|
2901
|
-
item?: Pick<StorageItem<unknown>, "key" | "scope">,
|
|
2902
|
-
) => {
|
|
2903
|
-
if (rollback.has(key)) {
|
|
2904
|
-
return;
|
|
2905
|
-
}
|
|
2906
|
-
if (scope === StorageScope.Memory) {
|
|
2907
|
-
rollback.set(key, {
|
|
2908
|
-
kind: "memory",
|
|
2909
|
-
value: memoryStore.has(key) ? memoryStore.get(key) : NOT_SET,
|
|
2910
|
-
});
|
|
2911
|
-
} else {
|
|
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
|
-
});
|
|
2931
|
-
}
|
|
2932
|
-
};
|
|
2933
|
-
|
|
2934
|
-
const tx: TransactionContext = {
|
|
2935
|
-
scope,
|
|
2936
|
-
getRaw: (key) => getRawValue(key, scope),
|
|
2937
|
-
setRaw: (key, value) => {
|
|
2938
|
-
rememberRollback(key);
|
|
2939
|
-
setRawValue(key, value, scope);
|
|
2940
|
-
},
|
|
2941
|
-
removeRaw: (key) => {
|
|
2942
|
-
rememberRollback(key);
|
|
2943
|
-
removeRawValue(key, scope);
|
|
2944
|
-
},
|
|
2945
|
-
getItem: (item) => {
|
|
2946
|
-
assertBatchScope([item], scope);
|
|
2947
|
-
return item.get();
|
|
2948
|
-
},
|
|
2949
|
-
setItem: (item, value) => {
|
|
2950
|
-
assertBatchScope([item], scope);
|
|
2951
|
-
rememberRollback(item.key, item);
|
|
2952
|
-
item.set(value);
|
|
2953
|
-
},
|
|
2954
|
-
removeItem: (item) => {
|
|
2955
|
-
assertBatchScope([item], scope);
|
|
2956
|
-
rememberRollback(item.key, item);
|
|
2957
|
-
item.delete();
|
|
2958
|
-
},
|
|
2959
|
-
};
|
|
2960
|
-
|
|
2961
|
-
try {
|
|
2962
|
-
return transaction(tx);
|
|
2963
|
-
} catch (error) {
|
|
2964
|
-
const rollbackEntries = Array.from(rollback.entries()).reverse();
|
|
2965
|
-
if (scope === StorageScope.Memory) {
|
|
2966
|
-
rollbackEntries.forEach(([key, record]) => {
|
|
2967
|
-
if (record.value === NOT_SET) {
|
|
2968
|
-
memoryStore.delete(key);
|
|
2969
|
-
} else {
|
|
2970
|
-
memoryStore.set(key, record.value);
|
|
2971
|
-
}
|
|
2972
|
-
notifyKeyListeners(memoryListeners, key);
|
|
2973
|
-
});
|
|
2974
|
-
} else {
|
|
2975
|
-
const groupedKeysToSet = new Map<
|
|
2976
|
-
AccessControl,
|
|
2977
|
-
{ keys: string[]; values: string[] }
|
|
2978
|
-
>();
|
|
2979
|
-
const keysToRemove: string[] = [];
|
|
2980
|
-
|
|
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) {
|
|
2998
|
-
keysToRemove.push(key);
|
|
2999
|
-
} else {
|
|
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
|
-
}
|
|
3009
|
-
}
|
|
3010
|
-
});
|
|
3011
|
-
|
|
3012
|
-
if (scope === StorageScope.Disk) {
|
|
3013
|
-
flushDiskWrites();
|
|
3014
|
-
}
|
|
3015
|
-
if (scope === StorageScope.Secure) {
|
|
3016
|
-
flushSecureWrites();
|
|
3017
|
-
}
|
|
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]),
|
|
3025
|
-
);
|
|
3026
|
-
});
|
|
3027
|
-
if (keysToRemove.length > 0) {
|
|
3028
|
-
WebStorage.removeBatch(keysToRemove, scope);
|
|
3029
|
-
keysToRemove.forEach((key) => cacheRawValue(scope, key, undefined));
|
|
3030
|
-
}
|
|
3031
|
-
}
|
|
3032
|
-
throw error;
|
|
3033
|
-
}
|
|
3034
|
-
});
|
|
3035
|
-
}
|
|
3036
|
-
|
|
3037
|
-
export type SecureAuthStorageConfig<K extends string = string> = Record<
|
|
3038
|
-
K,
|
|
3039
|
-
{
|
|
3040
|
-
ttlMs?: number;
|
|
3041
|
-
biometric?: boolean;
|
|
3042
|
-
biometricLevel?: BiometricLevel;
|
|
3043
|
-
accessControl?: AccessControl;
|
|
3044
|
-
}
|
|
3045
|
-
>;
|
|
3046
|
-
|
|
3047
|
-
export function createSecureAuthStorage<K extends string>(
|
|
3048
|
-
config: SecureAuthStorageConfig<K>,
|
|
3049
|
-
options?: { namespace?: string },
|
|
3050
|
-
): Record<K, StorageItem<string>> {
|
|
3051
|
-
const ns = options?.namespace ?? "auth";
|
|
3052
|
-
const result: Partial<Record<K, StorageItem<string>>> = {};
|
|
3053
|
-
|
|
3054
|
-
for (const key of typedKeys(config)) {
|
|
3055
|
-
const itemConfig = config[key];
|
|
3056
|
-
const expirationConfig =
|
|
3057
|
-
itemConfig.ttlMs !== undefined ? { ttlMs: itemConfig.ttlMs } : undefined;
|
|
3058
|
-
result[key] = createStorageItem<string>({
|
|
3059
|
-
key,
|
|
3060
|
-
scope: StorageScope.Secure,
|
|
3061
|
-
defaultValue: "",
|
|
3062
|
-
namespace: ns,
|
|
3063
|
-
...(itemConfig.biometric !== undefined
|
|
3064
|
-
? { biometric: itemConfig.biometric }
|
|
3065
|
-
: {}),
|
|
3066
|
-
...(itemConfig.biometricLevel !== undefined
|
|
3067
|
-
? { biometricLevel: itemConfig.biometricLevel }
|
|
3068
|
-
: {}),
|
|
3069
|
-
...(itemConfig.accessControl !== undefined
|
|
3070
|
-
? { accessControl: itemConfig.accessControl }
|
|
3071
|
-
: {}),
|
|
3072
|
-
...(expirationConfig !== undefined
|
|
3073
|
-
? { expiration: expirationConfig }
|
|
3074
|
-
: {}),
|
|
3075
|
-
});
|
|
3076
|
-
}
|
|
3077
|
-
|
|
3078
|
-
return result as Record<K, StorageItem<string>>;
|
|
3079
|
-
}
|
|
3080
|
-
|
|
3081
|
-
export function isKeychainLockedError(err: unknown): boolean {
|
|
3082
|
-
return isLockedStorageErrorCode(getStorageErrorCode(err));
|
|
3083
|
-
}
|