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