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.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
decodeNativeBatchValue,
|
|
11
11
|
serializeWithPrimitiveFastPath,
|
|
12
12
|
deserializeWithPrimitiveFastPath,
|
|
13
|
+
toVersionToken,
|
|
13
14
|
prefixKey,
|
|
14
15
|
isNamespaced,
|
|
15
16
|
} from "./internal";
|
|
@@ -22,6 +23,31 @@ export type Validator<T> = (value: unknown) => value is T;
|
|
|
22
23
|
export type ExpirationConfig = {
|
|
23
24
|
ttlMs: number;
|
|
24
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
|
+
};
|
|
25
51
|
|
|
26
52
|
export type MigrationContext = {
|
|
27
53
|
scope: StorageScope;
|
|
@@ -69,7 +95,11 @@ function typedKeys<K extends string, V>(record: Record<K, V>): K[] {
|
|
|
69
95
|
return Object.keys(record) as K[];
|
|
70
96
|
}
|
|
71
97
|
type NonMemoryScope = StorageScope.Disk | StorageScope.Secure;
|
|
72
|
-
type PendingSecureWrite = {
|
|
98
|
+
type PendingSecureWrite = {
|
|
99
|
+
key: string;
|
|
100
|
+
value: string | undefined;
|
|
101
|
+
accessControl?: AccessControl;
|
|
102
|
+
};
|
|
73
103
|
|
|
74
104
|
const registeredMigrations = new Map<number, Migration>();
|
|
75
105
|
const runMicrotask =
|
|
@@ -104,6 +134,52 @@ const scopedRawCache = new Map<NonMemoryScope, Map<string, string | undefined>>(
|
|
|
104
134
|
const pendingSecureWrites = new Map<string, PendingSecureWrite>();
|
|
105
135
|
let secureFlushScheduled = false;
|
|
106
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
|
+
}
|
|
107
183
|
|
|
108
184
|
function getScopedListeners(scope: NonMemoryScope): KeyListenerRegistry {
|
|
109
185
|
return scopedListeners.get(scope)!;
|
|
@@ -194,31 +270,47 @@ function flushSecureWrites(): void {
|
|
|
194
270
|
const writes = Array.from(pendingSecureWrites.values());
|
|
195
271
|
pendingSecureWrites.clear();
|
|
196
272
|
|
|
197
|
-
const
|
|
198
|
-
|
|
273
|
+
const groupedSetWrites = new Map<
|
|
274
|
+
AccessControl,
|
|
275
|
+
{ keys: string[]; values: string[] }
|
|
276
|
+
>();
|
|
199
277
|
const keysToRemove: string[] = [];
|
|
200
278
|
|
|
201
|
-
writes.forEach(({ key, value }) => {
|
|
279
|
+
writes.forEach(({ key, value, accessControl }) => {
|
|
202
280
|
if (value === undefined) {
|
|
203
281
|
keysToRemove.push(key);
|
|
204
282
|
} else {
|
|
205
|
-
|
|
206
|
-
|
|
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
|
+
}
|
|
207
291
|
}
|
|
208
292
|
});
|
|
209
293
|
|
|
210
294
|
const storageModule = getStorageModule();
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
storageModule.setBatch(
|
|
214
|
-
}
|
|
295
|
+
groupedSetWrites.forEach((group, accessControl) => {
|
|
296
|
+
storageModule.setSecureAccessControl(accessControl);
|
|
297
|
+
storageModule.setBatch(group.keys, group.values, StorageScope.Secure);
|
|
298
|
+
});
|
|
215
299
|
if (keysToRemove.length > 0) {
|
|
216
300
|
storageModule.removeBatch(keysToRemove, StorageScope.Secure);
|
|
217
301
|
}
|
|
218
302
|
}
|
|
219
303
|
|
|
220
|
-
function scheduleSecureWrite(
|
|
221
|
-
|
|
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);
|
|
222
314
|
if (secureFlushScheduled) {
|
|
223
315
|
return;
|
|
224
316
|
}
|
|
@@ -332,102 +424,212 @@ function writeMigrationVersion(scope: StorageScope, version: number): void {
|
|
|
332
424
|
|
|
333
425
|
export const storage = {
|
|
334
426
|
clear: (scope: StorageScope) => {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
427
|
+
measureOperation("storage:clear", scope, () => {
|
|
428
|
+
if (scope === StorageScope.Memory) {
|
|
429
|
+
memoryStore.clear();
|
|
430
|
+
notifyAllListeners(memoryListeners);
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
340
433
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
434
|
+
if (scope === StorageScope.Secure) {
|
|
435
|
+
flushSecureWrites();
|
|
436
|
+
pendingSecureWrites.clear();
|
|
437
|
+
}
|
|
345
438
|
|
|
346
|
-
|
|
347
|
-
|
|
439
|
+
clearScopeRawCache(scope);
|
|
440
|
+
getStorageModule().clear(scope);
|
|
441
|
+
});
|
|
348
442
|
},
|
|
349
443
|
clearAll: () => {
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
+
);
|
|
353
454
|
},
|
|
354
455
|
clearNamespace: (namespace: string, scope: StorageScope) => {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|
+
}
|
|
360
463
|
}
|
|
464
|
+
notifyAllListeners(memoryListeners);
|
|
465
|
+
return;
|
|
361
466
|
}
|
|
362
|
-
notifyAllListeners(memoryListeners);
|
|
363
|
-
return;
|
|
364
|
-
}
|
|
365
467
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
468
|
+
const keyPrefix = prefixKey(namespace, "");
|
|
469
|
+
if (scope === StorageScope.Secure) {
|
|
470
|
+
flushSecureWrites();
|
|
471
|
+
}
|
|
370
472
|
|
|
371
|
-
|
|
372
|
-
|
|
473
|
+
clearScopeRawCache(scope);
|
|
474
|
+
getStorageModule().removeByPrefix(keyPrefix, scope);
|
|
475
|
+
});
|
|
373
476
|
},
|
|
374
477
|
clearBiometric: () => {
|
|
375
|
-
|
|
478
|
+
measureOperation("storage:clearBiometric", StorageScope.Secure, () => {
|
|
479
|
+
getStorageModule().clearSecureBiometric();
|
|
480
|
+
});
|
|
376
481
|
},
|
|
377
482
|
has: (key: string, scope: StorageScope): boolean => {
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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
|
+
});
|
|
383
490
|
},
|
|
384
491
|
getAllKeys: (scope: StorageScope): string[] => {
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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
|
+
});
|
|
390
541
|
},
|
|
391
542
|
getAll: (scope: StorageScope): Record<string, string> => {
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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;
|
|
397
558
|
});
|
|
398
559
|
return result;
|
|
399
|
-
}
|
|
400
|
-
const keys = getStorageModule().getAllKeys(scope);
|
|
401
|
-
if (keys.length === 0) return result;
|
|
402
|
-
const values = getStorageModule().getBatch(keys, scope);
|
|
403
|
-
keys.forEach((key, idx) => {
|
|
404
|
-
const val = decodeNativeBatchValue(values[idx]);
|
|
405
|
-
if (val !== undefined) result[key] = val;
|
|
406
560
|
});
|
|
407
|
-
return result;
|
|
408
561
|
},
|
|
409
562
|
size: (scope: StorageScope): number => {
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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
|
+
});
|
|
415
570
|
},
|
|
416
571
|
setAccessControl: (level: AccessControl) => {
|
|
417
|
-
|
|
418
|
-
|
|
572
|
+
measureOperation("storage:setAccessControl", StorageScope.Secure, () => {
|
|
573
|
+
secureDefaultAccessControl = level;
|
|
574
|
+
getStorageModule().setSecureAccessControl(level);
|
|
575
|
+
});
|
|
419
576
|
},
|
|
420
577
|
setSecureWritesAsync: (enabled: boolean) => {
|
|
421
|
-
|
|
578
|
+
measureOperation(
|
|
579
|
+
"storage:setSecureWritesAsync",
|
|
580
|
+
StorageScope.Secure,
|
|
581
|
+
() => {
|
|
582
|
+
getStorageModule().setSecureWritesAsync(enabled);
|
|
583
|
+
},
|
|
584
|
+
);
|
|
422
585
|
},
|
|
423
586
|
flushSecureWrites: () => {
|
|
424
|
-
flushSecureWrites()
|
|
587
|
+
measureOperation("storage:flushSecureWrites", StorageScope.Secure, () => {
|
|
588
|
+
flushSecureWrites();
|
|
589
|
+
});
|
|
425
590
|
},
|
|
426
591
|
setKeychainAccessGroup: (group: string) => {
|
|
427
|
-
|
|
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();
|
|
428
618
|
},
|
|
429
619
|
};
|
|
430
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
|
+
|
|
431
633
|
export interface StorageItemConfig<T> {
|
|
432
634
|
key: string;
|
|
433
635
|
scope: StorageScope;
|
|
@@ -442,12 +644,18 @@ export interface StorageItemConfig<T> {
|
|
|
442
644
|
coalesceSecureWrites?: boolean;
|
|
443
645
|
namespace?: string;
|
|
444
646
|
biometric?: boolean;
|
|
647
|
+
biometricLevel?: BiometricLevel;
|
|
445
648
|
accessControl?: AccessControl;
|
|
446
649
|
}
|
|
447
650
|
|
|
448
651
|
export interface StorageItem<T> {
|
|
449
652
|
get: () => T;
|
|
653
|
+
getWithVersion: () => VersionedValue<T>;
|
|
450
654
|
set: (value: T | ((prev: T) => T)) => void;
|
|
655
|
+
setIfVersion: (
|
|
656
|
+
version: StorageVersion,
|
|
657
|
+
value: T | ((prev: T) => T),
|
|
658
|
+
) => boolean;
|
|
451
659
|
delete: () => void;
|
|
452
660
|
has: () => boolean;
|
|
453
661
|
subscribe: (callback: () => void) => () => void;
|
|
@@ -463,6 +671,7 @@ type StorageItemInternal<T> = StorageItem<T> & {
|
|
|
463
671
|
_hasExpiration: boolean;
|
|
464
672
|
_readCacheEnabled: boolean;
|
|
465
673
|
_isBiometric: boolean;
|
|
674
|
+
_defaultValue: T;
|
|
466
675
|
_secureAccessControl?: AccessControl;
|
|
467
676
|
};
|
|
468
677
|
|
|
@@ -498,8 +707,14 @@ export function createStorageItem<T = undefined>(
|
|
|
498
707
|
const serialize = config.serialize ?? defaultSerialize;
|
|
499
708
|
const deserialize = config.deserialize ?? defaultDeserialize;
|
|
500
709
|
const isMemory = config.scope === StorageScope.Memory;
|
|
501
|
-
const
|
|
502
|
-
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;
|
|
503
718
|
const secureAccessControl = config.accessControl;
|
|
504
719
|
const validate = config.validate;
|
|
505
720
|
const onValidationError = config.onValidationError;
|
|
@@ -512,8 +727,7 @@ export function createStorageItem<T = undefined>(
|
|
|
512
727
|
const coalesceSecureWrites =
|
|
513
728
|
config.scope === StorageScope.Secure &&
|
|
514
729
|
config.coalesceSecureWrites === true &&
|
|
515
|
-
!isBiometric
|
|
516
|
-
secureAccessControl === undefined;
|
|
730
|
+
!isBiometric;
|
|
517
731
|
const defaultValue = config.defaultValue as T;
|
|
518
732
|
const nonMemoryScope: NonMemoryScope | null =
|
|
519
733
|
config.scope === StorageScope.Disk
|
|
@@ -603,14 +817,22 @@ export function createStorageItem<T = undefined>(
|
|
|
603
817
|
|
|
604
818
|
const writeStoredRaw = (rawValue: string): void => {
|
|
605
819
|
if (isBiometric) {
|
|
606
|
-
getStorageModule().
|
|
820
|
+
getStorageModule().setSecureBiometricWithLevel(
|
|
821
|
+
storageKey,
|
|
822
|
+
rawValue,
|
|
823
|
+
resolvedBiometricLevel,
|
|
824
|
+
);
|
|
607
825
|
return;
|
|
608
826
|
}
|
|
609
827
|
|
|
610
828
|
cacheRawValue(nonMemoryScope!, storageKey, rawValue);
|
|
611
829
|
|
|
612
830
|
if (coalesceSecureWrites) {
|
|
613
|
-
scheduleSecureWrite(
|
|
831
|
+
scheduleSecureWrite(
|
|
832
|
+
storageKey,
|
|
833
|
+
rawValue,
|
|
834
|
+
secureAccessControl ?? secureDefaultAccessControl,
|
|
835
|
+
);
|
|
614
836
|
return;
|
|
615
837
|
}
|
|
616
838
|
|
|
@@ -633,7 +855,11 @@ export function createStorageItem<T = undefined>(
|
|
|
633
855
|
cacheRawValue(nonMemoryScope!, storageKey, undefined);
|
|
634
856
|
|
|
635
857
|
if (coalesceSecureWrites) {
|
|
636
|
-
scheduleSecureWrite(
|
|
858
|
+
scheduleSecureWrite(
|
|
859
|
+
storageKey,
|
|
860
|
+
undefined,
|
|
861
|
+
secureAccessControl ?? secureDefaultAccessControl,
|
|
862
|
+
);
|
|
637
863
|
return;
|
|
638
864
|
}
|
|
639
865
|
|
|
@@ -694,7 +920,7 @@ export function createStorageItem<T = undefined>(
|
|
|
694
920
|
return resolved;
|
|
695
921
|
};
|
|
696
922
|
|
|
697
|
-
const
|
|
923
|
+
const getInternal = (): T => {
|
|
698
924
|
const raw = readStoredRaw();
|
|
699
925
|
|
|
700
926
|
if (!memoryExpiration && raw === lastRaw && hasLastValue) {
|
|
@@ -771,40 +997,74 @@ export function createStorageItem<T = undefined>(
|
|
|
771
997
|
return lastValue;
|
|
772
998
|
};
|
|
773
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
|
+
|
|
774
1014
|
const set = (valueOrFn: T | ((prev: T) => T)): void => {
|
|
775
|
-
|
|
1015
|
+
measureOperation("item:set", config.scope, () => {
|
|
1016
|
+
const newValue = isUpdater(valueOrFn)
|
|
1017
|
+
? valueOrFn(getInternal())
|
|
1018
|
+
: valueOrFn;
|
|
776
1019
|
|
|
777
|
-
|
|
1020
|
+
invalidateParsedCache();
|
|
778
1021
|
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
1022
|
+
if (validate && !validate(newValue)) {
|
|
1023
|
+
throw new Error(
|
|
1024
|
+
`Validation failed for key "${storageKey}" in scope "${StorageScope[config.scope]}".`,
|
|
1025
|
+
);
|
|
1026
|
+
}
|
|
784
1027
|
|
|
785
|
-
|
|
1028
|
+
writeValueWithoutValidation(newValue);
|
|
1029
|
+
});
|
|
786
1030
|
};
|
|
787
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
|
+
|
|
788
1045
|
const deleteItem = (): void => {
|
|
789
|
-
|
|
1046
|
+
measureOperation("item:delete", config.scope, () => {
|
|
1047
|
+
invalidateParsedCache();
|
|
790
1048
|
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
1049
|
+
if (isMemory) {
|
|
1050
|
+
if (memoryExpiration) {
|
|
1051
|
+
memoryExpiration.delete(storageKey);
|
|
1052
|
+
}
|
|
1053
|
+
memoryStore.delete(storageKey);
|
|
1054
|
+
notifyKeyListeners(memoryListeners, storageKey);
|
|
1055
|
+
return;
|
|
794
1056
|
}
|
|
795
|
-
memoryStore.delete(storageKey);
|
|
796
|
-
notifyKeyListeners(memoryListeners, storageKey);
|
|
797
|
-
return;
|
|
798
|
-
}
|
|
799
1057
|
|
|
800
|
-
|
|
1058
|
+
removeStoredRaw();
|
|
1059
|
+
});
|
|
801
1060
|
};
|
|
802
1061
|
|
|
803
|
-
const hasItem = (): boolean =>
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
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
|
+
});
|
|
808
1068
|
|
|
809
1069
|
const subscribe = (callback: () => void): (() => void) => {
|
|
810
1070
|
ensureSubscription();
|
|
@@ -823,7 +1083,9 @@ export function createStorageItem<T = undefined>(
|
|
|
823
1083
|
|
|
824
1084
|
const storageItem: StorageItemInternal<T> = {
|
|
825
1085
|
get,
|
|
1086
|
+
getWithVersion,
|
|
826
1087
|
set,
|
|
1088
|
+
setIfVersion,
|
|
827
1089
|
delete: deleteItem,
|
|
828
1090
|
has: hasItem,
|
|
829
1091
|
subscribe,
|
|
@@ -837,6 +1099,7 @@ export function createStorageItem<T = undefined>(
|
|
|
837
1099
|
_hasExpiration: expiration !== undefined,
|
|
838
1100
|
_readCacheEnabled: readCache,
|
|
839
1101
|
_isBiometric: isBiometric,
|
|
1102
|
+
_defaultValue: defaultValue,
|
|
840
1103
|
...(secureAccessControl !== undefined
|
|
841
1104
|
? { _secureAccessControl: secureAccessControl }
|
|
842
1105
|
: {}),
|
|
@@ -857,6 +1120,7 @@ type BatchReadItem<T> = Pick<
|
|
|
857
1120
|
_hasExpiration?: boolean;
|
|
858
1121
|
_readCacheEnabled?: boolean;
|
|
859
1122
|
_isBiometric?: boolean;
|
|
1123
|
+
_defaultValue?: unknown;
|
|
860
1124
|
_secureAccessControl?: AccessControl;
|
|
861
1125
|
};
|
|
862
1126
|
type BatchRemoveItem = Pick<StorageItem<unknown>, "key" | "scope" | "delete">;
|
|
@@ -870,159 +1134,179 @@ export function getBatch(
|
|
|
870
1134
|
items: readonly BatchReadItem<unknown>[],
|
|
871
1135
|
scope: StorageScope,
|
|
872
1136
|
): unknown[] {
|
|
873
|
-
|
|
1137
|
+
return measureOperation(
|
|
1138
|
+
"batch:get",
|
|
1139
|
+
scope,
|
|
1140
|
+
() => {
|
|
1141
|
+
assertBatchScope(items, scope);
|
|
874
1142
|
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
1143
|
+
if (scope === StorageScope.Memory) {
|
|
1144
|
+
return items.map((item) => item.get());
|
|
1145
|
+
}
|
|
878
1146
|
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
const useBatchCache = items.every((item) => item._readCacheEnabled === true);
|
|
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
|
+
}
|
|
888
1155
|
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
1156
|
+
const rawValues = new Array<string | undefined>(items.length);
|
|
1157
|
+
const keysToFetch: string[] = [];
|
|
1158
|
+
const keyIndexes: number[] = [];
|
|
892
1159
|
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
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
|
+
}
|
|
900
1167
|
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
1168
|
+
if (item._readCacheEnabled === true) {
|
|
1169
|
+
if (hasCachedRawValue(scope, item.key)) {
|
|
1170
|
+
rawValues[index] = readCachedRawValue(scope, item.key);
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
907
1174
|
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
1175
|
+
keysToFetch.push(item.key);
|
|
1176
|
+
keyIndexes.push(index);
|
|
1177
|
+
});
|
|
911
1178
|
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
1179
|
+
if (keysToFetch.length > 0) {
|
|
1180
|
+
const fetchedValues = getStorageModule()
|
|
1181
|
+
.getBatch(keysToFetch, scope)
|
|
1182
|
+
.map((value) => decodeNativeBatchValue(value));
|
|
916
1183
|
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
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
|
+
});
|
|
922
1193
|
}
|
|
923
|
-
rawValues[targetIndex] = value;
|
|
924
|
-
cacheRawValue(scope, key, value);
|
|
925
|
-
});
|
|
926
|
-
}
|
|
927
1194
|
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
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
|
+
);
|
|
935
1205
|
}
|
|
936
1206
|
|
|
937
1207
|
export function setBatch<T>(
|
|
938
1208
|
items: readonly StorageBatchSetItem<T>[],
|
|
939
1209
|
scope: StorageScope,
|
|
940
1210
|
): void {
|
|
941
|
-
|
|
942
|
-
|
|
1211
|
+
measureOperation(
|
|
1212
|
+
"batch:set",
|
|
943
1213
|
scope,
|
|
944
|
-
|
|
1214
|
+
() => {
|
|
1215
|
+
assertBatchScope(
|
|
1216
|
+
items.map((batchEntry) => batchEntry.item),
|
|
1217
|
+
scope,
|
|
1218
|
+
);
|
|
945
1219
|
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
1220
|
+
if (scope === StorageScope.Memory) {
|
|
1221
|
+
items.forEach(({ item, value }) => item.set(value));
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
950
1224
|
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
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
|
+
}
|
|
964
1238
|
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
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;
|
|
981
1266
|
}
|
|
982
|
-
});
|
|
983
1267
|
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
storageModule.setBatch(group.keys, group.values, scope);
|
|
987
|
-
group.keys.forEach((key, index) =>
|
|
988
|
-
cacheRawValue(scope, key, group.values[index]),
|
|
1268
|
+
const useRawBatchPath = items.every(({ item }) =>
|
|
1269
|
+
canUseRawBatchPath(asInternal(item)),
|
|
989
1270
|
);
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
const useRawBatchPath = items.every(({ item }) =>
|
|
995
|
-
canUseRawBatchPath(asInternal(item)),
|
|
996
|
-
);
|
|
997
|
-
if (!useRawBatchPath) {
|
|
998
|
-
items.forEach(({ item, value }) => item.set(value));
|
|
999
|
-
return;
|
|
1000
|
-
}
|
|
1271
|
+
if (!useRawBatchPath) {
|
|
1272
|
+
items.forEach(({ item, value }) => item.set(value));
|
|
1273
|
+
return;
|
|
1274
|
+
}
|
|
1001
1275
|
|
|
1002
|
-
|
|
1003
|
-
|
|
1276
|
+
const keys = items.map((entry) => entry.item.key);
|
|
1277
|
+
const values = items.map((entry) => entry.item.serialize(entry.value));
|
|
1004
1278
|
|
|
1005
|
-
|
|
1006
|
-
|
|
1279
|
+
getStorageModule().setBatch(keys, values, scope);
|
|
1280
|
+
keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
|
|
1281
|
+
},
|
|
1282
|
+
items.length,
|
|
1283
|
+
);
|
|
1007
1284
|
}
|
|
1008
1285
|
|
|
1009
1286
|
export function removeBatch(
|
|
1010
1287
|
items: readonly BatchRemoveItem[],
|
|
1011
1288
|
scope: StorageScope,
|
|
1012
1289
|
): void {
|
|
1013
|
-
|
|
1290
|
+
measureOperation(
|
|
1291
|
+
"batch:remove",
|
|
1292
|
+
scope,
|
|
1293
|
+
() => {
|
|
1294
|
+
assertBatchScope(items, scope);
|
|
1014
1295
|
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1296
|
+
if (scope === StorageScope.Memory) {
|
|
1297
|
+
items.forEach((item) => item.delete());
|
|
1298
|
+
return;
|
|
1299
|
+
}
|
|
1019
1300
|
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
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
|
+
);
|
|
1026
1310
|
}
|
|
1027
1311
|
|
|
1028
1312
|
export function registerMigration(version: number, migration: Migration): void {
|
|
@@ -1040,92 +1324,124 @@ export function registerMigration(version: number, migration: Migration): void {
|
|
|
1040
1324
|
export function migrateToLatest(
|
|
1041
1325
|
scope: StorageScope = StorageScope.Disk,
|
|
1042
1326
|
): number {
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
.
|
|
1047
|
-
|
|
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
|
+
};
|
|
1048
1341
|
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
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
|
+
});
|
|
1056
1351
|
|
|
1057
|
-
|
|
1058
|
-
const migration = registeredMigrations.get(version);
|
|
1059
|
-
if (!migration) {
|
|
1060
|
-
return;
|
|
1061
|
-
}
|
|
1062
|
-
migration(context);
|
|
1063
|
-
writeMigrationVersion(scope, version);
|
|
1064
|
-
appliedVersion = version;
|
|
1352
|
+
return appliedVersion;
|
|
1065
1353
|
});
|
|
1066
|
-
|
|
1067
|
-
return appliedVersion;
|
|
1068
1354
|
}
|
|
1069
1355
|
|
|
1070
1356
|
export function runTransaction<T>(
|
|
1071
1357
|
scope: StorageScope,
|
|
1072
1358
|
transaction: (context: TransactionContext) => T,
|
|
1073
1359
|
): T {
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1360
|
+
return measureOperation("transaction:run", scope, () => {
|
|
1361
|
+
assertValidScope(scope);
|
|
1362
|
+
if (scope === StorageScope.Secure) {
|
|
1363
|
+
flushSecureWrites();
|
|
1364
|
+
}
|
|
1078
1365
|
|
|
1079
|
-
|
|
1366
|
+
const rollback = new Map<string, string | undefined>();
|
|
1080
1367
|
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1368
|
+
const rememberRollback = (key: string) => {
|
|
1369
|
+
if (rollback.has(key)) {
|
|
1370
|
+
return;
|
|
1371
|
+
}
|
|
1372
|
+
rollback.set(key, getRawValue(key, scope));
|
|
1373
|
+
};
|
|
1087
1374
|
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
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
|
+
};
|
|
1114
1401
|
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
.
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
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();
|
|
1125
1430
|
}
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
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
|
+
});
|
|
1129
1445
|
}
|
|
1130
1446
|
|
|
1131
1447
|
export type SecureAuthStorageConfig<K extends string = string> = Record<
|
|
@@ -1133,6 +1449,7 @@ export type SecureAuthStorageConfig<K extends string = string> = Record<
|
|
|
1133
1449
|
{
|
|
1134
1450
|
ttlMs?: number;
|
|
1135
1451
|
biometric?: boolean;
|
|
1452
|
+
biometricLevel?: BiometricLevel;
|
|
1136
1453
|
accessControl?: AccessControl;
|
|
1137
1454
|
}
|
|
1138
1455
|
>;
|
|
@@ -1156,6 +1473,9 @@ export function createSecureAuthStorage<K extends string>(
|
|
|
1156
1473
|
...(itemConfig.biometric !== undefined
|
|
1157
1474
|
? { biometric: itemConfig.biometric }
|
|
1158
1475
|
: {}),
|
|
1476
|
+
...(itemConfig.biometricLevel !== undefined
|
|
1477
|
+
? { biometricLevel: itemConfig.biometricLevel }
|
|
1478
|
+
: {}),
|
|
1159
1479
|
...(itemConfig.accessControl !== undefined
|
|
1160
1480
|
? { accessControl: itemConfig.accessControl }
|
|
1161
1481
|
: {}),
|