react-native-nitro-storage 0.3.2 → 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 +141 -30
- package/android/src/main/cpp/AndroidStorageAdapterCpp.cpp +22 -2
- package/android/src/main/cpp/AndroidStorageAdapterCpp.hpp +3 -0
- package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +54 -5
- package/cpp/bindings/HybridStorage.cpp +167 -22
- package/cpp/bindings/HybridStorage.hpp +12 -1
- package/cpp/core/NativeStorageAdapter.hpp +3 -0
- package/ios/IOSStorageAdapterCpp.hpp +16 -0
- package/ios/IOSStorageAdapterCpp.mm +135 -11
- package/lib/commonjs/index.js +466 -275
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +564 -270
- 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/module/index.js +466 -277
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +564 -272
- 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/typescript/Storage.nitro.d.ts +2 -0
- package/lib/typescript/Storage.nitro.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +38 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/index.web.d.ts +40 -1
- 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/nitrogen/generated/shared/c++/HybridStorageSpec.cpp +2 -0
- package/nitrogen/generated/shared/c++/HybridStorageSpec.hpp +2 -0
- package/package.json +1 -1
- package/src/Storage.nitro.ts +2 -0
- package/src/index.ts +616 -296
- package/src/index.web.ts +728 -288
- package/src/internal.ts +28 -0
package/src/index.web.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
assertValidScope,
|
|
8
8
|
serializeWithPrimitiveFastPath,
|
|
9
9
|
deserializeWithPrimitiveFastPath,
|
|
10
|
+
toVersionToken,
|
|
10
11
|
prefixKey,
|
|
11
12
|
isNamespaced,
|
|
12
13
|
} from "./internal";
|
|
@@ -18,6 +19,31 @@ export type Validator<T> = (value: unknown) => value is T;
|
|
|
18
19
|
export type ExpirationConfig = {
|
|
19
20
|
ttlMs: number;
|
|
20
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
|
+
};
|
|
21
47
|
|
|
22
48
|
export type MigrationContext = {
|
|
23
49
|
scope: StorageScope;
|
|
@@ -65,7 +91,11 @@ function typedKeys<K extends string, V>(record: Record<K, V>): K[] {
|
|
|
65
91
|
return Object.keys(record) as K[];
|
|
66
92
|
}
|
|
67
93
|
type NonMemoryScope = StorageScope.Disk | StorageScope.Secure;
|
|
68
|
-
type PendingSecureWrite = {
|
|
94
|
+
type PendingSecureWrite = {
|
|
95
|
+
key: string;
|
|
96
|
+
value: string | undefined;
|
|
97
|
+
accessControl?: AccessControl;
|
|
98
|
+
};
|
|
69
99
|
type BrowserStorageLike = {
|
|
70
100
|
setItem: (key: string, value: string) => void;
|
|
71
101
|
getItem: (key: string) => string | null;
|
|
@@ -93,6 +123,7 @@ export interface Storage {
|
|
|
93
123
|
clear(scope: number): void;
|
|
94
124
|
has(key: string, scope: number): boolean;
|
|
95
125
|
getAllKeys(scope: number): string[];
|
|
126
|
+
getKeysByPrefix(prefix: string, scope: number): string[];
|
|
96
127
|
size(scope: number): number;
|
|
97
128
|
setBatch(keys: string[], values: string[], scope: number): void;
|
|
98
129
|
getBatch(keys: string[], scope: number): (string | undefined)[];
|
|
@@ -106,6 +137,7 @@ export interface Storage {
|
|
|
106
137
|
setSecureWritesAsync(enabled: boolean): void;
|
|
107
138
|
setKeychainAccessGroup(group: string): void;
|
|
108
139
|
setSecureBiometric(key: string, value: string): void;
|
|
140
|
+
setSecureBiometricWithLevel(key: string, value: string, level: number): void;
|
|
109
141
|
getSecureBiometric(key: string): string | undefined;
|
|
110
142
|
deleteSecureBiometric(key: string): void;
|
|
111
143
|
hasSecureBiometric(key: string): boolean;
|
|
@@ -134,13 +166,90 @@ let secureFlushScheduled = false;
|
|
|
134
166
|
const SECURE_WEB_PREFIX = "__secure_";
|
|
135
167
|
const BIOMETRIC_WEB_PREFIX = "__bio_";
|
|
136
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();
|
|
137
234
|
|
|
138
235
|
function getBrowserStorage(scope: number): BrowserStorageLike | undefined {
|
|
139
236
|
if (scope === StorageScope.Disk) {
|
|
140
237
|
return globalThis.localStorage;
|
|
141
238
|
}
|
|
142
239
|
if (scope === StorageScope.Secure) {
|
|
143
|
-
|
|
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
|
+
};
|
|
144
253
|
}
|
|
145
254
|
return undefined;
|
|
146
255
|
}
|
|
@@ -206,6 +315,74 @@ function ensureWebScopeKeyIndex(scope: NonMemoryScope): Set<string> {
|
|
|
206
315
|
return getWebScopeKeyIndex(scope);
|
|
207
316
|
}
|
|
208
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
|
+
|
|
209
386
|
function getScopedListeners(scope: NonMemoryScope): KeyListenerRegistry {
|
|
210
387
|
return webScopeListeners.get(scope)!;
|
|
211
388
|
}
|
|
@@ -295,29 +472,46 @@ function flushSecureWrites(): void {
|
|
|
295
472
|
const writes = Array.from(pendingSecureWrites.values());
|
|
296
473
|
pendingSecureWrites.clear();
|
|
297
474
|
|
|
298
|
-
const
|
|
299
|
-
|
|
475
|
+
const groupedSetWrites = new Map<
|
|
476
|
+
AccessControl,
|
|
477
|
+
{ keys: string[]; values: string[] }
|
|
478
|
+
>();
|
|
300
479
|
const keysToRemove: string[] = [];
|
|
301
480
|
|
|
302
|
-
writes.forEach(({ key, value }) => {
|
|
481
|
+
writes.forEach(({ key, value, accessControl }) => {
|
|
303
482
|
if (value === undefined) {
|
|
304
483
|
keysToRemove.push(key);
|
|
305
484
|
} else {
|
|
306
|
-
|
|
307
|
-
|
|
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
|
+
}
|
|
308
493
|
}
|
|
309
494
|
});
|
|
310
495
|
|
|
311
|
-
|
|
312
|
-
WebStorage.
|
|
313
|
-
|
|
496
|
+
groupedSetWrites.forEach((group, accessControl) => {
|
|
497
|
+
WebStorage.setSecureAccessControl(accessControl);
|
|
498
|
+
WebStorage.setBatch(group.keys, group.values, StorageScope.Secure);
|
|
499
|
+
});
|
|
314
500
|
if (keysToRemove.length > 0) {
|
|
315
501
|
WebStorage.removeBatch(keysToRemove, StorageScope.Secure);
|
|
316
502
|
}
|
|
317
503
|
}
|
|
318
504
|
|
|
319
|
-
function scheduleSecureWrite(
|
|
320
|
-
|
|
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);
|
|
321
515
|
if (secureFlushScheduled) {
|
|
322
516
|
return;
|
|
323
517
|
}
|
|
@@ -491,6 +685,14 @@ const WebStorage: Storage = {
|
|
|
491
685
|
}
|
|
492
686
|
return Array.from(ensureWebScopeKeyIndex(scope));
|
|
493
687
|
},
|
|
688
|
+
getKeysByPrefix: (prefix: string, scope: number) => {
|
|
689
|
+
if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
|
|
690
|
+
return [];
|
|
691
|
+
}
|
|
692
|
+
return Array.from(ensureWebScopeKeyIndex(scope)).filter((key) =>
|
|
693
|
+
key.startsWith(prefix),
|
|
694
|
+
);
|
|
695
|
+
},
|
|
494
696
|
size: (scope: number) => {
|
|
495
697
|
if (scope === StorageScope.Disk || scope === StorageScope.Secure) {
|
|
496
698
|
return ensureWebScopeKeyIndex(scope).size;
|
|
@@ -501,6 +703,13 @@ const WebStorage: Storage = {
|
|
|
501
703
|
setSecureWritesAsync: (_enabled: boolean) => {},
|
|
502
704
|
setKeychainAccessGroup: () => {},
|
|
503
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) => {
|
|
504
713
|
if (
|
|
505
714
|
typeof __DEV__ !== "undefined" &&
|
|
506
715
|
__DEV__ &&
|
|
@@ -511,17 +720,22 @@ const WebStorage: Storage = {
|
|
|
511
720
|
"[NitroStorage] Biometric storage is not supported on web. Using localStorage.",
|
|
512
721
|
);
|
|
513
722
|
}
|
|
514
|
-
|
|
723
|
+
getBrowserStorage(StorageScope.Secure)?.setItem(
|
|
724
|
+
toBiometricStorageKey(key),
|
|
725
|
+
value,
|
|
726
|
+
);
|
|
515
727
|
ensureWebScopeKeyIndex(StorageScope.Secure).add(key);
|
|
516
728
|
notifyKeyListeners(getScopedListeners(StorageScope.Secure), key);
|
|
517
729
|
},
|
|
518
730
|
getSecureBiometric: (key: string) => {
|
|
519
731
|
return (
|
|
520
|
-
|
|
732
|
+
getBrowserStorage(StorageScope.Secure)?.getItem(
|
|
733
|
+
toBiometricStorageKey(key),
|
|
734
|
+
) ?? undefined
|
|
521
735
|
);
|
|
522
736
|
},
|
|
523
737
|
deleteSecureBiometric: (key: string) => {
|
|
524
|
-
const storage =
|
|
738
|
+
const storage = getBrowserStorage(StorageScope.Secure);
|
|
525
739
|
storage?.removeItem(toBiometricStorageKey(key));
|
|
526
740
|
if (storage?.getItem(toSecureStorageKey(key)) === null) {
|
|
527
741
|
ensureWebScopeKeyIndex(StorageScope.Secure).delete(key);
|
|
@@ -530,11 +744,13 @@ const WebStorage: Storage = {
|
|
|
530
744
|
},
|
|
531
745
|
hasSecureBiometric: (key: string) => {
|
|
532
746
|
return (
|
|
533
|
-
|
|
747
|
+
getBrowserStorage(StorageScope.Secure)?.getItem(
|
|
748
|
+
toBiometricStorageKey(key),
|
|
749
|
+
) !== null
|
|
534
750
|
);
|
|
535
751
|
},
|
|
536
752
|
clearSecureBiometric: () => {
|
|
537
|
-
const storage =
|
|
753
|
+
const storage = getBrowserStorage(StorageScope.Secure);
|
|
538
754
|
if (!storage) return;
|
|
539
755
|
const keysToNotify: string[] = [];
|
|
540
756
|
const toRemove: string[] = [];
|
|
@@ -621,85 +837,190 @@ function writeMigrationVersion(scope: StorageScope, version: number): void {
|
|
|
621
837
|
|
|
622
838
|
export const storage = {
|
|
623
839
|
clear: (scope: StorageScope) => {
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
840
|
+
measureOperation("storage:clear", scope, () => {
|
|
841
|
+
if (scope === StorageScope.Memory) {
|
|
842
|
+
memoryStore.clear();
|
|
843
|
+
notifyAllListeners(memoryListeners);
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
629
846
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
847
|
+
if (scope === StorageScope.Secure) {
|
|
848
|
+
flushSecureWrites();
|
|
849
|
+
pendingSecureWrites.clear();
|
|
850
|
+
}
|
|
634
851
|
|
|
635
|
-
|
|
636
|
-
|
|
852
|
+
clearScopeRawCache(scope);
|
|
853
|
+
WebStorage.clear(scope);
|
|
854
|
+
});
|
|
637
855
|
},
|
|
638
856
|
clearAll: () => {
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
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
|
+
);
|
|
642
867
|
},
|
|
643
868
|
clearNamespace: (namespace: string, scope: StorageScope) => {
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
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
|
+
}
|
|
649
876
|
}
|
|
877
|
+
notifyAllListeners(memoryListeners);
|
|
878
|
+
return;
|
|
650
879
|
}
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
WebStorage.removeByPrefix(keyPrefix, scope);
|
|
880
|
+
|
|
881
|
+
const keyPrefix = prefixKey(namespace, "");
|
|
882
|
+
if (scope === StorageScope.Secure) {
|
|
883
|
+
flushSecureWrites();
|
|
884
|
+
}
|
|
885
|
+
clearScopeRawCache(scope);
|
|
886
|
+
WebStorage.removeByPrefix(keyPrefix, scope);
|
|
887
|
+
});
|
|
660
888
|
},
|
|
661
889
|
clearBiometric: () => {
|
|
662
|
-
|
|
890
|
+
measureOperation("storage:clearBiometric", StorageScope.Secure, () => {
|
|
891
|
+
WebStorage.clearSecureBiometric();
|
|
892
|
+
});
|
|
663
893
|
},
|
|
664
894
|
has: (key: string, scope: StorageScope): boolean => {
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
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
|
+
});
|
|
668
900
|
},
|
|
669
901
|
getAllKeys: (scope: StorageScope): string[] => {
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
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
|
+
});
|
|
673
949
|
},
|
|
674
950
|
getAll: (scope: StorageScope): Record<string, string> => {
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
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;
|
|
680
964
|
});
|
|
681
965
|
return result;
|
|
682
|
-
}
|
|
683
|
-
const keys = WebStorage.getAllKeys(scope);
|
|
684
|
-
keys.forEach((key) => {
|
|
685
|
-
const val = WebStorage.get(key, scope);
|
|
686
|
-
if (val !== undefined) result[key] = val;
|
|
687
966
|
});
|
|
688
|
-
return result;
|
|
689
967
|
},
|
|
690
968
|
size: (scope: StorageScope): number => {
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
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);
|
|
694
980
|
},
|
|
695
|
-
setAccessControl: (_level: AccessControl) => {},
|
|
696
|
-
setSecureWritesAsync: (_enabled: boolean) => {},
|
|
697
981
|
flushSecureWrites: () => {
|
|
698
|
-
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();
|
|
699
1007
|
},
|
|
700
|
-
setKeychainAccessGroup: (_group: string) => {},
|
|
701
1008
|
};
|
|
702
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
|
+
|
|
703
1024
|
export interface StorageItemConfig<T> {
|
|
704
1025
|
key: string;
|
|
705
1026
|
scope: StorageScope;
|
|
@@ -714,12 +1035,18 @@ export interface StorageItemConfig<T> {
|
|
|
714
1035
|
coalesceSecureWrites?: boolean;
|
|
715
1036
|
namespace?: string;
|
|
716
1037
|
biometric?: boolean;
|
|
1038
|
+
biometricLevel?: BiometricLevel;
|
|
717
1039
|
accessControl?: AccessControl;
|
|
718
1040
|
}
|
|
719
1041
|
|
|
720
1042
|
export interface StorageItem<T> {
|
|
721
1043
|
get: () => T;
|
|
1044
|
+
getWithVersion: () => VersionedValue<T>;
|
|
722
1045
|
set: (value: T | ((prev: T) => T)) => void;
|
|
1046
|
+
setIfVersion: (
|
|
1047
|
+
version: StorageVersion,
|
|
1048
|
+
value: T | ((prev: T) => T),
|
|
1049
|
+
) => boolean;
|
|
723
1050
|
delete: () => void;
|
|
724
1051
|
has: () => boolean;
|
|
725
1052
|
subscribe: (callback: () => void) => () => void;
|
|
@@ -735,6 +1062,7 @@ type StorageItemInternal<T> = StorageItem<T> & {
|
|
|
735
1062
|
_hasExpiration: boolean;
|
|
736
1063
|
_readCacheEnabled: boolean;
|
|
737
1064
|
_isBiometric: boolean;
|
|
1065
|
+
_defaultValue: T;
|
|
738
1066
|
_secureAccessControl?: AccessControl;
|
|
739
1067
|
};
|
|
740
1068
|
|
|
@@ -770,8 +1098,14 @@ export function createStorageItem<T = undefined>(
|
|
|
770
1098
|
const serialize = config.serialize ?? defaultSerialize;
|
|
771
1099
|
const deserialize = config.deserialize ?? defaultDeserialize;
|
|
772
1100
|
const isMemory = config.scope === StorageScope.Memory;
|
|
773
|
-
const
|
|
774
|
-
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;
|
|
775
1109
|
const secureAccessControl = config.accessControl;
|
|
776
1110
|
const validate = config.validate;
|
|
777
1111
|
const onValidationError = config.onValidationError;
|
|
@@ -784,8 +1118,7 @@ export function createStorageItem<T = undefined>(
|
|
|
784
1118
|
const coalesceSecureWrites =
|
|
785
1119
|
config.scope === StorageScope.Secure &&
|
|
786
1120
|
config.coalesceSecureWrites === true &&
|
|
787
|
-
!isBiometric
|
|
788
|
-
secureAccessControl === undefined;
|
|
1121
|
+
!isBiometric;
|
|
789
1122
|
const defaultValue = config.defaultValue as T;
|
|
790
1123
|
const nonMemoryScope: NonMemoryScope | null =
|
|
791
1124
|
config.scope === StorageScope.Disk
|
|
@@ -827,6 +1160,7 @@ export function createStorageItem<T = undefined>(
|
|
|
827
1160
|
return;
|
|
828
1161
|
}
|
|
829
1162
|
|
|
1163
|
+
ensureWebStorageEventSubscription();
|
|
830
1164
|
unsubscribe = addKeyListener(
|
|
831
1165
|
getScopedListeners(nonMemoryScope!),
|
|
832
1166
|
storageKey,
|
|
@@ -874,14 +1208,22 @@ export function createStorageItem<T = undefined>(
|
|
|
874
1208
|
|
|
875
1209
|
const writeStoredRaw = (rawValue: string): void => {
|
|
876
1210
|
if (isBiometric) {
|
|
877
|
-
WebStorage.
|
|
1211
|
+
WebStorage.setSecureBiometricWithLevel(
|
|
1212
|
+
storageKey,
|
|
1213
|
+
rawValue,
|
|
1214
|
+
resolvedBiometricLevel,
|
|
1215
|
+
);
|
|
878
1216
|
return;
|
|
879
1217
|
}
|
|
880
1218
|
|
|
881
1219
|
cacheRawValue(nonMemoryScope!, storageKey, rawValue);
|
|
882
1220
|
|
|
883
1221
|
if (coalesceSecureWrites) {
|
|
884
|
-
scheduleSecureWrite(
|
|
1222
|
+
scheduleSecureWrite(
|
|
1223
|
+
storageKey,
|
|
1224
|
+
rawValue,
|
|
1225
|
+
secureAccessControl ?? AccessControl.WhenUnlocked,
|
|
1226
|
+
);
|
|
885
1227
|
return;
|
|
886
1228
|
}
|
|
887
1229
|
|
|
@@ -901,7 +1243,11 @@ export function createStorageItem<T = undefined>(
|
|
|
901
1243
|
cacheRawValue(nonMemoryScope!, storageKey, undefined);
|
|
902
1244
|
|
|
903
1245
|
if (coalesceSecureWrites) {
|
|
904
|
-
scheduleSecureWrite(
|
|
1246
|
+
scheduleSecureWrite(
|
|
1247
|
+
storageKey,
|
|
1248
|
+
undefined,
|
|
1249
|
+
secureAccessControl ?? AccessControl.WhenUnlocked,
|
|
1250
|
+
);
|
|
905
1251
|
return;
|
|
906
1252
|
}
|
|
907
1253
|
|
|
@@ -962,7 +1308,7 @@ export function createStorageItem<T = undefined>(
|
|
|
962
1308
|
return resolved;
|
|
963
1309
|
};
|
|
964
1310
|
|
|
965
|
-
const
|
|
1311
|
+
const getInternal = (): T => {
|
|
966
1312
|
const raw = readStoredRaw();
|
|
967
1313
|
|
|
968
1314
|
if (!memoryExpiration && raw === lastRaw && hasLastValue) {
|
|
@@ -1039,40 +1385,74 @@ export function createStorageItem<T = undefined>(
|
|
|
1039
1385
|
return lastValue;
|
|
1040
1386
|
};
|
|
1041
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
|
+
|
|
1042
1402
|
const set = (valueOrFn: T | ((prev: T) => T)): void => {
|
|
1043
|
-
|
|
1403
|
+
measureOperation("item:set", config.scope, () => {
|
|
1404
|
+
const newValue = isUpdater(valueOrFn)
|
|
1405
|
+
? valueOrFn(getInternal())
|
|
1406
|
+
: valueOrFn;
|
|
1044
1407
|
|
|
1045
|
-
|
|
1408
|
+
invalidateParsedCache();
|
|
1046
1409
|
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1410
|
+
if (validate && !validate(newValue)) {
|
|
1411
|
+
throw new Error(
|
|
1412
|
+
`Validation failed for key "${storageKey}" in scope "${StorageScope[config.scope]}".`,
|
|
1413
|
+
);
|
|
1414
|
+
}
|
|
1052
1415
|
|
|
1053
|
-
|
|
1416
|
+
writeValueWithoutValidation(newValue);
|
|
1417
|
+
});
|
|
1054
1418
|
};
|
|
1055
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
|
+
|
|
1056
1433
|
const deleteItem = (): void => {
|
|
1057
|
-
|
|
1434
|
+
measureOperation("item:delete", config.scope, () => {
|
|
1435
|
+
invalidateParsedCache();
|
|
1058
1436
|
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1437
|
+
if (isMemory) {
|
|
1438
|
+
if (memoryExpiration) {
|
|
1439
|
+
memoryExpiration.delete(storageKey);
|
|
1440
|
+
}
|
|
1441
|
+
memoryStore.delete(storageKey);
|
|
1442
|
+
notifyKeyListeners(memoryListeners, storageKey);
|
|
1443
|
+
return;
|
|
1062
1444
|
}
|
|
1063
|
-
memoryStore.delete(storageKey);
|
|
1064
|
-
notifyKeyListeners(memoryListeners, storageKey);
|
|
1065
|
-
return;
|
|
1066
|
-
}
|
|
1067
1445
|
|
|
1068
|
-
|
|
1446
|
+
removeStoredRaw();
|
|
1447
|
+
});
|
|
1069
1448
|
};
|
|
1070
1449
|
|
|
1071
|
-
const hasItem = (): boolean =>
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
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
|
+
});
|
|
1076
1456
|
|
|
1077
1457
|
const subscribe = (callback: () => void): (() => void) => {
|
|
1078
1458
|
ensureSubscription();
|
|
@@ -1088,7 +1468,9 @@ export function createStorageItem<T = undefined>(
|
|
|
1088
1468
|
|
|
1089
1469
|
const storageItem: StorageItemInternal<T> = {
|
|
1090
1470
|
get,
|
|
1471
|
+
getWithVersion,
|
|
1091
1472
|
set,
|
|
1473
|
+
setIfVersion,
|
|
1092
1474
|
delete: deleteItem,
|
|
1093
1475
|
has: hasItem,
|
|
1094
1476
|
subscribe,
|
|
@@ -1102,6 +1484,7 @@ export function createStorageItem<T = undefined>(
|
|
|
1102
1484
|
_hasExpiration: expiration !== undefined,
|
|
1103
1485
|
_readCacheEnabled: readCache,
|
|
1104
1486
|
_isBiometric: isBiometric,
|
|
1487
|
+
_defaultValue: defaultValue,
|
|
1105
1488
|
...(secureAccessControl !== undefined
|
|
1106
1489
|
? { _secureAccessControl: secureAccessControl }
|
|
1107
1490
|
: {}),
|
|
@@ -1122,6 +1505,7 @@ type BatchReadItem<T> = Pick<
|
|
|
1122
1505
|
_hasExpiration?: boolean;
|
|
1123
1506
|
_readCacheEnabled?: boolean;
|
|
1124
1507
|
_isBiometric?: boolean;
|
|
1508
|
+
_defaultValue?: unknown;
|
|
1125
1509
|
_secureAccessControl?: AccessControl;
|
|
1126
1510
|
};
|
|
1127
1511
|
type BatchRemoveItem = Pick<StorageItem<unknown>, "key" | "scope" | "delete">;
|
|
@@ -1135,154 +1519,174 @@ export function getBatch(
|
|
|
1135
1519
|
items: readonly BatchReadItem<unknown>[],
|
|
1136
1520
|
scope: StorageScope,
|
|
1137
1521
|
): unknown[] {
|
|
1138
|
-
|
|
1522
|
+
return measureOperation(
|
|
1523
|
+
"batch:get",
|
|
1524
|
+
scope,
|
|
1525
|
+
() => {
|
|
1526
|
+
assertBatchScope(items, scope);
|
|
1139
1527
|
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1528
|
+
if (scope === StorageScope.Memory) {
|
|
1529
|
+
return items.map((item) => item.get());
|
|
1530
|
+
}
|
|
1143
1531
|
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
const useBatchCache = items.every((item) => item._readCacheEnabled === true);
|
|
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
|
+
}
|
|
1153
1540
|
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1541
|
+
const rawValues = new Array<string | undefined>(items.length);
|
|
1542
|
+
const keysToFetch: string[] = [];
|
|
1543
|
+
const keyIndexes: number[] = [];
|
|
1157
1544
|
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
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
|
+
}
|
|
1165
1552
|
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1553
|
+
if (item._readCacheEnabled === true) {
|
|
1554
|
+
if (hasCachedRawValue(scope, item.key)) {
|
|
1555
|
+
rawValues[index] = readCachedRawValue(scope, item.key);
|
|
1556
|
+
return;
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1172
1559
|
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1560
|
+
keysToFetch.push(item.key);
|
|
1561
|
+
keyIndexes.push(index);
|
|
1562
|
+
});
|
|
1176
1563
|
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
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
|
+
});
|
|
1184
1575
|
}
|
|
1185
|
-
rawValues[targetIndex] = value;
|
|
1186
|
-
cacheRawValue(scope, key, value);
|
|
1187
|
-
});
|
|
1188
|
-
}
|
|
1189
1576
|
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
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
|
+
);
|
|
1197
1587
|
}
|
|
1198
1588
|
|
|
1199
1589
|
export function setBatch<T>(
|
|
1200
1590
|
items: readonly StorageBatchSetItem<T>[],
|
|
1201
1591
|
scope: StorageScope,
|
|
1202
1592
|
): void {
|
|
1203
|
-
|
|
1204
|
-
|
|
1593
|
+
measureOperation(
|
|
1594
|
+
"batch:set",
|
|
1205
1595
|
scope,
|
|
1206
|
-
|
|
1596
|
+
() => {
|
|
1597
|
+
assertBatchScope(
|
|
1598
|
+
items.map((batchEntry) => batchEntry.item),
|
|
1599
|
+
scope,
|
|
1600
|
+
);
|
|
1207
1601
|
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1602
|
+
if (scope === StorageScope.Memory) {
|
|
1603
|
+
items.forEach(({ item, value }) => item.set(value));
|
|
1604
|
+
return;
|
|
1605
|
+
}
|
|
1212
1606
|
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
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
|
+
}
|
|
1226
1620
|
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
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;
|
|
1242
1647
|
}
|
|
1243
|
-
});
|
|
1244
1648
|
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
WebStorage.setBatch(group.keys, group.values, scope);
|
|
1248
|
-
group.keys.forEach((key, index) =>
|
|
1249
|
-
cacheRawValue(scope, key, group.values[index]),
|
|
1649
|
+
const useRawBatchPath = items.every(({ item }) =>
|
|
1650
|
+
canUseRawBatchPath(asInternal(item)),
|
|
1250
1651
|
);
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1652
|
+
if (!useRawBatchPath) {
|
|
1653
|
+
items.forEach(({ item, value }) => item.set(value));
|
|
1654
|
+
return;
|
|
1655
|
+
}
|
|
1254
1656
|
|
|
1255
|
-
|
|
1256
|
-
|
|
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,
|
|
1257
1663
|
);
|
|
1258
|
-
if (!useRawBatchPath) {
|
|
1259
|
-
items.forEach(({ item, value }) => item.set(value));
|
|
1260
|
-
return;
|
|
1261
|
-
}
|
|
1262
|
-
|
|
1263
|
-
const keys = items.map((entry) => entry.item.key);
|
|
1264
|
-
const values = items.map((entry) => entry.item.serialize(entry.value));
|
|
1265
|
-
WebStorage.setBatch(keys, values, scope);
|
|
1266
|
-
keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
|
|
1267
1664
|
}
|
|
1268
1665
|
|
|
1269
1666
|
export function removeBatch(
|
|
1270
1667
|
items: readonly BatchRemoveItem[],
|
|
1271
1668
|
scope: StorageScope,
|
|
1272
1669
|
): void {
|
|
1273
|
-
|
|
1670
|
+
measureOperation(
|
|
1671
|
+
"batch:remove",
|
|
1672
|
+
scope,
|
|
1673
|
+
() => {
|
|
1674
|
+
assertBatchScope(items, scope);
|
|
1274
1675
|
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1676
|
+
if (scope === StorageScope.Memory) {
|
|
1677
|
+
items.forEach((item) => item.delete());
|
|
1678
|
+
return;
|
|
1679
|
+
}
|
|
1279
1680
|
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
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
|
+
);
|
|
1286
1690
|
}
|
|
1287
1691
|
|
|
1288
1692
|
export function registerMigration(version: number, migration: Migration): void {
|
|
@@ -1300,92 +1704,124 @@ export function registerMigration(version: number, migration: Migration): void {
|
|
|
1300
1704
|
export function migrateToLatest(
|
|
1301
1705
|
scope: StorageScope = StorageScope.Disk,
|
|
1302
1706
|
): number {
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
.
|
|
1307
|
-
|
|
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
|
+
};
|
|
1308
1721
|
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
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
|
+
});
|
|
1316
1731
|
|
|
1317
|
-
|
|
1318
|
-
const migration = registeredMigrations.get(version);
|
|
1319
|
-
if (!migration) {
|
|
1320
|
-
return;
|
|
1321
|
-
}
|
|
1322
|
-
migration(context);
|
|
1323
|
-
writeMigrationVersion(scope, version);
|
|
1324
|
-
appliedVersion = version;
|
|
1732
|
+
return appliedVersion;
|
|
1325
1733
|
});
|
|
1326
|
-
|
|
1327
|
-
return appliedVersion;
|
|
1328
1734
|
}
|
|
1329
1735
|
|
|
1330
1736
|
export function runTransaction<T>(
|
|
1331
1737
|
scope: StorageScope,
|
|
1332
1738
|
transaction: (context: TransactionContext) => T,
|
|
1333
1739
|
): T {
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1740
|
+
return measureOperation("transaction:run", scope, () => {
|
|
1741
|
+
assertValidScope(scope);
|
|
1742
|
+
if (scope === StorageScope.Secure) {
|
|
1743
|
+
flushSecureWrites();
|
|
1744
|
+
}
|
|
1338
1745
|
|
|
1339
|
-
|
|
1746
|
+
const rollback = new Map<string, string | undefined>();
|
|
1340
1747
|
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1748
|
+
const rememberRollback = (key: string) => {
|
|
1749
|
+
if (rollback.has(key)) {
|
|
1750
|
+
return;
|
|
1751
|
+
}
|
|
1752
|
+
rollback.set(key, getRawValue(key, scope));
|
|
1753
|
+
};
|
|
1347
1754
|
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
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
|
+
};
|
|
1374
1781
|
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
.
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
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();
|
|
1385
1810
|
}
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
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
|
+
});
|
|
1389
1825
|
}
|
|
1390
1826
|
|
|
1391
1827
|
export type SecureAuthStorageConfig<K extends string = string> = Record<
|
|
@@ -1393,6 +1829,7 @@ export type SecureAuthStorageConfig<K extends string = string> = Record<
|
|
|
1393
1829
|
{
|
|
1394
1830
|
ttlMs?: number;
|
|
1395
1831
|
biometric?: boolean;
|
|
1832
|
+
biometricLevel?: BiometricLevel;
|
|
1396
1833
|
accessControl?: AccessControl;
|
|
1397
1834
|
}
|
|
1398
1835
|
>;
|
|
@@ -1416,6 +1853,9 @@ export function createSecureAuthStorage<K extends string>(
|
|
|
1416
1853
|
...(itemConfig.biometric !== undefined
|
|
1417
1854
|
? { biometric: itemConfig.biometric }
|
|
1418
1855
|
: {}),
|
|
1856
|
+
...(itemConfig.biometricLevel !== undefined
|
|
1857
|
+
? { biometricLevel: itemConfig.biometricLevel }
|
|
1858
|
+
: {}),
|
|
1419
1859
|
...(itemConfig.accessControl !== undefined
|
|
1420
1860
|
? { accessControl: itemConfig.accessControl }
|
|
1421
1861
|
: {}),
|