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.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { useRef, useSyncExternalStore } from "react";
|
|
2
1
|
import { NitroModules } from "react-native-nitro-modules";
|
|
3
2
|
import type { Storage } from "./Storage.nitro";
|
|
4
3
|
import { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
|
|
@@ -11,6 +10,7 @@ import {
|
|
|
11
10
|
decodeNativeBatchValue,
|
|
12
11
|
serializeWithPrimitiveFastPath,
|
|
13
12
|
deserializeWithPrimitiveFastPath,
|
|
13
|
+
toVersionToken,
|
|
14
14
|
prefixKey,
|
|
15
15
|
isNamespaced,
|
|
16
16
|
} from "./internal";
|
|
@@ -23,6 +23,31 @@ export type Validator<T> = (value: unknown) => value is T;
|
|
|
23
23
|
export type ExpirationConfig = {
|
|
24
24
|
ttlMs: number;
|
|
25
25
|
};
|
|
26
|
+
export type StorageVersion = string;
|
|
27
|
+
export type VersionedValue<T> = {
|
|
28
|
+
value: T;
|
|
29
|
+
version: StorageVersion;
|
|
30
|
+
};
|
|
31
|
+
export type StorageMetricsEvent = {
|
|
32
|
+
operation: string;
|
|
33
|
+
scope: StorageScope;
|
|
34
|
+
durationMs: number;
|
|
35
|
+
keysCount: number;
|
|
36
|
+
};
|
|
37
|
+
export type StorageMetricsObserver = (event: StorageMetricsEvent) => void;
|
|
38
|
+
export type StorageMetricSummary = {
|
|
39
|
+
count: number;
|
|
40
|
+
totalDurationMs: number;
|
|
41
|
+
avgDurationMs: number;
|
|
42
|
+
maxDurationMs: number;
|
|
43
|
+
};
|
|
44
|
+
export type WebSecureStorageBackend = {
|
|
45
|
+
getItem: (key: string) => string | null;
|
|
46
|
+
setItem: (key: string, value: string) => void;
|
|
47
|
+
removeItem: (key: string) => void;
|
|
48
|
+
clear: () => void;
|
|
49
|
+
getAllKeys: () => string[];
|
|
50
|
+
};
|
|
26
51
|
|
|
27
52
|
export type MigrationContext = {
|
|
28
53
|
scope: StorageScope;
|
|
@@ -56,11 +81,25 @@ type RawBatchPathItem = {
|
|
|
56
81
|
_secureAccessControl?: AccessControl;
|
|
57
82
|
};
|
|
58
83
|
|
|
59
|
-
function asInternal(item: StorageItem<
|
|
60
|
-
return item as
|
|
84
|
+
function asInternal<T>(item: StorageItem<T>): StorageItemInternal<T> {
|
|
85
|
+
return item as StorageItemInternal<T>;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function isUpdater<T>(
|
|
89
|
+
valueOrFn: T | ((prev: T) => T),
|
|
90
|
+
): valueOrFn is (prev: T) => T {
|
|
91
|
+
return typeof valueOrFn === "function";
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function typedKeys<K extends string, V>(record: Record<K, V>): K[] {
|
|
95
|
+
return Object.keys(record) as K[];
|
|
61
96
|
}
|
|
62
97
|
type NonMemoryScope = StorageScope.Disk | StorageScope.Secure;
|
|
63
|
-
type PendingSecureWrite = {
|
|
98
|
+
type PendingSecureWrite = {
|
|
99
|
+
key: string;
|
|
100
|
+
value: string | undefined;
|
|
101
|
+
accessControl?: AccessControl;
|
|
102
|
+
};
|
|
64
103
|
|
|
65
104
|
const registeredMigrations = new Map<number, Migration>();
|
|
66
105
|
const runMicrotask =
|
|
@@ -95,6 +134,52 @@ const scopedRawCache = new Map<NonMemoryScope, Map<string, string | undefined>>(
|
|
|
95
134
|
const pendingSecureWrites = new Map<string, PendingSecureWrite>();
|
|
96
135
|
let secureFlushScheduled = false;
|
|
97
136
|
let secureDefaultAccessControl: AccessControl = AccessControl.WhenUnlocked;
|
|
137
|
+
let metricsObserver: StorageMetricsObserver | undefined;
|
|
138
|
+
const metricsCounters = new Map<
|
|
139
|
+
string,
|
|
140
|
+
{ count: number; totalDurationMs: number; maxDurationMs: number }
|
|
141
|
+
>();
|
|
142
|
+
|
|
143
|
+
function recordMetric(
|
|
144
|
+
operation: string,
|
|
145
|
+
scope: StorageScope,
|
|
146
|
+
durationMs: number,
|
|
147
|
+
keysCount = 1,
|
|
148
|
+
): void {
|
|
149
|
+
const existing = metricsCounters.get(operation);
|
|
150
|
+
if (!existing) {
|
|
151
|
+
metricsCounters.set(operation, {
|
|
152
|
+
count: 1,
|
|
153
|
+
totalDurationMs: durationMs,
|
|
154
|
+
maxDurationMs: durationMs,
|
|
155
|
+
});
|
|
156
|
+
} else {
|
|
157
|
+
existing.count += 1;
|
|
158
|
+
existing.totalDurationMs += durationMs;
|
|
159
|
+
existing.maxDurationMs = Math.max(existing.maxDurationMs, durationMs);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
metricsObserver?.({
|
|
163
|
+
operation,
|
|
164
|
+
scope,
|
|
165
|
+
durationMs,
|
|
166
|
+
keysCount,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function measureOperation<T>(
|
|
171
|
+
operation: string,
|
|
172
|
+
scope: StorageScope,
|
|
173
|
+
fn: () => T,
|
|
174
|
+
keysCount = 1,
|
|
175
|
+
): T {
|
|
176
|
+
const start = Date.now();
|
|
177
|
+
try {
|
|
178
|
+
return fn();
|
|
179
|
+
} finally {
|
|
180
|
+
recordMetric(operation, scope, Date.now() - start, keysCount);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
98
183
|
|
|
99
184
|
function getScopedListeners(scope: NonMemoryScope): KeyListenerRegistry {
|
|
100
185
|
return scopedListeners.get(scope)!;
|
|
@@ -185,31 +270,47 @@ function flushSecureWrites(): void {
|
|
|
185
270
|
const writes = Array.from(pendingSecureWrites.values());
|
|
186
271
|
pendingSecureWrites.clear();
|
|
187
272
|
|
|
188
|
-
const
|
|
189
|
-
|
|
273
|
+
const groupedSetWrites = new Map<
|
|
274
|
+
AccessControl,
|
|
275
|
+
{ keys: string[]; values: string[] }
|
|
276
|
+
>();
|
|
190
277
|
const keysToRemove: string[] = [];
|
|
191
278
|
|
|
192
|
-
writes.forEach(({ key, value }) => {
|
|
279
|
+
writes.forEach(({ key, value, accessControl }) => {
|
|
193
280
|
if (value === undefined) {
|
|
194
281
|
keysToRemove.push(key);
|
|
195
282
|
} else {
|
|
196
|
-
|
|
197
|
-
|
|
283
|
+
const resolvedAccessControl = accessControl ?? secureDefaultAccessControl;
|
|
284
|
+
const existingGroup = groupedSetWrites.get(resolvedAccessControl);
|
|
285
|
+
const group = existingGroup ?? { keys: [], values: [] };
|
|
286
|
+
group.keys.push(key);
|
|
287
|
+
group.values.push(value);
|
|
288
|
+
if (!existingGroup) {
|
|
289
|
+
groupedSetWrites.set(resolvedAccessControl, group);
|
|
290
|
+
}
|
|
198
291
|
}
|
|
199
292
|
});
|
|
200
293
|
|
|
201
294
|
const storageModule = getStorageModule();
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
storageModule.setBatch(
|
|
205
|
-
}
|
|
295
|
+
groupedSetWrites.forEach((group, accessControl) => {
|
|
296
|
+
storageModule.setSecureAccessControl(accessControl);
|
|
297
|
+
storageModule.setBatch(group.keys, group.values, StorageScope.Secure);
|
|
298
|
+
});
|
|
206
299
|
if (keysToRemove.length > 0) {
|
|
207
300
|
storageModule.removeBatch(keysToRemove, StorageScope.Secure);
|
|
208
301
|
}
|
|
209
302
|
}
|
|
210
303
|
|
|
211
|
-
function scheduleSecureWrite(
|
|
212
|
-
|
|
304
|
+
function scheduleSecureWrite(
|
|
305
|
+
key: string,
|
|
306
|
+
value: string | undefined,
|
|
307
|
+
accessControl?: AccessControl,
|
|
308
|
+
): void {
|
|
309
|
+
const pendingWrite: PendingSecureWrite = { key, value };
|
|
310
|
+
if (accessControl !== undefined) {
|
|
311
|
+
pendingWrite.accessControl = accessControl;
|
|
312
|
+
}
|
|
313
|
+
pendingSecureWrites.set(key, pendingWrite);
|
|
213
314
|
if (secureFlushScheduled) {
|
|
214
315
|
return;
|
|
215
316
|
}
|
|
@@ -323,103 +424,212 @@ function writeMigrationVersion(scope: StorageScope, version: number): void {
|
|
|
323
424
|
|
|
324
425
|
export const storage = {
|
|
325
426
|
clear: (scope: StorageScope) => {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
427
|
+
measureOperation("storage:clear", scope, () => {
|
|
428
|
+
if (scope === StorageScope.Memory) {
|
|
429
|
+
memoryStore.clear();
|
|
430
|
+
notifyAllListeners(memoryListeners);
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
331
433
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
434
|
+
if (scope === StorageScope.Secure) {
|
|
435
|
+
flushSecureWrites();
|
|
436
|
+
pendingSecureWrites.clear();
|
|
437
|
+
}
|
|
336
438
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
getStorageModule().clearSecureBiometric();
|
|
341
|
-
}
|
|
439
|
+
clearScopeRawCache(scope);
|
|
440
|
+
getStorageModule().clear(scope);
|
|
441
|
+
});
|
|
342
442
|
},
|
|
343
443
|
clearAll: () => {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
444
|
+
measureOperation(
|
|
445
|
+
"storage:clearAll",
|
|
446
|
+
StorageScope.Memory,
|
|
447
|
+
() => {
|
|
448
|
+
storage.clear(StorageScope.Memory);
|
|
449
|
+
storage.clear(StorageScope.Disk);
|
|
450
|
+
storage.clear(StorageScope.Secure);
|
|
451
|
+
},
|
|
452
|
+
3,
|
|
453
|
+
);
|
|
347
454
|
},
|
|
348
455
|
clearNamespace: (namespace: string, scope: StorageScope) => {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
456
|
+
measureOperation("storage:clearNamespace", scope, () => {
|
|
457
|
+
assertValidScope(scope);
|
|
458
|
+
if (scope === StorageScope.Memory) {
|
|
459
|
+
for (const key of memoryStore.keys()) {
|
|
460
|
+
if (isNamespaced(key, namespace)) {
|
|
461
|
+
memoryStore.delete(key);
|
|
462
|
+
}
|
|
354
463
|
}
|
|
464
|
+
notifyAllListeners(memoryListeners);
|
|
465
|
+
return;
|
|
355
466
|
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
}
|
|
359
|
-
if (scope === StorageScope.Secure) {
|
|
360
|
-
flushSecureWrites();
|
|
361
|
-
}
|
|
362
|
-
const keys = getStorageModule().getAllKeys(scope);
|
|
363
|
-
const namespacedKeys = keys.filter((k) => isNamespaced(k, namespace));
|
|
364
|
-
if (namespacedKeys.length > 0) {
|
|
365
|
-
getStorageModule().removeBatch(namespacedKeys, scope);
|
|
366
|
-
namespacedKeys.forEach((k) => cacheRawValue(scope, k, undefined));
|
|
467
|
+
|
|
468
|
+
const keyPrefix = prefixKey(namespace, "");
|
|
367
469
|
if (scope === StorageScope.Secure) {
|
|
368
|
-
|
|
470
|
+
flushSecureWrites();
|
|
369
471
|
}
|
|
370
|
-
|
|
472
|
+
|
|
473
|
+
clearScopeRawCache(scope);
|
|
474
|
+
getStorageModule().removeByPrefix(keyPrefix, scope);
|
|
475
|
+
});
|
|
371
476
|
},
|
|
372
477
|
clearBiometric: () => {
|
|
373
|
-
|
|
478
|
+
measureOperation("storage:clearBiometric", StorageScope.Secure, () => {
|
|
479
|
+
getStorageModule().clearSecureBiometric();
|
|
480
|
+
});
|
|
374
481
|
},
|
|
375
482
|
has: (key: string, scope: StorageScope): boolean => {
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
483
|
+
return measureOperation("storage:has", scope, () => {
|
|
484
|
+
assertValidScope(scope);
|
|
485
|
+
if (scope === StorageScope.Memory) {
|
|
486
|
+
return memoryStore.has(key);
|
|
487
|
+
}
|
|
488
|
+
return getStorageModule().has(key, scope);
|
|
489
|
+
});
|
|
381
490
|
},
|
|
382
491
|
getAllKeys: (scope: StorageScope): string[] => {
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
492
|
+
return measureOperation("storage:getAllKeys", scope, () => {
|
|
493
|
+
assertValidScope(scope);
|
|
494
|
+
if (scope === StorageScope.Memory) {
|
|
495
|
+
return Array.from(memoryStore.keys());
|
|
496
|
+
}
|
|
497
|
+
return getStorageModule().getAllKeys(scope);
|
|
498
|
+
});
|
|
499
|
+
},
|
|
500
|
+
getKeysByPrefix: (prefix: string, scope: StorageScope): string[] => {
|
|
501
|
+
return measureOperation("storage:getKeysByPrefix", scope, () => {
|
|
502
|
+
assertValidScope(scope);
|
|
503
|
+
if (scope === StorageScope.Memory) {
|
|
504
|
+
return Array.from(memoryStore.keys()).filter((key) =>
|
|
505
|
+
key.startsWith(prefix),
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
return getStorageModule().getKeysByPrefix(prefix, scope);
|
|
509
|
+
});
|
|
510
|
+
},
|
|
511
|
+
getByPrefix: (
|
|
512
|
+
prefix: string,
|
|
513
|
+
scope: StorageScope,
|
|
514
|
+
): Record<string, string> => {
|
|
515
|
+
return measureOperation("storage:getByPrefix", scope, () => {
|
|
516
|
+
const result: Record<string, string> = {};
|
|
517
|
+
const keys = storage.getKeysByPrefix(prefix, scope);
|
|
518
|
+
if (keys.length === 0) {
|
|
519
|
+
return result;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (scope === StorageScope.Memory) {
|
|
523
|
+
keys.forEach((key) => {
|
|
524
|
+
const value = memoryStore.get(key);
|
|
525
|
+
if (typeof value === "string") {
|
|
526
|
+
result[key] = value;
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
return result;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const values = getStorageModule().getBatch(keys, scope);
|
|
533
|
+
keys.forEach((key, idx) => {
|
|
534
|
+
const value = decodeNativeBatchValue(values[idx]);
|
|
535
|
+
if (value !== undefined) {
|
|
536
|
+
result[key] = value;
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
return result;
|
|
540
|
+
});
|
|
388
541
|
},
|
|
389
542
|
getAll: (scope: StorageScope): Record<string, string> => {
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
543
|
+
return measureOperation("storage:getAll", scope, () => {
|
|
544
|
+
assertValidScope(scope);
|
|
545
|
+
const result: Record<string, string> = {};
|
|
546
|
+
if (scope === StorageScope.Memory) {
|
|
547
|
+
memoryStore.forEach((value, key) => {
|
|
548
|
+
if (typeof value === "string") result[key] = value;
|
|
549
|
+
});
|
|
550
|
+
return result;
|
|
551
|
+
}
|
|
552
|
+
const keys = getStorageModule().getAllKeys(scope);
|
|
553
|
+
if (keys.length === 0) return result;
|
|
554
|
+
const values = getStorageModule().getBatch(keys, scope);
|
|
555
|
+
keys.forEach((key, idx) => {
|
|
556
|
+
const val = decodeNativeBatchValue(values[idx]);
|
|
557
|
+
if (val !== undefined) result[key] = val;
|
|
395
558
|
});
|
|
396
559
|
return result;
|
|
397
|
-
}
|
|
398
|
-
const keys = getStorageModule().getAllKeys(scope);
|
|
399
|
-
if (keys.length === 0) return result;
|
|
400
|
-
const values = getStorageModule().getBatch(keys, scope);
|
|
401
|
-
keys.forEach((key, idx) => {
|
|
402
|
-
const val = decodeNativeBatchValue(values[idx]);
|
|
403
|
-
if (val !== undefined) result[key] = val;
|
|
404
560
|
});
|
|
405
|
-
return result;
|
|
406
561
|
},
|
|
407
562
|
size: (scope: StorageScope): number => {
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
563
|
+
return measureOperation("storage:size", scope, () => {
|
|
564
|
+
assertValidScope(scope);
|
|
565
|
+
if (scope === StorageScope.Memory) {
|
|
566
|
+
return memoryStore.size;
|
|
567
|
+
}
|
|
568
|
+
return getStorageModule().size(scope);
|
|
569
|
+
});
|
|
413
570
|
},
|
|
414
571
|
setAccessControl: (level: AccessControl) => {
|
|
415
|
-
|
|
416
|
-
|
|
572
|
+
measureOperation("storage:setAccessControl", StorageScope.Secure, () => {
|
|
573
|
+
secureDefaultAccessControl = level;
|
|
574
|
+
getStorageModule().setSecureAccessControl(level);
|
|
575
|
+
});
|
|
576
|
+
},
|
|
577
|
+
setSecureWritesAsync: (enabled: boolean) => {
|
|
578
|
+
measureOperation(
|
|
579
|
+
"storage:setSecureWritesAsync",
|
|
580
|
+
StorageScope.Secure,
|
|
581
|
+
() => {
|
|
582
|
+
getStorageModule().setSecureWritesAsync(enabled);
|
|
583
|
+
},
|
|
584
|
+
);
|
|
585
|
+
},
|
|
586
|
+
flushSecureWrites: () => {
|
|
587
|
+
measureOperation("storage:flushSecureWrites", StorageScope.Secure, () => {
|
|
588
|
+
flushSecureWrites();
|
|
589
|
+
});
|
|
417
590
|
},
|
|
418
591
|
setKeychainAccessGroup: (group: string) => {
|
|
419
|
-
|
|
592
|
+
measureOperation(
|
|
593
|
+
"storage:setKeychainAccessGroup",
|
|
594
|
+
StorageScope.Secure,
|
|
595
|
+
() => {
|
|
596
|
+
getStorageModule().setKeychainAccessGroup(group);
|
|
597
|
+
},
|
|
598
|
+
);
|
|
599
|
+
},
|
|
600
|
+
setMetricsObserver: (observer?: StorageMetricsObserver) => {
|
|
601
|
+
metricsObserver = observer;
|
|
602
|
+
},
|
|
603
|
+
getMetricsSnapshot: (): Record<string, StorageMetricSummary> => {
|
|
604
|
+
const snapshot: Record<string, StorageMetricSummary> = {};
|
|
605
|
+
metricsCounters.forEach((value, key) => {
|
|
606
|
+
snapshot[key] = {
|
|
607
|
+
count: value.count,
|
|
608
|
+
totalDurationMs: value.totalDurationMs,
|
|
609
|
+
avgDurationMs:
|
|
610
|
+
value.count === 0 ? 0 : value.totalDurationMs / value.count,
|
|
611
|
+
maxDurationMs: value.maxDurationMs,
|
|
612
|
+
};
|
|
613
|
+
});
|
|
614
|
+
return snapshot;
|
|
615
|
+
},
|
|
616
|
+
resetMetrics: () => {
|
|
617
|
+
metricsCounters.clear();
|
|
420
618
|
},
|
|
421
619
|
};
|
|
422
620
|
|
|
621
|
+
export function setWebSecureStorageBackend(
|
|
622
|
+
_backend?: WebSecureStorageBackend,
|
|
623
|
+
): void {
|
|
624
|
+
// Native platforms do not use web secure backends.
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
export function getWebSecureStorageBackend():
|
|
628
|
+
| WebSecureStorageBackend
|
|
629
|
+
| undefined {
|
|
630
|
+
return undefined;
|
|
631
|
+
}
|
|
632
|
+
|
|
423
633
|
export interface StorageItemConfig<T> {
|
|
424
634
|
key: string;
|
|
425
635
|
scope: StorageScope;
|
|
@@ -434,12 +644,18 @@ export interface StorageItemConfig<T> {
|
|
|
434
644
|
coalesceSecureWrites?: boolean;
|
|
435
645
|
namespace?: string;
|
|
436
646
|
biometric?: boolean;
|
|
647
|
+
biometricLevel?: BiometricLevel;
|
|
437
648
|
accessControl?: AccessControl;
|
|
438
649
|
}
|
|
439
650
|
|
|
440
651
|
export interface StorageItem<T> {
|
|
441
652
|
get: () => T;
|
|
653
|
+
getWithVersion: () => VersionedValue<T>;
|
|
442
654
|
set: (value: T | ((prev: T) => T)) => void;
|
|
655
|
+
setIfVersion: (
|
|
656
|
+
version: StorageVersion,
|
|
657
|
+
value: T | ((prev: T) => T),
|
|
658
|
+
) => boolean;
|
|
443
659
|
delete: () => void;
|
|
444
660
|
has: () => boolean;
|
|
445
661
|
subscribe: (callback: () => void) => () => void;
|
|
@@ -455,6 +671,7 @@ type StorageItemInternal<T> = StorageItem<T> & {
|
|
|
455
671
|
_hasExpiration: boolean;
|
|
456
672
|
_readCacheEnabled: boolean;
|
|
457
673
|
_isBiometric: boolean;
|
|
674
|
+
_defaultValue: T;
|
|
458
675
|
_secureAccessControl?: AccessControl;
|
|
459
676
|
};
|
|
460
677
|
|
|
@@ -467,6 +684,14 @@ function canUseRawBatchPath(item: RawBatchPathItem): boolean {
|
|
|
467
684
|
);
|
|
468
685
|
}
|
|
469
686
|
|
|
687
|
+
function canUseSecureRawBatchPath(item: RawBatchPathItem): boolean {
|
|
688
|
+
return (
|
|
689
|
+
item._hasExpiration === false &&
|
|
690
|
+
item._hasValidation === false &&
|
|
691
|
+
item._isBiometric !== true
|
|
692
|
+
);
|
|
693
|
+
}
|
|
694
|
+
|
|
470
695
|
function defaultSerialize<T>(value: T): string {
|
|
471
696
|
return serializeWithPrimitiveFastPath(value);
|
|
472
697
|
}
|
|
@@ -482,8 +707,14 @@ export function createStorageItem<T = undefined>(
|
|
|
482
707
|
const serialize = config.serialize ?? defaultSerialize;
|
|
483
708
|
const deserialize = config.deserialize ?? defaultDeserialize;
|
|
484
709
|
const isMemory = config.scope === StorageScope.Memory;
|
|
485
|
-
const
|
|
486
|
-
config.
|
|
710
|
+
const resolvedBiometricLevel =
|
|
711
|
+
config.scope === StorageScope.Secure
|
|
712
|
+
? (config.biometricLevel ??
|
|
713
|
+
(config.biometric === true
|
|
714
|
+
? BiometricLevel.BiometryOnly
|
|
715
|
+
: BiometricLevel.None))
|
|
716
|
+
: BiometricLevel.None;
|
|
717
|
+
const isBiometric = resolvedBiometricLevel !== BiometricLevel.None;
|
|
487
718
|
const secureAccessControl = config.accessControl;
|
|
488
719
|
const validate = config.validate;
|
|
489
720
|
const onValidationError = config.onValidationError;
|
|
@@ -496,8 +727,8 @@ export function createStorageItem<T = undefined>(
|
|
|
496
727
|
const coalesceSecureWrites =
|
|
497
728
|
config.scope === StorageScope.Secure &&
|
|
498
729
|
config.coalesceSecureWrites === true &&
|
|
499
|
-
!isBiometric
|
|
500
|
-
|
|
730
|
+
!isBiometric;
|
|
731
|
+
const defaultValue = config.defaultValue as T;
|
|
501
732
|
const nonMemoryScope: NonMemoryScope | null =
|
|
502
733
|
config.scope === StorageScope.Disk
|
|
503
734
|
? StorageScope.Disk
|
|
@@ -514,11 +745,13 @@ export function createStorageItem<T = undefined>(
|
|
|
514
745
|
let lastRaw: unknown = undefined;
|
|
515
746
|
let lastValue: T | undefined;
|
|
516
747
|
let hasLastValue = false;
|
|
748
|
+
let lastExpiresAt: number | null | undefined = undefined;
|
|
517
749
|
|
|
518
750
|
const invalidateParsedCache = () => {
|
|
519
751
|
lastRaw = undefined;
|
|
520
752
|
lastValue = undefined;
|
|
521
753
|
hasLastValue = false;
|
|
754
|
+
lastExpiresAt = undefined;
|
|
522
755
|
};
|
|
523
756
|
|
|
524
757
|
const ensureSubscription = () => {
|
|
@@ -556,7 +789,7 @@ export function createStorageItem<T = undefined>(
|
|
|
556
789
|
return undefined;
|
|
557
790
|
}
|
|
558
791
|
}
|
|
559
|
-
return memoryStore.get(storageKey)
|
|
792
|
+
return memoryStore.get(storageKey);
|
|
560
793
|
}
|
|
561
794
|
|
|
562
795
|
if (
|
|
@@ -584,14 +817,22 @@ export function createStorageItem<T = undefined>(
|
|
|
584
817
|
|
|
585
818
|
const writeStoredRaw = (rawValue: string): void => {
|
|
586
819
|
if (isBiometric) {
|
|
587
|
-
getStorageModule().
|
|
820
|
+
getStorageModule().setSecureBiometricWithLevel(
|
|
821
|
+
storageKey,
|
|
822
|
+
rawValue,
|
|
823
|
+
resolvedBiometricLevel,
|
|
824
|
+
);
|
|
588
825
|
return;
|
|
589
826
|
}
|
|
590
827
|
|
|
591
828
|
cacheRawValue(nonMemoryScope!, storageKey, rawValue);
|
|
592
829
|
|
|
593
830
|
if (coalesceSecureWrites) {
|
|
594
|
-
scheduleSecureWrite(
|
|
831
|
+
scheduleSecureWrite(
|
|
832
|
+
storageKey,
|
|
833
|
+
rawValue,
|
|
834
|
+
secureAccessControl ?? secureDefaultAccessControl,
|
|
835
|
+
);
|
|
595
836
|
return;
|
|
596
837
|
}
|
|
597
838
|
|
|
@@ -614,7 +855,11 @@ export function createStorageItem<T = undefined>(
|
|
|
614
855
|
cacheRawValue(nonMemoryScope!, storageKey, undefined);
|
|
615
856
|
|
|
616
857
|
if (coalesceSecureWrites) {
|
|
617
|
-
scheduleSecureWrite(
|
|
858
|
+
scheduleSecureWrite(
|
|
859
|
+
storageKey,
|
|
860
|
+
undefined,
|
|
861
|
+
secureAccessControl ?? secureDefaultAccessControl,
|
|
862
|
+
);
|
|
618
863
|
return;
|
|
619
864
|
}
|
|
620
865
|
|
|
@@ -654,7 +899,7 @@ export function createStorageItem<T = undefined>(
|
|
|
654
899
|
return onValidationError(invalidValue);
|
|
655
900
|
}
|
|
656
901
|
|
|
657
|
-
return
|
|
902
|
+
return defaultValue;
|
|
658
903
|
};
|
|
659
904
|
|
|
660
905
|
const ensureValidatedValue = (
|
|
@@ -667,7 +912,7 @@ export function createStorageItem<T = undefined>(
|
|
|
667
912
|
|
|
668
913
|
const resolved = resolveInvalidValue(candidate);
|
|
669
914
|
if (validate && !validate(resolved)) {
|
|
670
|
-
return
|
|
915
|
+
return defaultValue;
|
|
671
916
|
}
|
|
672
917
|
if (hadStoredValue) {
|
|
673
918
|
writeValueWithoutValidation(resolved);
|
|
@@ -675,39 +920,64 @@ export function createStorageItem<T = undefined>(
|
|
|
675
920
|
return resolved;
|
|
676
921
|
};
|
|
677
922
|
|
|
678
|
-
const
|
|
923
|
+
const getInternal = (): T => {
|
|
679
924
|
const raw = readStoredRaw();
|
|
680
925
|
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
926
|
+
if (!memoryExpiration && raw === lastRaw && hasLastValue) {
|
|
927
|
+
if (!expiration || lastExpiresAt === null) {
|
|
928
|
+
return lastValue as T;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
if (typeof lastExpiresAt === "number") {
|
|
932
|
+
if (lastExpiresAt > Date.now()) {
|
|
933
|
+
return lastValue as T;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
removeStoredRaw();
|
|
937
|
+
invalidateParsedCache();
|
|
938
|
+
onExpired?.(storageKey);
|
|
939
|
+
lastValue = ensureValidatedValue(defaultValue, false);
|
|
940
|
+
hasLastValue = true;
|
|
941
|
+
return lastValue;
|
|
942
|
+
}
|
|
684
943
|
}
|
|
685
944
|
|
|
686
945
|
lastRaw = raw;
|
|
687
946
|
|
|
688
947
|
if (raw === undefined) {
|
|
689
|
-
|
|
948
|
+
lastExpiresAt = undefined;
|
|
949
|
+
lastValue = ensureValidatedValue(defaultValue, false);
|
|
690
950
|
hasLastValue = true;
|
|
691
951
|
return lastValue;
|
|
692
952
|
}
|
|
693
953
|
|
|
694
954
|
if (isMemory) {
|
|
955
|
+
lastExpiresAt = undefined;
|
|
695
956
|
lastValue = ensureValidatedValue(raw, true);
|
|
696
957
|
hasLastValue = true;
|
|
697
958
|
return lastValue;
|
|
698
959
|
}
|
|
699
960
|
|
|
700
|
-
|
|
961
|
+
if (typeof raw !== "string") {
|
|
962
|
+
lastExpiresAt = undefined;
|
|
963
|
+
lastValue = ensureValidatedValue(defaultValue, false);
|
|
964
|
+
hasLastValue = true;
|
|
965
|
+
return lastValue;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
let deserializableRaw = raw;
|
|
701
969
|
|
|
702
970
|
if (expiration) {
|
|
971
|
+
let envelopeExpiresAt: number | null = null;
|
|
703
972
|
try {
|
|
704
|
-
const parsed = JSON.parse(raw
|
|
973
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
705
974
|
if (isStoredEnvelope(parsed)) {
|
|
975
|
+
envelopeExpiresAt = parsed.expiresAt;
|
|
706
976
|
if (parsed.expiresAt <= Date.now()) {
|
|
707
977
|
removeStoredRaw();
|
|
708
978
|
invalidateParsedCache();
|
|
709
979
|
onExpired?.(storageKey);
|
|
710
|
-
lastValue = ensureValidatedValue(
|
|
980
|
+
lastValue = ensureValidatedValue(defaultValue, false);
|
|
711
981
|
hasLastValue = true;
|
|
712
982
|
return lastValue;
|
|
713
983
|
}
|
|
@@ -717,6 +987,9 @@ export function createStorageItem<T = undefined>(
|
|
|
717
987
|
} catch {
|
|
718
988
|
// Keep backward compatibility with legacy raw values.
|
|
719
989
|
}
|
|
990
|
+
lastExpiresAt = envelopeExpiresAt;
|
|
991
|
+
} else {
|
|
992
|
+
lastExpiresAt = undefined;
|
|
720
993
|
}
|
|
721
994
|
|
|
722
995
|
lastValue = ensureValidatedValue(deserialize(deserializableRaw), true);
|
|
@@ -724,44 +997,74 @@ export function createStorageItem<T = undefined>(
|
|
|
724
997
|
return lastValue;
|
|
725
998
|
};
|
|
726
999
|
|
|
1000
|
+
const getCurrentVersion = (): StorageVersion => {
|
|
1001
|
+
const raw = readStoredRaw();
|
|
1002
|
+
return toVersionToken(raw);
|
|
1003
|
+
};
|
|
1004
|
+
|
|
1005
|
+
const get = (): T =>
|
|
1006
|
+
measureOperation("item:get", config.scope, () => getInternal());
|
|
1007
|
+
|
|
1008
|
+
const getWithVersion = (): VersionedValue<T> =>
|
|
1009
|
+
measureOperation("item:getWithVersion", config.scope, () => ({
|
|
1010
|
+
value: getInternal(),
|
|
1011
|
+
version: getCurrentVersion(),
|
|
1012
|
+
}));
|
|
1013
|
+
|
|
727
1014
|
const set = (valueOrFn: T | ((prev: T) => T)): void => {
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
? (valueOrFn as (prev: T) => T)(currentValue)
|
|
1015
|
+
measureOperation("item:set", config.scope, () => {
|
|
1016
|
+
const newValue = isUpdater(valueOrFn)
|
|
1017
|
+
? valueOrFn(getInternal())
|
|
732
1018
|
: valueOrFn;
|
|
733
1019
|
|
|
734
|
-
|
|
1020
|
+
invalidateParsedCache();
|
|
735
1021
|
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
1022
|
+
if (validate && !validate(newValue)) {
|
|
1023
|
+
throw new Error(
|
|
1024
|
+
`Validation failed for key "${storageKey}" in scope "${StorageScope[config.scope]}".`,
|
|
1025
|
+
);
|
|
1026
|
+
}
|
|
741
1027
|
|
|
742
|
-
|
|
1028
|
+
writeValueWithoutValidation(newValue);
|
|
1029
|
+
});
|
|
743
1030
|
};
|
|
744
1031
|
|
|
1032
|
+
const setIfVersion = (
|
|
1033
|
+
version: StorageVersion,
|
|
1034
|
+
valueOrFn: T | ((prev: T) => T),
|
|
1035
|
+
): boolean =>
|
|
1036
|
+
measureOperation("item:setIfVersion", config.scope, () => {
|
|
1037
|
+
const currentVersion = getCurrentVersion();
|
|
1038
|
+
if (currentVersion !== version) {
|
|
1039
|
+
return false;
|
|
1040
|
+
}
|
|
1041
|
+
set(valueOrFn);
|
|
1042
|
+
return true;
|
|
1043
|
+
});
|
|
1044
|
+
|
|
745
1045
|
const deleteItem = (): void => {
|
|
746
|
-
|
|
1046
|
+
measureOperation("item:delete", config.scope, () => {
|
|
1047
|
+
invalidateParsedCache();
|
|
747
1048
|
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
1049
|
+
if (isMemory) {
|
|
1050
|
+
if (memoryExpiration) {
|
|
1051
|
+
memoryExpiration.delete(storageKey);
|
|
1052
|
+
}
|
|
1053
|
+
memoryStore.delete(storageKey);
|
|
1054
|
+
notifyKeyListeners(memoryListeners, storageKey);
|
|
1055
|
+
return;
|
|
751
1056
|
}
|
|
752
|
-
memoryStore.delete(storageKey);
|
|
753
|
-
notifyKeyListeners(memoryListeners, storageKey);
|
|
754
|
-
return;
|
|
755
|
-
}
|
|
756
1057
|
|
|
757
|
-
|
|
1058
|
+
removeStoredRaw();
|
|
1059
|
+
});
|
|
758
1060
|
};
|
|
759
1061
|
|
|
760
|
-
const hasItem = (): boolean =>
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
1062
|
+
const hasItem = (): boolean =>
|
|
1063
|
+
measureOperation("item:has", config.scope, () => {
|
|
1064
|
+
if (isMemory) return memoryStore.has(storageKey);
|
|
1065
|
+
if (isBiometric) return getStorageModule().hasSecureBiometric(storageKey);
|
|
1066
|
+
return getStorageModule().has(storageKey, config.scope);
|
|
1067
|
+
});
|
|
765
1068
|
|
|
766
1069
|
const subscribe = (callback: () => void): (() => void) => {
|
|
767
1070
|
ensureSubscription();
|
|
@@ -780,7 +1083,9 @@ export function createStorageItem<T = undefined>(
|
|
|
780
1083
|
|
|
781
1084
|
const storageItem: StorageItemInternal<T> = {
|
|
782
1085
|
get,
|
|
1086
|
+
getWithVersion,
|
|
783
1087
|
set,
|
|
1088
|
+
setIfVersion,
|
|
784
1089
|
delete: deleteItem,
|
|
785
1090
|
has: hasItem,
|
|
786
1091
|
subscribe,
|
|
@@ -794,54 +1099,18 @@ export function createStorageItem<T = undefined>(
|
|
|
794
1099
|
_hasExpiration: expiration !== undefined,
|
|
795
1100
|
_readCacheEnabled: readCache,
|
|
796
1101
|
_isBiometric: isBiometric,
|
|
797
|
-
|
|
1102
|
+
_defaultValue: defaultValue,
|
|
1103
|
+
...(secureAccessControl !== undefined
|
|
1104
|
+
? { _secureAccessControl: secureAccessControl }
|
|
1105
|
+
: {}),
|
|
798
1106
|
scope: config.scope,
|
|
799
1107
|
key: storageKey,
|
|
800
1108
|
};
|
|
801
1109
|
|
|
802
|
-
return storageItem
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
export function useStorage<T>(
|
|
806
|
-
item: StorageItem<T>,
|
|
807
|
-
): [T, (value: T | ((prev: T) => T)) => void] {
|
|
808
|
-
const value = useSyncExternalStore(item.subscribe, item.get, item.get);
|
|
809
|
-
return [value, item.set];
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
export function useStorageSelector<T, TSelected>(
|
|
813
|
-
item: StorageItem<T>,
|
|
814
|
-
selector: (value: T) => TSelected,
|
|
815
|
-
isEqual: (prev: TSelected, next: TSelected) => boolean = Object.is,
|
|
816
|
-
): [TSelected, (value: T | ((prev: T) => T)) => void] {
|
|
817
|
-
const selectedRef = useRef<
|
|
818
|
-
{ hasValue: false } | { hasValue: true; value: TSelected }
|
|
819
|
-
>({
|
|
820
|
-
hasValue: false,
|
|
821
|
-
});
|
|
822
|
-
|
|
823
|
-
const getSelectedSnapshot = () => {
|
|
824
|
-
const nextSelected = selector(item.get());
|
|
825
|
-
const current = selectedRef.current;
|
|
826
|
-
if (current.hasValue && isEqual(current.value, nextSelected)) {
|
|
827
|
-
return current.value;
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
selectedRef.current = { hasValue: true, value: nextSelected };
|
|
831
|
-
return nextSelected;
|
|
832
|
-
};
|
|
833
|
-
|
|
834
|
-
const selectedValue = useSyncExternalStore(
|
|
835
|
-
item.subscribe,
|
|
836
|
-
getSelectedSnapshot,
|
|
837
|
-
getSelectedSnapshot,
|
|
838
|
-
);
|
|
839
|
-
return [selectedValue, item.set];
|
|
1110
|
+
return storageItem;
|
|
840
1111
|
}
|
|
841
1112
|
|
|
842
|
-
export
|
|
843
|
-
return item.set;
|
|
844
|
-
}
|
|
1113
|
+
export { useStorage, useStorageSelector, useSetStorage } from "./storage-hooks";
|
|
845
1114
|
|
|
846
1115
|
type BatchReadItem<T> = Pick<
|
|
847
1116
|
StorageItem<T>,
|
|
@@ -851,6 +1120,7 @@ type BatchReadItem<T> = Pick<
|
|
|
851
1120
|
_hasExpiration?: boolean;
|
|
852
1121
|
_readCacheEnabled?: boolean;
|
|
853
1122
|
_isBiometric?: boolean;
|
|
1123
|
+
_defaultValue?: unknown;
|
|
854
1124
|
_secureAccessControl?: AccessControl;
|
|
855
1125
|
};
|
|
856
1126
|
type BatchRemoveItem = Pick<StorageItem<unknown>, "key" | "scope" | "delete">;
|
|
@@ -864,113 +1134,179 @@ export function getBatch(
|
|
|
864
1134
|
items: readonly BatchReadItem<unknown>[],
|
|
865
1135
|
scope: StorageScope,
|
|
866
1136
|
): unknown[] {
|
|
867
|
-
|
|
1137
|
+
return measureOperation(
|
|
1138
|
+
"batch:get",
|
|
1139
|
+
scope,
|
|
1140
|
+
() => {
|
|
1141
|
+
assertBatchScope(items, scope);
|
|
868
1142
|
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
1143
|
+
if (scope === StorageScope.Memory) {
|
|
1144
|
+
return items.map((item) => item.get());
|
|
1145
|
+
}
|
|
872
1146
|
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
1147
|
+
const useRawBatchPath = items.every((item) =>
|
|
1148
|
+
scope === StorageScope.Secure
|
|
1149
|
+
? canUseSecureRawBatchPath(item)
|
|
1150
|
+
: canUseRawBatchPath(item),
|
|
1151
|
+
);
|
|
1152
|
+
if (!useRawBatchPath) {
|
|
1153
|
+
return items.map((item) => item.get());
|
|
1154
|
+
}
|
|
878
1155
|
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
1156
|
+
const rawValues = new Array<string | undefined>(items.length);
|
|
1157
|
+
const keysToFetch: string[] = [];
|
|
1158
|
+
const keyIndexes: number[] = [];
|
|
882
1159
|
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
1160
|
+
items.forEach((item, index) => {
|
|
1161
|
+
if (scope === StorageScope.Secure) {
|
|
1162
|
+
if (hasPendingSecureWrite(item.key)) {
|
|
1163
|
+
rawValues[index] = readPendingSecureWrite(item.key);
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
890
1167
|
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
1168
|
+
if (item._readCacheEnabled === true) {
|
|
1169
|
+
if (hasCachedRawValue(scope, item.key)) {
|
|
1170
|
+
rawValues[index] = readCachedRawValue(scope, item.key);
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
897
1174
|
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
1175
|
+
keysToFetch.push(item.key);
|
|
1176
|
+
keyIndexes.push(index);
|
|
1177
|
+
});
|
|
901
1178
|
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
1179
|
+
if (keysToFetch.length > 0) {
|
|
1180
|
+
const fetchedValues = getStorageModule()
|
|
1181
|
+
.getBatch(keysToFetch, scope)
|
|
1182
|
+
.map((value) => decodeNativeBatchValue(value));
|
|
906
1183
|
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
1184
|
+
fetchedValues.forEach((value, index) => {
|
|
1185
|
+
const key = keysToFetch[index];
|
|
1186
|
+
const targetIndex = keyIndexes[index];
|
|
1187
|
+
if (key === undefined || targetIndex === undefined) {
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
rawValues[targetIndex] = value;
|
|
1191
|
+
cacheRawValue(scope, key, value);
|
|
1192
|
+
});
|
|
1193
|
+
}
|
|
914
1194
|
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
1195
|
+
return items.map((item, index) => {
|
|
1196
|
+
const raw = rawValues[index];
|
|
1197
|
+
if (raw === undefined) {
|
|
1198
|
+
return asInternal(item as StorageItem<unknown>)._defaultValue;
|
|
1199
|
+
}
|
|
1200
|
+
return item.deserialize(raw);
|
|
1201
|
+
});
|
|
1202
|
+
},
|
|
1203
|
+
items.length,
|
|
1204
|
+
);
|
|
922
1205
|
}
|
|
923
1206
|
|
|
924
1207
|
export function setBatch<T>(
|
|
925
1208
|
items: readonly StorageBatchSetItem<T>[],
|
|
926
1209
|
scope: StorageScope,
|
|
927
1210
|
): void {
|
|
928
|
-
|
|
929
|
-
|
|
1211
|
+
measureOperation(
|
|
1212
|
+
"batch:set",
|
|
930
1213
|
scope,
|
|
931
|
-
|
|
1214
|
+
() => {
|
|
1215
|
+
assertBatchScope(
|
|
1216
|
+
items.map((batchEntry) => batchEntry.item),
|
|
1217
|
+
scope,
|
|
1218
|
+
);
|
|
932
1219
|
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
1220
|
+
if (scope === StorageScope.Memory) {
|
|
1221
|
+
items.forEach(({ item, value }) => item.set(value));
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
937
1224
|
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
1225
|
+
if (scope === StorageScope.Secure) {
|
|
1226
|
+
const secureEntries = items.map(({ item, value }) => ({
|
|
1227
|
+
item,
|
|
1228
|
+
value,
|
|
1229
|
+
internal: asInternal(item),
|
|
1230
|
+
}));
|
|
1231
|
+
const canUseSecureBatchPath = secureEntries.every(({ internal }) =>
|
|
1232
|
+
canUseSecureRawBatchPath(internal),
|
|
1233
|
+
);
|
|
1234
|
+
if (!canUseSecureBatchPath) {
|
|
1235
|
+
items.forEach(({ item, value }) => item.set(value));
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
945
1238
|
|
|
946
|
-
|
|
947
|
-
|
|
1239
|
+
flushSecureWrites();
|
|
1240
|
+
const storageModule = getStorageModule();
|
|
1241
|
+
const groupedByAccessControl = new Map<
|
|
1242
|
+
number,
|
|
1243
|
+
{ keys: string[]; values: string[] }
|
|
1244
|
+
>();
|
|
1245
|
+
|
|
1246
|
+
secureEntries.forEach(({ item, value, internal }) => {
|
|
1247
|
+
const accessControl =
|
|
1248
|
+
internal._secureAccessControl ?? secureDefaultAccessControl;
|
|
1249
|
+
const existingGroup = groupedByAccessControl.get(accessControl);
|
|
1250
|
+
const group = existingGroup ?? { keys: [], values: [] };
|
|
1251
|
+
group.keys.push(item.key);
|
|
1252
|
+
group.values.push(item.serialize(value));
|
|
1253
|
+
if (!existingGroup) {
|
|
1254
|
+
groupedByAccessControl.set(accessControl, group);
|
|
1255
|
+
}
|
|
1256
|
+
});
|
|
1257
|
+
|
|
1258
|
+
groupedByAccessControl.forEach((group, accessControl) => {
|
|
1259
|
+
storageModule.setSecureAccessControl(accessControl);
|
|
1260
|
+
storageModule.setBatch(group.keys, group.values, scope);
|
|
1261
|
+
group.keys.forEach((key, index) =>
|
|
1262
|
+
cacheRawValue(scope, key, group.values[index]),
|
|
1263
|
+
);
|
|
1264
|
+
});
|
|
1265
|
+
return;
|
|
1266
|
+
}
|
|
948
1267
|
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
1268
|
+
const useRawBatchPath = items.every(({ item }) =>
|
|
1269
|
+
canUseRawBatchPath(asInternal(item)),
|
|
1270
|
+
);
|
|
1271
|
+
if (!useRawBatchPath) {
|
|
1272
|
+
items.forEach(({ item, value }) => item.set(value));
|
|
1273
|
+
return;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
const keys = items.map((entry) => entry.item.key);
|
|
1277
|
+
const values = items.map((entry) => entry.item.serialize(entry.value));
|
|
1278
|
+
|
|
1279
|
+
getStorageModule().setBatch(keys, values, scope);
|
|
1280
|
+
keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
|
|
1281
|
+
},
|
|
1282
|
+
items.length,
|
|
1283
|
+
);
|
|
955
1284
|
}
|
|
956
1285
|
|
|
957
1286
|
export function removeBatch(
|
|
958
1287
|
items: readonly BatchRemoveItem[],
|
|
959
1288
|
scope: StorageScope,
|
|
960
1289
|
): void {
|
|
961
|
-
|
|
1290
|
+
measureOperation(
|
|
1291
|
+
"batch:remove",
|
|
1292
|
+
scope,
|
|
1293
|
+
() => {
|
|
1294
|
+
assertBatchScope(items, scope);
|
|
962
1295
|
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
1296
|
+
if (scope === StorageScope.Memory) {
|
|
1297
|
+
items.forEach((item) => item.delete());
|
|
1298
|
+
return;
|
|
1299
|
+
}
|
|
967
1300
|
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
1301
|
+
const keys = items.map((item) => item.key);
|
|
1302
|
+
if (scope === StorageScope.Secure) {
|
|
1303
|
+
flushSecureWrites();
|
|
1304
|
+
}
|
|
1305
|
+
getStorageModule().removeBatch(keys, scope);
|
|
1306
|
+
keys.forEach((key) => cacheRawValue(scope, key, undefined));
|
|
1307
|
+
},
|
|
1308
|
+
items.length,
|
|
1309
|
+
);
|
|
974
1310
|
}
|
|
975
1311
|
|
|
976
1312
|
export function registerMigration(version: number, migration: Migration): void {
|
|
@@ -988,92 +1324,124 @@ export function registerMigration(version: number, migration: Migration): void {
|
|
|
988
1324
|
export function migrateToLatest(
|
|
989
1325
|
scope: StorageScope = StorageScope.Disk,
|
|
990
1326
|
): number {
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
.
|
|
995
|
-
|
|
1327
|
+
return measureOperation("migration:run", scope, () => {
|
|
1328
|
+
assertValidScope(scope);
|
|
1329
|
+
const currentVersion = readMigrationVersion(scope);
|
|
1330
|
+
const versions = Array.from(registeredMigrations.keys())
|
|
1331
|
+
.filter((version) => version > currentVersion)
|
|
1332
|
+
.sort((a, b) => a - b);
|
|
1333
|
+
|
|
1334
|
+
let appliedVersion = currentVersion;
|
|
1335
|
+
const context: MigrationContext = {
|
|
1336
|
+
scope,
|
|
1337
|
+
getRaw: (key) => getRawValue(key, scope),
|
|
1338
|
+
setRaw: (key, value) => setRawValue(key, value, scope),
|
|
1339
|
+
removeRaw: (key) => removeRawValue(key, scope),
|
|
1340
|
+
};
|
|
996
1341
|
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1342
|
+
versions.forEach((version) => {
|
|
1343
|
+
const migration = registeredMigrations.get(version);
|
|
1344
|
+
if (!migration) {
|
|
1345
|
+
return;
|
|
1346
|
+
}
|
|
1347
|
+
migration(context);
|
|
1348
|
+
writeMigrationVersion(scope, version);
|
|
1349
|
+
appliedVersion = version;
|
|
1350
|
+
});
|
|
1004
1351
|
|
|
1005
|
-
|
|
1006
|
-
const migration = registeredMigrations.get(version);
|
|
1007
|
-
if (!migration) {
|
|
1008
|
-
return;
|
|
1009
|
-
}
|
|
1010
|
-
migration(context);
|
|
1011
|
-
writeMigrationVersion(scope, version);
|
|
1012
|
-
appliedVersion = version;
|
|
1352
|
+
return appliedVersion;
|
|
1013
1353
|
});
|
|
1014
|
-
|
|
1015
|
-
return appliedVersion;
|
|
1016
1354
|
}
|
|
1017
1355
|
|
|
1018
1356
|
export function runTransaction<T>(
|
|
1019
1357
|
scope: StorageScope,
|
|
1020
1358
|
transaction: (context: TransactionContext) => T,
|
|
1021
1359
|
): T {
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1360
|
+
return measureOperation("transaction:run", scope, () => {
|
|
1361
|
+
assertValidScope(scope);
|
|
1362
|
+
if (scope === StorageScope.Secure) {
|
|
1363
|
+
flushSecureWrites();
|
|
1364
|
+
}
|
|
1026
1365
|
|
|
1027
|
-
|
|
1366
|
+
const rollback = new Map<string, string | undefined>();
|
|
1028
1367
|
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1368
|
+
const rememberRollback = (key: string) => {
|
|
1369
|
+
if (rollback.has(key)) {
|
|
1370
|
+
return;
|
|
1371
|
+
}
|
|
1372
|
+
rollback.set(key, getRawValue(key, scope));
|
|
1373
|
+
};
|
|
1035
1374
|
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1375
|
+
const tx: TransactionContext = {
|
|
1376
|
+
scope,
|
|
1377
|
+
getRaw: (key) => getRawValue(key, scope),
|
|
1378
|
+
setRaw: (key, value) => {
|
|
1379
|
+
rememberRollback(key);
|
|
1380
|
+
setRawValue(key, value, scope);
|
|
1381
|
+
},
|
|
1382
|
+
removeRaw: (key) => {
|
|
1383
|
+
rememberRollback(key);
|
|
1384
|
+
removeRawValue(key, scope);
|
|
1385
|
+
},
|
|
1386
|
+
getItem: (item) => {
|
|
1387
|
+
assertBatchScope([item], scope);
|
|
1388
|
+
return item.get();
|
|
1389
|
+
},
|
|
1390
|
+
setItem: (item, value) => {
|
|
1391
|
+
assertBatchScope([item], scope);
|
|
1392
|
+
rememberRollback(item.key);
|
|
1393
|
+
item.set(value);
|
|
1394
|
+
},
|
|
1395
|
+
removeItem: (item) => {
|
|
1396
|
+
assertBatchScope([item], scope);
|
|
1397
|
+
rememberRollback(item.key);
|
|
1398
|
+
item.delete();
|
|
1399
|
+
},
|
|
1400
|
+
};
|
|
1062
1401
|
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
.
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1402
|
+
try {
|
|
1403
|
+
return transaction(tx);
|
|
1404
|
+
} catch (error) {
|
|
1405
|
+
const rollbackEntries = Array.from(rollback.entries()).reverse();
|
|
1406
|
+
if (scope === StorageScope.Memory) {
|
|
1407
|
+
rollbackEntries.forEach(([key, previousValue]) => {
|
|
1408
|
+
if (previousValue === undefined) {
|
|
1409
|
+
removeRawValue(key, scope);
|
|
1410
|
+
} else {
|
|
1411
|
+
setRawValue(key, previousValue, scope);
|
|
1412
|
+
}
|
|
1413
|
+
});
|
|
1414
|
+
} else {
|
|
1415
|
+
const keysToSet: string[] = [];
|
|
1416
|
+
const valuesToSet: string[] = [];
|
|
1417
|
+
const keysToRemove: string[] = [];
|
|
1418
|
+
|
|
1419
|
+
rollbackEntries.forEach(([key, previousValue]) => {
|
|
1420
|
+
if (previousValue === undefined) {
|
|
1421
|
+
keysToRemove.push(key);
|
|
1422
|
+
} else {
|
|
1423
|
+
keysToSet.push(key);
|
|
1424
|
+
valuesToSet.push(previousValue);
|
|
1425
|
+
}
|
|
1426
|
+
});
|
|
1427
|
+
|
|
1428
|
+
if (scope === StorageScope.Secure) {
|
|
1429
|
+
flushSecureWrites();
|
|
1073
1430
|
}
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1431
|
+
if (keysToSet.length > 0) {
|
|
1432
|
+
getStorageModule().setBatch(keysToSet, valuesToSet, scope);
|
|
1433
|
+
keysToSet.forEach((key, index) =>
|
|
1434
|
+
cacheRawValue(scope, key, valuesToSet[index]),
|
|
1435
|
+
);
|
|
1436
|
+
}
|
|
1437
|
+
if (keysToRemove.length > 0) {
|
|
1438
|
+
getStorageModule().removeBatch(keysToRemove, scope);
|
|
1439
|
+
keysToRemove.forEach((key) => cacheRawValue(scope, key, undefined));
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
throw error;
|
|
1443
|
+
}
|
|
1444
|
+
});
|
|
1077
1445
|
}
|
|
1078
1446
|
|
|
1079
1447
|
export type SecureAuthStorageConfig<K extends string = string> = Record<
|
|
@@ -1081,6 +1449,7 @@ export type SecureAuthStorageConfig<K extends string = string> = Record<
|
|
|
1081
1449
|
{
|
|
1082
1450
|
ttlMs?: number;
|
|
1083
1451
|
biometric?: boolean;
|
|
1452
|
+
biometricLevel?: BiometricLevel;
|
|
1084
1453
|
accessControl?: AccessControl;
|
|
1085
1454
|
}
|
|
1086
1455
|
>;
|
|
@@ -1090,20 +1459,31 @@ export function createSecureAuthStorage<K extends string>(
|
|
|
1090
1459
|
options?: { namespace?: string },
|
|
1091
1460
|
): Record<K, StorageItem<string>> {
|
|
1092
1461
|
const ns = options?.namespace ?? "auth";
|
|
1093
|
-
const result
|
|
1462
|
+
const result: Partial<Record<K, StorageItem<string>>> = {};
|
|
1094
1463
|
|
|
1095
|
-
for (const key of
|
|
1464
|
+
for (const key of typedKeys(config)) {
|
|
1096
1465
|
const itemConfig = config[key];
|
|
1466
|
+
const expirationConfig =
|
|
1467
|
+
itemConfig.ttlMs !== undefined ? { ttlMs: itemConfig.ttlMs } : undefined;
|
|
1097
1468
|
result[key] = createStorageItem<string>({
|
|
1098
1469
|
key,
|
|
1099
1470
|
scope: StorageScope.Secure,
|
|
1100
1471
|
defaultValue: "",
|
|
1101
1472
|
namespace: ns,
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1473
|
+
...(itemConfig.biometric !== undefined
|
|
1474
|
+
? { biometric: itemConfig.biometric }
|
|
1475
|
+
: {}),
|
|
1476
|
+
...(itemConfig.biometricLevel !== undefined
|
|
1477
|
+
? { biometricLevel: itemConfig.biometricLevel }
|
|
1478
|
+
: {}),
|
|
1479
|
+
...(itemConfig.accessControl !== undefined
|
|
1480
|
+
? { accessControl: itemConfig.accessControl }
|
|
1481
|
+
: {}),
|
|
1482
|
+
...(expirationConfig !== undefined
|
|
1483
|
+
? { expiration: expirationConfig }
|
|
1484
|
+
: {}),
|
|
1105
1485
|
});
|
|
1106
1486
|
}
|
|
1107
1487
|
|
|
1108
|
-
return result
|
|
1488
|
+
return result as Record<K, StorageItem<string>>;
|
|
1109
1489
|
}
|