react-native-nitro-storage 0.9.0 → 0.10.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/CHANGELOG.md +89 -29
- package/README.md +81 -24
- 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 +110 -43
- 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 +852 -141
- 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 +110 -43
- 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 +853 -142
- 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 +7 -2
- 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 +1 -1
- package/src/capabilities.ts +3 -1
- package/src/core/durability.ts +139 -39
- 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 +1165 -202
- 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;
|
|
@@ -525,16 +586,23 @@ export function createStorageCore(
|
|
|
525
586
|
durability.flushSecureWrites();
|
|
526
587
|
}
|
|
527
588
|
|
|
528
|
-
function
|
|
529
|
-
durability.
|
|
589
|
+
function runSecurePromotion<T>(key: string, promotion: () => T): T {
|
|
590
|
+
return durability.runSecurePromotion(key, promotion);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function scheduleDiskWrite(
|
|
594
|
+
key: string,
|
|
595
|
+
value: string | undefined,
|
|
596
|
+
): PendingDiskWrite {
|
|
597
|
+
return durability.scheduleDiskWrite(key, value);
|
|
530
598
|
}
|
|
531
599
|
|
|
532
600
|
function scheduleSecureWrite(
|
|
533
601
|
key: string,
|
|
534
602
|
value: string | undefined,
|
|
535
603
|
accessControl?: AccessControl,
|
|
536
|
-
):
|
|
537
|
-
durability.scheduleSecureWrite(key, value, accessControl);
|
|
604
|
+
): PendingSecureWrite {
|
|
605
|
+
return durability.scheduleSecureWrite(key, value, accessControl);
|
|
538
606
|
}
|
|
539
607
|
|
|
540
608
|
function setDiskWritesAsyncMode(enabled: boolean): void {
|
|
@@ -565,6 +633,17 @@ export function createStorageCore(
|
|
|
565
633
|
return adapter.backend.get(key, scope);
|
|
566
634
|
}
|
|
567
635
|
|
|
636
|
+
function getStoredRawValueForRepresentation(
|
|
637
|
+
key: string,
|
|
638
|
+
scope: StorageScope,
|
|
639
|
+
representation: StorageRawCacheRepresentation,
|
|
640
|
+
): string | undefined {
|
|
641
|
+
if (representation === "plain") {
|
|
642
|
+
return getStoredRawValue(key, scope);
|
|
643
|
+
}
|
|
644
|
+
return adapter.backend.getSecureBiometric(key);
|
|
645
|
+
}
|
|
646
|
+
|
|
568
647
|
function getRawValue(key: string, scope: StorageScope): string | undefined {
|
|
569
648
|
assertValidScope(scope);
|
|
570
649
|
if (scope === StorageScope.Memory) {
|
|
@@ -617,6 +696,7 @@ export function createStorageCore(
|
|
|
617
696
|
}
|
|
618
697
|
|
|
619
698
|
if (scope === StorageScope.Secure) {
|
|
699
|
+
invalidateRawCache(scope, key);
|
|
620
700
|
flushSecureWrites();
|
|
621
701
|
clearPendingSecureWrite(key);
|
|
622
702
|
if (adapter.applyAccessControlOnSecureRawWrite) {
|
|
@@ -659,6 +739,7 @@ export function createStorageCore(
|
|
|
659
739
|
}
|
|
660
740
|
|
|
661
741
|
if (scope === StorageScope.Secure) {
|
|
742
|
+
invalidateRawCache(scope, key);
|
|
662
743
|
flushSecureWrites();
|
|
663
744
|
clearPendingSecureWrite(key);
|
|
664
745
|
}
|
|
@@ -689,6 +770,7 @@ export function createStorageCore(
|
|
|
689
770
|
getScopedListeners,
|
|
690
771
|
cacheRawValue,
|
|
691
772
|
readCachedRawValue,
|
|
773
|
+
invalidateRawCache,
|
|
692
774
|
clearScopeRawCache,
|
|
693
775
|
clearPendingDiskWrite,
|
|
694
776
|
clearPendingSecureWrite,
|
|
@@ -779,6 +861,11 @@ export function createStorageCore(
|
|
|
779
861
|
: unescapeCollidingRawValue(value),
|
|
780
862
|
)
|
|
781
863
|
: [];
|
|
864
|
+
if (scope === StorageScope.Secure) {
|
|
865
|
+
removeKeys.forEach((key) => {
|
|
866
|
+
invalidateRawCache(scope, key);
|
|
867
|
+
});
|
|
868
|
+
}
|
|
782
869
|
adapter.backend.removeBatch(removeKeys, scope);
|
|
783
870
|
removeKeys.forEach((key) => {
|
|
784
871
|
cacheRawValue(scope, key, undefined);
|
|
@@ -885,6 +972,7 @@ export function createStorageCore(
|
|
|
885
972
|
: {};
|
|
886
973
|
if (scope === StorageScope.Memory) {
|
|
887
974
|
memoryStore.clear();
|
|
975
|
+
memoryExpirationDeadlines.clear();
|
|
888
976
|
notifyAllListeners(memoryListeners);
|
|
889
977
|
emitBatchChange(
|
|
890
978
|
scope,
|
|
@@ -1044,6 +1132,7 @@ export function createStorageCore(
|
|
|
1044
1132
|
|
|
1045
1133
|
affectedKeys.forEach((key) => {
|
|
1046
1134
|
memoryStore.delete(key);
|
|
1135
|
+
memoryExpirationDeadlines.delete(key);
|
|
1047
1136
|
});
|
|
1048
1137
|
affectedKeys.forEach((key) => {
|
|
1049
1138
|
notifyKeyListeners(memoryListeners, key);
|
|
@@ -1103,7 +1192,48 @@ export function createStorageCore(
|
|
|
1103
1192
|
},
|
|
1104
1193
|
clearBiometric: () => {
|
|
1105
1194
|
measureOperation("storage:clearBiometric", StorageScope.Secure, () => {
|
|
1106
|
-
|
|
1195
|
+
flushSecureWrites();
|
|
1196
|
+
const readEventValues = shouldReadPreviousEventValues(
|
|
1197
|
+
StorageScope.Secure,
|
|
1198
|
+
);
|
|
1199
|
+
const shouldEmitChanges =
|
|
1200
|
+
storageEvents.hasListeners(StorageScope.Secure) ||
|
|
1201
|
+
eventObserver !== undefined;
|
|
1202
|
+
const biometricKeys = shouldEmitChanges
|
|
1203
|
+
? adapter.backend
|
|
1204
|
+
.getAllKeys(StorageScope.Secure)
|
|
1205
|
+
.filter((key) => adapter.backend.hasSecureBiometric(key))
|
|
1206
|
+
: [];
|
|
1207
|
+
const previousValues = readEventValues
|
|
1208
|
+
? biometricKeys.map((key) => {
|
|
1209
|
+
const raw = adapter.backend.getSecureBiometric(key);
|
|
1210
|
+
return raw === undefined
|
|
1211
|
+
? undefined
|
|
1212
|
+
: unescapeCollidingRawValue(raw);
|
|
1213
|
+
})
|
|
1214
|
+
: [];
|
|
1215
|
+
|
|
1216
|
+
clearScopeRawCache(StorageScope.Secure);
|
|
1217
|
+
try {
|
|
1218
|
+
adapter.backend.clearSecureBiometric();
|
|
1219
|
+
} finally {
|
|
1220
|
+
clearScopeRawCache(StorageScope.Secure);
|
|
1221
|
+
}
|
|
1222
|
+
emitBatchChange(
|
|
1223
|
+
StorageScope.Secure,
|
|
1224
|
+
"clear",
|
|
1225
|
+
adapter.changeSource,
|
|
1226
|
+
biometricKeys.map((key, index) =>
|
|
1227
|
+
createKeyChange(
|
|
1228
|
+
StorageScope.Secure,
|
|
1229
|
+
key,
|
|
1230
|
+
readEventValues ? previousValues[index] : undefined,
|
|
1231
|
+
undefined,
|
|
1232
|
+
"clear",
|
|
1233
|
+
adapter.changeSource,
|
|
1234
|
+
),
|
|
1235
|
+
),
|
|
1236
|
+
);
|
|
1107
1237
|
});
|
|
1108
1238
|
},
|
|
1109
1239
|
has: (key: string, scope: StorageScope): boolean => {
|
|
@@ -1376,6 +1506,9 @@ export function createStorageCore(
|
|
|
1376
1506
|
if (scope === StorageScope.Secure) {
|
|
1377
1507
|
flushSecureWrites();
|
|
1378
1508
|
adapter.backend.setSecureAccessControl(secureDefaultAccessControl);
|
|
1509
|
+
keys.forEach((key) => {
|
|
1510
|
+
invalidateRawCache(scope, key);
|
|
1511
|
+
});
|
|
1379
1512
|
}
|
|
1380
1513
|
if (scope === StorageScope.Disk) {
|
|
1381
1514
|
flushDiskWrites();
|
|
@@ -1414,8 +1547,11 @@ export function createStorageCore(
|
|
|
1414
1547
|
const onExpired = config.onExpired;
|
|
1415
1548
|
const expirationTtlMs = expiration?.ttlMs;
|
|
1416
1549
|
const memoryExpiration =
|
|
1417
|
-
expiration && isMemory ?
|
|
1550
|
+
expiration && isMemory ? memoryExpirationDeadlines : null;
|
|
1418
1551
|
const readCache = !isMemory && config.readCache === true;
|
|
1552
|
+
const rawCacheRepresentation: StorageRawCacheRepresentation = isBiometric
|
|
1553
|
+
? "biometric"
|
|
1554
|
+
: "plain";
|
|
1419
1555
|
const coalesceDiskWrites =
|
|
1420
1556
|
config.scope === StorageScope.Disk && config.coalesceDiskWrites === true;
|
|
1421
1557
|
const coalesceSecureWrites =
|
|
@@ -1527,34 +1663,53 @@ export function createStorageCore(
|
|
|
1527
1663
|
: memoryStored;
|
|
1528
1664
|
}
|
|
1529
1665
|
|
|
1666
|
+
if (nonMemoryScope === StorageScope.Disk) {
|
|
1667
|
+
if (durability.hasPendingDiskWrite(storageKey)) {
|
|
1668
|
+
return durability.readPendingDiskWrite(storageKey);
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
if (nonMemoryScope === StorageScope.Secure && !isBiometric) {
|
|
1673
|
+
if (durability.hasPendingSecureWrite(storageKey)) {
|
|
1674
|
+
return durability.readPendingSecureWrite(storageKey);
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1530
1678
|
migrateRenamesIfNeeded();
|
|
1531
1679
|
|
|
1532
1680
|
if (nonMemoryScope === StorageScope.Disk) {
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
return pending;
|
|
1681
|
+
if (durability.hasPendingDiskWrite(storageKey)) {
|
|
1682
|
+
return durability.readPendingDiskWrite(storageKey);
|
|
1536
1683
|
}
|
|
1537
1684
|
}
|
|
1538
1685
|
|
|
1539
1686
|
if (nonMemoryScope === StorageScope.Secure && !isBiometric) {
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
return pending;
|
|
1687
|
+
if (durability.hasPendingSecureWrite(storageKey)) {
|
|
1688
|
+
return durability.readPendingSecureWrite(storageKey);
|
|
1543
1689
|
}
|
|
1544
1690
|
}
|
|
1545
1691
|
|
|
1546
1692
|
if (readCache) {
|
|
1547
|
-
const
|
|
1548
|
-
const
|
|
1549
|
-
if (
|
|
1550
|
-
return
|
|
1693
|
+
const scope = resolveNonMemoryScope();
|
|
1694
|
+
const cachedEntry = getCachedRawValueEntry(scope, storageKey);
|
|
1695
|
+
if (cachedEntry?.has(rawCacheRepresentation)) {
|
|
1696
|
+
return cachedEntry.get(rawCacheRepresentation);
|
|
1551
1697
|
}
|
|
1552
1698
|
}
|
|
1553
1699
|
|
|
1554
1700
|
if (isBiometric) {
|
|
1555
|
-
|
|
1701
|
+
const raw = readBackendRaw(() =>
|
|
1556
1702
|
adapter.backend.getSecureBiometric(storageKey),
|
|
1557
1703
|
);
|
|
1704
|
+
if (readCache) {
|
|
1705
|
+
cacheRawValue(
|
|
1706
|
+
resolveNonMemoryScope(),
|
|
1707
|
+
storageKey,
|
|
1708
|
+
raw,
|
|
1709
|
+
rawCacheRepresentation,
|
|
1710
|
+
);
|
|
1711
|
+
}
|
|
1712
|
+
return raw;
|
|
1558
1713
|
}
|
|
1559
1714
|
|
|
1560
1715
|
const raw = readBackendRaw(() =>
|
|
@@ -1572,8 +1727,11 @@ export function createStorageCore(
|
|
|
1572
1727
|
} catch (error) {
|
|
1573
1728
|
onReadError?.(error);
|
|
1574
1729
|
if (fallbackToCacheOnReadError) {
|
|
1575
|
-
const
|
|
1730
|
+
const scope = resolveNonMemoryScope();
|
|
1731
|
+
const cached = readCachedRawValue(
|
|
1732
|
+
scope,
|
|
1576
1733
|
storageKey,
|
|
1734
|
+
rawCacheRepresentation,
|
|
1577
1735
|
);
|
|
1578
1736
|
if (cached !== undefined) {
|
|
1579
1737
|
return cached;
|
|
@@ -1584,14 +1742,446 @@ export function createStorageCore(
|
|
|
1584
1742
|
}
|
|
1585
1743
|
};
|
|
1586
1744
|
|
|
1587
|
-
|
|
1745
|
+
type StoredRawWriteOptions = {
|
|
1746
|
+
cleanupRenameSources?: boolean;
|
|
1747
|
+
};
|
|
1748
|
+
type RenameSnapshot = {
|
|
1749
|
+
key: string;
|
|
1750
|
+
plainValue?: string;
|
|
1751
|
+
biometricValue?: string;
|
|
1752
|
+
};
|
|
1753
|
+
|
|
1754
|
+
function getRenameSourceKeys(): string[] {
|
|
1755
|
+
return renameFromKeys.filter((key) => key !== storageKey);
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
function getRenameSourceRaw(key: string): string | undefined {
|
|
1759
|
+
return isBiometric
|
|
1760
|
+
? adapter.backend.getSecureBiometric(key)
|
|
1761
|
+
: adapter.backend.get(key, config.scope);
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
function invalidateRenameSourceCache(key: string): void {
|
|
1765
|
+
if (nonMemoryScope !== null) {
|
|
1766
|
+
invalidateRawCache(nonMemoryScope, key);
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
function removeRenameSource(key: string): void {
|
|
1771
|
+
invalidateRenameSourceCache(key);
|
|
1772
|
+
if (isBiometric) {
|
|
1773
|
+
adapter.backend.deleteSecureBiometric(key);
|
|
1774
|
+
invalidateRenameSourceCache(key);
|
|
1775
|
+
return;
|
|
1776
|
+
}
|
|
1777
|
+
adapter.backend.remove(key, config.scope);
|
|
1778
|
+
invalidateRenameSourceCache(key);
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
function restoreRenameSource(snapshot: RenameSnapshot): void {
|
|
1782
|
+
invalidateRenameSourceCache(snapshot.key);
|
|
1783
|
+
if (nonMemoryScope === StorageScope.Secure) {
|
|
1784
|
+
if (snapshot.biometricValue !== undefined) {
|
|
1785
|
+
adapter.backend.setSecureBiometricWithLevel(
|
|
1786
|
+
snapshot.key,
|
|
1787
|
+
snapshot.biometricValue,
|
|
1788
|
+
resolvedBiometricLevel === BiometricLevel.None
|
|
1789
|
+
? BiometricLevel.BiometryOnly
|
|
1790
|
+
: resolvedBiometricLevel,
|
|
1791
|
+
);
|
|
1792
|
+
} else {
|
|
1793
|
+
adapter.backend.deleteSecureBiometric(snapshot.key);
|
|
1794
|
+
}
|
|
1795
|
+
if (snapshot.plainValue !== undefined) {
|
|
1796
|
+
if (adapter.applyAccessControlOnSecureRawWrite) {
|
|
1797
|
+
adapter.backend.setSecureAccessControl(
|
|
1798
|
+
secureAccessControl ?? secureDefaultAccessControl,
|
|
1799
|
+
);
|
|
1800
|
+
}
|
|
1801
|
+
adapter.backend.set(snapshot.key, snapshot.plainValue, config.scope);
|
|
1802
|
+
} else if (snapshot.biometricValue === undefined) {
|
|
1803
|
+
adapter.backend.remove(snapshot.key, config.scope);
|
|
1804
|
+
}
|
|
1805
|
+
invalidateRenameSourceCache(snapshot.key);
|
|
1806
|
+
return;
|
|
1807
|
+
}
|
|
1808
|
+
if (snapshot.plainValue !== undefined) {
|
|
1809
|
+
adapter.backend.set(snapshot.key, snapshot.plainValue, config.scope);
|
|
1810
|
+
} else {
|
|
1811
|
+
adapter.backend.remove(snapshot.key, config.scope);
|
|
1812
|
+
}
|
|
1813
|
+
invalidateRenameSourceCache(snapshot.key);
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
function readRenameSnapshots(): RenameSnapshot[] {
|
|
1817
|
+
return getRenameSourceKeys().flatMap((key) => {
|
|
1818
|
+
const plainValue =
|
|
1819
|
+
nonMemoryScope === StorageScope.Secure
|
|
1820
|
+
? adapter.backend.get(key, config.scope)
|
|
1821
|
+
: getRenameSourceRaw(key);
|
|
1822
|
+
const biometricValue =
|
|
1823
|
+
nonMemoryScope === StorageScope.Secure
|
|
1824
|
+
? adapter.backend.getSecureBiometric(key)
|
|
1825
|
+
: undefined;
|
|
1826
|
+
if (plainValue === undefined && biometricValue === undefined) {
|
|
1827
|
+
return [];
|
|
1828
|
+
}
|
|
1829
|
+
return [
|
|
1830
|
+
{
|
|
1831
|
+
key,
|
|
1832
|
+
...(plainValue === undefined ? {} : { plainValue }),
|
|
1833
|
+
...(biometricValue === undefined ? {} : { biometricValue }),
|
|
1834
|
+
},
|
|
1835
|
+
];
|
|
1836
|
+
});
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
function selectedRenameValue(snapshot: RenameSnapshot): string | undefined {
|
|
1840
|
+
return isBiometric ? snapshot.biometricValue : snapshot.plainValue;
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
function hasPendingCurrentWrite(): boolean {
|
|
1844
|
+
if (nonMemoryScope === StorageScope.Disk) {
|
|
1845
|
+
return durability.hasPendingDiskWrite(storageKey);
|
|
1846
|
+
}
|
|
1847
|
+
if (nonMemoryScope === StorageScope.Secure && !isBiometric) {
|
|
1848
|
+
return durability.hasPendingSecureWrite(storageKey);
|
|
1849
|
+
}
|
|
1850
|
+
return false;
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
function hasPendingRenameSourceWrite(): boolean {
|
|
1854
|
+
if (isBiometric || nonMemoryScope === null) {
|
|
1855
|
+
return false;
|
|
1856
|
+
}
|
|
1857
|
+
return getRenameSourceKeys().some((key) =>
|
|
1858
|
+
nonMemoryScope === StorageScope.Disk
|
|
1859
|
+
? durability.hasPendingDiskWrite(key)
|
|
1860
|
+
: durability.hasPendingSecureWrite(key),
|
|
1861
|
+
);
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
function flushPendingRenameSourceWrites(): void {
|
|
1865
|
+
if (!hasPendingRenameSourceWrite()) {
|
|
1866
|
+
return;
|
|
1867
|
+
}
|
|
1868
|
+
if (nonMemoryScope === StorageScope.Disk) {
|
|
1869
|
+
flushDiskWrites();
|
|
1870
|
+
return;
|
|
1871
|
+
}
|
|
1872
|
+
flushSecureWrites();
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
function clearPendingCurrentWriteIf(
|
|
1876
|
+
write: PendingDiskWrite | PendingSecureWrite | undefined,
|
|
1877
|
+
): void {
|
|
1878
|
+
if (write === undefined) {
|
|
1879
|
+
return;
|
|
1880
|
+
}
|
|
1881
|
+
if (nonMemoryScope === StorageScope.Disk && "generation" in write) {
|
|
1882
|
+
durability.clearPendingDiskWriteIf(write);
|
|
1883
|
+
return;
|
|
1884
|
+
}
|
|
1885
|
+
if (nonMemoryScope === StorageScope.Secure && !isBiometric) {
|
|
1886
|
+
durability.clearPendingSecureWriteIf(write);
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
function flushPendingCurrentWrite(): void {
|
|
1891
|
+
if (nonMemoryScope === StorageScope.Disk) {
|
|
1892
|
+
flushDiskWrites();
|
|
1893
|
+
return;
|
|
1894
|
+
}
|
|
1895
|
+
if (nonMemoryScope === StorageScope.Secure && !isBiometric) {
|
|
1896
|
+
flushSecureWrites();
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
function removeCurrentBackendValue(): void {
|
|
1901
|
+
invalidateRawCache(resolveNonMemoryScope(), storageKey);
|
|
1902
|
+
if (isBiometric) {
|
|
1903
|
+
adapter.backend.deleteSecureBiometric(storageKey);
|
|
1904
|
+
return;
|
|
1905
|
+
}
|
|
1906
|
+
adapter.backend.remove(storageKey, config.scope);
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
function scheduleRenameSourceCleanup(): void {
|
|
1910
|
+
const sourceKeys = getRenameSourceKeys();
|
|
1911
|
+
sourceKeys.forEach(invalidateRenameSourceCache);
|
|
1912
|
+
|
|
1913
|
+
if (isBiometric) {
|
|
1914
|
+
sourceKeys.forEach(removeRenameSource);
|
|
1915
|
+
return;
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
if (
|
|
1919
|
+
nonMemoryScope === StorageScope.Disk &&
|
|
1920
|
+
(coalesceDiskWrites || isDiskWritesAsync())
|
|
1921
|
+
) {
|
|
1922
|
+
sourceKeys.forEach((key) => {
|
|
1923
|
+
scheduleDiskWrite(key, undefined);
|
|
1924
|
+
});
|
|
1925
|
+
return;
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
if (nonMemoryScope === StorageScope.Secure && coalesceSecureWrites) {
|
|
1929
|
+
sourceKeys.forEach((key) => {
|
|
1930
|
+
scheduleSecureWrite(
|
|
1931
|
+
key,
|
|
1932
|
+
undefined,
|
|
1933
|
+
secureAccessControl ?? secureDefaultAccessControl,
|
|
1934
|
+
);
|
|
1935
|
+
});
|
|
1936
|
+
return;
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
sourceKeys.forEach(removeRenameSource);
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
type ItemPendingSnapshot = {
|
|
1943
|
+
value: string | undefined;
|
|
1944
|
+
accessControl?: AccessControl;
|
|
1945
|
+
};
|
|
1946
|
+
type ItemStateRecord = {
|
|
1947
|
+
key: string;
|
|
1948
|
+
plainValue: string | undefined;
|
|
1949
|
+
biometricValue: string | undefined;
|
|
1950
|
+
pending?: ItemPendingSnapshot;
|
|
1951
|
+
};
|
|
1952
|
+
type ItemStateSnapshot = {
|
|
1953
|
+
records: readonly ItemStateRecord[];
|
|
1954
|
+
renamesMigrated: boolean;
|
|
1955
|
+
};
|
|
1956
|
+
|
|
1957
|
+
let atomicMutationDepth = 0;
|
|
1958
|
+
|
|
1959
|
+
function getItemStateKeys(): string[] {
|
|
1960
|
+
return Array.from(new Set([storageKey, ...getRenameSourceKeys()]));
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
function captureItemState(): ItemStateSnapshot {
|
|
1964
|
+
const records = getItemStateKeys().map((key) => {
|
|
1965
|
+
let pending: ItemPendingSnapshot | undefined;
|
|
1966
|
+
if (nonMemoryScope === StorageScope.Disk) {
|
|
1967
|
+
if (durability.hasPendingDiskWrite(key)) {
|
|
1968
|
+
pending = {
|
|
1969
|
+
value: durability.readPendingDiskWrite(key),
|
|
1970
|
+
};
|
|
1971
|
+
}
|
|
1972
|
+
} else if (
|
|
1973
|
+
nonMemoryScope === StorageScope.Secure &&
|
|
1974
|
+
!isBiometric &&
|
|
1975
|
+
durability.hasPendingSecureWrite(key)
|
|
1976
|
+
) {
|
|
1977
|
+
const accessControl = durability.readPendingSecureAccessControl(key);
|
|
1978
|
+
pending = {
|
|
1979
|
+
value: durability.readPendingSecureWrite(key),
|
|
1980
|
+
...(accessControl === undefined ? {} : { accessControl }),
|
|
1981
|
+
};
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
return {
|
|
1985
|
+
key,
|
|
1986
|
+
plainValue:
|
|
1987
|
+
nonMemoryScope === null
|
|
1988
|
+
? undefined
|
|
1989
|
+
: adapter.backend.get(key, config.scope),
|
|
1990
|
+
biometricValue:
|
|
1991
|
+
nonMemoryScope === StorageScope.Secure
|
|
1992
|
+
? adapter.backend.getSecureBiometric(key)
|
|
1993
|
+
: undefined,
|
|
1994
|
+
...(pending === undefined ? {} : { pending }),
|
|
1995
|
+
};
|
|
1996
|
+
});
|
|
1997
|
+
return { records, renamesMigrated };
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
function clearPendingItemWrite(key: string): void {
|
|
2001
|
+
if (nonMemoryScope === StorageScope.Disk) {
|
|
2002
|
+
durability.clearPendingDiskWrite(key);
|
|
2003
|
+
} else if (nonMemoryScope === StorageScope.Secure && !isBiometric) {
|
|
2004
|
+
durability.clearPendingSecureWrite(key);
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
function restoreItemState(snapshot: ItemStateSnapshot): {
|
|
2009
|
+
label: string;
|
|
2010
|
+
error: unknown;
|
|
2011
|
+
}[] {
|
|
2012
|
+
const rollbackErrors: { label: string; error: unknown }[] = [];
|
|
2013
|
+
const recordsByKey = new Map(
|
|
2014
|
+
snapshot.records.map((record) => [record.key, record]),
|
|
2015
|
+
);
|
|
2016
|
+
snapshot.records.forEach(({ key }) => {
|
|
2017
|
+
clearPendingItemWrite(key);
|
|
2018
|
+
if (nonMemoryScope !== null) {
|
|
2019
|
+
invalidateRawCache(nonMemoryScope, key);
|
|
2020
|
+
}
|
|
2021
|
+
});
|
|
2022
|
+
|
|
2023
|
+
const biometricRecords = snapshot.records.filter(
|
|
2024
|
+
({ biometricValue }) => biometricValue !== undefined,
|
|
2025
|
+
);
|
|
2026
|
+
if (nonMemoryScope === StorageScope.Secure) {
|
|
2027
|
+
biometricRecords.forEach(({ key, biometricValue }) => {
|
|
2028
|
+
try {
|
|
2029
|
+
adapter.backend.setSecureBiometricWithLevel(
|
|
2030
|
+
key,
|
|
2031
|
+
biometricValue as string,
|
|
2032
|
+
resolvedBiometricLevel === BiometricLevel.None
|
|
2033
|
+
? BiometricLevel.BiometryOnly
|
|
2034
|
+
: resolvedBiometricLevel,
|
|
2035
|
+
);
|
|
2036
|
+
cacheRawValue(
|
|
2037
|
+
StorageScope.Secure,
|
|
2038
|
+
key,
|
|
2039
|
+
biometricValue,
|
|
2040
|
+
"biometric",
|
|
2041
|
+
);
|
|
2042
|
+
} catch (error) {
|
|
2043
|
+
rollbackErrors.push({ label: "rollback biometric", error });
|
|
2044
|
+
}
|
|
2045
|
+
});
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
const plainSets = new Map<
|
|
2049
|
+
AccessControl,
|
|
2050
|
+
{ keys: string[]; values: string[] }
|
|
2051
|
+
>();
|
|
2052
|
+
const plainRemoves: string[] = [];
|
|
2053
|
+
snapshot.records.forEach(({ key, plainValue, biometricValue }) => {
|
|
2054
|
+
if (plainValue === undefined) {
|
|
2055
|
+
if (biometricValue === undefined) {
|
|
2056
|
+
plainRemoves.push(key);
|
|
2057
|
+
}
|
|
2058
|
+
return;
|
|
2059
|
+
}
|
|
2060
|
+
const accessControl = secureAccessControl ?? secureDefaultAccessControl;
|
|
2061
|
+
const group = plainSets.get(accessControl) ?? { keys: [], values: [] };
|
|
2062
|
+
group.keys.push(key);
|
|
2063
|
+
group.values.push(plainValue);
|
|
2064
|
+
plainSets.set(accessControl, group);
|
|
2065
|
+
});
|
|
2066
|
+
|
|
2067
|
+
plainSets.forEach((group, accessControl) => {
|
|
2068
|
+
try {
|
|
2069
|
+
if (nonMemoryScope === StorageScope.Secure) {
|
|
2070
|
+
adapter.backend.setSecureAccessControl(accessControl);
|
|
2071
|
+
}
|
|
2072
|
+
adapter.backend.setBatch(group.keys, group.values, config.scope);
|
|
2073
|
+
group.keys.forEach((key, index) => {
|
|
2074
|
+
cacheRawValue(
|
|
2075
|
+
resolveNonMemoryScope(),
|
|
2076
|
+
key,
|
|
2077
|
+
group.values[index],
|
|
2078
|
+
"plain",
|
|
2079
|
+
);
|
|
2080
|
+
});
|
|
2081
|
+
} catch (error) {
|
|
2082
|
+
rollbackErrors.push({ label: "rollback plain set", error });
|
|
2083
|
+
}
|
|
2084
|
+
});
|
|
2085
|
+
|
|
2086
|
+
if (plainRemoves.length > 0) {
|
|
2087
|
+
try {
|
|
2088
|
+
adapter.backend.removeBatch(plainRemoves, config.scope);
|
|
2089
|
+
plainRemoves.forEach((key) => {
|
|
2090
|
+
cacheRawValue(resolveNonMemoryScope(), key, undefined, "plain");
|
|
2091
|
+
});
|
|
2092
|
+
} catch (error) {
|
|
2093
|
+
rollbackErrors.push({ label: "rollback plain remove", error });
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
if (nonMemoryScope === StorageScope.Secure) {
|
|
2098
|
+
snapshot.records.forEach(({ key, biometricValue }) => {
|
|
2099
|
+
if (biometricValue !== undefined) {
|
|
2100
|
+
return;
|
|
2101
|
+
}
|
|
2102
|
+
try {
|
|
2103
|
+
adapter.backend.deleteSecureBiometric(key);
|
|
2104
|
+
cacheRawValue(StorageScope.Secure, key, undefined, "biometric");
|
|
2105
|
+
} catch (error) {
|
|
2106
|
+
rollbackErrors.push({ label: "rollback biometric remove", error });
|
|
2107
|
+
}
|
|
2108
|
+
});
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
snapshot.records.forEach(({ key, pending }) => {
|
|
2112
|
+
if (pending === undefined || nonMemoryScope === null) {
|
|
2113
|
+
return;
|
|
2114
|
+
}
|
|
2115
|
+
if (nonMemoryScope === StorageScope.Disk) {
|
|
2116
|
+
scheduleDiskWrite(key, pending.value);
|
|
2117
|
+
} else if (!isBiometric) {
|
|
2118
|
+
scheduleSecureWrite(
|
|
2119
|
+
key,
|
|
2120
|
+
pending.value,
|
|
2121
|
+
pending.accessControl ??
|
|
2122
|
+
secureAccessControl ??
|
|
2123
|
+
secureDefaultAccessControl,
|
|
2124
|
+
);
|
|
2125
|
+
}
|
|
2126
|
+
});
|
|
2127
|
+
|
|
2128
|
+
recordsByKey.forEach(({ key }) => {
|
|
2129
|
+
if (nonMemoryScope !== null) {
|
|
2130
|
+
invalidateRawCache(nonMemoryScope, key);
|
|
2131
|
+
}
|
|
2132
|
+
});
|
|
2133
|
+
renamesMigrated = snapshot.renamesMigrated;
|
|
2134
|
+
invalidateParsedCache();
|
|
2135
|
+
return rollbackErrors;
|
|
2136
|
+
}
|
|
2137
|
+
|
|
2138
|
+
function runAtomicItemMutation(mutation: () => void): void {
|
|
2139
|
+
if (isMemory || renameFromKeys.length === 0 || atomicMutationDepth > 0) {
|
|
2140
|
+
mutation();
|
|
2141
|
+
return;
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
const snapshot = captureItemState();
|
|
2145
|
+
atomicMutationDepth += 1;
|
|
2146
|
+
try {
|
|
2147
|
+
mutation();
|
|
2148
|
+
} catch (primaryError) {
|
|
2149
|
+
const rollbackErrors = restoreItemState(snapshot);
|
|
2150
|
+
if (rollbackErrors.length > 0) {
|
|
2151
|
+
throw createStorageCompositeError(
|
|
2152
|
+
"item mutation rollback",
|
|
2153
|
+
primaryError,
|
|
2154
|
+
rollbackErrors,
|
|
2155
|
+
);
|
|
2156
|
+
}
|
|
2157
|
+
throw primaryError;
|
|
2158
|
+
} finally {
|
|
2159
|
+
atomicMutationDepth -= 1;
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
const writeStoredRaw = (
|
|
2164
|
+
rawValue: string,
|
|
2165
|
+
options: StoredRawWriteOptions = {},
|
|
2166
|
+
): PendingDiskWrite | PendingSecureWrite | undefined => {
|
|
2167
|
+
const cleanupRenameSources = options.cleanupRenameSources !== false;
|
|
1588
2168
|
const oldValue = undefined;
|
|
1589
2169
|
if (isBiometric) {
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
2170
|
+
invalidateRawCache(StorageScope.Secure, storageKey);
|
|
2171
|
+
runSecurePromotion(storageKey, () => {
|
|
2172
|
+
try {
|
|
2173
|
+
adapter.backend.setSecureBiometricWithLevel(
|
|
2174
|
+
storageKey,
|
|
2175
|
+
rawValue,
|
|
2176
|
+
resolvedBiometricLevel,
|
|
2177
|
+
);
|
|
2178
|
+
} catch (error) {
|
|
2179
|
+
throw normalizeStorageError(error);
|
|
2180
|
+
}
|
|
2181
|
+
});
|
|
2182
|
+
if (cleanupRenameSources) {
|
|
2183
|
+
scheduleRenameSourceCleanup();
|
|
2184
|
+
}
|
|
1595
2185
|
emitKeyChange(
|
|
1596
2186
|
config.scope,
|
|
1597
2187
|
storageKey,
|
|
@@ -1600,14 +2190,20 @@ export function createStorageCore(
|
|
|
1600
2190
|
"set",
|
|
1601
2191
|
adapter.changeSource,
|
|
1602
2192
|
);
|
|
1603
|
-
return;
|
|
2193
|
+
return undefined;
|
|
1604
2194
|
}
|
|
1605
2195
|
|
|
2196
|
+
if (nonMemoryScope === StorageScope.Secure) {
|
|
2197
|
+
invalidateRawCache(StorageScope.Secure, storageKey);
|
|
2198
|
+
}
|
|
1606
2199
|
cacheRawValue(resolveNonMemoryScope(), storageKey, rawValue);
|
|
1607
2200
|
|
|
1608
2201
|
if (nonMemoryScope === StorageScope.Disk) {
|
|
1609
2202
|
if (coalesceDiskWrites || isDiskWritesAsync()) {
|
|
1610
|
-
scheduleDiskWrite(storageKey, rawValue);
|
|
2203
|
+
const pendingWrite = scheduleDiskWrite(storageKey, rawValue);
|
|
2204
|
+
if (cleanupRenameSources) {
|
|
2205
|
+
scheduleRenameSourceCleanup();
|
|
2206
|
+
}
|
|
1611
2207
|
emitKeyChange(
|
|
1612
2208
|
config.scope,
|
|
1613
2209
|
storageKey,
|
|
@@ -1616,18 +2212,21 @@ export function createStorageCore(
|
|
|
1616
2212
|
"set",
|
|
1617
2213
|
adapter.changeSource,
|
|
1618
2214
|
);
|
|
1619
|
-
return;
|
|
2215
|
+
return pendingWrite;
|
|
1620
2216
|
}
|
|
1621
2217
|
|
|
1622
2218
|
clearPendingDiskWrite(storageKey);
|
|
1623
2219
|
}
|
|
1624
2220
|
|
|
1625
2221
|
if (coalesceSecureWrites) {
|
|
1626
|
-
scheduleSecureWrite(
|
|
2222
|
+
const pendingWrite = scheduleSecureWrite(
|
|
1627
2223
|
storageKey,
|
|
1628
2224
|
rawValue,
|
|
1629
2225
|
secureAccessControl ?? secureDefaultAccessControl,
|
|
1630
2226
|
);
|
|
2227
|
+
if (cleanupRenameSources) {
|
|
2228
|
+
scheduleRenameSourceCleanup();
|
|
2229
|
+
}
|
|
1631
2230
|
emitKeyChange(
|
|
1632
2231
|
config.scope,
|
|
1633
2232
|
storageKey,
|
|
@@ -1636,7 +2235,7 @@ export function createStorageCore(
|
|
|
1636
2235
|
"set",
|
|
1637
2236
|
adapter.changeSource,
|
|
1638
2237
|
);
|
|
1639
|
-
return;
|
|
2238
|
+
return pendingWrite;
|
|
1640
2239
|
}
|
|
1641
2240
|
|
|
1642
2241
|
if (nonMemoryScope === StorageScope.Secure) {
|
|
@@ -1649,6 +2248,9 @@ export function createStorageCore(
|
|
|
1649
2248
|
}
|
|
1650
2249
|
|
|
1651
2250
|
adapter.backend.set(storageKey, rawValue, config.scope);
|
|
2251
|
+
if (cleanupRenameSources) {
|
|
2252
|
+
scheduleRenameSourceCleanup();
|
|
2253
|
+
}
|
|
1652
2254
|
emitKeyChange(
|
|
1653
2255
|
config.scope,
|
|
1654
2256
|
storageKey,
|
|
@@ -1657,6 +2259,7 @@ export function createStorageCore(
|
|
|
1657
2259
|
"set",
|
|
1658
2260
|
adapter.changeSource,
|
|
1659
2261
|
);
|
|
2262
|
+
return undefined;
|
|
1660
2263
|
};
|
|
1661
2264
|
|
|
1662
2265
|
const migrateRenamesIfNeeded = (): void => {
|
|
@@ -1665,42 +2268,101 @@ export function createStorageCore(
|
|
|
1665
2268
|
}
|
|
1666
2269
|
|
|
1667
2270
|
try {
|
|
2271
|
+
flushPendingRenameSourceWrites();
|
|
1668
2272
|
const hasCurrent = isBiometric
|
|
1669
2273
|
? adapter.backend.hasSecureBiometric(storageKey)
|
|
1670
2274
|
: adapter.backend.has(storageKey, config.scope);
|
|
2275
|
+
const snapshots = readRenameSnapshots();
|
|
1671
2276
|
|
|
1672
2277
|
if (hasCurrent) {
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
2278
|
+
try {
|
|
2279
|
+
snapshots.forEach(({ key }) => {
|
|
2280
|
+
removeRenameSource(key);
|
|
2281
|
+
});
|
|
2282
|
+
} catch (primaryError) {
|
|
2283
|
+
const rollbackErrors: { label: string; error: unknown }[] = [];
|
|
2284
|
+
snapshots.forEach((snapshot) => {
|
|
2285
|
+
try {
|
|
2286
|
+
restoreRenameSource(snapshot);
|
|
2287
|
+
} catch (error) {
|
|
2288
|
+
rollbackErrors.push({
|
|
2289
|
+
label: "rollback rename source",
|
|
2290
|
+
error,
|
|
2291
|
+
});
|
|
1677
2292
|
}
|
|
1678
|
-
}
|
|
1679
|
-
|
|
2293
|
+
});
|
|
2294
|
+
if (rollbackErrors.length > 0) {
|
|
2295
|
+
throw createStorageCompositeError(
|
|
2296
|
+
"rename cleanup",
|
|
2297
|
+
primaryError,
|
|
2298
|
+
rollbackErrors,
|
|
2299
|
+
);
|
|
1680
2300
|
}
|
|
2301
|
+
throw primaryError;
|
|
1681
2302
|
}
|
|
1682
2303
|
renamesMigrated = true;
|
|
1683
2304
|
return;
|
|
1684
2305
|
}
|
|
1685
2306
|
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
2307
|
+
const snapshot = snapshots.find(
|
|
2308
|
+
(candidate) => selectedRenameValue(candidate) !== undefined,
|
|
2309
|
+
);
|
|
2310
|
+
if (snapshot === undefined) {
|
|
2311
|
+
renamesMigrated = true;
|
|
2312
|
+
return;
|
|
2313
|
+
}
|
|
1693
2314
|
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
2315
|
+
let writeAttempted = false;
|
|
2316
|
+
let pendingCurrentWrite:
|
|
2317
|
+
PendingDiskWrite | PendingSecureWrite | undefined;
|
|
2318
|
+
try {
|
|
2319
|
+
writeAttempted = true;
|
|
2320
|
+
pendingCurrentWrite = writeStoredRaw(
|
|
2321
|
+
selectedRenameValue(snapshot) as string,
|
|
2322
|
+
{
|
|
2323
|
+
cleanupRenameSources: false,
|
|
2324
|
+
},
|
|
2325
|
+
);
|
|
2326
|
+
if (hasPendingCurrentWrite()) {
|
|
2327
|
+
flushPendingCurrentWrite();
|
|
2328
|
+
}
|
|
2329
|
+
snapshots.forEach(({ key }) => {
|
|
2330
|
+
removeRenameSource(key);
|
|
2331
|
+
});
|
|
2332
|
+
renamesMigrated = true;
|
|
2333
|
+
} catch (primaryError) {
|
|
2334
|
+
const rollbackErrors: { label: string; error: unknown }[] = [];
|
|
2335
|
+
clearPendingCurrentWriteIf(pendingCurrentWrite);
|
|
2336
|
+
if (writeAttempted) {
|
|
2337
|
+
try {
|
|
2338
|
+
removeCurrentBackendValue();
|
|
2339
|
+
} catch (error) {
|
|
2340
|
+
rollbackErrors.push({ label: "rollback current value", error });
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
snapshots.forEach((renameSnapshot) => {
|
|
2344
|
+
try {
|
|
2345
|
+
restoreRenameSource(renameSnapshot);
|
|
2346
|
+
} catch (error) {
|
|
2347
|
+
rollbackErrors.push({
|
|
2348
|
+
label: "rollback rename source",
|
|
2349
|
+
error,
|
|
2350
|
+
});
|
|
2351
|
+
}
|
|
2352
|
+
});
|
|
2353
|
+
invalidateRawCache(resolveNonMemoryScope(), storageKey);
|
|
2354
|
+
snapshots.forEach(({ key }) => {
|
|
2355
|
+
invalidateRenameSourceCache(key);
|
|
2356
|
+
});
|
|
2357
|
+
if (rollbackErrors.length > 0) {
|
|
2358
|
+
throw createStorageCompositeError(
|
|
2359
|
+
"rename migration",
|
|
2360
|
+
primaryError,
|
|
2361
|
+
rollbackErrors,
|
|
2362
|
+
);
|
|
1699
2363
|
}
|
|
1700
|
-
|
|
2364
|
+
throw primaryError;
|
|
1701
2365
|
}
|
|
1702
|
-
|
|
1703
|
-
renamesMigrated = true;
|
|
1704
2366
|
} catch (error) {
|
|
1705
2367
|
if (isKeychainLockedError(error)) {
|
|
1706
2368
|
onReadError?.(error);
|
|
@@ -1713,8 +2375,16 @@ export function createStorageCore(
|
|
|
1713
2375
|
const removeStoredRaw = (
|
|
1714
2376
|
operation: StorageChangeOperation = "remove",
|
|
1715
2377
|
): void => {
|
|
1716
|
-
const oldValue =
|
|
2378
|
+
const oldValue = isBiometric
|
|
2379
|
+
? getEventRawValueForRepresentation(
|
|
2380
|
+
config.scope,
|
|
2381
|
+
storageKey,
|
|
2382
|
+
"biometric",
|
|
2383
|
+
)
|
|
2384
|
+
: getEventRawValue(config.scope, storageKey);
|
|
1717
2385
|
if (isBiometric) {
|
|
2386
|
+
invalidateRawCache(StorageScope.Secure, storageKey);
|
|
2387
|
+
scheduleRenameSourceCleanup();
|
|
1718
2388
|
adapter.backend.deleteSecureBiometric(storageKey);
|
|
1719
2389
|
emitKeyChange(
|
|
1720
2390
|
config.scope,
|
|
@@ -1727,11 +2397,15 @@ export function createStorageCore(
|
|
|
1727
2397
|
return;
|
|
1728
2398
|
}
|
|
1729
2399
|
|
|
2400
|
+
if (nonMemoryScope === StorageScope.Secure) {
|
|
2401
|
+
invalidateRawCache(StorageScope.Secure, storageKey);
|
|
2402
|
+
}
|
|
1730
2403
|
cacheRawValue(resolveNonMemoryScope(), storageKey, undefined);
|
|
1731
2404
|
|
|
1732
2405
|
if (nonMemoryScope === StorageScope.Disk) {
|
|
1733
2406
|
if (coalesceDiskWrites || isDiskWritesAsync()) {
|
|
1734
2407
|
scheduleDiskWrite(storageKey, undefined);
|
|
2408
|
+
scheduleRenameSourceCleanup();
|
|
1735
2409
|
emitKeyChange(
|
|
1736
2410
|
config.scope,
|
|
1737
2411
|
storageKey,
|
|
@@ -1752,6 +2426,7 @@ export function createStorageCore(
|
|
|
1752
2426
|
undefined,
|
|
1753
2427
|
secureAccessControl ?? secureDefaultAccessControl,
|
|
1754
2428
|
);
|
|
2429
|
+
scheduleRenameSourceCleanup();
|
|
1755
2430
|
emitKeyChange(
|
|
1756
2431
|
config.scope,
|
|
1757
2432
|
storageKey,
|
|
@@ -1767,6 +2442,7 @@ export function createStorageCore(
|
|
|
1767
2442
|
clearPendingSecureWrite(storageKey);
|
|
1768
2443
|
}
|
|
1769
2444
|
|
|
2445
|
+
scheduleRenameSourceCleanup();
|
|
1770
2446
|
adapter.backend.remove(storageKey, config.scope);
|
|
1771
2447
|
emitKeyChange(
|
|
1772
2448
|
config.scope,
|
|
@@ -1783,6 +2459,8 @@ export function createStorageCore(
|
|
|
1783
2459
|
const oldValue = getEventRawValue(config.scope, storageKey);
|
|
1784
2460
|
if (memoryExpiration) {
|
|
1785
2461
|
memoryExpiration.set(storageKey, Date.now() + (expirationTtlMs ?? 0));
|
|
2462
|
+
} else {
|
|
2463
|
+
memoryExpirationDeadlines.delete(storageKey);
|
|
1786
2464
|
}
|
|
1787
2465
|
const storedValue =
|
|
1788
2466
|
typeof value === "string" ? escapeCollidingRawValue(value) : value;
|
|
@@ -1852,7 +2530,9 @@ export function createStorageCore(
|
|
|
1852
2530
|
return lastValue as T;
|
|
1853
2531
|
}
|
|
1854
2532
|
|
|
1855
|
-
|
|
2533
|
+
runAtomicItemMutation(() => {
|
|
2534
|
+
removeStoredRaw("expire");
|
|
2535
|
+
});
|
|
1856
2536
|
invalidateParsedCache();
|
|
1857
2537
|
onExpired?.(storageKey);
|
|
1858
2538
|
lastValue = ensureValidatedValue(defaultValue, false);
|
|
@@ -1896,7 +2576,9 @@ export function createStorageCore(
|
|
|
1896
2576
|
if (isStoredEnvelope(parsed)) {
|
|
1897
2577
|
envelopeExpiresAt = parsed.expiresAt;
|
|
1898
2578
|
if (parsed.expiresAt <= Date.now()) {
|
|
1899
|
-
|
|
2579
|
+
runAtomicItemMutation(() => {
|
|
2580
|
+
removeStoredRaw("expire");
|
|
2581
|
+
});
|
|
1900
2582
|
invalidateParsedCache();
|
|
1901
2583
|
onExpired?.(storageKey);
|
|
1902
2584
|
lastValue = ensureValidatedValue(defaultValue, false);
|
|
@@ -1943,18 +2625,20 @@ export function createStorageCore(
|
|
|
1943
2625
|
|
|
1944
2626
|
const set = (valueOrFn: T | ((prev: T) => T)): void => {
|
|
1945
2627
|
measureOperation("item:set", config.scope, () => {
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
2628
|
+
runAtomicItemMutation(() => {
|
|
2629
|
+
const newValue = isUpdater(valueOrFn)
|
|
2630
|
+
? valueOrFn(getInternal())
|
|
2631
|
+
: valueOrFn;
|
|
2632
|
+
|
|
2633
|
+
if (validate && !validate(newValue)) {
|
|
2634
|
+
throw new Error(
|
|
2635
|
+
`Validation failed for key "${storageKey}" in scope "${StorageScope[config.scope]}".`,
|
|
2636
|
+
);
|
|
2637
|
+
}
|
|
1955
2638
|
|
|
1956
|
-
|
|
1957
|
-
|
|
2639
|
+
invalidateParsedCache();
|
|
2640
|
+
writeValueWithoutValidation(newValue);
|
|
2641
|
+
});
|
|
1958
2642
|
});
|
|
1959
2643
|
};
|
|
1960
2644
|
|
|
@@ -1963,37 +2647,44 @@ export function createStorageCore(
|
|
|
1963
2647
|
valueOrFn: T | ((prev: T) => T),
|
|
1964
2648
|
): boolean =>
|
|
1965
2649
|
measureOperation("item:setIfVersion", config.scope, () => {
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
2650
|
+
let didSet = false;
|
|
2651
|
+
runAtomicItemMutation(() => {
|
|
2652
|
+
const currentVersion = getCurrentVersion();
|
|
2653
|
+
if (currentVersion !== version) {
|
|
2654
|
+
return;
|
|
2655
|
+
}
|
|
2656
|
+
set(valueOrFn);
|
|
2657
|
+
didSet = true;
|
|
2658
|
+
});
|
|
2659
|
+
return didSet;
|
|
1972
2660
|
});
|
|
1973
2661
|
|
|
1974
2662
|
const deleteItem = (): void => {
|
|
1975
2663
|
measureOperation("item:delete", config.scope, () => {
|
|
1976
|
-
|
|
2664
|
+
runAtomicItemMutation(() => {
|
|
2665
|
+
invalidateParsedCache();
|
|
1977
2666
|
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
2667
|
+
if (isMemory) {
|
|
2668
|
+
const oldValue = getEventRawValue(config.scope, storageKey);
|
|
2669
|
+
if (memoryExpiration) {
|
|
2670
|
+
memoryExpiration.delete(storageKey);
|
|
2671
|
+
}
|
|
2672
|
+
memoryExpirationDeadlines.delete(storageKey);
|
|
2673
|
+
memoryStore.delete(storageKey);
|
|
2674
|
+
notifyKeyListeners(memoryListeners, storageKey);
|
|
2675
|
+
emitKeyChange(
|
|
2676
|
+
config.scope,
|
|
2677
|
+
storageKey,
|
|
2678
|
+
oldValue,
|
|
2679
|
+
undefined,
|
|
2680
|
+
"remove",
|
|
2681
|
+
"memory",
|
|
2682
|
+
);
|
|
2683
|
+
return;
|
|
1982
2684
|
}
|
|
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
2685
|
|
|
1996
|
-
|
|
2686
|
+
removeStoredRaw();
|
|
2687
|
+
});
|
|
1997
2688
|
});
|
|
1998
2689
|
};
|
|
1999
2690
|
|
|
@@ -2098,14 +2789,18 @@ export function createStorageCore(
|
|
|
2098
2789
|
invalidateParsedCache();
|
|
2099
2790
|
},
|
|
2100
2791
|
_deleteMemoryEntry: () => {
|
|
2101
|
-
|
|
2102
|
-
memoryExpiration.delete(storageKey);
|
|
2103
|
-
}
|
|
2792
|
+
memoryExpirationDeadlines.delete(storageKey);
|
|
2104
2793
|
memoryStore.delete(storageKey);
|
|
2105
2794
|
invalidateParsedCache();
|
|
2106
2795
|
},
|
|
2107
2796
|
_hasValidation: validate !== undefined,
|
|
2108
2797
|
_hasExpiration: expiration !== undefined,
|
|
2798
|
+
_hasRenameFrom: renameFromKeys.length > 0,
|
|
2799
|
+
_renameFromKeys: renameFromKeys,
|
|
2800
|
+
_getRenameMigrationState: () => renamesMigrated,
|
|
2801
|
+
_setRenameMigrationState: (migrated) => {
|
|
2802
|
+
renamesMigrated = migrated;
|
|
2803
|
+
},
|
|
2109
2804
|
_readCacheEnabled: readCache,
|
|
2110
2805
|
_isBiometric: isBiometric,
|
|
2111
2806
|
_biometricLevel: resolvedBiometricLevel,
|
|
@@ -2127,6 +2822,15 @@ export function createStorageCore(
|
|
|
2127
2822
|
groupSet.add(storageItem as StorageItemInternal<unknown>);
|
|
2128
2823
|
}
|
|
2129
2824
|
|
|
2825
|
+
if (isMemory) {
|
|
2826
|
+
let items = memoryItemsByKey.get(storageKey);
|
|
2827
|
+
if (!items) {
|
|
2828
|
+
items = new Set();
|
|
2829
|
+
memoryItemsByKey.set(storageKey, items);
|
|
2830
|
+
}
|
|
2831
|
+
items.add(storageItem as StorageItemInternal<unknown>);
|
|
2832
|
+
}
|
|
2833
|
+
|
|
2130
2834
|
const registryKey = `${config.scope}:${storageKey}`;
|
|
2131
2835
|
registeredKeyCounts.set(
|
|
2132
2836
|
registryKey,
|
|
@@ -2165,26 +2869,23 @@ export function createStorageCore(
|
|
|
2165
2869
|
|
|
2166
2870
|
items.forEach((item, index) => {
|
|
2167
2871
|
if (scope === StorageScope.Disk) {
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
rawValues[index] = pending;
|
|
2872
|
+
if (durability.hasPendingDiskWrite(item.key)) {
|
|
2873
|
+
rawValues[index] = durability.readPendingDiskWrite(item.key);
|
|
2171
2874
|
return;
|
|
2172
2875
|
}
|
|
2173
2876
|
}
|
|
2174
2877
|
|
|
2175
2878
|
if (scope === StorageScope.Secure) {
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
rawValues[index] = pending;
|
|
2879
|
+
if (durability.hasPendingSecureWrite(item.key)) {
|
|
2880
|
+
rawValues[index] = durability.readPendingSecureWrite(item.key);
|
|
2179
2881
|
return;
|
|
2180
2882
|
}
|
|
2181
2883
|
}
|
|
2182
2884
|
|
|
2183
2885
|
if (item._readCacheEnabled === true) {
|
|
2184
|
-
const
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
rawValues[index] = cached;
|
|
2886
|
+
const cachedEntry = getCachedRawValueEntry(scope, item.key);
|
|
2887
|
+
if (cachedEntry?.has("plain")) {
|
|
2888
|
+
rawValues[index] = cachedEntry.get("plain");
|
|
2188
2889
|
return;
|
|
2189
2890
|
}
|
|
2190
2891
|
}
|
|
@@ -2293,6 +2994,10 @@ export function createStorageCore(
|
|
|
2293
2994
|
|
|
2294
2995
|
flushSecureWrites();
|
|
2295
2996
|
const keys = secureEntries.map(({ item }) => item.key);
|
|
2997
|
+
keys.forEach((key) => {
|
|
2998
|
+
invalidateRawCache(scope, key);
|
|
2999
|
+
});
|
|
3000
|
+
const serializedValues: string[] = [];
|
|
2296
3001
|
const oldValues = shouldReadPreviousEventValues(scope)
|
|
2297
3002
|
? adapter.backend
|
|
2298
3003
|
.getBatch(keys, scope)
|
|
@@ -2308,12 +3013,14 @@ export function createStorageCore(
|
|
|
2308
3013
|
>();
|
|
2309
3014
|
|
|
2310
3015
|
secureEntries.forEach(({ item, value, internal }) => {
|
|
3016
|
+
const serialized = item.serialize(value);
|
|
3017
|
+
serializedValues.push(serialized);
|
|
2311
3018
|
const accessControl =
|
|
2312
3019
|
internal._secureAccessControl ?? secureDefaultAccessControl;
|
|
2313
3020
|
const existingGroup = groupedByAccessControl.get(accessControl);
|
|
2314
3021
|
const group = existingGroup ?? { keys: [], values: [] };
|
|
2315
3022
|
group.keys.push(item.key);
|
|
2316
|
-
group.values.push(
|
|
3023
|
+
group.values.push(serialized);
|
|
2317
3024
|
if (!existingGroup) {
|
|
2318
3025
|
groupedByAccessControl.set(accessControl, group);
|
|
2319
3026
|
}
|
|
@@ -2326,20 +3033,24 @@ export function createStorageCore(
|
|
|
2326
3033
|
cacheRawValue(scope, key, group.values[index]);
|
|
2327
3034
|
});
|
|
2328
3035
|
});
|
|
3036
|
+
const willEmitChanges =
|
|
3037
|
+
storageEvents.hasListeners(scope) || eventObserver !== undefined;
|
|
2329
3038
|
emitBatchChange(
|
|
2330
3039
|
scope,
|
|
2331
3040
|
"setBatch",
|
|
2332
3041
|
adapter.changeSource,
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
3042
|
+
willEmitChanges
|
|
3043
|
+
? keys.map((key, index) =>
|
|
3044
|
+
createKeyChange(
|
|
3045
|
+
scope,
|
|
3046
|
+
key,
|
|
3047
|
+
oldValues[index],
|
|
3048
|
+
serializedValues[index],
|
|
3049
|
+
"setBatch",
|
|
3050
|
+
adapter.changeSource,
|
|
3051
|
+
),
|
|
3052
|
+
)
|
|
3053
|
+
: [],
|
|
2343
3054
|
);
|
|
2344
3055
|
return;
|
|
2345
3056
|
}
|
|
@@ -2423,6 +3134,17 @@ export function createStorageCore(
|
|
|
2423
3134
|
return;
|
|
2424
3135
|
}
|
|
2425
3136
|
|
|
3137
|
+
if (
|
|
3138
|
+
items.some(
|
|
3139
|
+
(item) => asInternal(item as StorageItem<unknown>)._hasRenameFrom,
|
|
3140
|
+
)
|
|
3141
|
+
) {
|
|
3142
|
+
items.forEach((item) => {
|
|
3143
|
+
asInternal(item as StorageItem<unknown>).delete();
|
|
3144
|
+
});
|
|
3145
|
+
return;
|
|
3146
|
+
}
|
|
3147
|
+
|
|
2426
3148
|
const keys = items.map((item) => item.key);
|
|
2427
3149
|
if (scope === StorageScope.Disk) {
|
|
2428
3150
|
flushDiskWrites();
|
|
@@ -2431,14 +3153,46 @@ export function createStorageCore(
|
|
|
2431
3153
|
flushSecureWrites();
|
|
2432
3154
|
}
|
|
2433
3155
|
const oldValues = shouldReadPreviousEventValues(scope)
|
|
2434
|
-
?
|
|
2435
|
-
.
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
3156
|
+
? (() => {
|
|
3157
|
+
const plainItems = items.filter(
|
|
3158
|
+
(item) =>
|
|
3159
|
+
asInternal(item as StorageItem<unknown>)._isBiometric !==
|
|
3160
|
+
true,
|
|
3161
|
+
);
|
|
3162
|
+
const plainValues =
|
|
3163
|
+
plainItems.length === 0
|
|
3164
|
+
? []
|
|
3165
|
+
: adapter.backend
|
|
3166
|
+
.getBatch(
|
|
3167
|
+
plainItems.map((item) => item.key),
|
|
3168
|
+
scope,
|
|
3169
|
+
)
|
|
3170
|
+
.map((value) =>
|
|
3171
|
+
value === undefined
|
|
3172
|
+
? undefined
|
|
3173
|
+
: unescapeCollidingRawValue(value),
|
|
3174
|
+
);
|
|
3175
|
+
let plainIndex = 0;
|
|
3176
|
+
return items.map((item) => {
|
|
3177
|
+
const internal = asInternal(item as StorageItem<unknown>);
|
|
3178
|
+
if (internal._isBiometric === true) {
|
|
3179
|
+
return getEventRawValueForRepresentation(
|
|
3180
|
+
scope,
|
|
3181
|
+
item.key,
|
|
3182
|
+
"biometric",
|
|
3183
|
+
);
|
|
3184
|
+
}
|
|
3185
|
+
const value = plainValues[plainIndex];
|
|
3186
|
+
plainIndex += 1;
|
|
3187
|
+
return value;
|
|
3188
|
+
});
|
|
3189
|
+
})()
|
|
2441
3190
|
: [];
|
|
3191
|
+
if (scope === StorageScope.Secure) {
|
|
3192
|
+
keys.forEach((key) => {
|
|
3193
|
+
invalidateRawCache(scope, key);
|
|
3194
|
+
});
|
|
3195
|
+
}
|
|
2442
3196
|
adapter.backend.removeBatch(keys, scope);
|
|
2443
3197
|
keys.forEach((key) => {
|
|
2444
3198
|
cacheRawValue(scope, key, undefined);
|
|
@@ -2515,46 +3269,118 @@ export function createStorageCore(
|
|
|
2515
3269
|
}
|
|
2516
3270
|
|
|
2517
3271
|
const NOT_SET = Symbol();
|
|
2518
|
-
|
|
3272
|
+
type TransactionRollbackEntry = {
|
|
3273
|
+
key: string;
|
|
3274
|
+
representation: StorageRawCacheRepresentation;
|
|
3275
|
+
record: RollbackRecord;
|
|
3276
|
+
};
|
|
3277
|
+
const rollback = new Map<string, TransactionRollbackEntry>();
|
|
3278
|
+
const itemRenameStates = new Map<StorageItemInternal<unknown>, boolean>();
|
|
2519
3279
|
|
|
2520
3280
|
const rememberRollback = (
|
|
2521
3281
|
key: string,
|
|
2522
3282
|
item?: Pick<StorageItem<unknown>, "key" | "scope">,
|
|
3283
|
+
includeOtherSecureRepresentation = false,
|
|
2523
3284
|
) => {
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
) {
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
3285
|
+
const internal = item
|
|
3286
|
+
? (item as StorageItemInternal<unknown>)
|
|
3287
|
+
: undefined;
|
|
3288
|
+
const representation: StorageRawCacheRepresentation =
|
|
3289
|
+
scope === StorageScope.Secure && internal?._isBiometric === true
|
|
3290
|
+
? "biometric"
|
|
3291
|
+
: "plain";
|
|
3292
|
+
const rememberRepresentation = (
|
|
3293
|
+
representationToRemember: StorageRawCacheRepresentation,
|
|
3294
|
+
) => {
|
|
3295
|
+
const identity = JSON.stringify([
|
|
3296
|
+
scope,
|
|
3297
|
+
key,
|
|
3298
|
+
representationToRemember,
|
|
3299
|
+
]);
|
|
3300
|
+
if (rollback.has(identity)) {
|
|
3301
|
+
return;
|
|
3302
|
+
}
|
|
3303
|
+
if (scope === StorageScope.Memory) {
|
|
3304
|
+
const expiresAt = memoryExpirationDeadlines.get(key);
|
|
3305
|
+
rollback.set(identity, {
|
|
3306
|
+
key,
|
|
3307
|
+
representation: representationToRemember,
|
|
3308
|
+
record: {
|
|
3309
|
+
kind: "memory",
|
|
3310
|
+
value: memoryStore.has(key) ? memoryStore.get(key) : NOT_SET,
|
|
3311
|
+
...(expiresAt === undefined ? {} : { expiresAt }),
|
|
3312
|
+
},
|
|
2544
3313
|
});
|
|
2545
3314
|
return;
|
|
2546
3315
|
}
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
3316
|
+
if (representationToRemember === "biometric") {
|
|
3317
|
+
rollback.set(identity, {
|
|
3318
|
+
key,
|
|
3319
|
+
representation: representationToRemember,
|
|
3320
|
+
record: {
|
|
3321
|
+
kind: "biometric",
|
|
3322
|
+
value: getStoredRawValueForRepresentation(
|
|
3323
|
+
key,
|
|
3324
|
+
scope,
|
|
3325
|
+
representationToRemember,
|
|
3326
|
+
),
|
|
3327
|
+
level:
|
|
3328
|
+
internal?._biometricLevel !== undefined &&
|
|
3329
|
+
internal._biometricLevel !== BiometricLevel.None
|
|
3330
|
+
? internal._biometricLevel
|
|
3331
|
+
: BiometricLevel.BiometryOnly,
|
|
3332
|
+
},
|
|
3333
|
+
});
|
|
3334
|
+
return;
|
|
3335
|
+
}
|
|
3336
|
+
rollback.set(identity, {
|
|
3337
|
+
key,
|
|
3338
|
+
representation: representationToRemember,
|
|
3339
|
+
record: {
|
|
3340
|
+
kind: "raw",
|
|
3341
|
+
value: getStoredRawValueForRepresentation(
|
|
3342
|
+
key,
|
|
3343
|
+
scope,
|
|
3344
|
+
representationToRemember,
|
|
3345
|
+
),
|
|
3346
|
+
...(scope === StorageScope.Secure &&
|
|
3347
|
+
internal?._secureAccessControl !== undefined
|
|
3348
|
+
? { accessControl: internal._secureAccessControl }
|
|
3349
|
+
: {}),
|
|
3350
|
+
},
|
|
2554
3351
|
});
|
|
3352
|
+
};
|
|
3353
|
+
|
|
3354
|
+
if (
|
|
3355
|
+
representation === "biometric" ||
|
|
3356
|
+
includeOtherSecureRepresentation
|
|
3357
|
+
) {
|
|
3358
|
+
rememberRepresentation("plain");
|
|
3359
|
+
}
|
|
3360
|
+
rememberRepresentation(representation);
|
|
3361
|
+
if (includeOtherSecureRepresentation) {
|
|
3362
|
+
rememberRepresentation("biometric");
|
|
2555
3363
|
}
|
|
2556
3364
|
};
|
|
2557
3365
|
|
|
3366
|
+
const rememberItemRollback = (
|
|
3367
|
+
item: Pick<StorageItem<unknown>, "key" | "scope">,
|
|
3368
|
+
): void => {
|
|
3369
|
+
const internal = item as StorageItemInternal<unknown>;
|
|
3370
|
+
const includeOtherSecureRepresentation =
|
|
3371
|
+
scope === StorageScope.Secure &&
|
|
3372
|
+
(internal._isBiometric === true || internal._hasRenameFrom === true);
|
|
3373
|
+
rememberRollback(item.key, item, includeOtherSecureRepresentation);
|
|
3374
|
+
if (internal._getRenameMigrationState) {
|
|
3375
|
+
if (!itemRenameStates.has(internal)) {
|
|
3376
|
+
itemRenameStates.set(internal, internal._getRenameMigrationState());
|
|
3377
|
+
}
|
|
3378
|
+
}
|
|
3379
|
+
(internal._renameFromKeys ?? EMPTY_KEYS).forEach((aliasKey) => {
|
|
3380
|
+
rememberRollback(aliasKey, item, true);
|
|
3381
|
+
});
|
|
3382
|
+
};
|
|
3383
|
+
|
|
2558
3384
|
const tx: TransactionContext = {
|
|
2559
3385
|
scope,
|
|
2560
3386
|
getRaw: (key) => getRawValue(key, scope),
|
|
@@ -2563,21 +3389,22 @@ export function createStorageCore(
|
|
|
2563
3389
|
setRawValue(key, value, scope);
|
|
2564
3390
|
},
|
|
2565
3391
|
removeRaw: (key) => {
|
|
2566
|
-
rememberRollback(key);
|
|
3392
|
+
rememberRollback(key, undefined, scope === StorageScope.Secure);
|
|
2567
3393
|
removeRawValue(key, scope);
|
|
2568
3394
|
},
|
|
2569
3395
|
getItem: (item) => {
|
|
2570
3396
|
assertBatchScope([item], scope);
|
|
3397
|
+
rememberItemRollback(item);
|
|
2571
3398
|
return item.get();
|
|
2572
3399
|
},
|
|
2573
3400
|
setItem: (item, value) => {
|
|
2574
3401
|
assertBatchScope([item], scope);
|
|
2575
|
-
|
|
3402
|
+
rememberItemRollback(item);
|
|
2576
3403
|
item.set(value);
|
|
2577
3404
|
},
|
|
2578
3405
|
removeItem: (item) => {
|
|
2579
3406
|
assertBatchScope([item], scope);
|
|
2580
|
-
|
|
3407
|
+
rememberItemRollback(item);
|
|
2581
3408
|
item.delete();
|
|
2582
3409
|
},
|
|
2583
3410
|
};
|
|
@@ -2585,21 +3412,56 @@ export function createStorageCore(
|
|
|
2585
3412
|
try {
|
|
2586
3413
|
return transaction(tx);
|
|
2587
3414
|
} catch (error) {
|
|
2588
|
-
|
|
3415
|
+
itemRenameStates.forEach((migrated, item) => {
|
|
3416
|
+
item._setRenameMigrationState(migrated);
|
|
3417
|
+
});
|
|
3418
|
+
const rollbackEntries = Array.from(rollback.values()).reverse();
|
|
2589
3419
|
const rollbackSource =
|
|
2590
3420
|
scope === StorageScope.Memory ? "memory" : adapter.changeSource;
|
|
2591
|
-
const
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
3421
|
+
const rollbackErrors: { label: string; error: unknown }[] = [];
|
|
3422
|
+
const readRollbackEventValue = (
|
|
3423
|
+
entry: TransactionRollbackEntry,
|
|
3424
|
+
): string | undefined => {
|
|
3425
|
+
try {
|
|
3426
|
+
return getEventRawValueForRepresentation(
|
|
3427
|
+
scope,
|
|
3428
|
+
entry.key,
|
|
3429
|
+
entry.representation,
|
|
3430
|
+
);
|
|
3431
|
+
} catch (readError) {
|
|
3432
|
+
rollbackErrors.push({
|
|
3433
|
+
label: `rollback read ${entry.representation}:${entry.key}`,
|
|
3434
|
+
error: readError,
|
|
3435
|
+
});
|
|
3436
|
+
return undefined;
|
|
3437
|
+
}
|
|
3438
|
+
};
|
|
3439
|
+
const preRollbackValues = rollbackEntries.map(readRollbackEventValue);
|
|
2595
3440
|
if (scope === StorageScope.Memory) {
|
|
2596
|
-
rollbackEntries.forEach((
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
3441
|
+
rollbackEntries.forEach((entry) => {
|
|
3442
|
+
try {
|
|
3443
|
+
const record = entry.record;
|
|
3444
|
+
if (record.kind !== "memory") {
|
|
3445
|
+
return;
|
|
3446
|
+
}
|
|
3447
|
+
if (record.value === NOT_SET) {
|
|
3448
|
+
memoryStore.delete(entry.key);
|
|
3449
|
+
} else {
|
|
3450
|
+
memoryStore.set(entry.key, record.value);
|
|
3451
|
+
}
|
|
3452
|
+
if (record.expiresAt === undefined) {
|
|
3453
|
+
memoryExpirationDeadlines.delete(entry.key);
|
|
3454
|
+
} else {
|
|
3455
|
+
memoryExpirationDeadlines.set(entry.key, record.expiresAt);
|
|
3456
|
+
}
|
|
3457
|
+
invalidateMemoryItemCaches(entry.key);
|
|
3458
|
+
notifyKeyListeners(memoryListeners, entry.key);
|
|
3459
|
+
} catch (rollbackError) {
|
|
3460
|
+
rollbackErrors.push({
|
|
3461
|
+
label: `rollback ${entry.representation}:${entry.key}`,
|
|
3462
|
+
error: rollbackError,
|
|
3463
|
+
});
|
|
2601
3464
|
}
|
|
2602
|
-
notifyKeyListeners(memoryListeners, key);
|
|
2603
3465
|
});
|
|
2604
3466
|
} else {
|
|
2605
3467
|
const groupedKeysToSet = new Map<
|
|
@@ -2607,18 +3469,14 @@ export function createStorageCore(
|
|
|
2607
3469
|
{ keys: string[]; values: string[] }
|
|
2608
3470
|
>();
|
|
2609
3471
|
const keysToRemove: string[] = [];
|
|
3472
|
+
const biometricEntries: TransactionRollbackEntry[] = [];
|
|
3473
|
+
const biometricValues = new Map<string, string | undefined>();
|
|
2610
3474
|
|
|
2611
|
-
rollbackEntries.forEach((
|
|
3475
|
+
rollbackEntries.forEach((entry) => {
|
|
3476
|
+
const { key, record } = entry;
|
|
2612
3477
|
if (record.kind === "biometric") {
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
} else {
|
|
2616
|
-
adapter.backend.setSecureBiometricWithLevel(
|
|
2617
|
-
key,
|
|
2618
|
-
record.value,
|
|
2619
|
-
record.level,
|
|
2620
|
-
);
|
|
2621
|
-
}
|
|
3478
|
+
biometricEntries.push(entry);
|
|
3479
|
+
biometricValues.set(key, record.value);
|
|
2622
3480
|
return;
|
|
2623
3481
|
}
|
|
2624
3482
|
if (record.kind !== "raw") {
|
|
@@ -2640,26 +3498,115 @@ export function createStorageCore(
|
|
|
2640
3498
|
});
|
|
2641
3499
|
|
|
2642
3500
|
if (scope === StorageScope.Disk) {
|
|
2643
|
-
|
|
3501
|
+
try {
|
|
3502
|
+
flushDiskWrites();
|
|
3503
|
+
} catch (flushError) {
|
|
3504
|
+
rollbackErrors.push({
|
|
3505
|
+
label: "rollback flush disk",
|
|
3506
|
+
error: flushError,
|
|
3507
|
+
});
|
|
3508
|
+
}
|
|
2644
3509
|
}
|
|
2645
3510
|
if (scope === StorageScope.Secure) {
|
|
2646
|
-
|
|
3511
|
+
try {
|
|
3512
|
+
flushSecureWrites();
|
|
3513
|
+
} catch (flushError) {
|
|
3514
|
+
rollbackErrors.push({
|
|
3515
|
+
label: "rollback flush secure",
|
|
3516
|
+
error: flushError,
|
|
3517
|
+
});
|
|
3518
|
+
}
|
|
3519
|
+
}
|
|
3520
|
+
if (scope === StorageScope.Secure) {
|
|
3521
|
+
rollbackEntries.forEach((entry) => {
|
|
3522
|
+
invalidateRawCache(scope, entry.key);
|
|
3523
|
+
});
|
|
2647
3524
|
}
|
|
3525
|
+
biometricEntries.forEach((entry) => {
|
|
3526
|
+
const { key, record } = entry;
|
|
3527
|
+
if (record.kind !== "biometric" || record.value === undefined) {
|
|
3528
|
+
return;
|
|
3529
|
+
}
|
|
3530
|
+
try {
|
|
3531
|
+
adapter.backend.setSecureBiometricWithLevel(
|
|
3532
|
+
key,
|
|
3533
|
+
record.value,
|
|
3534
|
+
record.level,
|
|
3535
|
+
);
|
|
3536
|
+
cacheRawValue(
|
|
3537
|
+
StorageScope.Secure,
|
|
3538
|
+
key,
|
|
3539
|
+
record.value,
|
|
3540
|
+
"biometric",
|
|
3541
|
+
);
|
|
3542
|
+
} catch (rollbackError) {
|
|
3543
|
+
rollbackErrors.push({
|
|
3544
|
+
label: "rollback biometric",
|
|
3545
|
+
error: rollbackError,
|
|
3546
|
+
});
|
|
3547
|
+
}
|
|
3548
|
+
});
|
|
2648
3549
|
groupedKeysToSet.forEach((group, accessControl) => {
|
|
2649
|
-
|
|
2650
|
-
|
|
3550
|
+
try {
|
|
3551
|
+
if (scope === StorageScope.Secure) {
|
|
3552
|
+
adapter.backend.setSecureAccessControl(accessControl);
|
|
3553
|
+
}
|
|
3554
|
+
adapter.backend.setBatch(group.keys, group.values, scope);
|
|
3555
|
+
group.keys.forEach((key, index) => {
|
|
3556
|
+
cacheRawValue(scope, key, group.values[index], "plain");
|
|
3557
|
+
});
|
|
3558
|
+
} catch (rollbackError) {
|
|
3559
|
+
rollbackErrors.push({
|
|
3560
|
+
label: "rollback plain setBatch",
|
|
3561
|
+
error: rollbackError,
|
|
3562
|
+
});
|
|
2651
3563
|
}
|
|
2652
|
-
adapter.backend.setBatch(group.keys, group.values, scope);
|
|
2653
|
-
group.keys.forEach((key, index) => {
|
|
2654
|
-
cacheRawValue(scope, key, group.values[index]);
|
|
2655
|
-
});
|
|
2656
3564
|
});
|
|
2657
3565
|
if (keysToRemove.length > 0) {
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
3566
|
+
const keysToRemoveWithoutBiometric = keysToRemove.filter(
|
|
3567
|
+
(key) => biometricValues.get(key) === undefined,
|
|
3568
|
+
);
|
|
3569
|
+
try {
|
|
3570
|
+
if (keysToRemoveWithoutBiometric.length > 0) {
|
|
3571
|
+
adapter.backend.removeBatch(
|
|
3572
|
+
keysToRemoveWithoutBiometric,
|
|
3573
|
+
scope,
|
|
3574
|
+
);
|
|
3575
|
+
}
|
|
3576
|
+
keysToRemove.forEach((key) => {
|
|
3577
|
+
cacheRawValue(scope, key, undefined, "plain");
|
|
3578
|
+
});
|
|
3579
|
+
} catch (rollbackError) {
|
|
3580
|
+
rollbackErrors.push({
|
|
3581
|
+
label: "rollback plain removeBatch",
|
|
3582
|
+
error: rollbackError,
|
|
3583
|
+
});
|
|
3584
|
+
}
|
|
2662
3585
|
}
|
|
3586
|
+
biometricEntries.forEach((entry) => {
|
|
3587
|
+
const { key, record } = entry;
|
|
3588
|
+
if (record.kind !== "biometric") {
|
|
3589
|
+
return;
|
|
3590
|
+
}
|
|
3591
|
+
if (record.value !== undefined) {
|
|
3592
|
+
return;
|
|
3593
|
+
}
|
|
3594
|
+
invalidateRawCache(StorageScope.Secure, key);
|
|
3595
|
+
try {
|
|
3596
|
+
adapter.backend.deleteSecureBiometric(key);
|
|
3597
|
+
cacheRawValue(
|
|
3598
|
+
StorageScope.Secure,
|
|
3599
|
+
key,
|
|
3600
|
+
record.value,
|
|
3601
|
+
"biometric",
|
|
3602
|
+
);
|
|
3603
|
+
} catch (rollbackError) {
|
|
3604
|
+
rollbackErrors.push({
|
|
3605
|
+
label: "rollback biometric",
|
|
3606
|
+
error: rollbackError,
|
|
3607
|
+
});
|
|
3608
|
+
}
|
|
3609
|
+
});
|
|
2663
3610
|
}
|
|
2664
3611
|
|
|
2665
3612
|
if (rollbackEntries.length > 0) {
|
|
@@ -2667,18 +3614,25 @@ export function createStorageCore(
|
|
|
2667
3614
|
scope,
|
|
2668
3615
|
"rollback",
|
|
2669
3616
|
rollbackSource,
|
|
2670
|
-
rollbackEntries.map((
|
|
3617
|
+
rollbackEntries.map((entry, index) =>
|
|
2671
3618
|
createKeyChange(
|
|
2672
3619
|
scope,
|
|
2673
|
-
key,
|
|
3620
|
+
entry.key,
|
|
2674
3621
|
preRollbackValues[index],
|
|
2675
|
-
|
|
3622
|
+
readRollbackEventValue(entry),
|
|
2676
3623
|
"rollback",
|
|
2677
3624
|
rollbackSource,
|
|
2678
3625
|
),
|
|
2679
3626
|
),
|
|
2680
3627
|
);
|
|
2681
3628
|
}
|
|
3629
|
+
if (rollbackErrors.length > 0) {
|
|
3630
|
+
throw createStorageCompositeError(
|
|
3631
|
+
"transaction rollback",
|
|
3632
|
+
error,
|
|
3633
|
+
rollbackErrors,
|
|
3634
|
+
);
|
|
3635
|
+
}
|
|
2682
3636
|
throw error;
|
|
2683
3637
|
}
|
|
2684
3638
|
});
|
|
@@ -2753,14 +3707,14 @@ export function createStorageCore(
|
|
|
2753
3707
|
config: SetItemConfig<TMember>,
|
|
2754
3708
|
): SetStorageItem<TMember> {
|
|
2755
3709
|
const { defaultValue, ...rest } = config;
|
|
2756
|
-
const initial:
|
|
3710
|
+
const initial: Record<string, true> = {};
|
|
2757
3711
|
if (defaultValue) {
|
|
2758
3712
|
for (const id of defaultValue) {
|
|
2759
3713
|
initial[id] = true;
|
|
2760
3714
|
}
|
|
2761
3715
|
}
|
|
2762
3716
|
|
|
2763
|
-
const item = createStorageItem<
|
|
3717
|
+
const item = createStorageItem<Record<string, true>>({
|
|
2764
3718
|
...rest,
|
|
2765
3719
|
defaultValue: initial,
|
|
2766
3720
|
});
|
|
@@ -2796,8 +3750,17 @@ export function createStorageCore(
|
|
|
2796
3750
|
return true;
|
|
2797
3751
|
};
|
|
2798
3752
|
|
|
3753
|
+
const getTyped = (): Partial<Record<TMember, true>> => {
|
|
3754
|
+
const typed: Partial<Record<TMember, true>> = {};
|
|
3755
|
+
for (const id of Object.keys(item.get())) {
|
|
3756
|
+
typed[id as TMember] = true;
|
|
3757
|
+
}
|
|
3758
|
+
return typed;
|
|
3759
|
+
};
|
|
3760
|
+
|
|
2799
3761
|
return {
|
|
2800
3762
|
get: item.get,
|
|
3763
|
+
getTyped,
|
|
2801
3764
|
has,
|
|
2802
3765
|
add,
|
|
2803
3766
|
delete: deleteId,
|