react-native-nitro-storage 0.9.0 → 0.10.1
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/CHANGELOG.md +101 -29
- package/README.md +83 -26
- package/android/build.gradle +3 -1
- package/android/src/main/cpp/AndroidStorageAdapterCpp.cpp +5 -2
- package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +240 -34
- package/cpp/bindings/HybridStorage.cpp +106 -283
- package/cpp/bindings/HybridStorage.hpp +16 -9
- package/docs/api-reference.md +75 -19
- package/docs/benchmarks.md +6 -1
- package/docs/keychain-lifecycle-testing.md +36 -0
- package/docs/recipes.md +1 -2
- package/docs/secure-storage.md +45 -9
- package/ios/IOSStorageAdapterCpp.mm +391 -175
- package/lib/commonjs/capabilities.js +3 -1
- package/lib/commonjs/capabilities.js.map +1 -1
- package/lib/commonjs/core/durability.js +111 -45
- package/lib/commonjs/core/durability.js.map +1 -1
- package/lib/commonjs/index.js +15 -5
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +219 -97
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/internal.js +3 -11
- package/lib/commonjs/internal.js.map +1 -1
- package/lib/commonjs/shared.js +62 -2
- package/lib/commonjs/shared.js.map +1 -1
- package/lib/commonjs/storage-core.js +883 -163
- package/lib/commonjs/storage-core.js.map +1 -1
- package/lib/commonjs/storage-runtime.js +5 -1
- package/lib/commonjs/storage-runtime.js.map +1 -1
- package/lib/commonjs/testing.js +13 -0
- package/lib/commonjs/testing.js.map +1 -1
- package/lib/module/capabilities.js +2 -1
- package/lib/module/capabilities.js.map +1 -1
- package/lib/module/core/durability.js +111 -45
- package/lib/module/core/durability.js.map +1 -1
- package/lib/module/index.js +12 -8
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +215 -99
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/internal.js +2 -9
- package/lib/module/internal.js.map +1 -1
- package/lib/module/shared.js +60 -2
- package/lib/module/shared.js.map +1 -1
- package/lib/module/storage-core.js +884 -164
- package/lib/module/storage-core.js.map +1 -1
- package/lib/module/storage-runtime.js +4 -1
- package/lib/module/storage-runtime.js.map +1 -1
- package/lib/module/testing.js +8 -1
- package/lib/module/testing.js.map +1 -1
- package/lib/typescript/capabilities.d.ts +2 -1
- package/lib/typescript/capabilities.d.ts.map +1 -1
- package/lib/typescript/core/durability.d.ts +8 -4
- package/lib/typescript/core/durability.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +2 -2
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/index.web.d.ts +3 -29
- package/lib/typescript/index.web.d.ts.map +1 -1
- package/lib/typescript/internal.d.ts +0 -2
- package/lib/typescript/internal.d.ts.map +1 -1
- package/lib/typescript/shared.d.ts +19 -0
- package/lib/typescript/shared.d.ts.map +1 -1
- package/lib/typescript/storage-core.d.ts +15 -6
- package/lib/typescript/storage-core.d.ts.map +1 -1
- package/lib/typescript/storage-runtime.d.ts +2 -1
- package/lib/typescript/storage-runtime.d.ts.map +1 -1
- package/lib/typescript/testing.d.ts +1 -1
- package/lib/typescript/testing.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/capabilities.ts +3 -1
- package/src/core/durability.ts +140 -43
- package/src/index.ts +20 -10
- package/src/index.web.ts +328 -166
- package/src/internal.ts +0 -14
- package/src/shared.ts +85 -0
- package/src/storage-core.ts +1198 -232
- package/src/storage-runtime.ts +6 -0
- package/src/testing.ts +8 -1
package/src/storage-core.ts
CHANGED
|
@@ -17,11 +17,13 @@ import {
|
|
|
17
17
|
assertBiometricLevel,
|
|
18
18
|
canUseRawBatchPath,
|
|
19
19
|
canUseSecureRawBatchPath,
|
|
20
|
+
createStorageCompositeError,
|
|
20
21
|
createKeyChange,
|
|
21
22
|
defaultDeserialize,
|
|
22
23
|
defaultSerialize,
|
|
23
24
|
isKeychainLockedError,
|
|
24
25
|
isUpdater,
|
|
26
|
+
normalizeStorageError,
|
|
25
27
|
notifyAllListeners,
|
|
26
28
|
notifyKeyListeners,
|
|
27
29
|
redactSecureKeyChange,
|
|
@@ -30,6 +32,8 @@ import {
|
|
|
30
32
|
type KeyListenerRegistry,
|
|
31
33
|
type Migration,
|
|
32
34
|
type NonMemoryScope,
|
|
35
|
+
type PendingDiskWrite,
|
|
36
|
+
type PendingSecureWrite,
|
|
33
37
|
type RollbackRecord,
|
|
34
38
|
type SecureAuthStorageConfig,
|
|
35
39
|
type StorageEventObserverOptions,
|
|
@@ -124,6 +128,10 @@ type StorageItemInternal<T> = StorageItem<T> & {
|
|
|
124
128
|
_deleteMemoryEntry: () => void;
|
|
125
129
|
_hasValidation: boolean;
|
|
126
130
|
_hasExpiration: boolean;
|
|
131
|
+
_hasRenameFrom: boolean;
|
|
132
|
+
_renameFromKeys: readonly string[];
|
|
133
|
+
_getRenameMigrationState: () => boolean;
|
|
134
|
+
_setRenameMigrationState: (migrated: boolean) => void;
|
|
127
135
|
_readCacheEnabled: boolean;
|
|
128
136
|
_isBiometric: boolean;
|
|
129
137
|
_biometricLevel: BiometricLevel;
|
|
@@ -138,6 +146,7 @@ export type BatchReadItem<T> = Pick<
|
|
|
138
146
|
> & {
|
|
139
147
|
_hasValidation?: boolean;
|
|
140
148
|
_hasExpiration?: boolean;
|
|
149
|
+
_hasRenameFrom?: boolean;
|
|
141
150
|
_readCacheEnabled?: boolean;
|
|
142
151
|
_isBiometric?: boolean;
|
|
143
152
|
_defaultValue?: unknown;
|
|
@@ -146,7 +155,10 @@ export type BatchReadItem<T> = Pick<
|
|
|
146
155
|
export type BatchRemoveItem = Pick<
|
|
147
156
|
StorageItem<unknown>,
|
|
148
157
|
"key" | "scope" | "delete"
|
|
149
|
-
|
|
158
|
+
> & {
|
|
159
|
+
_hasRenameFrom?: boolean;
|
|
160
|
+
_isBiometric?: boolean;
|
|
161
|
+
};
|
|
150
162
|
export type BatchValues<TItems extends readonly BatchReadItem<unknown>[]> = {
|
|
151
163
|
[Index in keyof TItems]: TItems[Index] extends BatchReadItem<infer Value>
|
|
152
164
|
? Value
|
|
@@ -178,14 +190,17 @@ export type StorageClearOptions = {
|
|
|
178
190
|
};
|
|
179
191
|
|
|
180
192
|
export type SetItemConfig<TMember extends string = string> = Omit<
|
|
181
|
-
StorageItemConfig<
|
|
193
|
+
StorageItemConfig<Record<string, true>>,
|
|
182
194
|
"defaultValue" | "serialize" | "deserialize"
|
|
183
195
|
> & {
|
|
184
196
|
defaultValue?: readonly TMember[];
|
|
185
197
|
};
|
|
186
198
|
|
|
187
199
|
export type SetStorageItem<TMember extends string = string> = {
|
|
188
|
-
|
|
200
|
+
/** Compatibility shape retained from the original set-item API. */
|
|
201
|
+
get: () => Record<string, true>;
|
|
202
|
+
/** Precise membership shape for new code. */
|
|
203
|
+
getTyped: () => Partial<Record<TMember, true>>;
|
|
189
204
|
has: (id: TMember) => boolean;
|
|
190
205
|
add: (id: TMember) => void;
|
|
191
206
|
delete: (id: TMember) => void;
|
|
@@ -197,7 +212,7 @@ export type SetStorageItem<TMember extends string = string> = {
|
|
|
197
212
|
subscribe: (callback: () => void) => () => void;
|
|
198
213
|
scope: StorageScope;
|
|
199
214
|
key: string;
|
|
200
|
-
item: StorageItem<
|
|
215
|
+
item: StorageItem<Record<string, true>>;
|
|
201
216
|
};
|
|
202
217
|
|
|
203
218
|
export type StorageCoreBackend = {
|
|
@@ -225,6 +240,8 @@ export type StorageCoreBackend = {
|
|
|
225
240
|
clearSecureBiometric(): void;
|
|
226
241
|
};
|
|
227
242
|
|
|
243
|
+
export type StorageRawCacheRepresentation = "plain" | "biometric";
|
|
244
|
+
|
|
228
245
|
export type StorageCoreAdapter = {
|
|
229
246
|
backend: StorageCoreBackend;
|
|
230
247
|
changeSource: StorageChangeSource;
|
|
@@ -249,8 +266,14 @@ export type StorageCoreInternals = {
|
|
|
249
266
|
scope: NonMemoryScope,
|
|
250
267
|
key: string,
|
|
251
268
|
value: string | undefined,
|
|
269
|
+
representation?: StorageRawCacheRepresentation,
|
|
252
270
|
): void;
|
|
253
|
-
readCachedRawValue(
|
|
271
|
+
readCachedRawValue(
|
|
272
|
+
scope: NonMemoryScope,
|
|
273
|
+
key: string,
|
|
274
|
+
representation?: StorageRawCacheRepresentation,
|
|
275
|
+
): string | undefined;
|
|
276
|
+
invalidateRawCache(scope: NonMemoryScope, key: string): void;
|
|
254
277
|
clearScopeRawCache(scope: NonMemoryScope): void;
|
|
255
278
|
clearPendingDiskWrite(key: string): void;
|
|
256
279
|
clearPendingSecureWrite(key: string): void;
|
|
@@ -297,15 +320,15 @@ export function createStorageCore(
|
|
|
297
320
|
const itemGroups = new Map<string, Set<StorageItemInternal<unknown>>>();
|
|
298
321
|
const registeredKeyCounts = new Map<string, number>();
|
|
299
322
|
const memoryStore = new Map<string, unknown>();
|
|
323
|
+
const memoryExpirationDeadlines = new Map<string, number>();
|
|
324
|
+
const memoryItemsByKey = new Map<string, Set<StorageItemInternal<unknown>>>();
|
|
300
325
|
const memoryListeners: KeyListenerRegistry = new Map();
|
|
301
326
|
const scopedListeners: Record<NonMemoryScope, KeyListenerRegistry> = {
|
|
302
327
|
[StorageScope.Disk]: new Map(),
|
|
303
328
|
[StorageScope.Secure]: new Map(),
|
|
304
329
|
};
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
Map<string, string | undefined>
|
|
308
|
-
> = {
|
|
330
|
+
type RawCacheEntry = Map<StorageRawCacheRepresentation, string | undefined>;
|
|
331
|
+
const scopedRawCache: Record<NonMemoryScope, Map<string, RawCacheEntry>> = {
|
|
309
332
|
[StorageScope.Disk]: new Map(),
|
|
310
333
|
[StorageScope.Secure]: new Map(),
|
|
311
334
|
};
|
|
@@ -333,31 +356,57 @@ export function createStorageCore(
|
|
|
333
356
|
return scopedListeners[scope];
|
|
334
357
|
}
|
|
335
358
|
|
|
336
|
-
function getScopeRawCache(
|
|
337
|
-
scope: NonMemoryScope,
|
|
338
|
-
): Map<string, string | undefined> {
|
|
359
|
+
function getScopeRawCache(scope: NonMemoryScope): Map<string, RawCacheEntry> {
|
|
339
360
|
return scopedRawCache[scope];
|
|
340
361
|
}
|
|
341
362
|
|
|
363
|
+
function getCachedRawValueEntry(
|
|
364
|
+
scope: NonMemoryScope,
|
|
365
|
+
key: string,
|
|
366
|
+
create = false,
|
|
367
|
+
): RawCacheEntry | undefined {
|
|
368
|
+
const scopeCache = getScopeRawCache(scope);
|
|
369
|
+
const existing = scopeCache.get(key);
|
|
370
|
+
if (existing || !create) {
|
|
371
|
+
return existing;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const entry: RawCacheEntry = new Map();
|
|
375
|
+
scopeCache.set(key, entry);
|
|
376
|
+
return entry;
|
|
377
|
+
}
|
|
378
|
+
|
|
342
379
|
function cacheRawValue(
|
|
343
380
|
scope: NonMemoryScope,
|
|
344
381
|
key: string,
|
|
345
382
|
value: string | undefined,
|
|
383
|
+
representation: StorageRawCacheRepresentation = "plain",
|
|
346
384
|
): void {
|
|
347
|
-
|
|
385
|
+
getCachedRawValueEntry(scope, key, true)?.set(representation, value);
|
|
348
386
|
}
|
|
349
387
|
|
|
350
388
|
function readCachedRawValue(
|
|
351
389
|
scope: NonMemoryScope,
|
|
352
390
|
key: string,
|
|
391
|
+
representation: StorageRawCacheRepresentation = "plain",
|
|
353
392
|
): string | undefined {
|
|
354
|
-
return
|
|
393
|
+
return getCachedRawValueEntry(scope, key)?.get(representation);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function invalidateRawCache(scope: NonMemoryScope, key: string): void {
|
|
397
|
+
getScopeRawCache(scope).delete(key);
|
|
355
398
|
}
|
|
356
399
|
|
|
357
400
|
function clearScopeRawCache(scope: NonMemoryScope): void {
|
|
358
401
|
getScopeRawCache(scope).clear();
|
|
359
402
|
}
|
|
360
403
|
|
|
404
|
+
function invalidateMemoryItemCaches(key: string): void {
|
|
405
|
+
memoryItemsByKey.get(key)?.forEach((item) => {
|
|
406
|
+
item._invalidateParsedCacheOnly();
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
|
|
361
410
|
function addKeyListener(
|
|
362
411
|
registry: KeyListenerRegistry,
|
|
363
412
|
key: string,
|
|
@@ -396,6 +445,18 @@ export function createStorageCore(
|
|
|
396
445
|
return getRawValue(key, scope);
|
|
397
446
|
}
|
|
398
447
|
|
|
448
|
+
function getEventRawValueForRepresentation(
|
|
449
|
+
scope: StorageScope,
|
|
450
|
+
key: string,
|
|
451
|
+
representation: StorageRawCacheRepresentation,
|
|
452
|
+
): string | undefined {
|
|
453
|
+
if (representation === "plain") {
|
|
454
|
+
return getEventRawValue(scope, key);
|
|
455
|
+
}
|
|
456
|
+
const raw = adapter.backend.getSecureBiometric(key);
|
|
457
|
+
return raw === undefined ? undefined : unescapeCollidingRawValue(raw);
|
|
458
|
+
}
|
|
459
|
+
|
|
399
460
|
function shouldReadPreviousEventValues(scope: StorageScope): boolean {
|
|
400
461
|
if (storageEvents.hasListeners(scope)) {
|
|
401
462
|
return true;
|
|
@@ -493,22 +554,6 @@ export function createStorageCore(
|
|
|
493
554
|
metrics.record(operation, scope, durationMs, keysCount);
|
|
494
555
|
}
|
|
495
556
|
|
|
496
|
-
function readPendingSecureWrite(key: string): string | undefined {
|
|
497
|
-
return durability.readPendingSecureWrite(key);
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
function readPendingDiskWrite(key: string): string | undefined {
|
|
501
|
-
return durability.readPendingDiskWrite(key);
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
function hasPendingDiskWrite(key: string): boolean {
|
|
505
|
-
return durability.hasPendingDiskWrite(key);
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
function hasPendingSecureWrite(key: string): boolean {
|
|
509
|
-
return durability.hasPendingSecureWrite(key);
|
|
510
|
-
}
|
|
511
|
-
|
|
512
557
|
function clearPendingDiskWrite(key: string): void {
|
|
513
558
|
durability.clearPendingDiskWrite(key);
|
|
514
559
|
}
|
|
@@ -525,16 +570,23 @@ export function createStorageCore(
|
|
|
525
570
|
durability.flushSecureWrites();
|
|
526
571
|
}
|
|
527
572
|
|
|
528
|
-
function
|
|
529
|
-
durability.
|
|
573
|
+
function runSecurePromotion<T>(key: string, promotion: () => T): T {
|
|
574
|
+
return durability.runSecurePromotion(key, promotion);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function scheduleDiskWrite(
|
|
578
|
+
key: string,
|
|
579
|
+
value: string | undefined,
|
|
580
|
+
): PendingDiskWrite {
|
|
581
|
+
return durability.scheduleDiskWrite(key, value);
|
|
530
582
|
}
|
|
531
583
|
|
|
532
584
|
function scheduleSecureWrite(
|
|
533
585
|
key: string,
|
|
534
586
|
value: string | undefined,
|
|
535
587
|
accessControl?: AccessControl,
|
|
536
|
-
):
|
|
537
|
-
durability.scheduleSecureWrite(key, value, accessControl);
|
|
588
|
+
): PendingSecureWrite {
|
|
589
|
+
return durability.scheduleSecureWrite(key, value, accessControl);
|
|
538
590
|
}
|
|
539
591
|
|
|
540
592
|
function setDiskWritesAsyncMode(enabled: boolean): void {
|
|
@@ -554,17 +606,34 @@ export function createStorageCore(
|
|
|
554
606
|
return typeof value === "string" ? value : undefined;
|
|
555
607
|
}
|
|
556
608
|
|
|
557
|
-
if (scope === StorageScope.Disk
|
|
558
|
-
|
|
609
|
+
if (scope === StorageScope.Disk) {
|
|
610
|
+
const pending = durability.getPendingDiskWrite(key);
|
|
611
|
+
if (pending !== undefined) {
|
|
612
|
+
return pending.value;
|
|
613
|
+
}
|
|
559
614
|
}
|
|
560
615
|
|
|
561
|
-
if (scope === StorageScope.Secure
|
|
562
|
-
|
|
616
|
+
if (scope === StorageScope.Secure) {
|
|
617
|
+
const pending = durability.getPendingSecureWrite(key);
|
|
618
|
+
if (pending !== undefined) {
|
|
619
|
+
return pending.value;
|
|
620
|
+
}
|
|
563
621
|
}
|
|
564
622
|
|
|
565
623
|
return adapter.backend.get(key, scope);
|
|
566
624
|
}
|
|
567
625
|
|
|
626
|
+
function getStoredRawValueForRepresentation(
|
|
627
|
+
key: string,
|
|
628
|
+
scope: StorageScope,
|
|
629
|
+
representation: StorageRawCacheRepresentation,
|
|
630
|
+
): string | undefined {
|
|
631
|
+
if (representation === "plain") {
|
|
632
|
+
return getStoredRawValue(key, scope);
|
|
633
|
+
}
|
|
634
|
+
return adapter.backend.getSecureBiometric(key);
|
|
635
|
+
}
|
|
636
|
+
|
|
568
637
|
function getRawValue(key: string, scope: StorageScope): string | undefined {
|
|
569
638
|
assertValidScope(scope);
|
|
570
639
|
if (scope === StorageScope.Memory) {
|
|
@@ -574,18 +643,22 @@ export function createStorageCore(
|
|
|
574
643
|
: undefined;
|
|
575
644
|
}
|
|
576
645
|
|
|
577
|
-
if (scope === StorageScope.Disk
|
|
578
|
-
const pending =
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
646
|
+
if (scope === StorageScope.Disk) {
|
|
647
|
+
const pending = durability.getPendingDiskWrite(key);
|
|
648
|
+
if (pending !== undefined) {
|
|
649
|
+
return pending.value === undefined
|
|
650
|
+
? undefined
|
|
651
|
+
: unescapeCollidingRawValue(pending.value);
|
|
652
|
+
}
|
|
582
653
|
}
|
|
583
654
|
|
|
584
|
-
if (scope === StorageScope.Secure
|
|
585
|
-
const pending =
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
655
|
+
if (scope === StorageScope.Secure) {
|
|
656
|
+
const pending = durability.getPendingSecureWrite(key);
|
|
657
|
+
if (pending !== undefined) {
|
|
658
|
+
return pending.value === undefined
|
|
659
|
+
? undefined
|
|
660
|
+
: unescapeCollidingRawValue(pending.value);
|
|
661
|
+
}
|
|
589
662
|
}
|
|
590
663
|
|
|
591
664
|
const raw = adapter.backend.get(key, scope);
|
|
@@ -617,6 +690,7 @@ export function createStorageCore(
|
|
|
617
690
|
}
|
|
618
691
|
|
|
619
692
|
if (scope === StorageScope.Secure) {
|
|
693
|
+
invalidateRawCache(scope, key);
|
|
620
694
|
flushSecureWrites();
|
|
621
695
|
clearPendingSecureWrite(key);
|
|
622
696
|
if (adapter.applyAccessControlOnSecureRawWrite) {
|
|
@@ -659,6 +733,7 @@ export function createStorageCore(
|
|
|
659
733
|
}
|
|
660
734
|
|
|
661
735
|
if (scope === StorageScope.Secure) {
|
|
736
|
+
invalidateRawCache(scope, key);
|
|
662
737
|
flushSecureWrites();
|
|
663
738
|
clearPendingSecureWrite(key);
|
|
664
739
|
}
|
|
@@ -689,6 +764,7 @@ export function createStorageCore(
|
|
|
689
764
|
getScopedListeners,
|
|
690
765
|
cacheRawValue,
|
|
691
766
|
readCachedRawValue,
|
|
767
|
+
invalidateRawCache,
|
|
692
768
|
clearScopeRawCache,
|
|
693
769
|
clearPendingDiskWrite,
|
|
694
770
|
clearPendingSecureWrite,
|
|
@@ -779,6 +855,11 @@ export function createStorageCore(
|
|
|
779
855
|
: unescapeCollidingRawValue(value),
|
|
780
856
|
)
|
|
781
857
|
: [];
|
|
858
|
+
if (scope === StorageScope.Secure) {
|
|
859
|
+
removeKeys.forEach((key) => {
|
|
860
|
+
invalidateRawCache(scope, key);
|
|
861
|
+
});
|
|
862
|
+
}
|
|
782
863
|
adapter.backend.removeBatch(removeKeys, scope);
|
|
783
864
|
removeKeys.forEach((key) => {
|
|
784
865
|
cacheRawValue(scope, key, undefined);
|
|
@@ -885,6 +966,7 @@ export function createStorageCore(
|
|
|
885
966
|
: {};
|
|
886
967
|
if (scope === StorageScope.Memory) {
|
|
887
968
|
memoryStore.clear();
|
|
969
|
+
memoryExpirationDeadlines.clear();
|
|
888
970
|
notifyAllListeners(memoryListeners);
|
|
889
971
|
emitBatchChange(
|
|
890
972
|
scope,
|
|
@@ -1044,6 +1126,7 @@ export function createStorageCore(
|
|
|
1044
1126
|
|
|
1045
1127
|
affectedKeys.forEach((key) => {
|
|
1046
1128
|
memoryStore.delete(key);
|
|
1129
|
+
memoryExpirationDeadlines.delete(key);
|
|
1047
1130
|
});
|
|
1048
1131
|
affectedKeys.forEach((key) => {
|
|
1049
1132
|
notifyKeyListeners(memoryListeners, key);
|
|
@@ -1103,7 +1186,48 @@ export function createStorageCore(
|
|
|
1103
1186
|
},
|
|
1104
1187
|
clearBiometric: () => {
|
|
1105
1188
|
measureOperation("storage:clearBiometric", StorageScope.Secure, () => {
|
|
1106
|
-
|
|
1189
|
+
flushSecureWrites();
|
|
1190
|
+
const readEventValues = shouldReadPreviousEventValues(
|
|
1191
|
+
StorageScope.Secure,
|
|
1192
|
+
);
|
|
1193
|
+
const shouldEmitChanges =
|
|
1194
|
+
storageEvents.hasListeners(StorageScope.Secure) ||
|
|
1195
|
+
eventObserver !== undefined;
|
|
1196
|
+
const biometricKeys = shouldEmitChanges
|
|
1197
|
+
? adapter.backend
|
|
1198
|
+
.getAllKeys(StorageScope.Secure)
|
|
1199
|
+
.filter((key) => adapter.backend.hasSecureBiometric(key))
|
|
1200
|
+
: [];
|
|
1201
|
+
const previousValues = readEventValues
|
|
1202
|
+
? biometricKeys.map((key) => {
|
|
1203
|
+
const raw = adapter.backend.getSecureBiometric(key);
|
|
1204
|
+
return raw === undefined
|
|
1205
|
+
? undefined
|
|
1206
|
+
: unescapeCollidingRawValue(raw);
|
|
1207
|
+
})
|
|
1208
|
+
: [];
|
|
1209
|
+
|
|
1210
|
+
clearScopeRawCache(StorageScope.Secure);
|
|
1211
|
+
try {
|
|
1212
|
+
adapter.backend.clearSecureBiometric();
|
|
1213
|
+
} finally {
|
|
1214
|
+
clearScopeRawCache(StorageScope.Secure);
|
|
1215
|
+
}
|
|
1216
|
+
emitBatchChange(
|
|
1217
|
+
StorageScope.Secure,
|
|
1218
|
+
"clear",
|
|
1219
|
+
adapter.changeSource,
|
|
1220
|
+
biometricKeys.map((key, index) =>
|
|
1221
|
+
createKeyChange(
|
|
1222
|
+
StorageScope.Secure,
|
|
1223
|
+
key,
|
|
1224
|
+
readEventValues ? previousValues[index] : undefined,
|
|
1225
|
+
undefined,
|
|
1226
|
+
"clear",
|
|
1227
|
+
adapter.changeSource,
|
|
1228
|
+
),
|
|
1229
|
+
),
|
|
1230
|
+
);
|
|
1107
1231
|
});
|
|
1108
1232
|
},
|
|
1109
1233
|
has: (key: string, scope: StorageScope): boolean => {
|
|
@@ -1376,6 +1500,9 @@ export function createStorageCore(
|
|
|
1376
1500
|
if (scope === StorageScope.Secure) {
|
|
1377
1501
|
flushSecureWrites();
|
|
1378
1502
|
adapter.backend.setSecureAccessControl(secureDefaultAccessControl);
|
|
1503
|
+
keys.forEach((key) => {
|
|
1504
|
+
invalidateRawCache(scope, key);
|
|
1505
|
+
});
|
|
1379
1506
|
}
|
|
1380
1507
|
if (scope === StorageScope.Disk) {
|
|
1381
1508
|
flushDiskWrites();
|
|
@@ -1414,8 +1541,11 @@ export function createStorageCore(
|
|
|
1414
1541
|
const onExpired = config.onExpired;
|
|
1415
1542
|
const expirationTtlMs = expiration?.ttlMs;
|
|
1416
1543
|
const memoryExpiration =
|
|
1417
|
-
expiration && isMemory ?
|
|
1544
|
+
expiration && isMemory ? memoryExpirationDeadlines : null;
|
|
1418
1545
|
const readCache = !isMemory && config.readCache === true;
|
|
1546
|
+
const rawCacheRepresentation: StorageRawCacheRepresentation = isBiometric
|
|
1547
|
+
? "biometric"
|
|
1548
|
+
: "plain";
|
|
1419
1549
|
const coalesceDiskWrites =
|
|
1420
1550
|
config.scope === StorageScope.Disk && config.coalesceDiskWrites === true;
|
|
1421
1551
|
const coalesceSecureWrites =
|
|
@@ -1527,34 +1657,57 @@ export function createStorageCore(
|
|
|
1527
1657
|
: memoryStored;
|
|
1528
1658
|
}
|
|
1529
1659
|
|
|
1660
|
+
if (nonMemoryScope === StorageScope.Disk) {
|
|
1661
|
+
const pending = durability.getPendingDiskWrite(storageKey);
|
|
1662
|
+
if (pending !== undefined) {
|
|
1663
|
+
return pending.value;
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
if (nonMemoryScope === StorageScope.Secure && !isBiometric) {
|
|
1668
|
+
const pending = durability.getPendingSecureWrite(storageKey);
|
|
1669
|
+
if (pending !== undefined) {
|
|
1670
|
+
return pending.value;
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1530
1674
|
migrateRenamesIfNeeded();
|
|
1531
1675
|
|
|
1532
1676
|
if (nonMemoryScope === StorageScope.Disk) {
|
|
1533
|
-
const pending = durability.
|
|
1677
|
+
const pending = durability.getPendingDiskWrite(storageKey);
|
|
1534
1678
|
if (pending !== undefined) {
|
|
1535
|
-
return pending;
|
|
1679
|
+
return pending.value;
|
|
1536
1680
|
}
|
|
1537
1681
|
}
|
|
1538
1682
|
|
|
1539
1683
|
if (nonMemoryScope === StorageScope.Secure && !isBiometric) {
|
|
1540
|
-
const pending = durability.
|
|
1684
|
+
const pending = durability.getPendingSecureWrite(storageKey);
|
|
1541
1685
|
if (pending !== undefined) {
|
|
1542
|
-
return pending;
|
|
1686
|
+
return pending.value;
|
|
1543
1687
|
}
|
|
1544
1688
|
}
|
|
1545
1689
|
|
|
1546
1690
|
if (readCache) {
|
|
1547
|
-
const
|
|
1548
|
-
const
|
|
1549
|
-
if (
|
|
1550
|
-
return
|
|
1691
|
+
const scope = resolveNonMemoryScope();
|
|
1692
|
+
const cachedEntry = getCachedRawValueEntry(scope, storageKey);
|
|
1693
|
+
if (cachedEntry?.has(rawCacheRepresentation)) {
|
|
1694
|
+
return cachedEntry.get(rawCacheRepresentation);
|
|
1551
1695
|
}
|
|
1552
1696
|
}
|
|
1553
1697
|
|
|
1554
1698
|
if (isBiometric) {
|
|
1555
|
-
|
|
1699
|
+
const raw = readBackendRaw(() =>
|
|
1556
1700
|
adapter.backend.getSecureBiometric(storageKey),
|
|
1557
1701
|
);
|
|
1702
|
+
if (readCache) {
|
|
1703
|
+
cacheRawValue(
|
|
1704
|
+
resolveNonMemoryScope(),
|
|
1705
|
+
storageKey,
|
|
1706
|
+
raw,
|
|
1707
|
+
rawCacheRepresentation,
|
|
1708
|
+
);
|
|
1709
|
+
}
|
|
1710
|
+
return raw;
|
|
1558
1711
|
}
|
|
1559
1712
|
|
|
1560
1713
|
const raw = readBackendRaw(() =>
|
|
@@ -1572,8 +1725,11 @@ export function createStorageCore(
|
|
|
1572
1725
|
} catch (error) {
|
|
1573
1726
|
onReadError?.(error);
|
|
1574
1727
|
if (fallbackToCacheOnReadError) {
|
|
1575
|
-
const
|
|
1728
|
+
const scope = resolveNonMemoryScope();
|
|
1729
|
+
const cached = readCachedRawValue(
|
|
1730
|
+
scope,
|
|
1576
1731
|
storageKey,
|
|
1732
|
+
rawCacheRepresentation,
|
|
1577
1733
|
);
|
|
1578
1734
|
if (cached !== undefined) {
|
|
1579
1735
|
return cached;
|
|
@@ -1584,14 +1740,447 @@ export function createStorageCore(
|
|
|
1584
1740
|
}
|
|
1585
1741
|
};
|
|
1586
1742
|
|
|
1587
|
-
|
|
1743
|
+
type StoredRawWriteOptions = {
|
|
1744
|
+
cleanupRenameSources?: boolean;
|
|
1745
|
+
};
|
|
1746
|
+
type RenameSnapshot = {
|
|
1747
|
+
key: string;
|
|
1748
|
+
plainValue?: string;
|
|
1749
|
+
biometricValue?: string;
|
|
1750
|
+
};
|
|
1751
|
+
|
|
1752
|
+
function getRenameSourceKeys(): string[] {
|
|
1753
|
+
return renameFromKeys.filter((key) => key !== storageKey);
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
function getRenameSourceRaw(key: string): string | undefined {
|
|
1757
|
+
return isBiometric
|
|
1758
|
+
? adapter.backend.getSecureBiometric(key)
|
|
1759
|
+
: adapter.backend.get(key, config.scope);
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
function invalidateRenameSourceCache(key: string): void {
|
|
1763
|
+
if (nonMemoryScope !== null) {
|
|
1764
|
+
invalidateRawCache(nonMemoryScope, key);
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
function removeRenameSource(key: string): void {
|
|
1769
|
+
invalidateRenameSourceCache(key);
|
|
1770
|
+
if (isBiometric) {
|
|
1771
|
+
adapter.backend.deleteSecureBiometric(key);
|
|
1772
|
+
invalidateRenameSourceCache(key);
|
|
1773
|
+
return;
|
|
1774
|
+
}
|
|
1775
|
+
adapter.backend.remove(key, config.scope);
|
|
1776
|
+
invalidateRenameSourceCache(key);
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
function restoreRenameSource(snapshot: RenameSnapshot): void {
|
|
1780
|
+
invalidateRenameSourceCache(snapshot.key);
|
|
1781
|
+
if (nonMemoryScope === StorageScope.Secure) {
|
|
1782
|
+
if (snapshot.biometricValue !== undefined) {
|
|
1783
|
+
adapter.backend.setSecureBiometricWithLevel(
|
|
1784
|
+
snapshot.key,
|
|
1785
|
+
snapshot.biometricValue,
|
|
1786
|
+
resolvedBiometricLevel === BiometricLevel.None
|
|
1787
|
+
? BiometricLevel.BiometryOnly
|
|
1788
|
+
: resolvedBiometricLevel,
|
|
1789
|
+
);
|
|
1790
|
+
} else {
|
|
1791
|
+
adapter.backend.deleteSecureBiometric(snapshot.key);
|
|
1792
|
+
}
|
|
1793
|
+
if (snapshot.plainValue !== undefined) {
|
|
1794
|
+
if (adapter.applyAccessControlOnSecureRawWrite) {
|
|
1795
|
+
adapter.backend.setSecureAccessControl(
|
|
1796
|
+
secureAccessControl ?? secureDefaultAccessControl,
|
|
1797
|
+
);
|
|
1798
|
+
}
|
|
1799
|
+
adapter.backend.set(snapshot.key, snapshot.plainValue, config.scope);
|
|
1800
|
+
} else if (snapshot.biometricValue === undefined) {
|
|
1801
|
+
adapter.backend.remove(snapshot.key, config.scope);
|
|
1802
|
+
}
|
|
1803
|
+
invalidateRenameSourceCache(snapshot.key);
|
|
1804
|
+
return;
|
|
1805
|
+
}
|
|
1806
|
+
if (snapshot.plainValue !== undefined) {
|
|
1807
|
+
adapter.backend.set(snapshot.key, snapshot.plainValue, config.scope);
|
|
1808
|
+
} else {
|
|
1809
|
+
adapter.backend.remove(snapshot.key, config.scope);
|
|
1810
|
+
}
|
|
1811
|
+
invalidateRenameSourceCache(snapshot.key);
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
function readRenameSnapshots(): RenameSnapshot[] {
|
|
1815
|
+
return getRenameSourceKeys().flatMap((key) => {
|
|
1816
|
+
const plainValue =
|
|
1817
|
+
nonMemoryScope === StorageScope.Secure
|
|
1818
|
+
? adapter.backend.get(key, config.scope)
|
|
1819
|
+
: getRenameSourceRaw(key);
|
|
1820
|
+
const biometricValue =
|
|
1821
|
+
nonMemoryScope === StorageScope.Secure
|
|
1822
|
+
? adapter.backend.getSecureBiometric(key)
|
|
1823
|
+
: undefined;
|
|
1824
|
+
if (plainValue === undefined && biometricValue === undefined) {
|
|
1825
|
+
return [];
|
|
1826
|
+
}
|
|
1827
|
+
return [
|
|
1828
|
+
{
|
|
1829
|
+
key,
|
|
1830
|
+
...(plainValue === undefined ? {} : { plainValue }),
|
|
1831
|
+
...(biometricValue === undefined ? {} : { biometricValue }),
|
|
1832
|
+
},
|
|
1833
|
+
];
|
|
1834
|
+
});
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
function selectedRenameValue(snapshot: RenameSnapshot): string | undefined {
|
|
1838
|
+
return isBiometric ? snapshot.biometricValue : snapshot.plainValue;
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
function hasPendingCurrentWrite(): boolean {
|
|
1842
|
+
if (nonMemoryScope === StorageScope.Disk) {
|
|
1843
|
+
return durability.hasPendingDiskWrite(storageKey);
|
|
1844
|
+
}
|
|
1845
|
+
if (nonMemoryScope === StorageScope.Secure && !isBiometric) {
|
|
1846
|
+
return durability.hasPendingSecureWrite(storageKey);
|
|
1847
|
+
}
|
|
1848
|
+
return false;
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
function hasPendingRenameSourceWrite(): boolean {
|
|
1852
|
+
if (isBiometric || nonMemoryScope === null) {
|
|
1853
|
+
return false;
|
|
1854
|
+
}
|
|
1855
|
+
return getRenameSourceKeys().some((key) =>
|
|
1856
|
+
nonMemoryScope === StorageScope.Disk
|
|
1857
|
+
? durability.hasPendingDiskWrite(key)
|
|
1858
|
+
: durability.hasPendingSecureWrite(key),
|
|
1859
|
+
);
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
function flushPendingRenameSourceWrites(): void {
|
|
1863
|
+
if (!hasPendingRenameSourceWrite()) {
|
|
1864
|
+
return;
|
|
1865
|
+
}
|
|
1866
|
+
if (nonMemoryScope === StorageScope.Disk) {
|
|
1867
|
+
flushDiskWrites();
|
|
1868
|
+
return;
|
|
1869
|
+
}
|
|
1870
|
+
flushSecureWrites();
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
function clearPendingCurrentWriteIf(
|
|
1874
|
+
write: PendingDiskWrite | PendingSecureWrite | undefined,
|
|
1875
|
+
): void {
|
|
1876
|
+
if (write === undefined) {
|
|
1877
|
+
return;
|
|
1878
|
+
}
|
|
1879
|
+
if (nonMemoryScope === StorageScope.Disk && "generation" in write) {
|
|
1880
|
+
durability.clearPendingDiskWriteIf(write);
|
|
1881
|
+
return;
|
|
1882
|
+
}
|
|
1883
|
+
if (nonMemoryScope === StorageScope.Secure && !isBiometric) {
|
|
1884
|
+
durability.clearPendingSecureWriteIf(write);
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
function flushPendingCurrentWrite(): void {
|
|
1889
|
+
if (nonMemoryScope === StorageScope.Disk) {
|
|
1890
|
+
flushDiskWrites();
|
|
1891
|
+
return;
|
|
1892
|
+
}
|
|
1893
|
+
if (nonMemoryScope === StorageScope.Secure && !isBiometric) {
|
|
1894
|
+
flushSecureWrites();
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
function removeCurrentBackendValue(): void {
|
|
1899
|
+
invalidateRawCache(resolveNonMemoryScope(), storageKey);
|
|
1900
|
+
if (isBiometric) {
|
|
1901
|
+
adapter.backend.deleteSecureBiometric(storageKey);
|
|
1902
|
+
return;
|
|
1903
|
+
}
|
|
1904
|
+
adapter.backend.remove(storageKey, config.scope);
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
function scheduleRenameSourceCleanup(): void {
|
|
1908
|
+
const sourceKeys = getRenameSourceKeys();
|
|
1909
|
+
sourceKeys.forEach(invalidateRenameSourceCache);
|
|
1910
|
+
|
|
1911
|
+
if (isBiometric) {
|
|
1912
|
+
sourceKeys.forEach(removeRenameSource);
|
|
1913
|
+
return;
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
if (
|
|
1917
|
+
nonMemoryScope === StorageScope.Disk &&
|
|
1918
|
+
(coalesceDiskWrites || isDiskWritesAsync())
|
|
1919
|
+
) {
|
|
1920
|
+
sourceKeys.forEach((key) => {
|
|
1921
|
+
scheduleDiskWrite(key, undefined);
|
|
1922
|
+
});
|
|
1923
|
+
return;
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
if (nonMemoryScope === StorageScope.Secure && coalesceSecureWrites) {
|
|
1927
|
+
sourceKeys.forEach((key) => {
|
|
1928
|
+
scheduleSecureWrite(
|
|
1929
|
+
key,
|
|
1930
|
+
undefined,
|
|
1931
|
+
secureAccessControl ?? secureDefaultAccessControl,
|
|
1932
|
+
);
|
|
1933
|
+
});
|
|
1934
|
+
return;
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
sourceKeys.forEach(removeRenameSource);
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
type ItemPendingSnapshot = {
|
|
1941
|
+
value: string | undefined;
|
|
1942
|
+
accessControl?: AccessControl;
|
|
1943
|
+
};
|
|
1944
|
+
type ItemStateRecord = {
|
|
1945
|
+
key: string;
|
|
1946
|
+
plainValue: string | undefined;
|
|
1947
|
+
biometricValue: string | undefined;
|
|
1948
|
+
pending?: ItemPendingSnapshot;
|
|
1949
|
+
};
|
|
1950
|
+
type ItemStateSnapshot = {
|
|
1951
|
+
records: readonly ItemStateRecord[];
|
|
1952
|
+
renamesMigrated: boolean;
|
|
1953
|
+
};
|
|
1954
|
+
|
|
1955
|
+
let atomicMutationDepth = 0;
|
|
1956
|
+
|
|
1957
|
+
function getItemStateKeys(): string[] {
|
|
1958
|
+
return Array.from(new Set([storageKey, ...getRenameSourceKeys()]));
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
function captureItemState(): ItemStateSnapshot {
|
|
1962
|
+
const records = getItemStateKeys().map((key) => {
|
|
1963
|
+
let pending: ItemPendingSnapshot | undefined;
|
|
1964
|
+
if (nonMemoryScope === StorageScope.Disk) {
|
|
1965
|
+
const pendingWrite = durability.getPendingDiskWrite(key);
|
|
1966
|
+
if (pendingWrite !== undefined) {
|
|
1967
|
+
pending = {
|
|
1968
|
+
value: pendingWrite.value,
|
|
1969
|
+
};
|
|
1970
|
+
}
|
|
1971
|
+
} else if (nonMemoryScope === StorageScope.Secure && !isBiometric) {
|
|
1972
|
+
const pendingWrite = durability.getPendingSecureWrite(key);
|
|
1973
|
+
if (pendingWrite !== undefined) {
|
|
1974
|
+
pending = {
|
|
1975
|
+
value: pendingWrite.value,
|
|
1976
|
+
...(pendingWrite.accessControl === undefined
|
|
1977
|
+
? {}
|
|
1978
|
+
: { accessControl: pendingWrite.accessControl }),
|
|
1979
|
+
};
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
return {
|
|
1984
|
+
key,
|
|
1985
|
+
plainValue:
|
|
1986
|
+
nonMemoryScope === null
|
|
1987
|
+
? undefined
|
|
1988
|
+
: adapter.backend.get(key, config.scope),
|
|
1989
|
+
biometricValue:
|
|
1990
|
+
nonMemoryScope === StorageScope.Secure
|
|
1991
|
+
? adapter.backend.getSecureBiometric(key)
|
|
1992
|
+
: undefined,
|
|
1993
|
+
...(pending === undefined ? {} : { pending }),
|
|
1994
|
+
};
|
|
1995
|
+
});
|
|
1996
|
+
return { records, renamesMigrated };
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
function clearPendingItemWrite(key: string): void {
|
|
2000
|
+
if (nonMemoryScope === StorageScope.Disk) {
|
|
2001
|
+
durability.clearPendingDiskWrite(key);
|
|
2002
|
+
} else if (nonMemoryScope === StorageScope.Secure && !isBiometric) {
|
|
2003
|
+
durability.clearPendingSecureWrite(key);
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
function restoreItemState(snapshot: ItemStateSnapshot): {
|
|
2008
|
+
label: string;
|
|
2009
|
+
error: unknown;
|
|
2010
|
+
}[] {
|
|
2011
|
+
const rollbackErrors: { label: string; error: unknown }[] = [];
|
|
2012
|
+
const recordsByKey = new Map(
|
|
2013
|
+
snapshot.records.map((record) => [record.key, record]),
|
|
2014
|
+
);
|
|
2015
|
+
snapshot.records.forEach(({ key }) => {
|
|
2016
|
+
clearPendingItemWrite(key);
|
|
2017
|
+
if (nonMemoryScope !== null) {
|
|
2018
|
+
invalidateRawCache(nonMemoryScope, key);
|
|
2019
|
+
}
|
|
2020
|
+
});
|
|
2021
|
+
|
|
2022
|
+
const biometricRecords = snapshot.records.filter(
|
|
2023
|
+
({ biometricValue }) => biometricValue !== undefined,
|
|
2024
|
+
);
|
|
2025
|
+
if (nonMemoryScope === StorageScope.Secure) {
|
|
2026
|
+
biometricRecords.forEach(({ key, biometricValue }) => {
|
|
2027
|
+
try {
|
|
2028
|
+
adapter.backend.setSecureBiometricWithLevel(
|
|
2029
|
+
key,
|
|
2030
|
+
biometricValue as string,
|
|
2031
|
+
resolvedBiometricLevel === BiometricLevel.None
|
|
2032
|
+
? BiometricLevel.BiometryOnly
|
|
2033
|
+
: resolvedBiometricLevel,
|
|
2034
|
+
);
|
|
2035
|
+
cacheRawValue(
|
|
2036
|
+
StorageScope.Secure,
|
|
2037
|
+
key,
|
|
2038
|
+
biometricValue,
|
|
2039
|
+
"biometric",
|
|
2040
|
+
);
|
|
2041
|
+
} catch (error) {
|
|
2042
|
+
rollbackErrors.push({ label: "rollback biometric", error });
|
|
2043
|
+
}
|
|
2044
|
+
});
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
const plainSets = new Map<
|
|
2048
|
+
AccessControl,
|
|
2049
|
+
{ keys: string[]; values: string[] }
|
|
2050
|
+
>();
|
|
2051
|
+
const plainRemoves: string[] = [];
|
|
2052
|
+
snapshot.records.forEach(({ key, plainValue, biometricValue }) => {
|
|
2053
|
+
if (plainValue === undefined) {
|
|
2054
|
+
if (biometricValue === undefined) {
|
|
2055
|
+
plainRemoves.push(key);
|
|
2056
|
+
}
|
|
2057
|
+
return;
|
|
2058
|
+
}
|
|
2059
|
+
const accessControl = secureAccessControl ?? secureDefaultAccessControl;
|
|
2060
|
+
const group = plainSets.get(accessControl) ?? { keys: [], values: [] };
|
|
2061
|
+
group.keys.push(key);
|
|
2062
|
+
group.values.push(plainValue);
|
|
2063
|
+
plainSets.set(accessControl, group);
|
|
2064
|
+
});
|
|
2065
|
+
|
|
2066
|
+
plainSets.forEach((group, accessControl) => {
|
|
2067
|
+
try {
|
|
2068
|
+
if (nonMemoryScope === StorageScope.Secure) {
|
|
2069
|
+
adapter.backend.setSecureAccessControl(accessControl);
|
|
2070
|
+
}
|
|
2071
|
+
adapter.backend.setBatch(group.keys, group.values, config.scope);
|
|
2072
|
+
group.keys.forEach((key, index) => {
|
|
2073
|
+
cacheRawValue(
|
|
2074
|
+
resolveNonMemoryScope(),
|
|
2075
|
+
key,
|
|
2076
|
+
group.values[index],
|
|
2077
|
+
"plain",
|
|
2078
|
+
);
|
|
2079
|
+
});
|
|
2080
|
+
} catch (error) {
|
|
2081
|
+
rollbackErrors.push({ label: "rollback plain set", error });
|
|
2082
|
+
}
|
|
2083
|
+
});
|
|
2084
|
+
|
|
2085
|
+
if (plainRemoves.length > 0) {
|
|
2086
|
+
try {
|
|
2087
|
+
adapter.backend.removeBatch(plainRemoves, config.scope);
|
|
2088
|
+
plainRemoves.forEach((key) => {
|
|
2089
|
+
cacheRawValue(resolveNonMemoryScope(), key, undefined, "plain");
|
|
2090
|
+
});
|
|
2091
|
+
} catch (error) {
|
|
2092
|
+
rollbackErrors.push({ label: "rollback plain remove", error });
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
if (nonMemoryScope === StorageScope.Secure) {
|
|
2097
|
+
snapshot.records.forEach(({ key, biometricValue }) => {
|
|
2098
|
+
if (biometricValue !== undefined) {
|
|
2099
|
+
return;
|
|
2100
|
+
}
|
|
2101
|
+
try {
|
|
2102
|
+
adapter.backend.deleteSecureBiometric(key);
|
|
2103
|
+
cacheRawValue(StorageScope.Secure, key, undefined, "biometric");
|
|
2104
|
+
} catch (error) {
|
|
2105
|
+
rollbackErrors.push({ label: "rollback biometric remove", error });
|
|
2106
|
+
}
|
|
2107
|
+
});
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
snapshot.records.forEach(({ key, pending }) => {
|
|
2111
|
+
if (pending === undefined || nonMemoryScope === null) {
|
|
2112
|
+
return;
|
|
2113
|
+
}
|
|
2114
|
+
if (nonMemoryScope === StorageScope.Disk) {
|
|
2115
|
+
scheduleDiskWrite(key, pending.value);
|
|
2116
|
+
} else if (!isBiometric) {
|
|
2117
|
+
scheduleSecureWrite(
|
|
2118
|
+
key,
|
|
2119
|
+
pending.value,
|
|
2120
|
+
pending.accessControl ??
|
|
2121
|
+
secureAccessControl ??
|
|
2122
|
+
secureDefaultAccessControl,
|
|
2123
|
+
);
|
|
2124
|
+
}
|
|
2125
|
+
});
|
|
2126
|
+
|
|
2127
|
+
recordsByKey.forEach(({ key }) => {
|
|
2128
|
+
if (nonMemoryScope !== null) {
|
|
2129
|
+
invalidateRawCache(nonMemoryScope, key);
|
|
2130
|
+
}
|
|
2131
|
+
});
|
|
2132
|
+
renamesMigrated = snapshot.renamesMigrated;
|
|
2133
|
+
invalidateParsedCache();
|
|
2134
|
+
return rollbackErrors;
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
function runAtomicItemMutation(mutation: () => void): void {
|
|
2138
|
+
if (isMemory || renameFromKeys.length === 0 || atomicMutationDepth > 0) {
|
|
2139
|
+
mutation();
|
|
2140
|
+
return;
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
const snapshot = captureItemState();
|
|
2144
|
+
atomicMutationDepth += 1;
|
|
2145
|
+
try {
|
|
2146
|
+
mutation();
|
|
2147
|
+
} catch (primaryError) {
|
|
2148
|
+
const rollbackErrors = restoreItemState(snapshot);
|
|
2149
|
+
if (rollbackErrors.length > 0) {
|
|
2150
|
+
throw createStorageCompositeError(
|
|
2151
|
+
"item mutation rollback",
|
|
2152
|
+
primaryError,
|
|
2153
|
+
rollbackErrors,
|
|
2154
|
+
);
|
|
2155
|
+
}
|
|
2156
|
+
throw primaryError;
|
|
2157
|
+
} finally {
|
|
2158
|
+
atomicMutationDepth -= 1;
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
const writeStoredRaw = (
|
|
2163
|
+
rawValue: string,
|
|
2164
|
+
options: StoredRawWriteOptions = {},
|
|
2165
|
+
): PendingDiskWrite | PendingSecureWrite | undefined => {
|
|
2166
|
+
const cleanupRenameSources = options.cleanupRenameSources !== false;
|
|
1588
2167
|
const oldValue = undefined;
|
|
1589
2168
|
if (isBiometric) {
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
2169
|
+
invalidateRawCache(StorageScope.Secure, storageKey);
|
|
2170
|
+
runSecurePromotion(storageKey, () => {
|
|
2171
|
+
try {
|
|
2172
|
+
adapter.backend.setSecureBiometricWithLevel(
|
|
2173
|
+
storageKey,
|
|
2174
|
+
rawValue,
|
|
2175
|
+
resolvedBiometricLevel,
|
|
2176
|
+
);
|
|
2177
|
+
} catch (error) {
|
|
2178
|
+
throw normalizeStorageError(error);
|
|
2179
|
+
}
|
|
2180
|
+
});
|
|
2181
|
+
if (cleanupRenameSources) {
|
|
2182
|
+
scheduleRenameSourceCleanup();
|
|
2183
|
+
}
|
|
1595
2184
|
emitKeyChange(
|
|
1596
2185
|
config.scope,
|
|
1597
2186
|
storageKey,
|
|
@@ -1600,14 +2189,20 @@ export function createStorageCore(
|
|
|
1600
2189
|
"set",
|
|
1601
2190
|
adapter.changeSource,
|
|
1602
2191
|
);
|
|
1603
|
-
return;
|
|
2192
|
+
return undefined;
|
|
1604
2193
|
}
|
|
1605
2194
|
|
|
2195
|
+
if (nonMemoryScope === StorageScope.Secure) {
|
|
2196
|
+
invalidateRawCache(StorageScope.Secure, storageKey);
|
|
2197
|
+
}
|
|
1606
2198
|
cacheRawValue(resolveNonMemoryScope(), storageKey, rawValue);
|
|
1607
2199
|
|
|
1608
2200
|
if (nonMemoryScope === StorageScope.Disk) {
|
|
1609
2201
|
if (coalesceDiskWrites || isDiskWritesAsync()) {
|
|
1610
|
-
scheduleDiskWrite(storageKey, rawValue);
|
|
2202
|
+
const pendingWrite = scheduleDiskWrite(storageKey, rawValue);
|
|
2203
|
+
if (cleanupRenameSources) {
|
|
2204
|
+
scheduleRenameSourceCleanup();
|
|
2205
|
+
}
|
|
1611
2206
|
emitKeyChange(
|
|
1612
2207
|
config.scope,
|
|
1613
2208
|
storageKey,
|
|
@@ -1616,18 +2211,21 @@ export function createStorageCore(
|
|
|
1616
2211
|
"set",
|
|
1617
2212
|
adapter.changeSource,
|
|
1618
2213
|
);
|
|
1619
|
-
return;
|
|
2214
|
+
return pendingWrite;
|
|
1620
2215
|
}
|
|
1621
2216
|
|
|
1622
2217
|
clearPendingDiskWrite(storageKey);
|
|
1623
2218
|
}
|
|
1624
2219
|
|
|
1625
2220
|
if (coalesceSecureWrites) {
|
|
1626
|
-
scheduleSecureWrite(
|
|
2221
|
+
const pendingWrite = scheduleSecureWrite(
|
|
1627
2222
|
storageKey,
|
|
1628
2223
|
rawValue,
|
|
1629
2224
|
secureAccessControl ?? secureDefaultAccessControl,
|
|
1630
2225
|
);
|
|
2226
|
+
if (cleanupRenameSources) {
|
|
2227
|
+
scheduleRenameSourceCleanup();
|
|
2228
|
+
}
|
|
1631
2229
|
emitKeyChange(
|
|
1632
2230
|
config.scope,
|
|
1633
2231
|
storageKey,
|
|
@@ -1636,7 +2234,7 @@ export function createStorageCore(
|
|
|
1636
2234
|
"set",
|
|
1637
2235
|
adapter.changeSource,
|
|
1638
2236
|
);
|
|
1639
|
-
return;
|
|
2237
|
+
return pendingWrite;
|
|
1640
2238
|
}
|
|
1641
2239
|
|
|
1642
2240
|
if (nonMemoryScope === StorageScope.Secure) {
|
|
@@ -1649,6 +2247,9 @@ export function createStorageCore(
|
|
|
1649
2247
|
}
|
|
1650
2248
|
|
|
1651
2249
|
adapter.backend.set(storageKey, rawValue, config.scope);
|
|
2250
|
+
if (cleanupRenameSources) {
|
|
2251
|
+
scheduleRenameSourceCleanup();
|
|
2252
|
+
}
|
|
1652
2253
|
emitKeyChange(
|
|
1653
2254
|
config.scope,
|
|
1654
2255
|
storageKey,
|
|
@@ -1657,6 +2258,7 @@ export function createStorageCore(
|
|
|
1657
2258
|
"set",
|
|
1658
2259
|
adapter.changeSource,
|
|
1659
2260
|
);
|
|
2261
|
+
return undefined;
|
|
1660
2262
|
};
|
|
1661
2263
|
|
|
1662
2264
|
const migrateRenamesIfNeeded = (): void => {
|
|
@@ -1665,42 +2267,101 @@ export function createStorageCore(
|
|
|
1665
2267
|
}
|
|
1666
2268
|
|
|
1667
2269
|
try {
|
|
2270
|
+
flushPendingRenameSourceWrites();
|
|
1668
2271
|
const hasCurrent = isBiometric
|
|
1669
2272
|
? adapter.backend.hasSecureBiometric(storageKey)
|
|
1670
2273
|
: adapter.backend.has(storageKey, config.scope);
|
|
2274
|
+
const snapshots = readRenameSnapshots();
|
|
1671
2275
|
|
|
1672
2276
|
if (hasCurrent) {
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
2277
|
+
try {
|
|
2278
|
+
snapshots.forEach(({ key }) => {
|
|
2279
|
+
removeRenameSource(key);
|
|
2280
|
+
});
|
|
2281
|
+
} catch (primaryError) {
|
|
2282
|
+
const rollbackErrors: { label: string; error: unknown }[] = [];
|
|
2283
|
+
snapshots.forEach((snapshot) => {
|
|
2284
|
+
try {
|
|
2285
|
+
restoreRenameSource(snapshot);
|
|
2286
|
+
} catch (error) {
|
|
2287
|
+
rollbackErrors.push({
|
|
2288
|
+
label: "rollback rename source",
|
|
2289
|
+
error,
|
|
2290
|
+
});
|
|
1677
2291
|
}
|
|
1678
|
-
}
|
|
1679
|
-
|
|
2292
|
+
});
|
|
2293
|
+
if (rollbackErrors.length > 0) {
|
|
2294
|
+
throw createStorageCompositeError(
|
|
2295
|
+
"rename cleanup",
|
|
2296
|
+
primaryError,
|
|
2297
|
+
rollbackErrors,
|
|
2298
|
+
);
|
|
1680
2299
|
}
|
|
2300
|
+
throw primaryError;
|
|
1681
2301
|
}
|
|
1682
2302
|
renamesMigrated = true;
|
|
1683
2303
|
return;
|
|
1684
2304
|
}
|
|
1685
2305
|
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
2306
|
+
const snapshot = snapshots.find(
|
|
2307
|
+
(candidate) => selectedRenameValue(candidate) !== undefined,
|
|
2308
|
+
);
|
|
2309
|
+
if (snapshot === undefined) {
|
|
2310
|
+
renamesMigrated = true;
|
|
2311
|
+
return;
|
|
2312
|
+
}
|
|
1693
2313
|
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
2314
|
+
let writeAttempted = false;
|
|
2315
|
+
let pendingCurrentWrite:
|
|
2316
|
+
PendingDiskWrite | PendingSecureWrite | undefined;
|
|
2317
|
+
try {
|
|
2318
|
+
writeAttempted = true;
|
|
2319
|
+
pendingCurrentWrite = writeStoredRaw(
|
|
2320
|
+
selectedRenameValue(snapshot) as string,
|
|
2321
|
+
{
|
|
2322
|
+
cleanupRenameSources: false,
|
|
2323
|
+
},
|
|
2324
|
+
);
|
|
2325
|
+
if (hasPendingCurrentWrite()) {
|
|
2326
|
+
flushPendingCurrentWrite();
|
|
2327
|
+
}
|
|
2328
|
+
snapshots.forEach(({ key }) => {
|
|
2329
|
+
removeRenameSource(key);
|
|
2330
|
+
});
|
|
2331
|
+
renamesMigrated = true;
|
|
2332
|
+
} catch (primaryError) {
|
|
2333
|
+
const rollbackErrors: { label: string; error: unknown }[] = [];
|
|
2334
|
+
clearPendingCurrentWriteIf(pendingCurrentWrite);
|
|
2335
|
+
if (writeAttempted) {
|
|
2336
|
+
try {
|
|
2337
|
+
removeCurrentBackendValue();
|
|
2338
|
+
} catch (error) {
|
|
2339
|
+
rollbackErrors.push({ label: "rollback current value", error });
|
|
2340
|
+
}
|
|
1699
2341
|
}
|
|
1700
|
-
|
|
2342
|
+
snapshots.forEach((renameSnapshot) => {
|
|
2343
|
+
try {
|
|
2344
|
+
restoreRenameSource(renameSnapshot);
|
|
2345
|
+
} catch (error) {
|
|
2346
|
+
rollbackErrors.push({
|
|
2347
|
+
label: "rollback rename source",
|
|
2348
|
+
error,
|
|
2349
|
+
});
|
|
2350
|
+
}
|
|
2351
|
+
});
|
|
2352
|
+
invalidateRawCache(resolveNonMemoryScope(), storageKey);
|
|
2353
|
+
snapshots.forEach(({ key }) => {
|
|
2354
|
+
invalidateRenameSourceCache(key);
|
|
2355
|
+
});
|
|
2356
|
+
if (rollbackErrors.length > 0) {
|
|
2357
|
+
throw createStorageCompositeError(
|
|
2358
|
+
"rename migration",
|
|
2359
|
+
primaryError,
|
|
2360
|
+
rollbackErrors,
|
|
2361
|
+
);
|
|
2362
|
+
}
|
|
2363
|
+
throw primaryError;
|
|
1701
2364
|
}
|
|
1702
|
-
|
|
1703
|
-
renamesMigrated = true;
|
|
1704
2365
|
} catch (error) {
|
|
1705
2366
|
if (isKeychainLockedError(error)) {
|
|
1706
2367
|
onReadError?.(error);
|
|
@@ -1713,8 +2374,16 @@ export function createStorageCore(
|
|
|
1713
2374
|
const removeStoredRaw = (
|
|
1714
2375
|
operation: StorageChangeOperation = "remove",
|
|
1715
2376
|
): void => {
|
|
1716
|
-
const oldValue =
|
|
2377
|
+
const oldValue = isBiometric
|
|
2378
|
+
? getEventRawValueForRepresentation(
|
|
2379
|
+
config.scope,
|
|
2380
|
+
storageKey,
|
|
2381
|
+
"biometric",
|
|
2382
|
+
)
|
|
2383
|
+
: getEventRawValue(config.scope, storageKey);
|
|
1717
2384
|
if (isBiometric) {
|
|
2385
|
+
invalidateRawCache(StorageScope.Secure, storageKey);
|
|
2386
|
+
scheduleRenameSourceCleanup();
|
|
1718
2387
|
adapter.backend.deleteSecureBiometric(storageKey);
|
|
1719
2388
|
emitKeyChange(
|
|
1720
2389
|
config.scope,
|
|
@@ -1727,11 +2396,15 @@ export function createStorageCore(
|
|
|
1727
2396
|
return;
|
|
1728
2397
|
}
|
|
1729
2398
|
|
|
2399
|
+
if (nonMemoryScope === StorageScope.Secure) {
|
|
2400
|
+
invalidateRawCache(StorageScope.Secure, storageKey);
|
|
2401
|
+
}
|
|
1730
2402
|
cacheRawValue(resolveNonMemoryScope(), storageKey, undefined);
|
|
1731
2403
|
|
|
1732
2404
|
if (nonMemoryScope === StorageScope.Disk) {
|
|
1733
2405
|
if (coalesceDiskWrites || isDiskWritesAsync()) {
|
|
1734
2406
|
scheduleDiskWrite(storageKey, undefined);
|
|
2407
|
+
scheduleRenameSourceCleanup();
|
|
1735
2408
|
emitKeyChange(
|
|
1736
2409
|
config.scope,
|
|
1737
2410
|
storageKey,
|
|
@@ -1752,6 +2425,7 @@ export function createStorageCore(
|
|
|
1752
2425
|
undefined,
|
|
1753
2426
|
secureAccessControl ?? secureDefaultAccessControl,
|
|
1754
2427
|
);
|
|
2428
|
+
scheduleRenameSourceCleanup();
|
|
1755
2429
|
emitKeyChange(
|
|
1756
2430
|
config.scope,
|
|
1757
2431
|
storageKey,
|
|
@@ -1767,6 +2441,7 @@ export function createStorageCore(
|
|
|
1767
2441
|
clearPendingSecureWrite(storageKey);
|
|
1768
2442
|
}
|
|
1769
2443
|
|
|
2444
|
+
scheduleRenameSourceCleanup();
|
|
1770
2445
|
adapter.backend.remove(storageKey, config.scope);
|
|
1771
2446
|
emitKeyChange(
|
|
1772
2447
|
config.scope,
|
|
@@ -1783,6 +2458,8 @@ export function createStorageCore(
|
|
|
1783
2458
|
const oldValue = getEventRawValue(config.scope, storageKey);
|
|
1784
2459
|
if (memoryExpiration) {
|
|
1785
2460
|
memoryExpiration.set(storageKey, Date.now() + (expirationTtlMs ?? 0));
|
|
2461
|
+
} else {
|
|
2462
|
+
memoryExpirationDeadlines.delete(storageKey);
|
|
1786
2463
|
}
|
|
1787
2464
|
const storedValue =
|
|
1788
2465
|
typeof value === "string" ? escapeCollidingRawValue(value) : value;
|
|
@@ -1852,7 +2529,9 @@ export function createStorageCore(
|
|
|
1852
2529
|
return lastValue as T;
|
|
1853
2530
|
}
|
|
1854
2531
|
|
|
1855
|
-
|
|
2532
|
+
runAtomicItemMutation(() => {
|
|
2533
|
+
removeStoredRaw("expire");
|
|
2534
|
+
});
|
|
1856
2535
|
invalidateParsedCache();
|
|
1857
2536
|
onExpired?.(storageKey);
|
|
1858
2537
|
lastValue = ensureValidatedValue(defaultValue, false);
|
|
@@ -1896,7 +2575,9 @@ export function createStorageCore(
|
|
|
1896
2575
|
if (isStoredEnvelope(parsed)) {
|
|
1897
2576
|
envelopeExpiresAt = parsed.expiresAt;
|
|
1898
2577
|
if (parsed.expiresAt <= Date.now()) {
|
|
1899
|
-
|
|
2578
|
+
runAtomicItemMutation(() => {
|
|
2579
|
+
removeStoredRaw("expire");
|
|
2580
|
+
});
|
|
1900
2581
|
invalidateParsedCache();
|
|
1901
2582
|
onExpired?.(storageKey);
|
|
1902
2583
|
lastValue = ensureValidatedValue(defaultValue, false);
|
|
@@ -1943,18 +2624,20 @@ export function createStorageCore(
|
|
|
1943
2624
|
|
|
1944
2625
|
const set = (valueOrFn: T | ((prev: T) => T)): void => {
|
|
1945
2626
|
measureOperation("item:set", config.scope, () => {
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
2627
|
+
runAtomicItemMutation(() => {
|
|
2628
|
+
const newValue = isUpdater(valueOrFn)
|
|
2629
|
+
? valueOrFn(getInternal())
|
|
2630
|
+
: valueOrFn;
|
|
2631
|
+
|
|
2632
|
+
if (validate && !validate(newValue)) {
|
|
2633
|
+
throw new Error(
|
|
2634
|
+
`Validation failed for key "${storageKey}" in scope "${StorageScope[config.scope]}".`,
|
|
2635
|
+
);
|
|
2636
|
+
}
|
|
1955
2637
|
|
|
1956
|
-
|
|
1957
|
-
|
|
2638
|
+
invalidateParsedCache();
|
|
2639
|
+
writeValueWithoutValidation(newValue);
|
|
2640
|
+
});
|
|
1958
2641
|
});
|
|
1959
2642
|
};
|
|
1960
2643
|
|
|
@@ -1963,37 +2646,44 @@ export function createStorageCore(
|
|
|
1963
2646
|
valueOrFn: T | ((prev: T) => T),
|
|
1964
2647
|
): boolean =>
|
|
1965
2648
|
measureOperation("item:setIfVersion", config.scope, () => {
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
2649
|
+
let didSet = false;
|
|
2650
|
+
runAtomicItemMutation(() => {
|
|
2651
|
+
const currentVersion = getCurrentVersion();
|
|
2652
|
+
if (currentVersion !== version) {
|
|
2653
|
+
return;
|
|
2654
|
+
}
|
|
2655
|
+
set(valueOrFn);
|
|
2656
|
+
didSet = true;
|
|
2657
|
+
});
|
|
2658
|
+
return didSet;
|
|
1972
2659
|
});
|
|
1973
2660
|
|
|
1974
2661
|
const deleteItem = (): void => {
|
|
1975
2662
|
measureOperation("item:delete", config.scope, () => {
|
|
1976
|
-
|
|
2663
|
+
runAtomicItemMutation(() => {
|
|
2664
|
+
invalidateParsedCache();
|
|
1977
2665
|
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
2666
|
+
if (isMemory) {
|
|
2667
|
+
const oldValue = getEventRawValue(config.scope, storageKey);
|
|
2668
|
+
if (memoryExpiration) {
|
|
2669
|
+
memoryExpiration.delete(storageKey);
|
|
2670
|
+
}
|
|
2671
|
+
memoryExpirationDeadlines.delete(storageKey);
|
|
2672
|
+
memoryStore.delete(storageKey);
|
|
2673
|
+
notifyKeyListeners(memoryListeners, storageKey);
|
|
2674
|
+
emitKeyChange(
|
|
2675
|
+
config.scope,
|
|
2676
|
+
storageKey,
|
|
2677
|
+
oldValue,
|
|
2678
|
+
undefined,
|
|
2679
|
+
"remove",
|
|
2680
|
+
"memory",
|
|
2681
|
+
);
|
|
2682
|
+
return;
|
|
1982
2683
|
}
|
|
1983
|
-
memoryStore.delete(storageKey);
|
|
1984
|
-
notifyKeyListeners(memoryListeners, storageKey);
|
|
1985
|
-
emitKeyChange(
|
|
1986
|
-
config.scope,
|
|
1987
|
-
storageKey,
|
|
1988
|
-
oldValue,
|
|
1989
|
-
undefined,
|
|
1990
|
-
"remove",
|
|
1991
|
-
"memory",
|
|
1992
|
-
);
|
|
1993
|
-
return;
|
|
1994
|
-
}
|
|
1995
2684
|
|
|
1996
|
-
|
|
2685
|
+
removeStoredRaw();
|
|
2686
|
+
});
|
|
1997
2687
|
});
|
|
1998
2688
|
};
|
|
1999
2689
|
|
|
@@ -2023,13 +2713,15 @@ export function createStorageCore(
|
|
|
2023
2713
|
if (isMemory) return memoryStore.has(storageKey);
|
|
2024
2714
|
if (isBiometric) return adapter.backend.hasSecureBiometric(storageKey);
|
|
2025
2715
|
if (nonMemoryScope === StorageScope.Disk) {
|
|
2026
|
-
|
|
2027
|
-
|
|
2716
|
+
const pending = durability.getPendingDiskWrite(storageKey);
|
|
2717
|
+
if (pending !== undefined) {
|
|
2718
|
+
return pending.value !== undefined;
|
|
2028
2719
|
}
|
|
2029
2720
|
}
|
|
2030
2721
|
if (nonMemoryScope === StorageScope.Secure) {
|
|
2031
|
-
|
|
2032
|
-
|
|
2722
|
+
const pending = durability.getPendingSecureWrite(storageKey);
|
|
2723
|
+
if (pending !== undefined) {
|
|
2724
|
+
return pending.value !== undefined;
|
|
2033
2725
|
}
|
|
2034
2726
|
}
|
|
2035
2727
|
return adapter.backend.has(storageKey, config.scope);
|
|
@@ -2098,14 +2790,18 @@ export function createStorageCore(
|
|
|
2098
2790
|
invalidateParsedCache();
|
|
2099
2791
|
},
|
|
2100
2792
|
_deleteMemoryEntry: () => {
|
|
2101
|
-
|
|
2102
|
-
memoryExpiration.delete(storageKey);
|
|
2103
|
-
}
|
|
2793
|
+
memoryExpirationDeadlines.delete(storageKey);
|
|
2104
2794
|
memoryStore.delete(storageKey);
|
|
2105
2795
|
invalidateParsedCache();
|
|
2106
2796
|
},
|
|
2107
2797
|
_hasValidation: validate !== undefined,
|
|
2108
2798
|
_hasExpiration: expiration !== undefined,
|
|
2799
|
+
_hasRenameFrom: renameFromKeys.length > 0,
|
|
2800
|
+
_renameFromKeys: renameFromKeys,
|
|
2801
|
+
_getRenameMigrationState: () => renamesMigrated,
|
|
2802
|
+
_setRenameMigrationState: (migrated) => {
|
|
2803
|
+
renamesMigrated = migrated;
|
|
2804
|
+
},
|
|
2109
2805
|
_readCacheEnabled: readCache,
|
|
2110
2806
|
_isBiometric: isBiometric,
|
|
2111
2807
|
_biometricLevel: resolvedBiometricLevel,
|
|
@@ -2127,6 +2823,15 @@ export function createStorageCore(
|
|
|
2127
2823
|
groupSet.add(storageItem as StorageItemInternal<unknown>);
|
|
2128
2824
|
}
|
|
2129
2825
|
|
|
2826
|
+
if (isMemory) {
|
|
2827
|
+
let items = memoryItemsByKey.get(storageKey);
|
|
2828
|
+
if (!items) {
|
|
2829
|
+
items = new Set();
|
|
2830
|
+
memoryItemsByKey.set(storageKey, items);
|
|
2831
|
+
}
|
|
2832
|
+
items.add(storageItem as StorageItemInternal<unknown>);
|
|
2833
|
+
}
|
|
2834
|
+
|
|
2130
2835
|
const registryKey = `${config.scope}:${storageKey}`;
|
|
2131
2836
|
registeredKeyCounts.set(
|
|
2132
2837
|
registryKey,
|
|
@@ -2165,26 +2870,25 @@ export function createStorageCore(
|
|
|
2165
2870
|
|
|
2166
2871
|
items.forEach((item, index) => {
|
|
2167
2872
|
if (scope === StorageScope.Disk) {
|
|
2168
|
-
const pending = durability.
|
|
2873
|
+
const pending = durability.getPendingDiskWrite(item.key);
|
|
2169
2874
|
if (pending !== undefined) {
|
|
2170
|
-
rawValues[index] = pending;
|
|
2875
|
+
rawValues[index] = pending.value;
|
|
2171
2876
|
return;
|
|
2172
2877
|
}
|
|
2173
2878
|
}
|
|
2174
2879
|
|
|
2175
2880
|
if (scope === StorageScope.Secure) {
|
|
2176
|
-
const pending = durability.
|
|
2881
|
+
const pending = durability.getPendingSecureWrite(item.key);
|
|
2177
2882
|
if (pending !== undefined) {
|
|
2178
|
-
rawValues[index] = pending;
|
|
2883
|
+
rawValues[index] = pending.value;
|
|
2179
2884
|
return;
|
|
2180
2885
|
}
|
|
2181
2886
|
}
|
|
2182
2887
|
|
|
2183
2888
|
if (item._readCacheEnabled === true) {
|
|
2184
|
-
const
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
rawValues[index] = cached;
|
|
2889
|
+
const cachedEntry = getCachedRawValueEntry(scope, item.key);
|
|
2890
|
+
if (cachedEntry?.has("plain")) {
|
|
2891
|
+
rawValues[index] = cachedEntry.get("plain");
|
|
2188
2892
|
return;
|
|
2189
2893
|
}
|
|
2190
2894
|
}
|
|
@@ -2293,6 +2997,10 @@ export function createStorageCore(
|
|
|
2293
2997
|
|
|
2294
2998
|
flushSecureWrites();
|
|
2295
2999
|
const keys = secureEntries.map(({ item }) => item.key);
|
|
3000
|
+
keys.forEach((key) => {
|
|
3001
|
+
invalidateRawCache(scope, key);
|
|
3002
|
+
});
|
|
3003
|
+
const serializedValues: string[] = [];
|
|
2296
3004
|
const oldValues = shouldReadPreviousEventValues(scope)
|
|
2297
3005
|
? adapter.backend
|
|
2298
3006
|
.getBatch(keys, scope)
|
|
@@ -2308,12 +3016,14 @@ export function createStorageCore(
|
|
|
2308
3016
|
>();
|
|
2309
3017
|
|
|
2310
3018
|
secureEntries.forEach(({ item, value, internal }) => {
|
|
3019
|
+
const serialized = item.serialize(value);
|
|
3020
|
+
serializedValues.push(serialized);
|
|
2311
3021
|
const accessControl =
|
|
2312
3022
|
internal._secureAccessControl ?? secureDefaultAccessControl;
|
|
2313
3023
|
const existingGroup = groupedByAccessControl.get(accessControl);
|
|
2314
3024
|
const group = existingGroup ?? { keys: [], values: [] };
|
|
2315
3025
|
group.keys.push(item.key);
|
|
2316
|
-
group.values.push(
|
|
3026
|
+
group.values.push(serialized);
|
|
2317
3027
|
if (!existingGroup) {
|
|
2318
3028
|
groupedByAccessControl.set(accessControl, group);
|
|
2319
3029
|
}
|
|
@@ -2326,20 +3036,24 @@ export function createStorageCore(
|
|
|
2326
3036
|
cacheRawValue(scope, key, group.values[index]);
|
|
2327
3037
|
});
|
|
2328
3038
|
});
|
|
3039
|
+
const willEmitChanges =
|
|
3040
|
+
storageEvents.hasListeners(scope) || eventObserver !== undefined;
|
|
2329
3041
|
emitBatchChange(
|
|
2330
3042
|
scope,
|
|
2331
3043
|
"setBatch",
|
|
2332
3044
|
adapter.changeSource,
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
3045
|
+
willEmitChanges
|
|
3046
|
+
? keys.map((key, index) =>
|
|
3047
|
+
createKeyChange(
|
|
3048
|
+
scope,
|
|
3049
|
+
key,
|
|
3050
|
+
oldValues[index],
|
|
3051
|
+
serializedValues[index],
|
|
3052
|
+
"setBatch",
|
|
3053
|
+
adapter.changeSource,
|
|
3054
|
+
),
|
|
3055
|
+
)
|
|
3056
|
+
: [],
|
|
2343
3057
|
);
|
|
2344
3058
|
return;
|
|
2345
3059
|
}
|
|
@@ -2423,6 +3137,17 @@ export function createStorageCore(
|
|
|
2423
3137
|
return;
|
|
2424
3138
|
}
|
|
2425
3139
|
|
|
3140
|
+
if (
|
|
3141
|
+
items.some(
|
|
3142
|
+
(item) => asInternal(item as StorageItem<unknown>)._hasRenameFrom,
|
|
3143
|
+
)
|
|
3144
|
+
) {
|
|
3145
|
+
items.forEach((item) => {
|
|
3146
|
+
asInternal(item as StorageItem<unknown>).delete();
|
|
3147
|
+
});
|
|
3148
|
+
return;
|
|
3149
|
+
}
|
|
3150
|
+
|
|
2426
3151
|
const keys = items.map((item) => item.key);
|
|
2427
3152
|
if (scope === StorageScope.Disk) {
|
|
2428
3153
|
flushDiskWrites();
|
|
@@ -2431,14 +3156,46 @@ export function createStorageCore(
|
|
|
2431
3156
|
flushSecureWrites();
|
|
2432
3157
|
}
|
|
2433
3158
|
const oldValues = shouldReadPreviousEventValues(scope)
|
|
2434
|
-
?
|
|
2435
|
-
.
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
3159
|
+
? (() => {
|
|
3160
|
+
const plainItems = items.filter(
|
|
3161
|
+
(item) =>
|
|
3162
|
+
asInternal(item as StorageItem<unknown>)._isBiometric !==
|
|
3163
|
+
true,
|
|
3164
|
+
);
|
|
3165
|
+
const plainValues =
|
|
3166
|
+
plainItems.length === 0
|
|
3167
|
+
? []
|
|
3168
|
+
: adapter.backend
|
|
3169
|
+
.getBatch(
|
|
3170
|
+
plainItems.map((item) => item.key),
|
|
3171
|
+
scope,
|
|
3172
|
+
)
|
|
3173
|
+
.map((value) =>
|
|
3174
|
+
value === undefined
|
|
3175
|
+
? undefined
|
|
3176
|
+
: unescapeCollidingRawValue(value),
|
|
3177
|
+
);
|
|
3178
|
+
let plainIndex = 0;
|
|
3179
|
+
return items.map((item) => {
|
|
3180
|
+
const internal = asInternal(item as StorageItem<unknown>);
|
|
3181
|
+
if (internal._isBiometric === true) {
|
|
3182
|
+
return getEventRawValueForRepresentation(
|
|
3183
|
+
scope,
|
|
3184
|
+
item.key,
|
|
3185
|
+
"biometric",
|
|
3186
|
+
);
|
|
3187
|
+
}
|
|
3188
|
+
const value = plainValues[plainIndex];
|
|
3189
|
+
plainIndex += 1;
|
|
3190
|
+
return value;
|
|
3191
|
+
});
|
|
3192
|
+
})()
|
|
2441
3193
|
: [];
|
|
3194
|
+
if (scope === StorageScope.Secure) {
|
|
3195
|
+
keys.forEach((key) => {
|
|
3196
|
+
invalidateRawCache(scope, key);
|
|
3197
|
+
});
|
|
3198
|
+
}
|
|
2442
3199
|
adapter.backend.removeBatch(keys, scope);
|
|
2443
3200
|
keys.forEach((key) => {
|
|
2444
3201
|
cacheRawValue(scope, key, undefined);
|
|
@@ -2515,46 +3272,118 @@ export function createStorageCore(
|
|
|
2515
3272
|
}
|
|
2516
3273
|
|
|
2517
3274
|
const NOT_SET = Symbol();
|
|
2518
|
-
|
|
3275
|
+
type TransactionRollbackEntry = {
|
|
3276
|
+
key: string;
|
|
3277
|
+
representation: StorageRawCacheRepresentation;
|
|
3278
|
+
record: RollbackRecord;
|
|
3279
|
+
};
|
|
3280
|
+
const rollback = new Map<string, TransactionRollbackEntry>();
|
|
3281
|
+
const itemRenameStates = new Map<StorageItemInternal<unknown>, boolean>();
|
|
2519
3282
|
|
|
2520
3283
|
const rememberRollback = (
|
|
2521
3284
|
key: string,
|
|
2522
3285
|
item?: Pick<StorageItem<unknown>, "key" | "scope">,
|
|
3286
|
+
includeOtherSecureRepresentation = false,
|
|
2523
3287
|
) => {
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
) {
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
3288
|
+
const internal = item
|
|
3289
|
+
? (item as StorageItemInternal<unknown>)
|
|
3290
|
+
: undefined;
|
|
3291
|
+
const representation: StorageRawCacheRepresentation =
|
|
3292
|
+
scope === StorageScope.Secure && internal?._isBiometric === true
|
|
3293
|
+
? "biometric"
|
|
3294
|
+
: "plain";
|
|
3295
|
+
const rememberRepresentation = (
|
|
3296
|
+
representationToRemember: StorageRawCacheRepresentation,
|
|
3297
|
+
) => {
|
|
3298
|
+
const identity = JSON.stringify([
|
|
3299
|
+
scope,
|
|
3300
|
+
key,
|
|
3301
|
+
representationToRemember,
|
|
3302
|
+
]);
|
|
3303
|
+
if (rollback.has(identity)) {
|
|
3304
|
+
return;
|
|
3305
|
+
}
|
|
3306
|
+
if (scope === StorageScope.Memory) {
|
|
3307
|
+
const expiresAt = memoryExpirationDeadlines.get(key);
|
|
3308
|
+
rollback.set(identity, {
|
|
3309
|
+
key,
|
|
3310
|
+
representation: representationToRemember,
|
|
3311
|
+
record: {
|
|
3312
|
+
kind: "memory",
|
|
3313
|
+
value: memoryStore.has(key) ? memoryStore.get(key) : NOT_SET,
|
|
3314
|
+
...(expiresAt === undefined ? {} : { expiresAt }),
|
|
3315
|
+
},
|
|
2544
3316
|
});
|
|
2545
3317
|
return;
|
|
2546
3318
|
}
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
3319
|
+
if (representationToRemember === "biometric") {
|
|
3320
|
+
rollback.set(identity, {
|
|
3321
|
+
key,
|
|
3322
|
+
representation: representationToRemember,
|
|
3323
|
+
record: {
|
|
3324
|
+
kind: "biometric",
|
|
3325
|
+
value: getStoredRawValueForRepresentation(
|
|
3326
|
+
key,
|
|
3327
|
+
scope,
|
|
3328
|
+
representationToRemember,
|
|
3329
|
+
),
|
|
3330
|
+
level:
|
|
3331
|
+
internal?._biometricLevel !== undefined &&
|
|
3332
|
+
internal._biometricLevel !== BiometricLevel.None
|
|
3333
|
+
? internal._biometricLevel
|
|
3334
|
+
: BiometricLevel.BiometryOnly,
|
|
3335
|
+
},
|
|
3336
|
+
});
|
|
3337
|
+
return;
|
|
3338
|
+
}
|
|
3339
|
+
rollback.set(identity, {
|
|
3340
|
+
key,
|
|
3341
|
+
representation: representationToRemember,
|
|
3342
|
+
record: {
|
|
3343
|
+
kind: "raw",
|
|
3344
|
+
value: getStoredRawValueForRepresentation(
|
|
3345
|
+
key,
|
|
3346
|
+
scope,
|
|
3347
|
+
representationToRemember,
|
|
3348
|
+
),
|
|
3349
|
+
...(scope === StorageScope.Secure &&
|
|
3350
|
+
internal?._secureAccessControl !== undefined
|
|
3351
|
+
? { accessControl: internal._secureAccessControl }
|
|
3352
|
+
: {}),
|
|
3353
|
+
},
|
|
2554
3354
|
});
|
|
3355
|
+
};
|
|
3356
|
+
|
|
3357
|
+
if (
|
|
3358
|
+
representation === "biometric" ||
|
|
3359
|
+
includeOtherSecureRepresentation
|
|
3360
|
+
) {
|
|
3361
|
+
rememberRepresentation("plain");
|
|
3362
|
+
}
|
|
3363
|
+
rememberRepresentation(representation);
|
|
3364
|
+
if (includeOtherSecureRepresentation) {
|
|
3365
|
+
rememberRepresentation("biometric");
|
|
2555
3366
|
}
|
|
2556
3367
|
};
|
|
2557
3368
|
|
|
3369
|
+
const rememberItemRollback = (
|
|
3370
|
+
item: Pick<StorageItem<unknown>, "key" | "scope">,
|
|
3371
|
+
): void => {
|
|
3372
|
+
const internal = item as StorageItemInternal<unknown>;
|
|
3373
|
+
const includeOtherSecureRepresentation =
|
|
3374
|
+
scope === StorageScope.Secure &&
|
|
3375
|
+
(internal._isBiometric === true || internal._hasRenameFrom === true);
|
|
3376
|
+
rememberRollback(item.key, item, includeOtherSecureRepresentation);
|
|
3377
|
+
if (internal._getRenameMigrationState) {
|
|
3378
|
+
if (!itemRenameStates.has(internal)) {
|
|
3379
|
+
itemRenameStates.set(internal, internal._getRenameMigrationState());
|
|
3380
|
+
}
|
|
3381
|
+
}
|
|
3382
|
+
(internal._renameFromKeys ?? EMPTY_KEYS).forEach((aliasKey) => {
|
|
3383
|
+
rememberRollback(aliasKey, item, true);
|
|
3384
|
+
});
|
|
3385
|
+
};
|
|
3386
|
+
|
|
2558
3387
|
const tx: TransactionContext = {
|
|
2559
3388
|
scope,
|
|
2560
3389
|
getRaw: (key) => getRawValue(key, scope),
|
|
@@ -2563,21 +3392,22 @@ export function createStorageCore(
|
|
|
2563
3392
|
setRawValue(key, value, scope);
|
|
2564
3393
|
},
|
|
2565
3394
|
removeRaw: (key) => {
|
|
2566
|
-
rememberRollback(key);
|
|
3395
|
+
rememberRollback(key, undefined, scope === StorageScope.Secure);
|
|
2567
3396
|
removeRawValue(key, scope);
|
|
2568
3397
|
},
|
|
2569
3398
|
getItem: (item) => {
|
|
2570
3399
|
assertBatchScope([item], scope);
|
|
3400
|
+
rememberItemRollback(item);
|
|
2571
3401
|
return item.get();
|
|
2572
3402
|
},
|
|
2573
3403
|
setItem: (item, value) => {
|
|
2574
3404
|
assertBatchScope([item], scope);
|
|
2575
|
-
|
|
3405
|
+
rememberItemRollback(item);
|
|
2576
3406
|
item.set(value);
|
|
2577
3407
|
},
|
|
2578
3408
|
removeItem: (item) => {
|
|
2579
3409
|
assertBatchScope([item], scope);
|
|
2580
|
-
|
|
3410
|
+
rememberItemRollback(item);
|
|
2581
3411
|
item.delete();
|
|
2582
3412
|
},
|
|
2583
3413
|
};
|
|
@@ -2585,21 +3415,56 @@ export function createStorageCore(
|
|
|
2585
3415
|
try {
|
|
2586
3416
|
return transaction(tx);
|
|
2587
3417
|
} catch (error) {
|
|
2588
|
-
|
|
3418
|
+
itemRenameStates.forEach((migrated, item) => {
|
|
3419
|
+
item._setRenameMigrationState(migrated);
|
|
3420
|
+
});
|
|
3421
|
+
const rollbackEntries = Array.from(rollback.values()).reverse();
|
|
2589
3422
|
const rollbackSource =
|
|
2590
3423
|
scope === StorageScope.Memory ? "memory" : adapter.changeSource;
|
|
2591
|
-
const
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
3424
|
+
const rollbackErrors: { label: string; error: unknown }[] = [];
|
|
3425
|
+
const readRollbackEventValue = (
|
|
3426
|
+
entry: TransactionRollbackEntry,
|
|
3427
|
+
): string | undefined => {
|
|
3428
|
+
try {
|
|
3429
|
+
return getEventRawValueForRepresentation(
|
|
3430
|
+
scope,
|
|
3431
|
+
entry.key,
|
|
3432
|
+
entry.representation,
|
|
3433
|
+
);
|
|
3434
|
+
} catch (readError) {
|
|
3435
|
+
rollbackErrors.push({
|
|
3436
|
+
label: `rollback read ${entry.representation}:${entry.key}`,
|
|
3437
|
+
error: readError,
|
|
3438
|
+
});
|
|
3439
|
+
return undefined;
|
|
3440
|
+
}
|
|
3441
|
+
};
|
|
3442
|
+
const preRollbackValues = rollbackEntries.map(readRollbackEventValue);
|
|
2595
3443
|
if (scope === StorageScope.Memory) {
|
|
2596
|
-
rollbackEntries.forEach((
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
3444
|
+
rollbackEntries.forEach((entry) => {
|
|
3445
|
+
try {
|
|
3446
|
+
const record = entry.record;
|
|
3447
|
+
if (record.kind !== "memory") {
|
|
3448
|
+
return;
|
|
3449
|
+
}
|
|
3450
|
+
if (record.value === NOT_SET) {
|
|
3451
|
+
memoryStore.delete(entry.key);
|
|
3452
|
+
} else {
|
|
3453
|
+
memoryStore.set(entry.key, record.value);
|
|
3454
|
+
}
|
|
3455
|
+
if (record.expiresAt === undefined) {
|
|
3456
|
+
memoryExpirationDeadlines.delete(entry.key);
|
|
3457
|
+
} else {
|
|
3458
|
+
memoryExpirationDeadlines.set(entry.key, record.expiresAt);
|
|
3459
|
+
}
|
|
3460
|
+
invalidateMemoryItemCaches(entry.key);
|
|
3461
|
+
notifyKeyListeners(memoryListeners, entry.key);
|
|
3462
|
+
} catch (rollbackError) {
|
|
3463
|
+
rollbackErrors.push({
|
|
3464
|
+
label: `rollback ${entry.representation}:${entry.key}`,
|
|
3465
|
+
error: rollbackError,
|
|
3466
|
+
});
|
|
2601
3467
|
}
|
|
2602
|
-
notifyKeyListeners(memoryListeners, key);
|
|
2603
3468
|
});
|
|
2604
3469
|
} else {
|
|
2605
3470
|
const groupedKeysToSet = new Map<
|
|
@@ -2607,18 +3472,14 @@ export function createStorageCore(
|
|
|
2607
3472
|
{ keys: string[]; values: string[] }
|
|
2608
3473
|
>();
|
|
2609
3474
|
const keysToRemove: string[] = [];
|
|
3475
|
+
const biometricEntries: TransactionRollbackEntry[] = [];
|
|
3476
|
+
const biometricValues = new Map<string, string | undefined>();
|
|
2610
3477
|
|
|
2611
|
-
rollbackEntries.forEach((
|
|
3478
|
+
rollbackEntries.forEach((entry) => {
|
|
3479
|
+
const { key, record } = entry;
|
|
2612
3480
|
if (record.kind === "biometric") {
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
} else {
|
|
2616
|
-
adapter.backend.setSecureBiometricWithLevel(
|
|
2617
|
-
key,
|
|
2618
|
-
record.value,
|
|
2619
|
-
record.level,
|
|
2620
|
-
);
|
|
2621
|
-
}
|
|
3481
|
+
biometricEntries.push(entry);
|
|
3482
|
+
biometricValues.set(key, record.value);
|
|
2622
3483
|
return;
|
|
2623
3484
|
}
|
|
2624
3485
|
if (record.kind !== "raw") {
|
|
@@ -2640,26 +3501,115 @@ export function createStorageCore(
|
|
|
2640
3501
|
});
|
|
2641
3502
|
|
|
2642
3503
|
if (scope === StorageScope.Disk) {
|
|
2643
|
-
|
|
3504
|
+
try {
|
|
3505
|
+
flushDiskWrites();
|
|
3506
|
+
} catch (flushError) {
|
|
3507
|
+
rollbackErrors.push({
|
|
3508
|
+
label: "rollback flush disk",
|
|
3509
|
+
error: flushError,
|
|
3510
|
+
});
|
|
3511
|
+
}
|
|
2644
3512
|
}
|
|
2645
3513
|
if (scope === StorageScope.Secure) {
|
|
2646
|
-
|
|
3514
|
+
try {
|
|
3515
|
+
flushSecureWrites();
|
|
3516
|
+
} catch (flushError) {
|
|
3517
|
+
rollbackErrors.push({
|
|
3518
|
+
label: "rollback flush secure",
|
|
3519
|
+
error: flushError,
|
|
3520
|
+
});
|
|
3521
|
+
}
|
|
2647
3522
|
}
|
|
3523
|
+
if (scope === StorageScope.Secure) {
|
|
3524
|
+
rollbackEntries.forEach((entry) => {
|
|
3525
|
+
invalidateRawCache(scope, entry.key);
|
|
3526
|
+
});
|
|
3527
|
+
}
|
|
3528
|
+
biometricEntries.forEach((entry) => {
|
|
3529
|
+
const { key, record } = entry;
|
|
3530
|
+
if (record.kind !== "biometric" || record.value === undefined) {
|
|
3531
|
+
return;
|
|
3532
|
+
}
|
|
3533
|
+
try {
|
|
3534
|
+
adapter.backend.setSecureBiometricWithLevel(
|
|
3535
|
+
key,
|
|
3536
|
+
record.value,
|
|
3537
|
+
record.level,
|
|
3538
|
+
);
|
|
3539
|
+
cacheRawValue(
|
|
3540
|
+
StorageScope.Secure,
|
|
3541
|
+
key,
|
|
3542
|
+
record.value,
|
|
3543
|
+
"biometric",
|
|
3544
|
+
);
|
|
3545
|
+
} catch (rollbackError) {
|
|
3546
|
+
rollbackErrors.push({
|
|
3547
|
+
label: "rollback biometric",
|
|
3548
|
+
error: rollbackError,
|
|
3549
|
+
});
|
|
3550
|
+
}
|
|
3551
|
+
});
|
|
2648
3552
|
groupedKeysToSet.forEach((group, accessControl) => {
|
|
2649
|
-
|
|
2650
|
-
|
|
3553
|
+
try {
|
|
3554
|
+
if (scope === StorageScope.Secure) {
|
|
3555
|
+
adapter.backend.setSecureAccessControl(accessControl);
|
|
3556
|
+
}
|
|
3557
|
+
adapter.backend.setBatch(group.keys, group.values, scope);
|
|
3558
|
+
group.keys.forEach((key, index) => {
|
|
3559
|
+
cacheRawValue(scope, key, group.values[index], "plain");
|
|
3560
|
+
});
|
|
3561
|
+
} catch (rollbackError) {
|
|
3562
|
+
rollbackErrors.push({
|
|
3563
|
+
label: "rollback plain setBatch",
|
|
3564
|
+
error: rollbackError,
|
|
3565
|
+
});
|
|
2651
3566
|
}
|
|
2652
|
-
adapter.backend.setBatch(group.keys, group.values, scope);
|
|
2653
|
-
group.keys.forEach((key, index) => {
|
|
2654
|
-
cacheRawValue(scope, key, group.values[index]);
|
|
2655
|
-
});
|
|
2656
3567
|
});
|
|
2657
3568
|
if (keysToRemove.length > 0) {
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
3569
|
+
const keysToRemoveWithoutBiometric = keysToRemove.filter(
|
|
3570
|
+
(key) => biometricValues.get(key) === undefined,
|
|
3571
|
+
);
|
|
3572
|
+
try {
|
|
3573
|
+
if (keysToRemoveWithoutBiometric.length > 0) {
|
|
3574
|
+
adapter.backend.removeBatch(
|
|
3575
|
+
keysToRemoveWithoutBiometric,
|
|
3576
|
+
scope,
|
|
3577
|
+
);
|
|
3578
|
+
}
|
|
3579
|
+
keysToRemove.forEach((key) => {
|
|
3580
|
+
cacheRawValue(scope, key, undefined, "plain");
|
|
3581
|
+
});
|
|
3582
|
+
} catch (rollbackError) {
|
|
3583
|
+
rollbackErrors.push({
|
|
3584
|
+
label: "rollback plain removeBatch",
|
|
3585
|
+
error: rollbackError,
|
|
3586
|
+
});
|
|
3587
|
+
}
|
|
2662
3588
|
}
|
|
3589
|
+
biometricEntries.forEach((entry) => {
|
|
3590
|
+
const { key, record } = entry;
|
|
3591
|
+
if (record.kind !== "biometric") {
|
|
3592
|
+
return;
|
|
3593
|
+
}
|
|
3594
|
+
if (record.value !== undefined) {
|
|
3595
|
+
return;
|
|
3596
|
+
}
|
|
3597
|
+
invalidateRawCache(StorageScope.Secure, key);
|
|
3598
|
+
try {
|
|
3599
|
+
adapter.backend.deleteSecureBiometric(key);
|
|
3600
|
+
cacheRawValue(
|
|
3601
|
+
StorageScope.Secure,
|
|
3602
|
+
key,
|
|
3603
|
+
record.value,
|
|
3604
|
+
"biometric",
|
|
3605
|
+
);
|
|
3606
|
+
} catch (rollbackError) {
|
|
3607
|
+
rollbackErrors.push({
|
|
3608
|
+
label: "rollback biometric",
|
|
3609
|
+
error: rollbackError,
|
|
3610
|
+
});
|
|
3611
|
+
}
|
|
3612
|
+
});
|
|
2663
3613
|
}
|
|
2664
3614
|
|
|
2665
3615
|
if (rollbackEntries.length > 0) {
|
|
@@ -2667,18 +3617,25 @@ export function createStorageCore(
|
|
|
2667
3617
|
scope,
|
|
2668
3618
|
"rollback",
|
|
2669
3619
|
rollbackSource,
|
|
2670
|
-
rollbackEntries.map((
|
|
3620
|
+
rollbackEntries.map((entry, index) =>
|
|
2671
3621
|
createKeyChange(
|
|
2672
3622
|
scope,
|
|
2673
|
-
key,
|
|
3623
|
+
entry.key,
|
|
2674
3624
|
preRollbackValues[index],
|
|
2675
|
-
|
|
3625
|
+
readRollbackEventValue(entry),
|
|
2676
3626
|
"rollback",
|
|
2677
3627
|
rollbackSource,
|
|
2678
3628
|
),
|
|
2679
3629
|
),
|
|
2680
3630
|
);
|
|
2681
3631
|
}
|
|
3632
|
+
if (rollbackErrors.length > 0) {
|
|
3633
|
+
throw createStorageCompositeError(
|
|
3634
|
+
"transaction rollback",
|
|
3635
|
+
error,
|
|
3636
|
+
rollbackErrors,
|
|
3637
|
+
);
|
|
3638
|
+
}
|
|
2682
3639
|
throw error;
|
|
2683
3640
|
}
|
|
2684
3641
|
});
|
|
@@ -2753,14 +3710,14 @@ export function createStorageCore(
|
|
|
2753
3710
|
config: SetItemConfig<TMember>,
|
|
2754
3711
|
): SetStorageItem<TMember> {
|
|
2755
3712
|
const { defaultValue, ...rest } = config;
|
|
2756
|
-
const initial:
|
|
3713
|
+
const initial: Record<string, true> = {};
|
|
2757
3714
|
if (defaultValue) {
|
|
2758
3715
|
for (const id of defaultValue) {
|
|
2759
3716
|
initial[id] = true;
|
|
2760
3717
|
}
|
|
2761
3718
|
}
|
|
2762
3719
|
|
|
2763
|
-
const item = createStorageItem<
|
|
3720
|
+
const item = createStorageItem<Record<string, true>>({
|
|
2764
3721
|
...rest,
|
|
2765
3722
|
defaultValue: initial,
|
|
2766
3723
|
});
|
|
@@ -2796,8 +3753,17 @@ export function createStorageCore(
|
|
|
2796
3753
|
return true;
|
|
2797
3754
|
};
|
|
2798
3755
|
|
|
3756
|
+
const getTyped = (): Partial<Record<TMember, true>> => {
|
|
3757
|
+
const typed: Partial<Record<TMember, true>> = {};
|
|
3758
|
+
for (const id of Object.keys(item.get())) {
|
|
3759
|
+
typed[id as TMember] = true;
|
|
3760
|
+
}
|
|
3761
|
+
return typed;
|
|
3762
|
+
};
|
|
3763
|
+
|
|
2799
3764
|
return {
|
|
2800
3765
|
get: item.get,
|
|
3766
|
+
getTyped,
|
|
2801
3767
|
has,
|
|
2802
3768
|
add,
|
|
2803
3769
|
delete: deleteId,
|