react-native-nitro-storage 0.3.1 → 0.4.0
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 +334 -34
- package/android/CMakeLists.txt +2 -0
- package/android/src/main/cpp/AndroidStorageAdapterCpp.cpp +26 -2
- package/android/src/main/cpp/AndroidStorageAdapterCpp.hpp +4 -0
- package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +90 -18
- package/cpp/bindings/HybridStorage.cpp +214 -23
- package/cpp/bindings/HybridStorage.hpp +31 -3
- package/cpp/core/NativeStorageAdapter.hpp +4 -0
- package/ios/IOSStorageAdapterCpp.hpp +17 -0
- package/ios/IOSStorageAdapterCpp.mm +140 -10
- package/lib/commonjs/index.js +555 -288
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +750 -309
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/internal.js +25 -0
- package/lib/commonjs/internal.js.map +1 -1
- package/lib/commonjs/storage-hooks.js +36 -0
- package/lib/commonjs/storage-hooks.js.map +1 -0
- package/lib/module/index.js +537 -287
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +732 -308
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/internal.js +24 -0
- package/lib/module/internal.js.map +1 -1
- package/lib/module/storage-hooks.js +30 -0
- package/lib/module/storage-hooks.js.map +1 -0
- package/lib/typescript/Storage.nitro.d.ts +4 -0
- package/lib/typescript/Storage.nitro.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +41 -4
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/index.web.d.ts +45 -4
- package/lib/typescript/index.web.d.ts.map +1 -1
- package/lib/typescript/internal.d.ts +1 -0
- package/lib/typescript/internal.d.ts.map +1 -1
- package/lib/typescript/storage-hooks.d.ts +10 -0
- package/lib/typescript/storage-hooks.d.ts.map +1 -0
- package/nitrogen/generated/shared/c++/HybridStorageSpec.cpp +4 -0
- package/nitrogen/generated/shared/c++/HybridStorageSpec.hpp +4 -0
- package/package.json +5 -3
- package/src/Storage.nitro.ts +4 -0
- package/src/index.ts +704 -324
- package/src/index.web.ts +929 -346
- package/src/internal.ts +28 -0
- package/src/storage-hooks.ts +48 -0
package/src/index.web.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { useRef, useSyncExternalStore } from "react";
|
|
2
1
|
import { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
|
|
3
2
|
import {
|
|
4
3
|
MIGRATION_VERSION_KEY,
|
|
@@ -8,6 +7,7 @@ import {
|
|
|
8
7
|
assertValidScope,
|
|
9
8
|
serializeWithPrimitiveFastPath,
|
|
10
9
|
deserializeWithPrimitiveFastPath,
|
|
10
|
+
toVersionToken,
|
|
11
11
|
prefixKey,
|
|
12
12
|
isNamespaced,
|
|
13
13
|
} from "./internal";
|
|
@@ -19,6 +19,31 @@ export type Validator<T> = (value: unknown) => value is T;
|
|
|
19
19
|
export type ExpirationConfig = {
|
|
20
20
|
ttlMs: number;
|
|
21
21
|
};
|
|
22
|
+
export type StorageVersion = string;
|
|
23
|
+
export type VersionedValue<T> = {
|
|
24
|
+
value: T;
|
|
25
|
+
version: StorageVersion;
|
|
26
|
+
};
|
|
27
|
+
export type StorageMetricsEvent = {
|
|
28
|
+
operation: string;
|
|
29
|
+
scope: StorageScope;
|
|
30
|
+
durationMs: number;
|
|
31
|
+
keysCount: number;
|
|
32
|
+
};
|
|
33
|
+
export type StorageMetricsObserver = (event: StorageMetricsEvent) => void;
|
|
34
|
+
export type StorageMetricSummary = {
|
|
35
|
+
count: number;
|
|
36
|
+
totalDurationMs: number;
|
|
37
|
+
avgDurationMs: number;
|
|
38
|
+
maxDurationMs: number;
|
|
39
|
+
};
|
|
40
|
+
export type WebSecureStorageBackend = {
|
|
41
|
+
getItem: (key: string) => string | null;
|
|
42
|
+
setItem: (key: string, value: string) => void;
|
|
43
|
+
removeItem: (key: string) => void;
|
|
44
|
+
clear: () => void;
|
|
45
|
+
getAllKeys: () => string[];
|
|
46
|
+
};
|
|
22
47
|
|
|
23
48
|
export type MigrationContext = {
|
|
24
49
|
scope: StorageScope;
|
|
@@ -52,11 +77,25 @@ type RawBatchPathItem = {
|
|
|
52
77
|
_secureAccessControl?: AccessControl;
|
|
53
78
|
};
|
|
54
79
|
|
|
55
|
-
function asInternal(item: StorageItem<
|
|
56
|
-
return item as
|
|
80
|
+
function asInternal<T>(item: StorageItem<T>): StorageItemInternal<T> {
|
|
81
|
+
return item as StorageItemInternal<T>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function isUpdater<T>(
|
|
85
|
+
valueOrFn: T | ((prev: T) => T),
|
|
86
|
+
): valueOrFn is (prev: T) => T {
|
|
87
|
+
return typeof valueOrFn === "function";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function typedKeys<K extends string, V>(record: Record<K, V>): K[] {
|
|
91
|
+
return Object.keys(record) as K[];
|
|
57
92
|
}
|
|
58
93
|
type NonMemoryScope = StorageScope.Disk | StorageScope.Secure;
|
|
59
|
-
type PendingSecureWrite = {
|
|
94
|
+
type PendingSecureWrite = {
|
|
95
|
+
key: string;
|
|
96
|
+
value: string | undefined;
|
|
97
|
+
accessControl?: AccessControl;
|
|
98
|
+
};
|
|
60
99
|
type BrowserStorageLike = {
|
|
61
100
|
setItem: (key: string, value: string) => void;
|
|
62
101
|
getItem: (key: string) => string | null;
|
|
@@ -84,17 +123,21 @@ export interface Storage {
|
|
|
84
123
|
clear(scope: number): void;
|
|
85
124
|
has(key: string, scope: number): boolean;
|
|
86
125
|
getAllKeys(scope: number): string[];
|
|
126
|
+
getKeysByPrefix(prefix: string, scope: number): string[];
|
|
87
127
|
size(scope: number): number;
|
|
88
128
|
setBatch(keys: string[], values: string[], scope: number): void;
|
|
89
129
|
getBatch(keys: string[], scope: number): (string | undefined)[];
|
|
90
130
|
removeBatch(keys: string[], scope: number): void;
|
|
131
|
+
removeByPrefix(prefix: string, scope: number): void;
|
|
91
132
|
addOnChange(
|
|
92
133
|
scope: number,
|
|
93
134
|
callback: (key: string, value: string | undefined) => void,
|
|
94
135
|
): () => void;
|
|
95
136
|
setSecureAccessControl(level: number): void;
|
|
137
|
+
setSecureWritesAsync(enabled: boolean): void;
|
|
96
138
|
setKeychainAccessGroup(group: string): void;
|
|
97
139
|
setSecureBiometric(key: string, value: string): void;
|
|
140
|
+
setSecureBiometricWithLevel(key: string, value: string, level: number): void;
|
|
98
141
|
getSecureBiometric(key: string): string | undefined;
|
|
99
142
|
deleteSecureBiometric(key: string): void;
|
|
100
143
|
hasSecureBiometric(key: string): boolean;
|
|
@@ -113,18 +156,100 @@ const scopedRawCache = new Map<NonMemoryScope, Map<string, string | undefined>>(
|
|
|
113
156
|
[StorageScope.Secure, new Map()],
|
|
114
157
|
],
|
|
115
158
|
);
|
|
159
|
+
const webScopeKeyIndex = new Map<NonMemoryScope, Set<string>>([
|
|
160
|
+
[StorageScope.Disk, new Set()],
|
|
161
|
+
[StorageScope.Secure, new Set()],
|
|
162
|
+
]);
|
|
163
|
+
const hydratedWebScopeKeyIndex = new Set<NonMemoryScope>();
|
|
116
164
|
const pendingSecureWrites = new Map<string, PendingSecureWrite>();
|
|
117
165
|
let secureFlushScheduled = false;
|
|
118
166
|
const SECURE_WEB_PREFIX = "__secure_";
|
|
119
167
|
const BIOMETRIC_WEB_PREFIX = "__bio_";
|
|
120
168
|
let hasWarnedAboutWebBiometricFallback = false;
|
|
169
|
+
let hasWebStorageEventSubscription = false;
|
|
170
|
+
let metricsObserver: StorageMetricsObserver | undefined;
|
|
171
|
+
const metricsCounters = new Map<
|
|
172
|
+
string,
|
|
173
|
+
{ count: number; totalDurationMs: number; maxDurationMs: number }
|
|
174
|
+
>();
|
|
175
|
+
|
|
176
|
+
function recordMetric(
|
|
177
|
+
operation: string,
|
|
178
|
+
scope: StorageScope,
|
|
179
|
+
durationMs: number,
|
|
180
|
+
keysCount = 1,
|
|
181
|
+
): void {
|
|
182
|
+
const existing = metricsCounters.get(operation);
|
|
183
|
+
if (!existing) {
|
|
184
|
+
metricsCounters.set(operation, {
|
|
185
|
+
count: 1,
|
|
186
|
+
totalDurationMs: durationMs,
|
|
187
|
+
maxDurationMs: durationMs,
|
|
188
|
+
});
|
|
189
|
+
} else {
|
|
190
|
+
existing.count += 1;
|
|
191
|
+
existing.totalDurationMs += durationMs;
|
|
192
|
+
existing.maxDurationMs = Math.max(existing.maxDurationMs, durationMs);
|
|
193
|
+
}
|
|
194
|
+
metricsObserver?.({ operation, scope, durationMs, keysCount });
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function measureOperation<T>(
|
|
198
|
+
operation: string,
|
|
199
|
+
scope: StorageScope,
|
|
200
|
+
fn: () => T,
|
|
201
|
+
keysCount = 1,
|
|
202
|
+
): T {
|
|
203
|
+
const start = Date.now();
|
|
204
|
+
try {
|
|
205
|
+
return fn();
|
|
206
|
+
} finally {
|
|
207
|
+
recordMetric(operation, scope, Date.now() - start, keysCount);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function createLocalStorageWebSecureBackend(): WebSecureStorageBackend {
|
|
212
|
+
return {
|
|
213
|
+
getItem: (key) => globalThis.localStorage?.getItem(key) ?? null,
|
|
214
|
+
setItem: (key, value) => globalThis.localStorage?.setItem(key, value),
|
|
215
|
+
removeItem: (key) => globalThis.localStorage?.removeItem(key),
|
|
216
|
+
clear: () => globalThis.localStorage?.clear(),
|
|
217
|
+
getAllKeys: () => {
|
|
218
|
+
const storage = globalThis.localStorage;
|
|
219
|
+
if (!storage) return [];
|
|
220
|
+
const keys: string[] = [];
|
|
221
|
+
for (let index = 0; index < storage.length; index += 1) {
|
|
222
|
+
const key = storage.key(index);
|
|
223
|
+
if (key) {
|
|
224
|
+
keys.push(key);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return keys;
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
let webSecureStorageBackend: WebSecureStorageBackend | undefined =
|
|
233
|
+
createLocalStorageWebSecureBackend();
|
|
121
234
|
|
|
122
235
|
function getBrowserStorage(scope: number): BrowserStorageLike | undefined {
|
|
123
236
|
if (scope === StorageScope.Disk) {
|
|
124
237
|
return globalThis.localStorage;
|
|
125
238
|
}
|
|
126
239
|
if (scope === StorageScope.Secure) {
|
|
127
|
-
|
|
240
|
+
if (!webSecureStorageBackend) {
|
|
241
|
+
return undefined;
|
|
242
|
+
}
|
|
243
|
+
return {
|
|
244
|
+
setItem: (key, value) => webSecureStorageBackend?.setItem(key, value),
|
|
245
|
+
getItem: (key) => webSecureStorageBackend?.getItem(key) ?? null,
|
|
246
|
+
removeItem: (key) => webSecureStorageBackend?.removeItem(key),
|
|
247
|
+
clear: () => webSecureStorageBackend?.clear(),
|
|
248
|
+
key: (index) => webSecureStorageBackend?.getAllKeys()[index] ?? null,
|
|
249
|
+
get length() {
|
|
250
|
+
return webSecureStorageBackend?.getAllKeys().length ?? 0;
|
|
251
|
+
},
|
|
252
|
+
};
|
|
128
253
|
}
|
|
129
254
|
return undefined;
|
|
130
255
|
}
|
|
@@ -145,6 +270,119 @@ function fromBiometricStorageKey(key: string): string {
|
|
|
145
270
|
return key.slice(BIOMETRIC_WEB_PREFIX.length);
|
|
146
271
|
}
|
|
147
272
|
|
|
273
|
+
function getWebScopeKeyIndex(scope: NonMemoryScope): Set<string> {
|
|
274
|
+
return webScopeKeyIndex.get(scope)!;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function hydrateWebScopeKeyIndex(scope: NonMemoryScope): void {
|
|
278
|
+
if (hydratedWebScopeKeyIndex.has(scope)) {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const storage = getBrowserStorage(scope);
|
|
283
|
+
const keyIndex = getWebScopeKeyIndex(scope);
|
|
284
|
+
keyIndex.clear();
|
|
285
|
+
if (storage) {
|
|
286
|
+
for (let index = 0; index < storage.length; index += 1) {
|
|
287
|
+
const key = storage.key(index);
|
|
288
|
+
if (!key) {
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
if (scope === StorageScope.Disk) {
|
|
292
|
+
if (
|
|
293
|
+
!key.startsWith(SECURE_WEB_PREFIX) &&
|
|
294
|
+
!key.startsWith(BIOMETRIC_WEB_PREFIX)
|
|
295
|
+
) {
|
|
296
|
+
keyIndex.add(key);
|
|
297
|
+
}
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (key.startsWith(SECURE_WEB_PREFIX)) {
|
|
302
|
+
keyIndex.add(fromSecureStorageKey(key));
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
if (key.startsWith(BIOMETRIC_WEB_PREFIX)) {
|
|
306
|
+
keyIndex.add(fromBiometricStorageKey(key));
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
hydratedWebScopeKeyIndex.add(scope);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function ensureWebScopeKeyIndex(scope: NonMemoryScope): Set<string> {
|
|
314
|
+
hydrateWebScopeKeyIndex(scope);
|
|
315
|
+
return getWebScopeKeyIndex(scope);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function handleWebStorageEvent(event: StorageEvent): void {
|
|
319
|
+
const key = event.key;
|
|
320
|
+
if (key === null) {
|
|
321
|
+
clearScopeRawCache(StorageScope.Disk);
|
|
322
|
+
clearScopeRawCache(StorageScope.Secure);
|
|
323
|
+
ensureWebScopeKeyIndex(StorageScope.Disk).clear();
|
|
324
|
+
ensureWebScopeKeyIndex(StorageScope.Secure).clear();
|
|
325
|
+
notifyAllListeners(getScopedListeners(StorageScope.Disk));
|
|
326
|
+
notifyAllListeners(getScopedListeners(StorageScope.Secure));
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (key.startsWith(SECURE_WEB_PREFIX)) {
|
|
331
|
+
const plainKey = fromSecureStorageKey(key);
|
|
332
|
+
if (event.newValue === null) {
|
|
333
|
+
ensureWebScopeKeyIndex(StorageScope.Secure).delete(plainKey);
|
|
334
|
+
cacheRawValue(StorageScope.Secure, plainKey, undefined);
|
|
335
|
+
} else {
|
|
336
|
+
ensureWebScopeKeyIndex(StorageScope.Secure).add(plainKey);
|
|
337
|
+
cacheRawValue(StorageScope.Secure, plainKey, event.newValue);
|
|
338
|
+
}
|
|
339
|
+
notifyKeyListeners(getScopedListeners(StorageScope.Secure), plainKey);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (key.startsWith(BIOMETRIC_WEB_PREFIX)) {
|
|
344
|
+
const plainKey = fromBiometricStorageKey(key);
|
|
345
|
+
if (event.newValue === null) {
|
|
346
|
+
if (
|
|
347
|
+
getBrowserStorage(StorageScope.Secure)?.getItem(
|
|
348
|
+
toSecureStorageKey(plainKey),
|
|
349
|
+
) === null
|
|
350
|
+
) {
|
|
351
|
+
ensureWebScopeKeyIndex(StorageScope.Secure).delete(plainKey);
|
|
352
|
+
}
|
|
353
|
+
cacheRawValue(StorageScope.Secure, plainKey, undefined);
|
|
354
|
+
} else {
|
|
355
|
+
ensureWebScopeKeyIndex(StorageScope.Secure).add(plainKey);
|
|
356
|
+
cacheRawValue(StorageScope.Secure, plainKey, event.newValue);
|
|
357
|
+
}
|
|
358
|
+
notifyKeyListeners(getScopedListeners(StorageScope.Secure), plainKey);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (event.newValue === null) {
|
|
363
|
+
ensureWebScopeKeyIndex(StorageScope.Disk).delete(key);
|
|
364
|
+
cacheRawValue(StorageScope.Disk, key, undefined);
|
|
365
|
+
} else {
|
|
366
|
+
ensureWebScopeKeyIndex(StorageScope.Disk).add(key);
|
|
367
|
+
cacheRawValue(StorageScope.Disk, key, event.newValue);
|
|
368
|
+
}
|
|
369
|
+
notifyKeyListeners(getScopedListeners(StorageScope.Disk), key);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function ensureWebStorageEventSubscription(): void {
|
|
373
|
+
if (hasWebStorageEventSubscription) {
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
if (
|
|
377
|
+
typeof window === "undefined" ||
|
|
378
|
+
typeof window.addEventListener !== "function"
|
|
379
|
+
) {
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
window.addEventListener("storage", handleWebStorageEvent);
|
|
383
|
+
hasWebStorageEventSubscription = true;
|
|
384
|
+
}
|
|
385
|
+
|
|
148
386
|
function getScopedListeners(scope: NonMemoryScope): KeyListenerRegistry {
|
|
149
387
|
return webScopeListeners.get(scope)!;
|
|
150
388
|
}
|
|
@@ -234,29 +472,46 @@ function flushSecureWrites(): void {
|
|
|
234
472
|
const writes = Array.from(pendingSecureWrites.values());
|
|
235
473
|
pendingSecureWrites.clear();
|
|
236
474
|
|
|
237
|
-
const
|
|
238
|
-
|
|
475
|
+
const groupedSetWrites = new Map<
|
|
476
|
+
AccessControl,
|
|
477
|
+
{ keys: string[]; values: string[] }
|
|
478
|
+
>();
|
|
239
479
|
const keysToRemove: string[] = [];
|
|
240
480
|
|
|
241
|
-
writes.forEach(({ key, value }) => {
|
|
481
|
+
writes.forEach(({ key, value, accessControl }) => {
|
|
242
482
|
if (value === undefined) {
|
|
243
483
|
keysToRemove.push(key);
|
|
244
484
|
} else {
|
|
245
|
-
|
|
246
|
-
|
|
485
|
+
const resolvedAccessControl = accessControl ?? AccessControl.WhenUnlocked;
|
|
486
|
+
const existingGroup = groupedSetWrites.get(resolvedAccessControl);
|
|
487
|
+
const group = existingGroup ?? { keys: [], values: [] };
|
|
488
|
+
group.keys.push(key);
|
|
489
|
+
group.values.push(value);
|
|
490
|
+
if (!existingGroup) {
|
|
491
|
+
groupedSetWrites.set(resolvedAccessControl, group);
|
|
492
|
+
}
|
|
247
493
|
}
|
|
248
494
|
});
|
|
249
495
|
|
|
250
|
-
|
|
251
|
-
WebStorage.
|
|
252
|
-
|
|
496
|
+
groupedSetWrites.forEach((group, accessControl) => {
|
|
497
|
+
WebStorage.setSecureAccessControl(accessControl);
|
|
498
|
+
WebStorage.setBatch(group.keys, group.values, StorageScope.Secure);
|
|
499
|
+
});
|
|
253
500
|
if (keysToRemove.length > 0) {
|
|
254
501
|
WebStorage.removeBatch(keysToRemove, StorageScope.Secure);
|
|
255
502
|
}
|
|
256
503
|
}
|
|
257
504
|
|
|
258
|
-
function scheduleSecureWrite(
|
|
259
|
-
|
|
505
|
+
function scheduleSecureWrite(
|
|
506
|
+
key: string,
|
|
507
|
+
value: string | undefined,
|
|
508
|
+
accessControl?: AccessControl,
|
|
509
|
+
): void {
|
|
510
|
+
const pendingWrite: PendingSecureWrite = { key, value };
|
|
511
|
+
if (accessControl !== undefined) {
|
|
512
|
+
pendingWrite.accessControl = accessControl;
|
|
513
|
+
}
|
|
514
|
+
pendingSecureWrites.set(key, pendingWrite);
|
|
260
515
|
if (secureFlushScheduled) {
|
|
261
516
|
return;
|
|
262
517
|
}
|
|
@@ -277,6 +532,7 @@ const WebStorage: Storage = {
|
|
|
277
532
|
scope === StorageScope.Secure ? toSecureStorageKey(key) : key;
|
|
278
533
|
storage.setItem(storageKey, value);
|
|
279
534
|
if (scope === StorageScope.Disk || scope === StorageScope.Secure) {
|
|
535
|
+
ensureWebScopeKeyIndex(scope).add(key);
|
|
280
536
|
notifyKeyListeners(getScopedListeners(scope), key);
|
|
281
537
|
}
|
|
282
538
|
},
|
|
@@ -298,6 +554,7 @@ const WebStorage: Storage = {
|
|
|
298
554
|
storage.removeItem(key);
|
|
299
555
|
}
|
|
300
556
|
if (scope === StorageScope.Disk || scope === StorageScope.Secure) {
|
|
557
|
+
ensureWebScopeKeyIndex(scope).delete(key);
|
|
301
558
|
notifyKeyListeners(getScopedListeners(scope), key);
|
|
302
559
|
}
|
|
303
560
|
},
|
|
@@ -335,6 +592,7 @@ const WebStorage: Storage = {
|
|
|
335
592
|
storage.clear();
|
|
336
593
|
}
|
|
337
594
|
if (scope === StorageScope.Disk || scope === StorageScope.Secure) {
|
|
595
|
+
ensureWebScopeKeyIndex(scope).clear();
|
|
338
596
|
notifyAllListeners(getScopedListeners(scope));
|
|
339
597
|
}
|
|
340
598
|
},
|
|
@@ -345,11 +603,17 @@ const WebStorage: Storage = {
|
|
|
345
603
|
}
|
|
346
604
|
|
|
347
605
|
keys.forEach((key, index) => {
|
|
606
|
+
const value = values[index];
|
|
607
|
+
if (value === undefined) {
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
348
610
|
const storageKey =
|
|
349
611
|
scope === StorageScope.Secure ? toSecureStorageKey(key) : key;
|
|
350
|
-
storage.setItem(storageKey,
|
|
612
|
+
storage.setItem(storageKey, value);
|
|
351
613
|
});
|
|
352
614
|
if (scope === StorageScope.Disk || scope === StorageScope.Secure) {
|
|
615
|
+
const keyIndex = ensureWebScopeKeyIndex(scope);
|
|
616
|
+
keys.forEach((key) => keyIndex.add(key));
|
|
353
617
|
const listeners = getScopedListeners(scope);
|
|
354
618
|
keys.forEach((key) => notifyKeyListeners(listeners, key));
|
|
355
619
|
}
|
|
@@ -363,9 +627,41 @@ const WebStorage: Storage = {
|
|
|
363
627
|
});
|
|
364
628
|
},
|
|
365
629
|
removeBatch: (keys: string[], scope: number) => {
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
630
|
+
const storage = getBrowserStorage(scope);
|
|
631
|
+
if (!storage) {
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
if (scope === StorageScope.Secure) {
|
|
636
|
+
keys.forEach((key) => {
|
|
637
|
+
storage.removeItem(toSecureStorageKey(key));
|
|
638
|
+
storage.removeItem(toBiometricStorageKey(key));
|
|
639
|
+
});
|
|
640
|
+
} else {
|
|
641
|
+
keys.forEach((key) => {
|
|
642
|
+
storage.removeItem(key);
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
if (scope === StorageScope.Disk || scope === StorageScope.Secure) {
|
|
647
|
+
const keyIndex = ensureWebScopeKeyIndex(scope);
|
|
648
|
+
keys.forEach((key) => keyIndex.delete(key));
|
|
649
|
+
const listeners = getScopedListeners(scope);
|
|
650
|
+
keys.forEach((key) => notifyKeyListeners(listeners, key));
|
|
651
|
+
}
|
|
652
|
+
},
|
|
653
|
+
removeByPrefix: (prefix: string, scope: number) => {
|
|
654
|
+
if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
const keyIndex = ensureWebScopeKeyIndex(scope);
|
|
659
|
+
const keys = Array.from(keyIndex).filter((key) => key.startsWith(prefix));
|
|
660
|
+
if (keys.length === 0) {
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
WebStorage.removeBatch(keys, scope);
|
|
369
665
|
},
|
|
370
666
|
addOnChange: (
|
|
371
667
|
_scope: number,
|
|
@@ -384,38 +680,36 @@ const WebStorage: Storage = {
|
|
|
384
680
|
return storage?.getItem(key) !== null;
|
|
385
681
|
},
|
|
386
682
|
getAllKeys: (scope: number) => {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
if (scope === StorageScope.Secure) {
|
|
396
|
-
if (k.startsWith(SECURE_WEB_PREFIX)) {
|
|
397
|
-
keys.add(fromSecureStorageKey(k));
|
|
398
|
-
} else if (k.startsWith(BIOMETRIC_WEB_PREFIX)) {
|
|
399
|
-
keys.add(fromBiometricStorageKey(k));
|
|
400
|
-
}
|
|
401
|
-
continue;
|
|
402
|
-
}
|
|
403
|
-
if (
|
|
404
|
-
k.startsWith(SECURE_WEB_PREFIX) ||
|
|
405
|
-
k.startsWith(BIOMETRIC_WEB_PREFIX)
|
|
406
|
-
) {
|
|
407
|
-
continue;
|
|
408
|
-
}
|
|
409
|
-
keys.add(k);
|
|
683
|
+
if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
|
|
684
|
+
return [];
|
|
685
|
+
}
|
|
686
|
+
return Array.from(ensureWebScopeKeyIndex(scope));
|
|
687
|
+
},
|
|
688
|
+
getKeysByPrefix: (prefix: string, scope: number) => {
|
|
689
|
+
if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
|
|
690
|
+
return [];
|
|
410
691
|
}
|
|
411
|
-
return Array.from(
|
|
692
|
+
return Array.from(ensureWebScopeKeyIndex(scope)).filter((key) =>
|
|
693
|
+
key.startsWith(prefix),
|
|
694
|
+
);
|
|
412
695
|
},
|
|
413
696
|
size: (scope: number) => {
|
|
414
|
-
|
|
697
|
+
if (scope === StorageScope.Disk || scope === StorageScope.Secure) {
|
|
698
|
+
return ensureWebScopeKeyIndex(scope).size;
|
|
699
|
+
}
|
|
700
|
+
return 0;
|
|
415
701
|
},
|
|
416
702
|
setSecureAccessControl: () => {},
|
|
703
|
+
setSecureWritesAsync: (_enabled: boolean) => {},
|
|
417
704
|
setKeychainAccessGroup: () => {},
|
|
418
705
|
setSecureBiometric: (key: string, value: string) => {
|
|
706
|
+
WebStorage.setSecureBiometricWithLevel(
|
|
707
|
+
key,
|
|
708
|
+
value,
|
|
709
|
+
BiometricLevel.BiometryOnly,
|
|
710
|
+
);
|
|
711
|
+
},
|
|
712
|
+
setSecureBiometricWithLevel: (key: string, value: string, _level: number) => {
|
|
419
713
|
if (
|
|
420
714
|
typeof __DEV__ !== "undefined" &&
|
|
421
715
|
__DEV__ &&
|
|
@@ -426,25 +720,37 @@ const WebStorage: Storage = {
|
|
|
426
720
|
"[NitroStorage] Biometric storage is not supported on web. Using localStorage.",
|
|
427
721
|
);
|
|
428
722
|
}
|
|
429
|
-
|
|
723
|
+
getBrowserStorage(StorageScope.Secure)?.setItem(
|
|
724
|
+
toBiometricStorageKey(key),
|
|
725
|
+
value,
|
|
726
|
+
);
|
|
727
|
+
ensureWebScopeKeyIndex(StorageScope.Secure).add(key);
|
|
430
728
|
notifyKeyListeners(getScopedListeners(StorageScope.Secure), key);
|
|
431
729
|
},
|
|
432
730
|
getSecureBiometric: (key: string) => {
|
|
433
731
|
return (
|
|
434
|
-
|
|
732
|
+
getBrowserStorage(StorageScope.Secure)?.getItem(
|
|
733
|
+
toBiometricStorageKey(key),
|
|
734
|
+
) ?? undefined
|
|
435
735
|
);
|
|
436
736
|
},
|
|
437
737
|
deleteSecureBiometric: (key: string) => {
|
|
438
|
-
|
|
738
|
+
const storage = getBrowserStorage(StorageScope.Secure);
|
|
739
|
+
storage?.removeItem(toBiometricStorageKey(key));
|
|
740
|
+
if (storage?.getItem(toSecureStorageKey(key)) === null) {
|
|
741
|
+
ensureWebScopeKeyIndex(StorageScope.Secure).delete(key);
|
|
742
|
+
}
|
|
439
743
|
notifyKeyListeners(getScopedListeners(StorageScope.Secure), key);
|
|
440
744
|
},
|
|
441
745
|
hasSecureBiometric: (key: string) => {
|
|
442
746
|
return (
|
|
443
|
-
|
|
747
|
+
getBrowserStorage(StorageScope.Secure)?.getItem(
|
|
748
|
+
toBiometricStorageKey(key),
|
|
749
|
+
) !== null
|
|
444
750
|
);
|
|
445
751
|
},
|
|
446
752
|
clearSecureBiometric: () => {
|
|
447
|
-
const storage =
|
|
753
|
+
const storage = getBrowserStorage(StorageScope.Secure);
|
|
448
754
|
if (!storage) return;
|
|
449
755
|
const keysToNotify: string[] = [];
|
|
450
756
|
const toRemove: string[] = [];
|
|
@@ -456,6 +762,12 @@ const WebStorage: Storage = {
|
|
|
456
762
|
}
|
|
457
763
|
}
|
|
458
764
|
toRemove.forEach((k) => storage.removeItem(k));
|
|
765
|
+
const keyIndex = ensureWebScopeKeyIndex(StorageScope.Secure);
|
|
766
|
+
keysToNotify.forEach((key) => {
|
|
767
|
+
if (storage.getItem(toSecureStorageKey(key)) === null) {
|
|
768
|
+
keyIndex.delete(key);
|
|
769
|
+
}
|
|
770
|
+
});
|
|
459
771
|
const listeners = getScopedListeners(StorageScope.Secure);
|
|
460
772
|
keysToNotify.forEach((key) => notifyKeyListeners(listeners, key));
|
|
461
773
|
},
|
|
@@ -525,90 +837,190 @@ function writeMigrationVersion(scope: StorageScope, version: number): void {
|
|
|
525
837
|
|
|
526
838
|
export const storage = {
|
|
527
839
|
clear: (scope: StorageScope) => {
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
840
|
+
measureOperation("storage:clear", scope, () => {
|
|
841
|
+
if (scope === StorageScope.Memory) {
|
|
842
|
+
memoryStore.clear();
|
|
843
|
+
notifyAllListeners(memoryListeners);
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
533
846
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
847
|
+
if (scope === StorageScope.Secure) {
|
|
848
|
+
flushSecureWrites();
|
|
849
|
+
pendingSecureWrites.clear();
|
|
850
|
+
}
|
|
538
851
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
WebStorage.clearSecureBiometric();
|
|
543
|
-
}
|
|
852
|
+
clearScopeRawCache(scope);
|
|
853
|
+
WebStorage.clear(scope);
|
|
854
|
+
});
|
|
544
855
|
},
|
|
545
856
|
clearAll: () => {
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
857
|
+
measureOperation(
|
|
858
|
+
"storage:clearAll",
|
|
859
|
+
StorageScope.Memory,
|
|
860
|
+
() => {
|
|
861
|
+
storage.clear(StorageScope.Memory);
|
|
862
|
+
storage.clear(StorageScope.Disk);
|
|
863
|
+
storage.clear(StorageScope.Secure);
|
|
864
|
+
},
|
|
865
|
+
3,
|
|
866
|
+
);
|
|
549
867
|
},
|
|
550
868
|
clearNamespace: (namespace: string, scope: StorageScope) => {
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
869
|
+
measureOperation("storage:clearNamespace", scope, () => {
|
|
870
|
+
assertValidScope(scope);
|
|
871
|
+
if (scope === StorageScope.Memory) {
|
|
872
|
+
for (const key of memoryStore.keys()) {
|
|
873
|
+
if (isNamespaced(key, namespace)) {
|
|
874
|
+
memoryStore.delete(key);
|
|
875
|
+
}
|
|
556
876
|
}
|
|
877
|
+
notifyAllListeners(memoryListeners);
|
|
878
|
+
return;
|
|
557
879
|
}
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
}
|
|
561
|
-
if (scope === StorageScope.Secure) {
|
|
562
|
-
flushSecureWrites();
|
|
563
|
-
}
|
|
564
|
-
const keys = WebStorage.getAllKeys(scope);
|
|
565
|
-
const namespacedKeys = keys.filter((k) => isNamespaced(k, namespace));
|
|
566
|
-
if (namespacedKeys.length > 0) {
|
|
567
|
-
WebStorage.removeBatch(namespacedKeys, scope);
|
|
568
|
-
namespacedKeys.forEach((k) => cacheRawValue(scope, k, undefined));
|
|
880
|
+
|
|
881
|
+
const keyPrefix = prefixKey(namespace, "");
|
|
569
882
|
if (scope === StorageScope.Secure) {
|
|
570
|
-
|
|
883
|
+
flushSecureWrites();
|
|
571
884
|
}
|
|
572
|
-
|
|
885
|
+
clearScopeRawCache(scope);
|
|
886
|
+
WebStorage.removeByPrefix(keyPrefix, scope);
|
|
887
|
+
});
|
|
573
888
|
},
|
|
574
889
|
clearBiometric: () => {
|
|
575
|
-
|
|
890
|
+
measureOperation("storage:clearBiometric", StorageScope.Secure, () => {
|
|
891
|
+
WebStorage.clearSecureBiometric();
|
|
892
|
+
});
|
|
576
893
|
},
|
|
577
894
|
has: (key: string, scope: StorageScope): boolean => {
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
895
|
+
return measureOperation("storage:has", scope, () => {
|
|
896
|
+
assertValidScope(scope);
|
|
897
|
+
if (scope === StorageScope.Memory) return memoryStore.has(key);
|
|
898
|
+
return WebStorage.has(key, scope);
|
|
899
|
+
});
|
|
581
900
|
},
|
|
582
901
|
getAllKeys: (scope: StorageScope): string[] => {
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
902
|
+
return measureOperation("storage:getAllKeys", scope, () => {
|
|
903
|
+
assertValidScope(scope);
|
|
904
|
+
if (scope === StorageScope.Memory) return Array.from(memoryStore.keys());
|
|
905
|
+
return WebStorage.getAllKeys(scope);
|
|
906
|
+
});
|
|
907
|
+
},
|
|
908
|
+
getKeysByPrefix: (prefix: string, scope: StorageScope): string[] => {
|
|
909
|
+
return measureOperation("storage:getKeysByPrefix", scope, () => {
|
|
910
|
+
assertValidScope(scope);
|
|
911
|
+
if (scope === StorageScope.Memory) {
|
|
912
|
+
return Array.from(memoryStore.keys()).filter((key) =>
|
|
913
|
+
key.startsWith(prefix),
|
|
914
|
+
);
|
|
915
|
+
}
|
|
916
|
+
return WebStorage.getKeysByPrefix(prefix, scope);
|
|
917
|
+
});
|
|
918
|
+
},
|
|
919
|
+
getByPrefix: (
|
|
920
|
+
prefix: string,
|
|
921
|
+
scope: StorageScope,
|
|
922
|
+
): Record<string, string> => {
|
|
923
|
+
return measureOperation("storage:getByPrefix", scope, () => {
|
|
924
|
+
const result: Record<string, string> = {};
|
|
925
|
+
const keys = storage.getKeysByPrefix(prefix, scope);
|
|
926
|
+
if (keys.length === 0) {
|
|
927
|
+
return result;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
if (scope === StorageScope.Memory) {
|
|
931
|
+
keys.forEach((key) => {
|
|
932
|
+
const value = memoryStore.get(key);
|
|
933
|
+
if (typeof value === "string") {
|
|
934
|
+
result[key] = value;
|
|
935
|
+
}
|
|
936
|
+
});
|
|
937
|
+
return result;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
const values = WebStorage.getBatch(keys, scope);
|
|
941
|
+
keys.forEach((key, index) => {
|
|
942
|
+
const value = values[index];
|
|
943
|
+
if (value !== undefined) {
|
|
944
|
+
result[key] = value;
|
|
945
|
+
}
|
|
946
|
+
});
|
|
947
|
+
return result;
|
|
948
|
+
});
|
|
586
949
|
},
|
|
587
950
|
getAll: (scope: StorageScope): Record<string, string> => {
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
951
|
+
return measureOperation("storage:getAll", scope, () => {
|
|
952
|
+
assertValidScope(scope);
|
|
953
|
+
const result: Record<string, string> = {};
|
|
954
|
+
if (scope === StorageScope.Memory) {
|
|
955
|
+
memoryStore.forEach((value, key) => {
|
|
956
|
+
if (typeof value === "string") result[key] = value;
|
|
957
|
+
});
|
|
958
|
+
return result;
|
|
959
|
+
}
|
|
960
|
+
const keys = WebStorage.getAllKeys(scope);
|
|
961
|
+
keys.forEach((key) => {
|
|
962
|
+
const val = WebStorage.get(key, scope);
|
|
963
|
+
if (val !== undefined) result[key] = val;
|
|
593
964
|
});
|
|
594
965
|
return result;
|
|
595
|
-
}
|
|
596
|
-
const keys = WebStorage.getAllKeys(scope);
|
|
597
|
-
keys.forEach((key) => {
|
|
598
|
-
const val = WebStorage.get(key, scope);
|
|
599
|
-
if (val !== undefined) result[key] = val;
|
|
600
966
|
});
|
|
601
|
-
return result;
|
|
602
967
|
},
|
|
603
968
|
size: (scope: StorageScope): number => {
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
969
|
+
return measureOperation("storage:size", scope, () => {
|
|
970
|
+
assertValidScope(scope);
|
|
971
|
+
if (scope === StorageScope.Memory) return memoryStore.size;
|
|
972
|
+
return WebStorage.size(scope);
|
|
973
|
+
});
|
|
974
|
+
},
|
|
975
|
+
setAccessControl: (_level: AccessControl) => {
|
|
976
|
+
recordMetric("storage:setAccessControl", StorageScope.Secure, 0);
|
|
977
|
+
},
|
|
978
|
+
setSecureWritesAsync: (_enabled: boolean) => {
|
|
979
|
+
recordMetric("storage:setSecureWritesAsync", StorageScope.Secure, 0);
|
|
980
|
+
},
|
|
981
|
+
flushSecureWrites: () => {
|
|
982
|
+
measureOperation("storage:flushSecureWrites", StorageScope.Secure, () => {
|
|
983
|
+
flushSecureWrites();
|
|
984
|
+
});
|
|
985
|
+
},
|
|
986
|
+
setKeychainAccessGroup: (_group: string) => {
|
|
987
|
+
recordMetric("storage:setKeychainAccessGroup", StorageScope.Secure, 0);
|
|
988
|
+
},
|
|
989
|
+
setMetricsObserver: (observer?: StorageMetricsObserver) => {
|
|
990
|
+
metricsObserver = observer;
|
|
991
|
+
},
|
|
992
|
+
getMetricsSnapshot: (): Record<string, StorageMetricSummary> => {
|
|
993
|
+
const snapshot: Record<string, StorageMetricSummary> = {};
|
|
994
|
+
metricsCounters.forEach((value, key) => {
|
|
995
|
+
snapshot[key] = {
|
|
996
|
+
count: value.count,
|
|
997
|
+
totalDurationMs: value.totalDurationMs,
|
|
998
|
+
avgDurationMs:
|
|
999
|
+
value.count === 0 ? 0 : value.totalDurationMs / value.count,
|
|
1000
|
+
maxDurationMs: value.maxDurationMs,
|
|
1001
|
+
};
|
|
1002
|
+
});
|
|
1003
|
+
return snapshot;
|
|
1004
|
+
},
|
|
1005
|
+
resetMetrics: () => {
|
|
1006
|
+
metricsCounters.clear();
|
|
607
1007
|
},
|
|
608
|
-
setAccessControl: (_level: AccessControl) => {},
|
|
609
|
-
setKeychainAccessGroup: (_group: string) => {},
|
|
610
1008
|
};
|
|
611
1009
|
|
|
1010
|
+
export function setWebSecureStorageBackend(
|
|
1011
|
+
backend?: WebSecureStorageBackend,
|
|
1012
|
+
): void {
|
|
1013
|
+
webSecureStorageBackend = backend ?? createLocalStorageWebSecureBackend();
|
|
1014
|
+
hydratedWebScopeKeyIndex.delete(StorageScope.Secure);
|
|
1015
|
+
clearScopeRawCache(StorageScope.Secure);
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
export function getWebSecureStorageBackend():
|
|
1019
|
+
| WebSecureStorageBackend
|
|
1020
|
+
| undefined {
|
|
1021
|
+
return webSecureStorageBackend;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
612
1024
|
export interface StorageItemConfig<T> {
|
|
613
1025
|
key: string;
|
|
614
1026
|
scope: StorageScope;
|
|
@@ -623,12 +1035,18 @@ export interface StorageItemConfig<T> {
|
|
|
623
1035
|
coalesceSecureWrites?: boolean;
|
|
624
1036
|
namespace?: string;
|
|
625
1037
|
biometric?: boolean;
|
|
1038
|
+
biometricLevel?: BiometricLevel;
|
|
626
1039
|
accessControl?: AccessControl;
|
|
627
1040
|
}
|
|
628
1041
|
|
|
629
1042
|
export interface StorageItem<T> {
|
|
630
1043
|
get: () => T;
|
|
1044
|
+
getWithVersion: () => VersionedValue<T>;
|
|
631
1045
|
set: (value: T | ((prev: T) => T)) => void;
|
|
1046
|
+
setIfVersion: (
|
|
1047
|
+
version: StorageVersion,
|
|
1048
|
+
value: T | ((prev: T) => T),
|
|
1049
|
+
) => boolean;
|
|
632
1050
|
delete: () => void;
|
|
633
1051
|
has: () => boolean;
|
|
634
1052
|
subscribe: (callback: () => void) => () => void;
|
|
@@ -644,6 +1062,7 @@ type StorageItemInternal<T> = StorageItem<T> & {
|
|
|
644
1062
|
_hasExpiration: boolean;
|
|
645
1063
|
_readCacheEnabled: boolean;
|
|
646
1064
|
_isBiometric: boolean;
|
|
1065
|
+
_defaultValue: T;
|
|
647
1066
|
_secureAccessControl?: AccessControl;
|
|
648
1067
|
};
|
|
649
1068
|
|
|
@@ -656,6 +1075,14 @@ function canUseRawBatchPath(item: RawBatchPathItem): boolean {
|
|
|
656
1075
|
);
|
|
657
1076
|
}
|
|
658
1077
|
|
|
1078
|
+
function canUseSecureRawBatchPath(item: RawBatchPathItem): boolean {
|
|
1079
|
+
return (
|
|
1080
|
+
item._hasExpiration === false &&
|
|
1081
|
+
item._hasValidation === false &&
|
|
1082
|
+
item._isBiometric !== true
|
|
1083
|
+
);
|
|
1084
|
+
}
|
|
1085
|
+
|
|
659
1086
|
function defaultSerialize<T>(value: T): string {
|
|
660
1087
|
return serializeWithPrimitiveFastPath(value);
|
|
661
1088
|
}
|
|
@@ -671,8 +1098,14 @@ export function createStorageItem<T = undefined>(
|
|
|
671
1098
|
const serialize = config.serialize ?? defaultSerialize;
|
|
672
1099
|
const deserialize = config.deserialize ?? defaultDeserialize;
|
|
673
1100
|
const isMemory = config.scope === StorageScope.Memory;
|
|
674
|
-
const
|
|
675
|
-
config.
|
|
1101
|
+
const resolvedBiometricLevel =
|
|
1102
|
+
config.scope === StorageScope.Secure
|
|
1103
|
+
? (config.biometricLevel ??
|
|
1104
|
+
(config.biometric === true
|
|
1105
|
+
? BiometricLevel.BiometryOnly
|
|
1106
|
+
: BiometricLevel.None))
|
|
1107
|
+
: BiometricLevel.None;
|
|
1108
|
+
const isBiometric = resolvedBiometricLevel !== BiometricLevel.None;
|
|
676
1109
|
const secureAccessControl = config.accessControl;
|
|
677
1110
|
const validate = config.validate;
|
|
678
1111
|
const onValidationError = config.onValidationError;
|
|
@@ -685,8 +1118,8 @@ export function createStorageItem<T = undefined>(
|
|
|
685
1118
|
const coalesceSecureWrites =
|
|
686
1119
|
config.scope === StorageScope.Secure &&
|
|
687
1120
|
config.coalesceSecureWrites === true &&
|
|
688
|
-
!isBiometric
|
|
689
|
-
|
|
1121
|
+
!isBiometric;
|
|
1122
|
+
const defaultValue = config.defaultValue as T;
|
|
690
1123
|
const nonMemoryScope: NonMemoryScope | null =
|
|
691
1124
|
config.scope === StorageScope.Disk
|
|
692
1125
|
? StorageScope.Disk
|
|
@@ -703,11 +1136,13 @@ export function createStorageItem<T = undefined>(
|
|
|
703
1136
|
let lastRaw: unknown = undefined;
|
|
704
1137
|
let lastValue: T | undefined;
|
|
705
1138
|
let hasLastValue = false;
|
|
1139
|
+
let lastExpiresAt: number | null | undefined = undefined;
|
|
706
1140
|
|
|
707
1141
|
const invalidateParsedCache = () => {
|
|
708
1142
|
lastRaw = undefined;
|
|
709
1143
|
lastValue = undefined;
|
|
710
1144
|
hasLastValue = false;
|
|
1145
|
+
lastExpiresAt = undefined;
|
|
711
1146
|
};
|
|
712
1147
|
|
|
713
1148
|
const ensureSubscription = () => {
|
|
@@ -725,6 +1160,7 @@ export function createStorageItem<T = undefined>(
|
|
|
725
1160
|
return;
|
|
726
1161
|
}
|
|
727
1162
|
|
|
1163
|
+
ensureWebStorageEventSubscription();
|
|
728
1164
|
unsubscribe = addKeyListener(
|
|
729
1165
|
getScopedListeners(nonMemoryScope!),
|
|
730
1166
|
storageKey,
|
|
@@ -744,7 +1180,7 @@ export function createStorageItem<T = undefined>(
|
|
|
744
1180
|
return undefined;
|
|
745
1181
|
}
|
|
746
1182
|
}
|
|
747
|
-
return memoryStore.get(storageKey)
|
|
1183
|
+
return memoryStore.get(storageKey);
|
|
748
1184
|
}
|
|
749
1185
|
|
|
750
1186
|
if (
|
|
@@ -772,14 +1208,22 @@ export function createStorageItem<T = undefined>(
|
|
|
772
1208
|
|
|
773
1209
|
const writeStoredRaw = (rawValue: string): void => {
|
|
774
1210
|
if (isBiometric) {
|
|
775
|
-
WebStorage.
|
|
1211
|
+
WebStorage.setSecureBiometricWithLevel(
|
|
1212
|
+
storageKey,
|
|
1213
|
+
rawValue,
|
|
1214
|
+
resolvedBiometricLevel,
|
|
1215
|
+
);
|
|
776
1216
|
return;
|
|
777
1217
|
}
|
|
778
1218
|
|
|
779
1219
|
cacheRawValue(nonMemoryScope!, storageKey, rawValue);
|
|
780
1220
|
|
|
781
1221
|
if (coalesceSecureWrites) {
|
|
782
|
-
scheduleSecureWrite(
|
|
1222
|
+
scheduleSecureWrite(
|
|
1223
|
+
storageKey,
|
|
1224
|
+
rawValue,
|
|
1225
|
+
secureAccessControl ?? AccessControl.WhenUnlocked,
|
|
1226
|
+
);
|
|
783
1227
|
return;
|
|
784
1228
|
}
|
|
785
1229
|
|
|
@@ -799,7 +1243,11 @@ export function createStorageItem<T = undefined>(
|
|
|
799
1243
|
cacheRawValue(nonMemoryScope!, storageKey, undefined);
|
|
800
1244
|
|
|
801
1245
|
if (coalesceSecureWrites) {
|
|
802
|
-
scheduleSecureWrite(
|
|
1246
|
+
scheduleSecureWrite(
|
|
1247
|
+
storageKey,
|
|
1248
|
+
undefined,
|
|
1249
|
+
secureAccessControl ?? AccessControl.WhenUnlocked,
|
|
1250
|
+
);
|
|
803
1251
|
return;
|
|
804
1252
|
}
|
|
805
1253
|
|
|
@@ -839,7 +1287,7 @@ export function createStorageItem<T = undefined>(
|
|
|
839
1287
|
return onValidationError(invalidValue);
|
|
840
1288
|
}
|
|
841
1289
|
|
|
842
|
-
return
|
|
1290
|
+
return defaultValue;
|
|
843
1291
|
};
|
|
844
1292
|
|
|
845
1293
|
const ensureValidatedValue = (
|
|
@@ -852,7 +1300,7 @@ export function createStorageItem<T = undefined>(
|
|
|
852
1300
|
|
|
853
1301
|
const resolved = resolveInvalidValue(candidate);
|
|
854
1302
|
if (validate && !validate(resolved)) {
|
|
855
|
-
return
|
|
1303
|
+
return defaultValue;
|
|
856
1304
|
}
|
|
857
1305
|
if (hadStoredValue) {
|
|
858
1306
|
writeValueWithoutValidation(resolved);
|
|
@@ -860,39 +1308,64 @@ export function createStorageItem<T = undefined>(
|
|
|
860
1308
|
return resolved;
|
|
861
1309
|
};
|
|
862
1310
|
|
|
863
|
-
const
|
|
1311
|
+
const getInternal = (): T => {
|
|
864
1312
|
const raw = readStoredRaw();
|
|
865
1313
|
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
1314
|
+
if (!memoryExpiration && raw === lastRaw && hasLastValue) {
|
|
1315
|
+
if (!expiration || lastExpiresAt === null) {
|
|
1316
|
+
return lastValue as T;
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
if (typeof lastExpiresAt === "number") {
|
|
1320
|
+
if (lastExpiresAt > Date.now()) {
|
|
1321
|
+
return lastValue as T;
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
removeStoredRaw();
|
|
1325
|
+
invalidateParsedCache();
|
|
1326
|
+
onExpired?.(storageKey);
|
|
1327
|
+
lastValue = ensureValidatedValue(defaultValue, false);
|
|
1328
|
+
hasLastValue = true;
|
|
1329
|
+
return lastValue;
|
|
1330
|
+
}
|
|
869
1331
|
}
|
|
870
1332
|
|
|
871
1333
|
lastRaw = raw;
|
|
872
1334
|
|
|
873
1335
|
if (raw === undefined) {
|
|
874
|
-
|
|
1336
|
+
lastExpiresAt = undefined;
|
|
1337
|
+
lastValue = ensureValidatedValue(defaultValue, false);
|
|
875
1338
|
hasLastValue = true;
|
|
876
1339
|
return lastValue;
|
|
877
1340
|
}
|
|
878
1341
|
|
|
879
1342
|
if (isMemory) {
|
|
1343
|
+
lastExpiresAt = undefined;
|
|
880
1344
|
lastValue = ensureValidatedValue(raw, true);
|
|
881
1345
|
hasLastValue = true;
|
|
882
1346
|
return lastValue;
|
|
883
1347
|
}
|
|
884
1348
|
|
|
885
|
-
|
|
1349
|
+
if (typeof raw !== "string") {
|
|
1350
|
+
lastExpiresAt = undefined;
|
|
1351
|
+
lastValue = ensureValidatedValue(defaultValue, false);
|
|
1352
|
+
hasLastValue = true;
|
|
1353
|
+
return lastValue;
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
let deserializableRaw = raw;
|
|
886
1357
|
|
|
887
1358
|
if (expiration) {
|
|
1359
|
+
let envelopeExpiresAt: number | null = null;
|
|
888
1360
|
try {
|
|
889
|
-
const parsed = JSON.parse(raw
|
|
1361
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
890
1362
|
if (isStoredEnvelope(parsed)) {
|
|
1363
|
+
envelopeExpiresAt = parsed.expiresAt;
|
|
891
1364
|
if (parsed.expiresAt <= Date.now()) {
|
|
892
1365
|
removeStoredRaw();
|
|
893
1366
|
invalidateParsedCache();
|
|
894
1367
|
onExpired?.(storageKey);
|
|
895
|
-
lastValue = ensureValidatedValue(
|
|
1368
|
+
lastValue = ensureValidatedValue(defaultValue, false);
|
|
896
1369
|
hasLastValue = true;
|
|
897
1370
|
return lastValue;
|
|
898
1371
|
}
|
|
@@ -902,6 +1375,9 @@ export function createStorageItem<T = undefined>(
|
|
|
902
1375
|
} catch {
|
|
903
1376
|
// Keep backward compatibility with legacy raw values.
|
|
904
1377
|
}
|
|
1378
|
+
lastExpiresAt = envelopeExpiresAt;
|
|
1379
|
+
} else {
|
|
1380
|
+
lastExpiresAt = undefined;
|
|
905
1381
|
}
|
|
906
1382
|
|
|
907
1383
|
lastValue = ensureValidatedValue(deserialize(deserializableRaw), true);
|
|
@@ -909,44 +1385,74 @@ export function createStorageItem<T = undefined>(
|
|
|
909
1385
|
return lastValue;
|
|
910
1386
|
};
|
|
911
1387
|
|
|
1388
|
+
const getCurrentVersion = (): StorageVersion => {
|
|
1389
|
+
const raw = readStoredRaw();
|
|
1390
|
+
return toVersionToken(raw);
|
|
1391
|
+
};
|
|
1392
|
+
|
|
1393
|
+
const get = (): T =>
|
|
1394
|
+
measureOperation("item:get", config.scope, () => getInternal());
|
|
1395
|
+
|
|
1396
|
+
const getWithVersion = (): VersionedValue<T> =>
|
|
1397
|
+
measureOperation("item:getWithVersion", config.scope, () => ({
|
|
1398
|
+
value: getInternal(),
|
|
1399
|
+
version: getCurrentVersion(),
|
|
1400
|
+
}));
|
|
1401
|
+
|
|
912
1402
|
const set = (valueOrFn: T | ((prev: T) => T)): void => {
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
? (valueOrFn as (prev: T) => T)(currentValue)
|
|
1403
|
+
measureOperation("item:set", config.scope, () => {
|
|
1404
|
+
const newValue = isUpdater(valueOrFn)
|
|
1405
|
+
? valueOrFn(getInternal())
|
|
917
1406
|
: valueOrFn;
|
|
918
1407
|
|
|
919
|
-
|
|
1408
|
+
invalidateParsedCache();
|
|
920
1409
|
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
1410
|
+
if (validate && !validate(newValue)) {
|
|
1411
|
+
throw new Error(
|
|
1412
|
+
`Validation failed for key "${storageKey}" in scope "${StorageScope[config.scope]}".`,
|
|
1413
|
+
);
|
|
1414
|
+
}
|
|
926
1415
|
|
|
927
|
-
|
|
1416
|
+
writeValueWithoutValidation(newValue);
|
|
1417
|
+
});
|
|
928
1418
|
};
|
|
929
1419
|
|
|
1420
|
+
const setIfVersion = (
|
|
1421
|
+
version: StorageVersion,
|
|
1422
|
+
valueOrFn: T | ((prev: T) => T),
|
|
1423
|
+
): boolean =>
|
|
1424
|
+
measureOperation("item:setIfVersion", config.scope, () => {
|
|
1425
|
+
const currentVersion = getCurrentVersion();
|
|
1426
|
+
if (currentVersion !== version) {
|
|
1427
|
+
return false;
|
|
1428
|
+
}
|
|
1429
|
+
set(valueOrFn);
|
|
1430
|
+
return true;
|
|
1431
|
+
});
|
|
1432
|
+
|
|
930
1433
|
const deleteItem = (): void => {
|
|
931
|
-
|
|
1434
|
+
measureOperation("item:delete", config.scope, () => {
|
|
1435
|
+
invalidateParsedCache();
|
|
932
1436
|
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
1437
|
+
if (isMemory) {
|
|
1438
|
+
if (memoryExpiration) {
|
|
1439
|
+
memoryExpiration.delete(storageKey);
|
|
1440
|
+
}
|
|
1441
|
+
memoryStore.delete(storageKey);
|
|
1442
|
+
notifyKeyListeners(memoryListeners, storageKey);
|
|
1443
|
+
return;
|
|
936
1444
|
}
|
|
937
|
-
memoryStore.delete(storageKey);
|
|
938
|
-
notifyKeyListeners(memoryListeners, storageKey);
|
|
939
|
-
return;
|
|
940
|
-
}
|
|
941
1445
|
|
|
942
|
-
|
|
1446
|
+
removeStoredRaw();
|
|
1447
|
+
});
|
|
943
1448
|
};
|
|
944
1449
|
|
|
945
|
-
const hasItem = (): boolean =>
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
1450
|
+
const hasItem = (): boolean =>
|
|
1451
|
+
measureOperation("item:has", config.scope, () => {
|
|
1452
|
+
if (isMemory) return memoryStore.has(storageKey);
|
|
1453
|
+
if (isBiometric) return WebStorage.hasSecureBiometric(storageKey);
|
|
1454
|
+
return WebStorage.has(storageKey, config.scope);
|
|
1455
|
+
});
|
|
950
1456
|
|
|
951
1457
|
const subscribe = (callback: () => void): (() => void) => {
|
|
952
1458
|
ensureSubscription();
|
|
@@ -962,7 +1468,9 @@ export function createStorageItem<T = undefined>(
|
|
|
962
1468
|
|
|
963
1469
|
const storageItem: StorageItemInternal<T> = {
|
|
964
1470
|
get,
|
|
1471
|
+
getWithVersion,
|
|
965
1472
|
set,
|
|
1473
|
+
setIfVersion,
|
|
966
1474
|
delete: deleteItem,
|
|
967
1475
|
has: hasItem,
|
|
968
1476
|
subscribe,
|
|
@@ -976,54 +1484,18 @@ export function createStorageItem<T = undefined>(
|
|
|
976
1484
|
_hasExpiration: expiration !== undefined,
|
|
977
1485
|
_readCacheEnabled: readCache,
|
|
978
1486
|
_isBiometric: isBiometric,
|
|
979
|
-
|
|
1487
|
+
_defaultValue: defaultValue,
|
|
1488
|
+
...(secureAccessControl !== undefined
|
|
1489
|
+
? { _secureAccessControl: secureAccessControl }
|
|
1490
|
+
: {}),
|
|
980
1491
|
scope: config.scope,
|
|
981
1492
|
key: storageKey,
|
|
982
1493
|
};
|
|
983
1494
|
|
|
984
|
-
return storageItem
|
|
985
|
-
}
|
|
986
|
-
|
|
987
|
-
export function useStorage<T>(
|
|
988
|
-
item: StorageItem<T>,
|
|
989
|
-
): [T, (value: T | ((prev: T) => T)) => void] {
|
|
990
|
-
const value = useSyncExternalStore(item.subscribe, item.get, item.get);
|
|
991
|
-
return [value, item.set];
|
|
1495
|
+
return storageItem;
|
|
992
1496
|
}
|
|
993
1497
|
|
|
994
|
-
export
|
|
995
|
-
item: StorageItem<T>,
|
|
996
|
-
selector: (value: T) => TSelected,
|
|
997
|
-
isEqual: (prev: TSelected, next: TSelected) => boolean = Object.is,
|
|
998
|
-
): [TSelected, (value: T | ((prev: T) => T)) => void] {
|
|
999
|
-
const selectedRef = useRef<
|
|
1000
|
-
{ hasValue: false } | { hasValue: true; value: TSelected }
|
|
1001
|
-
>({
|
|
1002
|
-
hasValue: false,
|
|
1003
|
-
});
|
|
1004
|
-
|
|
1005
|
-
const getSelectedSnapshot = () => {
|
|
1006
|
-
const nextSelected = selector(item.get());
|
|
1007
|
-
const current = selectedRef.current;
|
|
1008
|
-
if (current.hasValue && isEqual(current.value, nextSelected)) {
|
|
1009
|
-
return current.value;
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
|
-
selectedRef.current = { hasValue: true, value: nextSelected };
|
|
1013
|
-
return nextSelected;
|
|
1014
|
-
};
|
|
1015
|
-
|
|
1016
|
-
const selectedValue = useSyncExternalStore(
|
|
1017
|
-
item.subscribe,
|
|
1018
|
-
getSelectedSnapshot,
|
|
1019
|
-
getSelectedSnapshot,
|
|
1020
|
-
);
|
|
1021
|
-
return [selectedValue, item.set];
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
export function useSetStorage<T>(item: StorageItem<T>) {
|
|
1025
|
-
return item.set;
|
|
1026
|
-
}
|
|
1498
|
+
export { useStorage, useStorageSelector, useSetStorage } from "./storage-hooks";
|
|
1027
1499
|
|
|
1028
1500
|
type BatchReadItem<T> = Pick<
|
|
1029
1501
|
StorageItem<T>,
|
|
@@ -1033,6 +1505,7 @@ type BatchReadItem<T> = Pick<
|
|
|
1033
1505
|
_hasExpiration?: boolean;
|
|
1034
1506
|
_readCacheEnabled?: boolean;
|
|
1035
1507
|
_isBiometric?: boolean;
|
|
1508
|
+
_defaultValue?: unknown;
|
|
1036
1509
|
_secureAccessControl?: AccessControl;
|
|
1037
1510
|
};
|
|
1038
1511
|
type BatchRemoveItem = Pick<StorageItem<unknown>, "key" | "scope" | "delete">;
|
|
@@ -1046,108 +1519,174 @@ export function getBatch(
|
|
|
1046
1519
|
items: readonly BatchReadItem<unknown>[],
|
|
1047
1520
|
scope: StorageScope,
|
|
1048
1521
|
): unknown[] {
|
|
1049
|
-
|
|
1522
|
+
return measureOperation(
|
|
1523
|
+
"batch:get",
|
|
1524
|
+
scope,
|
|
1525
|
+
() => {
|
|
1526
|
+
assertBatchScope(items, scope);
|
|
1050
1527
|
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1528
|
+
if (scope === StorageScope.Memory) {
|
|
1529
|
+
return items.map((item) => item.get());
|
|
1530
|
+
}
|
|
1054
1531
|
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1532
|
+
const useRawBatchPath = items.every((item) =>
|
|
1533
|
+
scope === StorageScope.Secure
|
|
1534
|
+
? canUseSecureRawBatchPath(item)
|
|
1535
|
+
: canUseRawBatchPath(item),
|
|
1536
|
+
);
|
|
1537
|
+
if (!useRawBatchPath) {
|
|
1538
|
+
return items.map((item) => item.get());
|
|
1539
|
+
}
|
|
1060
1540
|
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1541
|
+
const rawValues = new Array<string | undefined>(items.length);
|
|
1542
|
+
const keysToFetch: string[] = [];
|
|
1543
|
+
const keyIndexes: number[] = [];
|
|
1064
1544
|
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1545
|
+
items.forEach((item, index) => {
|
|
1546
|
+
if (scope === StorageScope.Secure) {
|
|
1547
|
+
if (hasPendingSecureWrite(item.key)) {
|
|
1548
|
+
rawValues[index] = readPendingSecureWrite(item.key);
|
|
1549
|
+
return;
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1072
1552
|
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1553
|
+
if (item._readCacheEnabled === true) {
|
|
1554
|
+
if (hasCachedRawValue(scope, item.key)) {
|
|
1555
|
+
rawValues[index] = readCachedRawValue(scope, item.key);
|
|
1556
|
+
return;
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1079
1559
|
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1560
|
+
keysToFetch.push(item.key);
|
|
1561
|
+
keyIndexes.push(index);
|
|
1562
|
+
});
|
|
1083
1563
|
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1564
|
+
if (keysToFetch.length > 0) {
|
|
1565
|
+
const fetchedValues = WebStorage.getBatch(keysToFetch, scope);
|
|
1566
|
+
fetchedValues.forEach((value, index) => {
|
|
1567
|
+
const key = keysToFetch[index];
|
|
1568
|
+
const targetIndex = keyIndexes[index];
|
|
1569
|
+
if (key === undefined || targetIndex === undefined) {
|
|
1570
|
+
return;
|
|
1571
|
+
}
|
|
1572
|
+
rawValues[targetIndex] = value;
|
|
1573
|
+
cacheRawValue(scope, key, value);
|
|
1574
|
+
});
|
|
1575
|
+
}
|
|
1093
1576
|
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1577
|
+
return items.map((item, index) => {
|
|
1578
|
+
const raw = rawValues[index];
|
|
1579
|
+
if (raw === undefined) {
|
|
1580
|
+
return asInternal(item as StorageItem<unknown>)._defaultValue;
|
|
1581
|
+
}
|
|
1582
|
+
return item.deserialize(raw);
|
|
1583
|
+
});
|
|
1584
|
+
},
|
|
1585
|
+
items.length,
|
|
1586
|
+
);
|
|
1101
1587
|
}
|
|
1102
1588
|
|
|
1103
1589
|
export function setBatch<T>(
|
|
1104
1590
|
items: readonly StorageBatchSetItem<T>[],
|
|
1105
1591
|
scope: StorageScope,
|
|
1106
1592
|
): void {
|
|
1107
|
-
|
|
1108
|
-
|
|
1593
|
+
measureOperation(
|
|
1594
|
+
"batch:set",
|
|
1109
1595
|
scope,
|
|
1110
|
-
|
|
1596
|
+
() => {
|
|
1597
|
+
assertBatchScope(
|
|
1598
|
+
items.map((batchEntry) => batchEntry.item),
|
|
1599
|
+
scope,
|
|
1600
|
+
);
|
|
1111
1601
|
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1602
|
+
if (scope === StorageScope.Memory) {
|
|
1603
|
+
items.forEach(({ item, value }) => item.set(value));
|
|
1604
|
+
return;
|
|
1605
|
+
}
|
|
1116
1606
|
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1607
|
+
if (scope === StorageScope.Secure) {
|
|
1608
|
+
const secureEntries = items.map(({ item, value }) => ({
|
|
1609
|
+
item,
|
|
1610
|
+
value,
|
|
1611
|
+
internal: asInternal(item),
|
|
1612
|
+
}));
|
|
1613
|
+
const canUseSecureBatchPath = secureEntries.every(({ internal }) =>
|
|
1614
|
+
canUseSecureRawBatchPath(internal),
|
|
1615
|
+
);
|
|
1616
|
+
if (!canUseSecureBatchPath) {
|
|
1617
|
+
items.forEach(({ item, value }) => item.set(value));
|
|
1618
|
+
return;
|
|
1619
|
+
}
|
|
1124
1620
|
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1621
|
+
flushSecureWrites();
|
|
1622
|
+
const groupedByAccessControl = new Map<
|
|
1623
|
+
number,
|
|
1624
|
+
{ keys: string[]; values: string[] }
|
|
1625
|
+
>();
|
|
1626
|
+
|
|
1627
|
+
secureEntries.forEach(({ item, value, internal }) => {
|
|
1628
|
+
const accessControl =
|
|
1629
|
+
internal._secureAccessControl ?? AccessControl.WhenUnlocked;
|
|
1630
|
+
const existingGroup = groupedByAccessControl.get(accessControl);
|
|
1631
|
+
const group = existingGroup ?? { keys: [], values: [] };
|
|
1632
|
+
group.keys.push(item.key);
|
|
1633
|
+
group.values.push(item.serialize(value));
|
|
1634
|
+
if (!existingGroup) {
|
|
1635
|
+
groupedByAccessControl.set(accessControl, group);
|
|
1636
|
+
}
|
|
1637
|
+
});
|
|
1638
|
+
|
|
1639
|
+
groupedByAccessControl.forEach((group, accessControl) => {
|
|
1640
|
+
WebStorage.setSecureAccessControl(accessControl);
|
|
1641
|
+
WebStorage.setBatch(group.keys, group.values, scope);
|
|
1642
|
+
group.keys.forEach((key, index) =>
|
|
1643
|
+
cacheRawValue(scope, key, group.values[index]),
|
|
1644
|
+
);
|
|
1645
|
+
});
|
|
1646
|
+
return;
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
const useRawBatchPath = items.every(({ item }) =>
|
|
1650
|
+
canUseRawBatchPath(asInternal(item)),
|
|
1651
|
+
);
|
|
1652
|
+
if (!useRawBatchPath) {
|
|
1653
|
+
items.forEach(({ item, value }) => item.set(value));
|
|
1654
|
+
return;
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
const keys = items.map((entry) => entry.item.key);
|
|
1658
|
+
const values = items.map((entry) => entry.item.serialize(entry.value));
|
|
1659
|
+
WebStorage.setBatch(keys, values, scope);
|
|
1660
|
+
keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
|
|
1661
|
+
},
|
|
1662
|
+
items.length,
|
|
1663
|
+
);
|
|
1132
1664
|
}
|
|
1133
1665
|
|
|
1134
1666
|
export function removeBatch(
|
|
1135
1667
|
items: readonly BatchRemoveItem[],
|
|
1136
1668
|
scope: StorageScope,
|
|
1137
1669
|
): void {
|
|
1138
|
-
|
|
1670
|
+
measureOperation(
|
|
1671
|
+
"batch:remove",
|
|
1672
|
+
scope,
|
|
1673
|
+
() => {
|
|
1674
|
+
assertBatchScope(items, scope);
|
|
1139
1675
|
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1676
|
+
if (scope === StorageScope.Memory) {
|
|
1677
|
+
items.forEach((item) => item.delete());
|
|
1678
|
+
return;
|
|
1679
|
+
}
|
|
1144
1680
|
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1681
|
+
const keys = items.map((item) => item.key);
|
|
1682
|
+
if (scope === StorageScope.Secure) {
|
|
1683
|
+
flushSecureWrites();
|
|
1684
|
+
}
|
|
1685
|
+
WebStorage.removeBatch(keys, scope);
|
|
1686
|
+
keys.forEach((key) => cacheRawValue(scope, key, undefined));
|
|
1687
|
+
},
|
|
1688
|
+
items.length,
|
|
1689
|
+
);
|
|
1151
1690
|
}
|
|
1152
1691
|
|
|
1153
1692
|
export function registerMigration(version: number, migration: Migration): void {
|
|
@@ -1165,92 +1704,124 @@ export function registerMigration(version: number, migration: Migration): void {
|
|
|
1165
1704
|
export function migrateToLatest(
|
|
1166
1705
|
scope: StorageScope = StorageScope.Disk,
|
|
1167
1706
|
): number {
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
.
|
|
1172
|
-
|
|
1707
|
+
return measureOperation("migration:run", scope, () => {
|
|
1708
|
+
assertValidScope(scope);
|
|
1709
|
+
const currentVersion = readMigrationVersion(scope);
|
|
1710
|
+
const versions = Array.from(registeredMigrations.keys())
|
|
1711
|
+
.filter((version) => version > currentVersion)
|
|
1712
|
+
.sort((a, b) => a - b);
|
|
1713
|
+
|
|
1714
|
+
let appliedVersion = currentVersion;
|
|
1715
|
+
const context: MigrationContext = {
|
|
1716
|
+
scope,
|
|
1717
|
+
getRaw: (key) => getRawValue(key, scope),
|
|
1718
|
+
setRaw: (key, value) => setRawValue(key, value, scope),
|
|
1719
|
+
removeRaw: (key) => removeRawValue(key, scope),
|
|
1720
|
+
};
|
|
1173
1721
|
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1722
|
+
versions.forEach((version) => {
|
|
1723
|
+
const migration = registeredMigrations.get(version);
|
|
1724
|
+
if (!migration) {
|
|
1725
|
+
return;
|
|
1726
|
+
}
|
|
1727
|
+
migration(context);
|
|
1728
|
+
writeMigrationVersion(scope, version);
|
|
1729
|
+
appliedVersion = version;
|
|
1730
|
+
});
|
|
1181
1731
|
|
|
1182
|
-
|
|
1183
|
-
const migration = registeredMigrations.get(version);
|
|
1184
|
-
if (!migration) {
|
|
1185
|
-
return;
|
|
1186
|
-
}
|
|
1187
|
-
migration(context);
|
|
1188
|
-
writeMigrationVersion(scope, version);
|
|
1189
|
-
appliedVersion = version;
|
|
1732
|
+
return appliedVersion;
|
|
1190
1733
|
});
|
|
1191
|
-
|
|
1192
|
-
return appliedVersion;
|
|
1193
1734
|
}
|
|
1194
1735
|
|
|
1195
1736
|
export function runTransaction<T>(
|
|
1196
1737
|
scope: StorageScope,
|
|
1197
1738
|
transaction: (context: TransactionContext) => T,
|
|
1198
1739
|
): T {
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1740
|
+
return measureOperation("transaction:run", scope, () => {
|
|
1741
|
+
assertValidScope(scope);
|
|
1742
|
+
if (scope === StorageScope.Secure) {
|
|
1743
|
+
flushSecureWrites();
|
|
1744
|
+
}
|
|
1203
1745
|
|
|
1204
|
-
|
|
1746
|
+
const rollback = new Map<string, string | undefined>();
|
|
1205
1747
|
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1748
|
+
const rememberRollback = (key: string) => {
|
|
1749
|
+
if (rollback.has(key)) {
|
|
1750
|
+
return;
|
|
1751
|
+
}
|
|
1752
|
+
rollback.set(key, getRawValue(key, scope));
|
|
1753
|
+
};
|
|
1212
1754
|
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1755
|
+
const tx: TransactionContext = {
|
|
1756
|
+
scope,
|
|
1757
|
+
getRaw: (key) => getRawValue(key, scope),
|
|
1758
|
+
setRaw: (key, value) => {
|
|
1759
|
+
rememberRollback(key);
|
|
1760
|
+
setRawValue(key, value, scope);
|
|
1761
|
+
},
|
|
1762
|
+
removeRaw: (key) => {
|
|
1763
|
+
rememberRollback(key);
|
|
1764
|
+
removeRawValue(key, scope);
|
|
1765
|
+
},
|
|
1766
|
+
getItem: (item) => {
|
|
1767
|
+
assertBatchScope([item], scope);
|
|
1768
|
+
return item.get();
|
|
1769
|
+
},
|
|
1770
|
+
setItem: (item, value) => {
|
|
1771
|
+
assertBatchScope([item], scope);
|
|
1772
|
+
rememberRollback(item.key);
|
|
1773
|
+
item.set(value);
|
|
1774
|
+
},
|
|
1775
|
+
removeItem: (item) => {
|
|
1776
|
+
assertBatchScope([item], scope);
|
|
1777
|
+
rememberRollback(item.key);
|
|
1778
|
+
item.delete();
|
|
1779
|
+
},
|
|
1780
|
+
};
|
|
1239
1781
|
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
.
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1782
|
+
try {
|
|
1783
|
+
return transaction(tx);
|
|
1784
|
+
} catch (error) {
|
|
1785
|
+
const rollbackEntries = Array.from(rollback.entries()).reverse();
|
|
1786
|
+
if (scope === StorageScope.Memory) {
|
|
1787
|
+
rollbackEntries.forEach(([key, previousValue]) => {
|
|
1788
|
+
if (previousValue === undefined) {
|
|
1789
|
+
removeRawValue(key, scope);
|
|
1790
|
+
} else {
|
|
1791
|
+
setRawValue(key, previousValue, scope);
|
|
1792
|
+
}
|
|
1793
|
+
});
|
|
1794
|
+
} else {
|
|
1795
|
+
const keysToSet: string[] = [];
|
|
1796
|
+
const valuesToSet: string[] = [];
|
|
1797
|
+
const keysToRemove: string[] = [];
|
|
1798
|
+
|
|
1799
|
+
rollbackEntries.forEach(([key, previousValue]) => {
|
|
1800
|
+
if (previousValue === undefined) {
|
|
1801
|
+
keysToRemove.push(key);
|
|
1802
|
+
} else {
|
|
1803
|
+
keysToSet.push(key);
|
|
1804
|
+
valuesToSet.push(previousValue);
|
|
1805
|
+
}
|
|
1806
|
+
});
|
|
1807
|
+
|
|
1808
|
+
if (scope === StorageScope.Secure) {
|
|
1809
|
+
flushSecureWrites();
|
|
1250
1810
|
}
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1811
|
+
if (keysToSet.length > 0) {
|
|
1812
|
+
WebStorage.setBatch(keysToSet, valuesToSet, scope);
|
|
1813
|
+
keysToSet.forEach((key, index) =>
|
|
1814
|
+
cacheRawValue(scope, key, valuesToSet[index]),
|
|
1815
|
+
);
|
|
1816
|
+
}
|
|
1817
|
+
if (keysToRemove.length > 0) {
|
|
1818
|
+
WebStorage.removeBatch(keysToRemove, scope);
|
|
1819
|
+
keysToRemove.forEach((key) => cacheRawValue(scope, key, undefined));
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
throw error;
|
|
1823
|
+
}
|
|
1824
|
+
});
|
|
1254
1825
|
}
|
|
1255
1826
|
|
|
1256
1827
|
export type SecureAuthStorageConfig<K extends string = string> = Record<
|
|
@@ -1258,6 +1829,7 @@ export type SecureAuthStorageConfig<K extends string = string> = Record<
|
|
|
1258
1829
|
{
|
|
1259
1830
|
ttlMs?: number;
|
|
1260
1831
|
biometric?: boolean;
|
|
1832
|
+
biometricLevel?: BiometricLevel;
|
|
1261
1833
|
accessControl?: AccessControl;
|
|
1262
1834
|
}
|
|
1263
1835
|
>;
|
|
@@ -1267,20 +1839,31 @@ export function createSecureAuthStorage<K extends string>(
|
|
|
1267
1839
|
options?: { namespace?: string },
|
|
1268
1840
|
): Record<K, StorageItem<string>> {
|
|
1269
1841
|
const ns = options?.namespace ?? "auth";
|
|
1270
|
-
const result
|
|
1842
|
+
const result: Partial<Record<K, StorageItem<string>>> = {};
|
|
1271
1843
|
|
|
1272
|
-
for (const key of
|
|
1844
|
+
for (const key of typedKeys(config)) {
|
|
1273
1845
|
const itemConfig = config[key];
|
|
1846
|
+
const expirationConfig =
|
|
1847
|
+
itemConfig.ttlMs !== undefined ? { ttlMs: itemConfig.ttlMs } : undefined;
|
|
1274
1848
|
result[key] = createStorageItem<string>({
|
|
1275
1849
|
key,
|
|
1276
1850
|
scope: StorageScope.Secure,
|
|
1277
1851
|
defaultValue: "",
|
|
1278
1852
|
namespace: ns,
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1853
|
+
...(itemConfig.biometric !== undefined
|
|
1854
|
+
? { biometric: itemConfig.biometric }
|
|
1855
|
+
: {}),
|
|
1856
|
+
...(itemConfig.biometricLevel !== undefined
|
|
1857
|
+
? { biometricLevel: itemConfig.biometricLevel }
|
|
1858
|
+
: {}),
|
|
1859
|
+
...(itemConfig.accessControl !== undefined
|
|
1860
|
+
? { accessControl: itemConfig.accessControl }
|
|
1861
|
+
: {}),
|
|
1862
|
+
...(expirationConfig !== undefined
|
|
1863
|
+
? { expiration: expirationConfig }
|
|
1864
|
+
: {}),
|
|
1282
1865
|
});
|
|
1283
1866
|
}
|
|
1284
1867
|
|
|
1285
|
-
return result
|
|
1868
|
+
return result as Record<K, StorageItem<string>>;
|
|
1286
1869
|
}
|