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
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { createDurabilityCoordinator } from "./core/durability";
|
|
4
4
|
import { createMetricsRegistry } from "./core/metrics";
|
|
5
5
|
import { MIGRATION_VERSION_KEY, isStoredEnvelope, assertBatchScope, assertValidScope, toVersionToken, prefixKey, isNamespaced, escapeCollidingRawValue, unescapeCollidingRawValue } from "./internal";
|
|
6
|
-
import { assertAccessControlLevel, assertBiometricLevel, canUseRawBatchPath, canUseSecureRawBatchPath, createKeyChange, defaultDeserialize, defaultSerialize, isKeychainLockedError, isUpdater, notifyAllListeners, notifyKeyListeners, redactSecureKeyChange, typedKeys } from "./shared";
|
|
6
|
+
import { assertAccessControlLevel, assertBiometricLevel, canUseRawBatchPath, canUseSecureRawBatchPath, createStorageCompositeError, createKeyChange, defaultDeserialize, defaultSerialize, isKeychainLockedError, isUpdater, normalizeStorageError, notifyAllListeners, notifyKeyListeners, redactSecureKeyChange, typedKeys } from "./shared";
|
|
7
7
|
import { StorageEventRegistry } from "./storage-events";
|
|
8
8
|
import { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
|
|
9
9
|
const EMPTY_KEYS = Object.freeze([]);
|
|
@@ -15,6 +15,8 @@ export function createStorageCore(buildAdapter) {
|
|
|
15
15
|
const itemGroups = new Map();
|
|
16
16
|
const registeredKeyCounts = new Map();
|
|
17
17
|
const memoryStore = new Map();
|
|
18
|
+
const memoryExpirationDeadlines = new Map();
|
|
19
|
+
const memoryItemsByKey = new Map();
|
|
18
20
|
const memoryListeners = new Map();
|
|
19
21
|
const scopedListeners = {
|
|
20
22
|
[StorageScope.Disk]: new Map(),
|
|
@@ -49,15 +51,33 @@ export function createStorageCore(buildAdapter) {
|
|
|
49
51
|
function getScopeRawCache(scope) {
|
|
50
52
|
return scopedRawCache[scope];
|
|
51
53
|
}
|
|
52
|
-
function
|
|
53
|
-
getScopeRawCache(scope)
|
|
54
|
+
function getCachedRawValueEntry(scope, key, create = false) {
|
|
55
|
+
const scopeCache = getScopeRawCache(scope);
|
|
56
|
+
const existing = scopeCache.get(key);
|
|
57
|
+
if (existing || !create) {
|
|
58
|
+
return existing;
|
|
59
|
+
}
|
|
60
|
+
const entry = new Map();
|
|
61
|
+
scopeCache.set(key, entry);
|
|
62
|
+
return entry;
|
|
63
|
+
}
|
|
64
|
+
function cacheRawValue(scope, key, value, representation = "plain") {
|
|
65
|
+
getCachedRawValueEntry(scope, key, true)?.set(representation, value);
|
|
66
|
+
}
|
|
67
|
+
function readCachedRawValue(scope, key, representation = "plain") {
|
|
68
|
+
return getCachedRawValueEntry(scope, key)?.get(representation);
|
|
54
69
|
}
|
|
55
|
-
function
|
|
56
|
-
|
|
70
|
+
function invalidateRawCache(scope, key) {
|
|
71
|
+
getScopeRawCache(scope).delete(key);
|
|
57
72
|
}
|
|
58
73
|
function clearScopeRawCache(scope) {
|
|
59
74
|
getScopeRawCache(scope).clear();
|
|
60
75
|
}
|
|
76
|
+
function invalidateMemoryItemCaches(key) {
|
|
77
|
+
memoryItemsByKey.get(key)?.forEach(item => {
|
|
78
|
+
item._invalidateParsedCacheOnly();
|
|
79
|
+
});
|
|
80
|
+
}
|
|
61
81
|
function addKeyListener(registry, key, listener) {
|
|
62
82
|
let listeners = registry.get(key);
|
|
63
83
|
if (!listeners) {
|
|
@@ -83,6 +103,13 @@ export function createStorageCore(buildAdapter) {
|
|
|
83
103
|
}
|
|
84
104
|
return getRawValue(key, scope);
|
|
85
105
|
}
|
|
106
|
+
function getEventRawValueForRepresentation(scope, key, representation) {
|
|
107
|
+
if (representation === "plain") {
|
|
108
|
+
return getEventRawValue(scope, key);
|
|
109
|
+
}
|
|
110
|
+
const raw = adapter.backend.getSecureBiometric(key);
|
|
111
|
+
return raw === undefined ? undefined : unescapeCollidingRawValue(raw);
|
|
112
|
+
}
|
|
86
113
|
function shouldReadPreviousEventValues(scope) {
|
|
87
114
|
if (storageEvents.hasListeners(scope)) {
|
|
88
115
|
return true;
|
|
@@ -155,11 +182,14 @@ export function createStorageCore(buildAdapter) {
|
|
|
155
182
|
function flushSecureWrites() {
|
|
156
183
|
durability.flushSecureWrites();
|
|
157
184
|
}
|
|
185
|
+
function runSecurePromotion(key, promotion) {
|
|
186
|
+
return durability.runSecurePromotion(key, promotion);
|
|
187
|
+
}
|
|
158
188
|
function scheduleDiskWrite(key, value) {
|
|
159
|
-
durability.scheduleDiskWrite(key, value);
|
|
189
|
+
return durability.scheduleDiskWrite(key, value);
|
|
160
190
|
}
|
|
161
191
|
function scheduleSecureWrite(key, value, accessControl) {
|
|
162
|
-
durability.scheduleSecureWrite(key, value, accessControl);
|
|
192
|
+
return durability.scheduleSecureWrite(key, value, accessControl);
|
|
163
193
|
}
|
|
164
194
|
function setDiskWritesAsyncMode(enabled) {
|
|
165
195
|
durability.setDiskWritesAsync(enabled);
|
|
@@ -180,6 +210,12 @@ export function createStorageCore(buildAdapter) {
|
|
|
180
210
|
}
|
|
181
211
|
return adapter.backend.get(key, scope);
|
|
182
212
|
}
|
|
213
|
+
function getStoredRawValueForRepresentation(key, scope, representation) {
|
|
214
|
+
if (representation === "plain") {
|
|
215
|
+
return getStoredRawValue(key, scope);
|
|
216
|
+
}
|
|
217
|
+
return adapter.backend.getSecureBiometric(key);
|
|
218
|
+
}
|
|
183
219
|
function getRawValue(key, scope) {
|
|
184
220
|
assertValidScope(scope);
|
|
185
221
|
if (scope === StorageScope.Memory) {
|
|
@@ -218,6 +254,7 @@ export function createStorageCore(buildAdapter) {
|
|
|
218
254
|
clearPendingDiskWrite(key);
|
|
219
255
|
}
|
|
220
256
|
if (scope === StorageScope.Secure) {
|
|
257
|
+
invalidateRawCache(scope, key);
|
|
221
258
|
flushSecureWrites();
|
|
222
259
|
clearPendingSecureWrite(key);
|
|
223
260
|
if (adapter.applyAccessControlOnSecureRawWrite) {
|
|
@@ -248,6 +285,7 @@ export function createStorageCore(buildAdapter) {
|
|
|
248
285
|
clearPendingDiskWrite(key);
|
|
249
286
|
}
|
|
250
287
|
if (scope === StorageScope.Secure) {
|
|
288
|
+
invalidateRawCache(scope, key);
|
|
251
289
|
flushSecureWrites();
|
|
252
290
|
clearPendingSecureWrite(key);
|
|
253
291
|
}
|
|
@@ -267,6 +305,7 @@ export function createStorageCore(buildAdapter) {
|
|
|
267
305
|
getScopedListeners,
|
|
268
306
|
cacheRawValue,
|
|
269
307
|
readCachedRawValue,
|
|
308
|
+
invalidateRawCache,
|
|
270
309
|
clearScopeRawCache,
|
|
271
310
|
clearPendingDiskWrite,
|
|
272
311
|
clearPendingSecureWrite,
|
|
@@ -320,6 +359,11 @@ export function createStorageCore(buildAdapter) {
|
|
|
320
359
|
return;
|
|
321
360
|
}
|
|
322
361
|
const previousValues = shouldReadPreviousEventValues(scope) ? adapter.backend.getBatch(removeKeys, scope).map(value => value === undefined ? undefined : unescapeCollidingRawValue(value)) : [];
|
|
362
|
+
if (scope === StorageScope.Secure) {
|
|
363
|
+
removeKeys.forEach(key => {
|
|
364
|
+
invalidateRawCache(scope, key);
|
|
365
|
+
});
|
|
366
|
+
}
|
|
323
367
|
adapter.backend.removeBatch(removeKeys, scope);
|
|
324
368
|
removeKeys.forEach(key => {
|
|
325
369
|
cacheRawValue(scope, key, undefined);
|
|
@@ -387,6 +431,7 @@ export function createStorageCore(buildAdapter) {
|
|
|
387
431
|
const previousValues = shouldReadPreviousEventValues(scope) ? storage.getAll(scope) : {};
|
|
388
432
|
if (scope === StorageScope.Memory) {
|
|
389
433
|
memoryStore.clear();
|
|
434
|
+
memoryExpirationDeadlines.clear();
|
|
390
435
|
notifyAllListeners(memoryListeners);
|
|
391
436
|
emitBatchChange(scope, "clear", "memory", Object.keys(previousValues).map(key => createKeyChange(scope, key, previousValues[key], undefined, "clear", "memory")));
|
|
392
437
|
return;
|
|
@@ -490,6 +535,7 @@ export function createStorageCore(buildAdapter) {
|
|
|
490
535
|
}
|
|
491
536
|
affectedKeys.forEach(key => {
|
|
492
537
|
memoryStore.delete(key);
|
|
538
|
+
memoryExpirationDeadlines.delete(key);
|
|
493
539
|
});
|
|
494
540
|
affectedKeys.forEach(key => {
|
|
495
541
|
notifyKeyListeners(memoryListeners, key);
|
|
@@ -520,7 +566,21 @@ export function createStorageCore(buildAdapter) {
|
|
|
520
566
|
},
|
|
521
567
|
clearBiometric: () => {
|
|
522
568
|
measureOperation("storage:clearBiometric", StorageScope.Secure, () => {
|
|
523
|
-
|
|
569
|
+
flushSecureWrites();
|
|
570
|
+
const readEventValues = shouldReadPreviousEventValues(StorageScope.Secure);
|
|
571
|
+
const shouldEmitChanges = storageEvents.hasListeners(StorageScope.Secure) || eventObserver !== undefined;
|
|
572
|
+
const biometricKeys = shouldEmitChanges ? adapter.backend.getAllKeys(StorageScope.Secure).filter(key => adapter.backend.hasSecureBiometric(key)) : [];
|
|
573
|
+
const previousValues = readEventValues ? biometricKeys.map(key => {
|
|
574
|
+
const raw = adapter.backend.getSecureBiometric(key);
|
|
575
|
+
return raw === undefined ? undefined : unescapeCollidingRawValue(raw);
|
|
576
|
+
}) : [];
|
|
577
|
+
clearScopeRawCache(StorageScope.Secure);
|
|
578
|
+
try {
|
|
579
|
+
adapter.backend.clearSecureBiometric();
|
|
580
|
+
} finally {
|
|
581
|
+
clearScopeRawCache(StorageScope.Secure);
|
|
582
|
+
}
|
|
583
|
+
emitBatchChange(StorageScope.Secure, "clear", adapter.changeSource, biometricKeys.map((key, index) => createKeyChange(StorageScope.Secure, key, readEventValues ? previousValues[index] : undefined, undefined, "clear", adapter.changeSource)));
|
|
524
584
|
});
|
|
525
585
|
},
|
|
526
586
|
has: (key, scope) => {
|
|
@@ -745,6 +805,9 @@ export function createStorageCore(buildAdapter) {
|
|
|
745
805
|
if (scope === StorageScope.Secure) {
|
|
746
806
|
flushSecureWrites();
|
|
747
807
|
adapter.backend.setSecureAccessControl(secureDefaultAccessControl);
|
|
808
|
+
keys.forEach(key => {
|
|
809
|
+
invalidateRawCache(scope, key);
|
|
810
|
+
});
|
|
748
811
|
}
|
|
749
812
|
if (scope === StorageScope.Disk) {
|
|
750
813
|
flushDiskWrites();
|
|
@@ -770,8 +833,9 @@ export function createStorageCore(buildAdapter) {
|
|
|
770
833
|
const expiration = config.expiration;
|
|
771
834
|
const onExpired = config.onExpired;
|
|
772
835
|
const expirationTtlMs = expiration?.ttlMs;
|
|
773
|
-
const memoryExpiration = expiration && isMemory ?
|
|
836
|
+
const memoryExpiration = expiration && isMemory ? memoryExpirationDeadlines : null;
|
|
774
837
|
const readCache = !isMemory && config.readCache === true;
|
|
838
|
+
const rawCacheRepresentation = isBiometric ? "biometric" : "plain";
|
|
775
839
|
const coalesceDiskWrites = config.scope === StorageScope.Disk && config.coalesceDiskWrites === true;
|
|
776
840
|
const coalesceSecureWrites = config.scope === StorageScope.Secure && config.coalesceSecureWrites === true && !isBiometric;
|
|
777
841
|
const defaultValue = config.defaultValue;
|
|
@@ -841,28 +905,40 @@ export function createStorageCore(buildAdapter) {
|
|
|
841
905
|
const memoryStored = memoryStore.get(storageKey);
|
|
842
906
|
return typeof memoryStored === "string" ? unescapeCollidingRawValue(memoryStored) : memoryStored;
|
|
843
907
|
}
|
|
908
|
+
if (nonMemoryScope === StorageScope.Disk) {
|
|
909
|
+
if (durability.hasPendingDiskWrite(storageKey)) {
|
|
910
|
+
return durability.readPendingDiskWrite(storageKey);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
if (nonMemoryScope === StorageScope.Secure && !isBiometric) {
|
|
914
|
+
if (durability.hasPendingSecureWrite(storageKey)) {
|
|
915
|
+
return durability.readPendingSecureWrite(storageKey);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
844
918
|
migrateRenamesIfNeeded();
|
|
845
919
|
if (nonMemoryScope === StorageScope.Disk) {
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
return pending;
|
|
920
|
+
if (durability.hasPendingDiskWrite(storageKey)) {
|
|
921
|
+
return durability.readPendingDiskWrite(storageKey);
|
|
849
922
|
}
|
|
850
923
|
}
|
|
851
924
|
if (nonMemoryScope === StorageScope.Secure && !isBiometric) {
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
return pending;
|
|
925
|
+
if (durability.hasPendingSecureWrite(storageKey)) {
|
|
926
|
+
return durability.readPendingSecureWrite(storageKey);
|
|
855
927
|
}
|
|
856
928
|
}
|
|
857
929
|
if (readCache) {
|
|
858
|
-
const
|
|
859
|
-
const
|
|
860
|
-
if (
|
|
861
|
-
return
|
|
930
|
+
const scope = resolveNonMemoryScope();
|
|
931
|
+
const cachedEntry = getCachedRawValueEntry(scope, storageKey);
|
|
932
|
+
if (cachedEntry?.has(rawCacheRepresentation)) {
|
|
933
|
+
return cachedEntry.get(rawCacheRepresentation);
|
|
862
934
|
}
|
|
863
935
|
}
|
|
864
936
|
if (isBiometric) {
|
|
865
|
-
|
|
937
|
+
const raw = readBackendRaw(() => adapter.backend.getSecureBiometric(storageKey));
|
|
938
|
+
if (readCache) {
|
|
939
|
+
cacheRawValue(resolveNonMemoryScope(), storageKey, raw, rawCacheRepresentation);
|
|
940
|
+
}
|
|
941
|
+
return raw;
|
|
866
942
|
}
|
|
867
943
|
const raw = readBackendRaw(() => adapter.backend.get(storageKey, config.scope));
|
|
868
944
|
cacheRawValue(resolveNonMemoryScope(), storageKey, raw);
|
|
@@ -874,7 +950,8 @@ export function createStorageCore(buildAdapter) {
|
|
|
874
950
|
} catch (error) {
|
|
875
951
|
onReadError?.(error);
|
|
876
952
|
if (fallbackToCacheOnReadError) {
|
|
877
|
-
const
|
|
953
|
+
const scope = resolveNonMemoryScope();
|
|
954
|
+
const cached = readCachedRawValue(scope, storageKey, rawCacheRepresentation);
|
|
878
955
|
if (cached !== undefined) {
|
|
879
956
|
return cached;
|
|
880
957
|
}
|
|
@@ -883,26 +960,375 @@ export function createStorageCore(buildAdapter) {
|
|
|
883
960
|
throw error;
|
|
884
961
|
}
|
|
885
962
|
};
|
|
886
|
-
|
|
963
|
+
function getRenameSourceKeys() {
|
|
964
|
+
return renameFromKeys.filter(key => key !== storageKey);
|
|
965
|
+
}
|
|
966
|
+
function getRenameSourceRaw(key) {
|
|
967
|
+
return isBiometric ? adapter.backend.getSecureBiometric(key) : adapter.backend.get(key, config.scope);
|
|
968
|
+
}
|
|
969
|
+
function invalidateRenameSourceCache(key) {
|
|
970
|
+
if (nonMemoryScope !== null) {
|
|
971
|
+
invalidateRawCache(nonMemoryScope, key);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
function removeRenameSource(key) {
|
|
975
|
+
invalidateRenameSourceCache(key);
|
|
976
|
+
if (isBiometric) {
|
|
977
|
+
adapter.backend.deleteSecureBiometric(key);
|
|
978
|
+
invalidateRenameSourceCache(key);
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
adapter.backend.remove(key, config.scope);
|
|
982
|
+
invalidateRenameSourceCache(key);
|
|
983
|
+
}
|
|
984
|
+
function restoreRenameSource(snapshot) {
|
|
985
|
+
invalidateRenameSourceCache(snapshot.key);
|
|
986
|
+
if (nonMemoryScope === StorageScope.Secure) {
|
|
987
|
+
if (snapshot.biometricValue !== undefined) {
|
|
988
|
+
adapter.backend.setSecureBiometricWithLevel(snapshot.key, snapshot.biometricValue, resolvedBiometricLevel === BiometricLevel.None ? BiometricLevel.BiometryOnly : resolvedBiometricLevel);
|
|
989
|
+
} else {
|
|
990
|
+
adapter.backend.deleteSecureBiometric(snapshot.key);
|
|
991
|
+
}
|
|
992
|
+
if (snapshot.plainValue !== undefined) {
|
|
993
|
+
if (adapter.applyAccessControlOnSecureRawWrite) {
|
|
994
|
+
adapter.backend.setSecureAccessControl(secureAccessControl ?? secureDefaultAccessControl);
|
|
995
|
+
}
|
|
996
|
+
adapter.backend.set(snapshot.key, snapshot.plainValue, config.scope);
|
|
997
|
+
} else if (snapshot.biometricValue === undefined) {
|
|
998
|
+
adapter.backend.remove(snapshot.key, config.scope);
|
|
999
|
+
}
|
|
1000
|
+
invalidateRenameSourceCache(snapshot.key);
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
if (snapshot.plainValue !== undefined) {
|
|
1004
|
+
adapter.backend.set(snapshot.key, snapshot.plainValue, config.scope);
|
|
1005
|
+
} else {
|
|
1006
|
+
adapter.backend.remove(snapshot.key, config.scope);
|
|
1007
|
+
}
|
|
1008
|
+
invalidateRenameSourceCache(snapshot.key);
|
|
1009
|
+
}
|
|
1010
|
+
function readRenameSnapshots() {
|
|
1011
|
+
return getRenameSourceKeys().flatMap(key => {
|
|
1012
|
+
const plainValue = nonMemoryScope === StorageScope.Secure ? adapter.backend.get(key, config.scope) : getRenameSourceRaw(key);
|
|
1013
|
+
const biometricValue = nonMemoryScope === StorageScope.Secure ? adapter.backend.getSecureBiometric(key) : undefined;
|
|
1014
|
+
if (plainValue === undefined && biometricValue === undefined) {
|
|
1015
|
+
return [];
|
|
1016
|
+
}
|
|
1017
|
+
return [{
|
|
1018
|
+
key,
|
|
1019
|
+
...(plainValue === undefined ? {} : {
|
|
1020
|
+
plainValue
|
|
1021
|
+
}),
|
|
1022
|
+
...(biometricValue === undefined ? {} : {
|
|
1023
|
+
biometricValue
|
|
1024
|
+
})
|
|
1025
|
+
}];
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
function selectedRenameValue(snapshot) {
|
|
1029
|
+
return isBiometric ? snapshot.biometricValue : snapshot.plainValue;
|
|
1030
|
+
}
|
|
1031
|
+
function hasPendingCurrentWrite() {
|
|
1032
|
+
if (nonMemoryScope === StorageScope.Disk) {
|
|
1033
|
+
return durability.hasPendingDiskWrite(storageKey);
|
|
1034
|
+
}
|
|
1035
|
+
if (nonMemoryScope === StorageScope.Secure && !isBiometric) {
|
|
1036
|
+
return durability.hasPendingSecureWrite(storageKey);
|
|
1037
|
+
}
|
|
1038
|
+
return false;
|
|
1039
|
+
}
|
|
1040
|
+
function hasPendingRenameSourceWrite() {
|
|
1041
|
+
if (isBiometric || nonMemoryScope === null) {
|
|
1042
|
+
return false;
|
|
1043
|
+
}
|
|
1044
|
+
return getRenameSourceKeys().some(key => nonMemoryScope === StorageScope.Disk ? durability.hasPendingDiskWrite(key) : durability.hasPendingSecureWrite(key));
|
|
1045
|
+
}
|
|
1046
|
+
function flushPendingRenameSourceWrites() {
|
|
1047
|
+
if (!hasPendingRenameSourceWrite()) {
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
if (nonMemoryScope === StorageScope.Disk) {
|
|
1051
|
+
flushDiskWrites();
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
flushSecureWrites();
|
|
1055
|
+
}
|
|
1056
|
+
function clearPendingCurrentWriteIf(write) {
|
|
1057
|
+
if (write === undefined) {
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
if (nonMemoryScope === StorageScope.Disk && "generation" in write) {
|
|
1061
|
+
durability.clearPendingDiskWriteIf(write);
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
if (nonMemoryScope === StorageScope.Secure && !isBiometric) {
|
|
1065
|
+
durability.clearPendingSecureWriteIf(write);
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
function flushPendingCurrentWrite() {
|
|
1069
|
+
if (nonMemoryScope === StorageScope.Disk) {
|
|
1070
|
+
flushDiskWrites();
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
if (nonMemoryScope === StorageScope.Secure && !isBiometric) {
|
|
1074
|
+
flushSecureWrites();
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
function removeCurrentBackendValue() {
|
|
1078
|
+
invalidateRawCache(resolveNonMemoryScope(), storageKey);
|
|
1079
|
+
if (isBiometric) {
|
|
1080
|
+
adapter.backend.deleteSecureBiometric(storageKey);
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
adapter.backend.remove(storageKey, config.scope);
|
|
1084
|
+
}
|
|
1085
|
+
function scheduleRenameSourceCleanup() {
|
|
1086
|
+
const sourceKeys = getRenameSourceKeys();
|
|
1087
|
+
sourceKeys.forEach(invalidateRenameSourceCache);
|
|
1088
|
+
if (isBiometric) {
|
|
1089
|
+
sourceKeys.forEach(removeRenameSource);
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
if (nonMemoryScope === StorageScope.Disk && (coalesceDiskWrites || isDiskWritesAsync())) {
|
|
1093
|
+
sourceKeys.forEach(key => {
|
|
1094
|
+
scheduleDiskWrite(key, undefined);
|
|
1095
|
+
});
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
if (nonMemoryScope === StorageScope.Secure && coalesceSecureWrites) {
|
|
1099
|
+
sourceKeys.forEach(key => {
|
|
1100
|
+
scheduleSecureWrite(key, undefined, secureAccessControl ?? secureDefaultAccessControl);
|
|
1101
|
+
});
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1104
|
+
sourceKeys.forEach(removeRenameSource);
|
|
1105
|
+
}
|
|
1106
|
+
let atomicMutationDepth = 0;
|
|
1107
|
+
function getItemStateKeys() {
|
|
1108
|
+
return Array.from(new Set([storageKey, ...getRenameSourceKeys()]));
|
|
1109
|
+
}
|
|
1110
|
+
function captureItemState() {
|
|
1111
|
+
const records = getItemStateKeys().map(key => {
|
|
1112
|
+
let pending;
|
|
1113
|
+
if (nonMemoryScope === StorageScope.Disk) {
|
|
1114
|
+
if (durability.hasPendingDiskWrite(key)) {
|
|
1115
|
+
pending = {
|
|
1116
|
+
value: durability.readPendingDiskWrite(key)
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
} else if (nonMemoryScope === StorageScope.Secure && !isBiometric && durability.hasPendingSecureWrite(key)) {
|
|
1120
|
+
const accessControl = durability.readPendingSecureAccessControl(key);
|
|
1121
|
+
pending = {
|
|
1122
|
+
value: durability.readPendingSecureWrite(key),
|
|
1123
|
+
...(accessControl === undefined ? {} : {
|
|
1124
|
+
accessControl
|
|
1125
|
+
})
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
return {
|
|
1129
|
+
key,
|
|
1130
|
+
plainValue: nonMemoryScope === null ? undefined : adapter.backend.get(key, config.scope),
|
|
1131
|
+
biometricValue: nonMemoryScope === StorageScope.Secure ? adapter.backend.getSecureBiometric(key) : undefined,
|
|
1132
|
+
...(pending === undefined ? {} : {
|
|
1133
|
+
pending
|
|
1134
|
+
})
|
|
1135
|
+
};
|
|
1136
|
+
});
|
|
1137
|
+
return {
|
|
1138
|
+
records,
|
|
1139
|
+
renamesMigrated
|
|
1140
|
+
};
|
|
1141
|
+
}
|
|
1142
|
+
function clearPendingItemWrite(key) {
|
|
1143
|
+
if (nonMemoryScope === StorageScope.Disk) {
|
|
1144
|
+
durability.clearPendingDiskWrite(key);
|
|
1145
|
+
} else if (nonMemoryScope === StorageScope.Secure && !isBiometric) {
|
|
1146
|
+
durability.clearPendingSecureWrite(key);
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
function restoreItemState(snapshot) {
|
|
1150
|
+
const rollbackErrors = [];
|
|
1151
|
+
const recordsByKey = new Map(snapshot.records.map(record => [record.key, record]));
|
|
1152
|
+
snapshot.records.forEach(({
|
|
1153
|
+
key
|
|
1154
|
+
}) => {
|
|
1155
|
+
clearPendingItemWrite(key);
|
|
1156
|
+
if (nonMemoryScope !== null) {
|
|
1157
|
+
invalidateRawCache(nonMemoryScope, key);
|
|
1158
|
+
}
|
|
1159
|
+
});
|
|
1160
|
+
const biometricRecords = snapshot.records.filter(({
|
|
1161
|
+
biometricValue
|
|
1162
|
+
}) => biometricValue !== undefined);
|
|
1163
|
+
if (nonMemoryScope === StorageScope.Secure) {
|
|
1164
|
+
biometricRecords.forEach(({
|
|
1165
|
+
key,
|
|
1166
|
+
biometricValue
|
|
1167
|
+
}) => {
|
|
1168
|
+
try {
|
|
1169
|
+
adapter.backend.setSecureBiometricWithLevel(key, biometricValue, resolvedBiometricLevel === BiometricLevel.None ? BiometricLevel.BiometryOnly : resolvedBiometricLevel);
|
|
1170
|
+
cacheRawValue(StorageScope.Secure, key, biometricValue, "biometric");
|
|
1171
|
+
} catch (error) {
|
|
1172
|
+
rollbackErrors.push({
|
|
1173
|
+
label: "rollback biometric",
|
|
1174
|
+
error
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1179
|
+
const plainSets = new Map();
|
|
1180
|
+
const plainRemoves = [];
|
|
1181
|
+
snapshot.records.forEach(({
|
|
1182
|
+
key,
|
|
1183
|
+
plainValue,
|
|
1184
|
+
biometricValue
|
|
1185
|
+
}) => {
|
|
1186
|
+
if (plainValue === undefined) {
|
|
1187
|
+
if (biometricValue === undefined) {
|
|
1188
|
+
plainRemoves.push(key);
|
|
1189
|
+
}
|
|
1190
|
+
return;
|
|
1191
|
+
}
|
|
1192
|
+
const accessControl = secureAccessControl ?? secureDefaultAccessControl;
|
|
1193
|
+
const group = plainSets.get(accessControl) ?? {
|
|
1194
|
+
keys: [],
|
|
1195
|
+
values: []
|
|
1196
|
+
};
|
|
1197
|
+
group.keys.push(key);
|
|
1198
|
+
group.values.push(plainValue);
|
|
1199
|
+
plainSets.set(accessControl, group);
|
|
1200
|
+
});
|
|
1201
|
+
plainSets.forEach((group, accessControl) => {
|
|
1202
|
+
try {
|
|
1203
|
+
if (nonMemoryScope === StorageScope.Secure) {
|
|
1204
|
+
adapter.backend.setSecureAccessControl(accessControl);
|
|
1205
|
+
}
|
|
1206
|
+
adapter.backend.setBatch(group.keys, group.values, config.scope);
|
|
1207
|
+
group.keys.forEach((key, index) => {
|
|
1208
|
+
cacheRawValue(resolveNonMemoryScope(), key, group.values[index], "plain");
|
|
1209
|
+
});
|
|
1210
|
+
} catch (error) {
|
|
1211
|
+
rollbackErrors.push({
|
|
1212
|
+
label: "rollback plain set",
|
|
1213
|
+
error
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
});
|
|
1217
|
+
if (plainRemoves.length > 0) {
|
|
1218
|
+
try {
|
|
1219
|
+
adapter.backend.removeBatch(plainRemoves, config.scope);
|
|
1220
|
+
plainRemoves.forEach(key => {
|
|
1221
|
+
cacheRawValue(resolveNonMemoryScope(), key, undefined, "plain");
|
|
1222
|
+
});
|
|
1223
|
+
} catch (error) {
|
|
1224
|
+
rollbackErrors.push({
|
|
1225
|
+
label: "rollback plain remove",
|
|
1226
|
+
error
|
|
1227
|
+
});
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
if (nonMemoryScope === StorageScope.Secure) {
|
|
1231
|
+
snapshot.records.forEach(({
|
|
1232
|
+
key,
|
|
1233
|
+
biometricValue
|
|
1234
|
+
}) => {
|
|
1235
|
+
if (biometricValue !== undefined) {
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
try {
|
|
1239
|
+
adapter.backend.deleteSecureBiometric(key);
|
|
1240
|
+
cacheRawValue(StorageScope.Secure, key, undefined, "biometric");
|
|
1241
|
+
} catch (error) {
|
|
1242
|
+
rollbackErrors.push({
|
|
1243
|
+
label: "rollback biometric remove",
|
|
1244
|
+
error
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
1247
|
+
});
|
|
1248
|
+
}
|
|
1249
|
+
snapshot.records.forEach(({
|
|
1250
|
+
key,
|
|
1251
|
+
pending
|
|
1252
|
+
}) => {
|
|
1253
|
+
if (pending === undefined || nonMemoryScope === null) {
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
if (nonMemoryScope === StorageScope.Disk) {
|
|
1257
|
+
scheduleDiskWrite(key, pending.value);
|
|
1258
|
+
} else if (!isBiometric) {
|
|
1259
|
+
scheduleSecureWrite(key, pending.value, pending.accessControl ?? secureAccessControl ?? secureDefaultAccessControl);
|
|
1260
|
+
}
|
|
1261
|
+
});
|
|
1262
|
+
recordsByKey.forEach(({
|
|
1263
|
+
key
|
|
1264
|
+
}) => {
|
|
1265
|
+
if (nonMemoryScope !== null) {
|
|
1266
|
+
invalidateRawCache(nonMemoryScope, key);
|
|
1267
|
+
}
|
|
1268
|
+
});
|
|
1269
|
+
renamesMigrated = snapshot.renamesMigrated;
|
|
1270
|
+
invalidateParsedCache();
|
|
1271
|
+
return rollbackErrors;
|
|
1272
|
+
}
|
|
1273
|
+
function runAtomicItemMutation(mutation) {
|
|
1274
|
+
if (isMemory || renameFromKeys.length === 0 || atomicMutationDepth > 0) {
|
|
1275
|
+
mutation();
|
|
1276
|
+
return;
|
|
1277
|
+
}
|
|
1278
|
+
const snapshot = captureItemState();
|
|
1279
|
+
atomicMutationDepth += 1;
|
|
1280
|
+
try {
|
|
1281
|
+
mutation();
|
|
1282
|
+
} catch (primaryError) {
|
|
1283
|
+
const rollbackErrors = restoreItemState(snapshot);
|
|
1284
|
+
if (rollbackErrors.length > 0) {
|
|
1285
|
+
throw createStorageCompositeError("item mutation rollback", primaryError, rollbackErrors);
|
|
1286
|
+
}
|
|
1287
|
+
throw primaryError;
|
|
1288
|
+
} finally {
|
|
1289
|
+
atomicMutationDepth -= 1;
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
const writeStoredRaw = (rawValue, options = {}) => {
|
|
1293
|
+
const cleanupRenameSources = options.cleanupRenameSources !== false;
|
|
887
1294
|
const oldValue = undefined;
|
|
888
1295
|
if (isBiometric) {
|
|
889
|
-
|
|
1296
|
+
invalidateRawCache(StorageScope.Secure, storageKey);
|
|
1297
|
+
runSecurePromotion(storageKey, () => {
|
|
1298
|
+
try {
|
|
1299
|
+
adapter.backend.setSecureBiometricWithLevel(storageKey, rawValue, resolvedBiometricLevel);
|
|
1300
|
+
} catch (error) {
|
|
1301
|
+
throw normalizeStorageError(error);
|
|
1302
|
+
}
|
|
1303
|
+
});
|
|
1304
|
+
if (cleanupRenameSources) {
|
|
1305
|
+
scheduleRenameSourceCleanup();
|
|
1306
|
+
}
|
|
890
1307
|
emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", adapter.changeSource);
|
|
891
|
-
return;
|
|
1308
|
+
return undefined;
|
|
1309
|
+
}
|
|
1310
|
+
if (nonMemoryScope === StorageScope.Secure) {
|
|
1311
|
+
invalidateRawCache(StorageScope.Secure, storageKey);
|
|
892
1312
|
}
|
|
893
1313
|
cacheRawValue(resolveNonMemoryScope(), storageKey, rawValue);
|
|
894
1314
|
if (nonMemoryScope === StorageScope.Disk) {
|
|
895
1315
|
if (coalesceDiskWrites || isDiskWritesAsync()) {
|
|
896
|
-
scheduleDiskWrite(storageKey, rawValue);
|
|
1316
|
+
const pendingWrite = scheduleDiskWrite(storageKey, rawValue);
|
|
1317
|
+
if (cleanupRenameSources) {
|
|
1318
|
+
scheduleRenameSourceCleanup();
|
|
1319
|
+
}
|
|
897
1320
|
emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", adapter.changeSource);
|
|
898
|
-
return;
|
|
1321
|
+
return pendingWrite;
|
|
899
1322
|
}
|
|
900
1323
|
clearPendingDiskWrite(storageKey);
|
|
901
1324
|
}
|
|
902
1325
|
if (coalesceSecureWrites) {
|
|
903
|
-
scheduleSecureWrite(storageKey, rawValue, secureAccessControl ?? secureDefaultAccessControl);
|
|
1326
|
+
const pendingWrite = scheduleSecureWrite(storageKey, rawValue, secureAccessControl ?? secureDefaultAccessControl);
|
|
1327
|
+
if (cleanupRenameSources) {
|
|
1328
|
+
scheduleRenameSourceCleanup();
|
|
1329
|
+
}
|
|
904
1330
|
emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", adapter.changeSource);
|
|
905
|
-
return;
|
|
1331
|
+
return pendingWrite;
|
|
906
1332
|
}
|
|
907
1333
|
if (nonMemoryScope === StorageScope.Secure) {
|
|
908
1334
|
clearPendingSecureWrite(storageKey);
|
|
@@ -911,41 +1337,102 @@ export function createStorageCore(buildAdapter) {
|
|
|
911
1337
|
}
|
|
912
1338
|
}
|
|
913
1339
|
adapter.backend.set(storageKey, rawValue, config.scope);
|
|
1340
|
+
if (cleanupRenameSources) {
|
|
1341
|
+
scheduleRenameSourceCleanup();
|
|
1342
|
+
}
|
|
914
1343
|
emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", adapter.changeSource);
|
|
1344
|
+
return undefined;
|
|
915
1345
|
};
|
|
916
1346
|
const migrateRenamesIfNeeded = () => {
|
|
917
1347
|
if (renamesMigrated) {
|
|
918
1348
|
return;
|
|
919
1349
|
}
|
|
920
1350
|
try {
|
|
1351
|
+
flushPendingRenameSourceWrites();
|
|
921
1352
|
const hasCurrent = isBiometric ? adapter.backend.hasSecureBiometric(storageKey) : adapter.backend.has(storageKey, config.scope);
|
|
1353
|
+
const snapshots = readRenameSnapshots();
|
|
922
1354
|
if (hasCurrent) {
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
1355
|
+
try {
|
|
1356
|
+
snapshots.forEach(({
|
|
1357
|
+
key
|
|
1358
|
+
}) => {
|
|
1359
|
+
removeRenameSource(key);
|
|
1360
|
+
});
|
|
1361
|
+
} catch (primaryError) {
|
|
1362
|
+
const rollbackErrors = [];
|
|
1363
|
+
snapshots.forEach(snapshot => {
|
|
1364
|
+
try {
|
|
1365
|
+
restoreRenameSource(snapshot);
|
|
1366
|
+
} catch (error) {
|
|
1367
|
+
rollbackErrors.push({
|
|
1368
|
+
label: "rollback rename source",
|
|
1369
|
+
error
|
|
1370
|
+
});
|
|
927
1371
|
}
|
|
928
|
-
}
|
|
929
|
-
|
|
1372
|
+
});
|
|
1373
|
+
if (rollbackErrors.length > 0) {
|
|
1374
|
+
throw createStorageCompositeError("rename cleanup", primaryError, rollbackErrors);
|
|
930
1375
|
}
|
|
1376
|
+
throw primaryError;
|
|
931
1377
|
}
|
|
932
1378
|
renamesMigrated = true;
|
|
933
1379
|
return;
|
|
934
1380
|
}
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
1381
|
+
const snapshot = snapshots.find(candidate => selectedRenameValue(candidate) !== undefined);
|
|
1382
|
+
if (snapshot === undefined) {
|
|
1383
|
+
renamesMigrated = true;
|
|
1384
|
+
return;
|
|
1385
|
+
}
|
|
1386
|
+
let writeAttempted = false;
|
|
1387
|
+
let pendingCurrentWrite;
|
|
1388
|
+
try {
|
|
1389
|
+
writeAttempted = true;
|
|
1390
|
+
pendingCurrentWrite = writeStoredRaw(selectedRenameValue(snapshot), {
|
|
1391
|
+
cleanupRenameSources: false
|
|
1392
|
+
});
|
|
1393
|
+
if (hasPendingCurrentWrite()) {
|
|
1394
|
+
flushPendingCurrentWrite();
|
|
939
1395
|
}
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
1396
|
+
snapshots.forEach(({
|
|
1397
|
+
key
|
|
1398
|
+
}) => {
|
|
1399
|
+
removeRenameSource(key);
|
|
1400
|
+
});
|
|
1401
|
+
renamesMigrated = true;
|
|
1402
|
+
} catch (primaryError) {
|
|
1403
|
+
const rollbackErrors = [];
|
|
1404
|
+
clearPendingCurrentWriteIf(pendingCurrentWrite);
|
|
1405
|
+
if (writeAttempted) {
|
|
1406
|
+
try {
|
|
1407
|
+
removeCurrentBackendValue();
|
|
1408
|
+
} catch (error) {
|
|
1409
|
+
rollbackErrors.push({
|
|
1410
|
+
label: "rollback current value",
|
|
1411
|
+
error
|
|
1412
|
+
});
|
|
1413
|
+
}
|
|
945
1414
|
}
|
|
946
|
-
|
|
1415
|
+
snapshots.forEach(renameSnapshot => {
|
|
1416
|
+
try {
|
|
1417
|
+
restoreRenameSource(renameSnapshot);
|
|
1418
|
+
} catch (error) {
|
|
1419
|
+
rollbackErrors.push({
|
|
1420
|
+
label: "rollback rename source",
|
|
1421
|
+
error
|
|
1422
|
+
});
|
|
1423
|
+
}
|
|
1424
|
+
});
|
|
1425
|
+
invalidateRawCache(resolveNonMemoryScope(), storageKey);
|
|
1426
|
+
snapshots.forEach(({
|
|
1427
|
+
key
|
|
1428
|
+
}) => {
|
|
1429
|
+
invalidateRenameSourceCache(key);
|
|
1430
|
+
});
|
|
1431
|
+
if (rollbackErrors.length > 0) {
|
|
1432
|
+
throw createStorageCompositeError("rename migration", primaryError, rollbackErrors);
|
|
1433
|
+
}
|
|
1434
|
+
throw primaryError;
|
|
947
1435
|
}
|
|
948
|
-
renamesMigrated = true;
|
|
949
1436
|
} catch (error) {
|
|
950
1437
|
if (isKeychainLockedError(error)) {
|
|
951
1438
|
onReadError?.(error);
|
|
@@ -955,16 +1442,22 @@ export function createStorageCore(buildAdapter) {
|
|
|
955
1442
|
}
|
|
956
1443
|
};
|
|
957
1444
|
const removeStoredRaw = (operation = "remove") => {
|
|
958
|
-
const oldValue = getEventRawValue(config.scope, storageKey);
|
|
1445
|
+
const oldValue = isBiometric ? getEventRawValueForRepresentation(config.scope, storageKey, "biometric") : getEventRawValue(config.scope, storageKey);
|
|
959
1446
|
if (isBiometric) {
|
|
1447
|
+
invalidateRawCache(StorageScope.Secure, storageKey);
|
|
1448
|
+
scheduleRenameSourceCleanup();
|
|
960
1449
|
adapter.backend.deleteSecureBiometric(storageKey);
|
|
961
1450
|
emitKeyChange(config.scope, storageKey, oldValue, undefined, operation, adapter.changeSource);
|
|
962
1451
|
return;
|
|
963
1452
|
}
|
|
1453
|
+
if (nonMemoryScope === StorageScope.Secure) {
|
|
1454
|
+
invalidateRawCache(StorageScope.Secure, storageKey);
|
|
1455
|
+
}
|
|
964
1456
|
cacheRawValue(resolveNonMemoryScope(), storageKey, undefined);
|
|
965
1457
|
if (nonMemoryScope === StorageScope.Disk) {
|
|
966
1458
|
if (coalesceDiskWrites || isDiskWritesAsync()) {
|
|
967
1459
|
scheduleDiskWrite(storageKey, undefined);
|
|
1460
|
+
scheduleRenameSourceCleanup();
|
|
968
1461
|
emitKeyChange(config.scope, storageKey, oldValue, undefined, operation, adapter.changeSource);
|
|
969
1462
|
return;
|
|
970
1463
|
}
|
|
@@ -972,12 +1465,14 @@ export function createStorageCore(buildAdapter) {
|
|
|
972
1465
|
}
|
|
973
1466
|
if (coalesceSecureWrites) {
|
|
974
1467
|
scheduleSecureWrite(storageKey, undefined, secureAccessControl ?? secureDefaultAccessControl);
|
|
1468
|
+
scheduleRenameSourceCleanup();
|
|
975
1469
|
emitKeyChange(config.scope, storageKey, oldValue, undefined, operation, adapter.changeSource);
|
|
976
1470
|
return;
|
|
977
1471
|
}
|
|
978
1472
|
if (nonMemoryScope === StorageScope.Secure) {
|
|
979
1473
|
clearPendingSecureWrite(storageKey);
|
|
980
1474
|
}
|
|
1475
|
+
scheduleRenameSourceCleanup();
|
|
981
1476
|
adapter.backend.remove(storageKey, config.scope);
|
|
982
1477
|
emitKeyChange(config.scope, storageKey, oldValue, undefined, operation, adapter.changeSource);
|
|
983
1478
|
};
|
|
@@ -986,6 +1481,8 @@ export function createStorageCore(buildAdapter) {
|
|
|
986
1481
|
const oldValue = getEventRawValue(config.scope, storageKey);
|
|
987
1482
|
if (memoryExpiration) {
|
|
988
1483
|
memoryExpiration.set(storageKey, Date.now() + (expirationTtlMs ?? 0));
|
|
1484
|
+
} else {
|
|
1485
|
+
memoryExpirationDeadlines.delete(storageKey);
|
|
989
1486
|
}
|
|
990
1487
|
const storedValue = typeof value === "string" ? escapeCollidingRawValue(value) : value;
|
|
991
1488
|
memoryStore.set(storageKey, storedValue);
|
|
@@ -1034,7 +1531,9 @@ export function createStorageCore(buildAdapter) {
|
|
|
1034
1531
|
if (lastExpiresAt > Date.now()) {
|
|
1035
1532
|
return lastValue;
|
|
1036
1533
|
}
|
|
1037
|
-
|
|
1534
|
+
runAtomicItemMutation(() => {
|
|
1535
|
+
removeStoredRaw("expire");
|
|
1536
|
+
});
|
|
1038
1537
|
invalidateParsedCache();
|
|
1039
1538
|
onExpired?.(storageKey);
|
|
1040
1539
|
lastValue = ensureValidatedValue(defaultValue, false);
|
|
@@ -1072,7 +1571,9 @@ export function createStorageCore(buildAdapter) {
|
|
|
1072
1571
|
if (isStoredEnvelope(parsed)) {
|
|
1073
1572
|
envelopeExpiresAt = parsed.expiresAt;
|
|
1074
1573
|
if (parsed.expiresAt <= Date.now()) {
|
|
1075
|
-
|
|
1574
|
+
runAtomicItemMutation(() => {
|
|
1575
|
+
removeStoredRaw("expire");
|
|
1576
|
+
});
|
|
1076
1577
|
invalidateParsedCache();
|
|
1077
1578
|
onExpired?.(storageKey);
|
|
1078
1579
|
lastValue = ensureValidatedValue(defaultValue, false);
|
|
@@ -1109,36 +1610,45 @@ export function createStorageCore(buildAdapter) {
|
|
|
1109
1610
|
}));
|
|
1110
1611
|
const set = valueOrFn => {
|
|
1111
1612
|
measureOperation("item:set", config.scope, () => {
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1613
|
+
runAtomicItemMutation(() => {
|
|
1614
|
+
const newValue = isUpdater(valueOrFn) ? valueOrFn(getInternal()) : valueOrFn;
|
|
1615
|
+
if (validate && !validate(newValue)) {
|
|
1616
|
+
throw new Error(`Validation failed for key "${storageKey}" in scope "${StorageScope[config.scope]}".`);
|
|
1617
|
+
}
|
|
1618
|
+
invalidateParsedCache();
|
|
1619
|
+
writeValueWithoutValidation(newValue);
|
|
1620
|
+
});
|
|
1118
1621
|
});
|
|
1119
1622
|
};
|
|
1120
1623
|
const setIfVersion = (version, valueOrFn) => measureOperation("item:setIfVersion", config.scope, () => {
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1624
|
+
let didSet = false;
|
|
1625
|
+
runAtomicItemMutation(() => {
|
|
1626
|
+
const currentVersion = getCurrentVersion();
|
|
1627
|
+
if (currentVersion !== version) {
|
|
1628
|
+
return;
|
|
1629
|
+
}
|
|
1630
|
+
set(valueOrFn);
|
|
1631
|
+
didSet = true;
|
|
1632
|
+
});
|
|
1633
|
+
return didSet;
|
|
1127
1634
|
});
|
|
1128
1635
|
const deleteItem = () => {
|
|
1129
1636
|
measureOperation("item:delete", config.scope, () => {
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
memoryExpiration
|
|
1637
|
+
runAtomicItemMutation(() => {
|
|
1638
|
+
invalidateParsedCache();
|
|
1639
|
+
if (isMemory) {
|
|
1640
|
+
const oldValue = getEventRawValue(config.scope, storageKey);
|
|
1641
|
+
if (memoryExpiration) {
|
|
1642
|
+
memoryExpiration.delete(storageKey);
|
|
1643
|
+
}
|
|
1644
|
+
memoryExpirationDeadlines.delete(storageKey);
|
|
1645
|
+
memoryStore.delete(storageKey);
|
|
1646
|
+
notifyKeyListeners(memoryListeners, storageKey);
|
|
1647
|
+
emitKeyChange(config.scope, storageKey, oldValue, undefined, "remove", "memory");
|
|
1648
|
+
return;
|
|
1135
1649
|
}
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
emitKeyChange(config.scope, storageKey, oldValue, undefined, "remove", "memory");
|
|
1139
|
-
return;
|
|
1140
|
-
}
|
|
1141
|
-
removeStoredRaw();
|
|
1650
|
+
removeStoredRaw();
|
|
1651
|
+
});
|
|
1142
1652
|
});
|
|
1143
1653
|
};
|
|
1144
1654
|
const merge = partial => {
|
|
@@ -1231,14 +1741,18 @@ export function createStorageCore(buildAdapter) {
|
|
|
1231
1741
|
invalidateParsedCache();
|
|
1232
1742
|
},
|
|
1233
1743
|
_deleteMemoryEntry: () => {
|
|
1234
|
-
|
|
1235
|
-
memoryExpiration.delete(storageKey);
|
|
1236
|
-
}
|
|
1744
|
+
memoryExpirationDeadlines.delete(storageKey);
|
|
1237
1745
|
memoryStore.delete(storageKey);
|
|
1238
1746
|
invalidateParsedCache();
|
|
1239
1747
|
},
|
|
1240
1748
|
_hasValidation: validate !== undefined,
|
|
1241
1749
|
_hasExpiration: expiration !== undefined,
|
|
1750
|
+
_hasRenameFrom: renameFromKeys.length > 0,
|
|
1751
|
+
_renameFromKeys: renameFromKeys,
|
|
1752
|
+
_getRenameMigrationState: () => renamesMigrated,
|
|
1753
|
+
_setRenameMigrationState: migrated => {
|
|
1754
|
+
renamesMigrated = migrated;
|
|
1755
|
+
},
|
|
1242
1756
|
_readCacheEnabled: readCache,
|
|
1243
1757
|
_isBiometric: isBiometric,
|
|
1244
1758
|
_biometricLevel: resolvedBiometricLevel,
|
|
@@ -1260,6 +1774,14 @@ export function createStorageCore(buildAdapter) {
|
|
|
1260
1774
|
}
|
|
1261
1775
|
groupSet.add(storageItem);
|
|
1262
1776
|
}
|
|
1777
|
+
if (isMemory) {
|
|
1778
|
+
let items = memoryItemsByKey.get(storageKey);
|
|
1779
|
+
if (!items) {
|
|
1780
|
+
items = new Set();
|
|
1781
|
+
memoryItemsByKey.set(storageKey, items);
|
|
1782
|
+
}
|
|
1783
|
+
items.add(storageItem);
|
|
1784
|
+
}
|
|
1263
1785
|
const registryKey = `${config.scope}:${storageKey}`;
|
|
1264
1786
|
registeredKeyCounts.set(registryKey, (registeredKeyCounts.get(registryKey) ?? 0) + 1);
|
|
1265
1787
|
return storageItem;
|
|
@@ -1279,24 +1801,21 @@ export function createStorageCore(buildAdapter) {
|
|
|
1279
1801
|
const keyIndexes = [];
|
|
1280
1802
|
items.forEach((item, index) => {
|
|
1281
1803
|
if (scope === StorageScope.Disk) {
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
rawValues[index] = pending;
|
|
1804
|
+
if (durability.hasPendingDiskWrite(item.key)) {
|
|
1805
|
+
rawValues[index] = durability.readPendingDiskWrite(item.key);
|
|
1285
1806
|
return;
|
|
1286
1807
|
}
|
|
1287
1808
|
}
|
|
1288
1809
|
if (scope === StorageScope.Secure) {
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
rawValues[index] = pending;
|
|
1810
|
+
if (durability.hasPendingSecureWrite(item.key)) {
|
|
1811
|
+
rawValues[index] = durability.readPendingSecureWrite(item.key);
|
|
1292
1812
|
return;
|
|
1293
1813
|
}
|
|
1294
1814
|
}
|
|
1295
1815
|
if (item._readCacheEnabled === true) {
|
|
1296
|
-
const
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
rawValues[index] = cached;
|
|
1816
|
+
const cachedEntry = getCachedRawValueEntry(scope, item.key);
|
|
1817
|
+
if (cachedEntry?.has("plain")) {
|
|
1818
|
+
rawValues[index] = cachedEntry.get("plain");
|
|
1300
1819
|
return;
|
|
1301
1820
|
}
|
|
1302
1821
|
}
|
|
@@ -1391,6 +1910,10 @@ export function createStorageCore(buildAdapter) {
|
|
|
1391
1910
|
const keys = secureEntries.map(({
|
|
1392
1911
|
item
|
|
1393
1912
|
}) => item.key);
|
|
1913
|
+
keys.forEach(key => {
|
|
1914
|
+
invalidateRawCache(scope, key);
|
|
1915
|
+
});
|
|
1916
|
+
const serializedValues = [];
|
|
1394
1917
|
const oldValues = shouldReadPreviousEventValues(scope) ? adapter.backend.getBatch(keys, scope).map(value => value === undefined ? undefined : unescapeCollidingRawValue(value)) : [];
|
|
1395
1918
|
const groupedByAccessControl = new Map();
|
|
1396
1919
|
secureEntries.forEach(({
|
|
@@ -1398,6 +1921,8 @@ export function createStorageCore(buildAdapter) {
|
|
|
1398
1921
|
value,
|
|
1399
1922
|
internal
|
|
1400
1923
|
}) => {
|
|
1924
|
+
const serialized = item.serialize(value);
|
|
1925
|
+
serializedValues.push(serialized);
|
|
1401
1926
|
const accessControl = internal._secureAccessControl ?? secureDefaultAccessControl;
|
|
1402
1927
|
const existingGroup = groupedByAccessControl.get(accessControl);
|
|
1403
1928
|
const group = existingGroup ?? {
|
|
@@ -1405,7 +1930,7 @@ export function createStorageCore(buildAdapter) {
|
|
|
1405
1930
|
values: []
|
|
1406
1931
|
};
|
|
1407
1932
|
group.keys.push(item.key);
|
|
1408
|
-
group.values.push(
|
|
1933
|
+
group.values.push(serialized);
|
|
1409
1934
|
if (!existingGroup) {
|
|
1410
1935
|
groupedByAccessControl.set(accessControl, group);
|
|
1411
1936
|
}
|
|
@@ -1417,10 +1942,8 @@ export function createStorageCore(buildAdapter) {
|
|
|
1417
1942
|
cacheRawValue(scope, key, group.values[index]);
|
|
1418
1943
|
});
|
|
1419
1944
|
});
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
value
|
|
1423
|
-
}, index) => createKeyChange(scope, item.key, oldValues[index], item.serialize(value), "setBatch", adapter.changeSource)));
|
|
1945
|
+
const willEmitChanges = storageEvents.hasListeners(scope) || eventObserver !== undefined;
|
|
1946
|
+
emitBatchChange(scope, "setBatch", adapter.changeSource, willEmitChanges ? keys.map((key, index) => createKeyChange(scope, key, oldValues[index], serializedValues[index], "setBatch", adapter.changeSource)) : []);
|
|
1424
1947
|
return;
|
|
1425
1948
|
}
|
|
1426
1949
|
flushDiskWrites();
|
|
@@ -1460,6 +1983,12 @@ export function createStorageCore(buildAdapter) {
|
|
|
1460
1983
|
emitBatchChange(scope, "removeBatch", "memory", changes);
|
|
1461
1984
|
return;
|
|
1462
1985
|
}
|
|
1986
|
+
if (items.some(item => asInternal(item)._hasRenameFrom)) {
|
|
1987
|
+
items.forEach(item => {
|
|
1988
|
+
asInternal(item).delete();
|
|
1989
|
+
});
|
|
1990
|
+
return;
|
|
1991
|
+
}
|
|
1463
1992
|
const keys = items.map(item => item.key);
|
|
1464
1993
|
if (scope === StorageScope.Disk) {
|
|
1465
1994
|
flushDiskWrites();
|
|
@@ -1467,7 +1996,25 @@ export function createStorageCore(buildAdapter) {
|
|
|
1467
1996
|
if (scope === StorageScope.Secure) {
|
|
1468
1997
|
flushSecureWrites();
|
|
1469
1998
|
}
|
|
1470
|
-
const oldValues = shouldReadPreviousEventValues(scope) ?
|
|
1999
|
+
const oldValues = shouldReadPreviousEventValues(scope) ? (() => {
|
|
2000
|
+
const plainItems = items.filter(item => asInternal(item)._isBiometric !== true);
|
|
2001
|
+
const plainValues = plainItems.length === 0 ? [] : adapter.backend.getBatch(plainItems.map(item => item.key), scope).map(value => value === undefined ? undefined : unescapeCollidingRawValue(value));
|
|
2002
|
+
let plainIndex = 0;
|
|
2003
|
+
return items.map(item => {
|
|
2004
|
+
const internal = asInternal(item);
|
|
2005
|
+
if (internal._isBiometric === true) {
|
|
2006
|
+
return getEventRawValueForRepresentation(scope, item.key, "biometric");
|
|
2007
|
+
}
|
|
2008
|
+
const value = plainValues[plainIndex];
|
|
2009
|
+
plainIndex += 1;
|
|
2010
|
+
return value;
|
|
2011
|
+
});
|
|
2012
|
+
})() : [];
|
|
2013
|
+
if (scope === StorageScope.Secure) {
|
|
2014
|
+
keys.forEach(key => {
|
|
2015
|
+
invalidateRawCache(scope, key);
|
|
2016
|
+
});
|
|
2017
|
+
}
|
|
1471
2018
|
adapter.backend.removeBatch(keys, scope);
|
|
1472
2019
|
keys.forEach(key => {
|
|
1473
2020
|
cacheRawValue(scope, key, undefined);
|
|
@@ -1515,33 +2062,74 @@ export function createStorageCore(buildAdapter) {
|
|
|
1515
2062
|
}
|
|
1516
2063
|
const NOT_SET = Symbol();
|
|
1517
2064
|
const rollback = new Map();
|
|
1518
|
-
const
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
}
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
2065
|
+
const itemRenameStates = new Map();
|
|
2066
|
+
const rememberRollback = (key, item, includeOtherSecureRepresentation = false) => {
|
|
2067
|
+
const internal = item ? item : undefined;
|
|
2068
|
+
const representation = scope === StorageScope.Secure && internal?._isBiometric === true ? "biometric" : "plain";
|
|
2069
|
+
const rememberRepresentation = representationToRemember => {
|
|
2070
|
+
const identity = JSON.stringify([scope, key, representationToRemember]);
|
|
2071
|
+
if (rollback.has(identity)) {
|
|
2072
|
+
return;
|
|
2073
|
+
}
|
|
2074
|
+
if (scope === StorageScope.Memory) {
|
|
2075
|
+
const expiresAt = memoryExpirationDeadlines.get(key);
|
|
2076
|
+
rollback.set(identity, {
|
|
2077
|
+
key,
|
|
2078
|
+
representation: representationToRemember,
|
|
2079
|
+
record: {
|
|
2080
|
+
kind: "memory",
|
|
2081
|
+
value: memoryStore.has(key) ? memoryStore.get(key) : NOT_SET,
|
|
2082
|
+
...(expiresAt === undefined ? {} : {
|
|
2083
|
+
expiresAt
|
|
2084
|
+
})
|
|
2085
|
+
}
|
|
2086
|
+
});
|
|
2087
|
+
return;
|
|
2088
|
+
}
|
|
2089
|
+
if (representationToRemember === "biometric") {
|
|
2090
|
+
rollback.set(identity, {
|
|
2091
|
+
key,
|
|
2092
|
+
representation: representationToRemember,
|
|
2093
|
+
record: {
|
|
2094
|
+
kind: "biometric",
|
|
2095
|
+
value: getStoredRawValueForRepresentation(key, scope, representationToRemember),
|
|
2096
|
+
level: internal?._biometricLevel !== undefined && internal._biometricLevel !== BiometricLevel.None ? internal._biometricLevel : BiometricLevel.BiometryOnly
|
|
2097
|
+
}
|
|
1534
2098
|
});
|
|
1535
2099
|
return;
|
|
1536
2100
|
}
|
|
1537
|
-
rollback.set(
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
2101
|
+
rollback.set(identity, {
|
|
2102
|
+
key,
|
|
2103
|
+
representation: representationToRemember,
|
|
2104
|
+
record: {
|
|
2105
|
+
kind: "raw",
|
|
2106
|
+
value: getStoredRawValueForRepresentation(key, scope, representationToRemember),
|
|
2107
|
+
...(scope === StorageScope.Secure && internal?._secureAccessControl !== undefined ? {
|
|
2108
|
+
accessControl: internal._secureAccessControl
|
|
2109
|
+
} : {})
|
|
2110
|
+
}
|
|
1543
2111
|
});
|
|
2112
|
+
};
|
|
2113
|
+
if (representation === "biometric" || includeOtherSecureRepresentation) {
|
|
2114
|
+
rememberRepresentation("plain");
|
|
1544
2115
|
}
|
|
2116
|
+
rememberRepresentation(representation);
|
|
2117
|
+
if (includeOtherSecureRepresentation) {
|
|
2118
|
+
rememberRepresentation("biometric");
|
|
2119
|
+
}
|
|
2120
|
+
};
|
|
2121
|
+
const rememberItemRollback = item => {
|
|
2122
|
+
const internal = item;
|
|
2123
|
+
const includeOtherSecureRepresentation = scope === StorageScope.Secure && (internal._isBiometric === true || internal._hasRenameFrom === true);
|
|
2124
|
+
rememberRollback(item.key, item, includeOtherSecureRepresentation);
|
|
2125
|
+
if (internal._getRenameMigrationState) {
|
|
2126
|
+
if (!itemRenameStates.has(internal)) {
|
|
2127
|
+
itemRenameStates.set(internal, internal._getRenameMigrationState());
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
(internal._renameFromKeys ?? EMPTY_KEYS).forEach(aliasKey => {
|
|
2131
|
+
rememberRollback(aliasKey, item, true);
|
|
2132
|
+
});
|
|
1545
2133
|
};
|
|
1546
2134
|
const tx = {
|
|
1547
2135
|
scope,
|
|
@@ -1551,49 +2139,85 @@ export function createStorageCore(buildAdapter) {
|
|
|
1551
2139
|
setRawValue(key, value, scope);
|
|
1552
2140
|
},
|
|
1553
2141
|
removeRaw: key => {
|
|
1554
|
-
rememberRollback(key);
|
|
2142
|
+
rememberRollback(key, undefined, scope === StorageScope.Secure);
|
|
1555
2143
|
removeRawValue(key, scope);
|
|
1556
2144
|
},
|
|
1557
2145
|
getItem: item => {
|
|
1558
2146
|
assertBatchScope([item], scope);
|
|
2147
|
+
rememberItemRollback(item);
|
|
1559
2148
|
return item.get();
|
|
1560
2149
|
},
|
|
1561
2150
|
setItem: (item, value) => {
|
|
1562
2151
|
assertBatchScope([item], scope);
|
|
1563
|
-
|
|
2152
|
+
rememberItemRollback(item);
|
|
1564
2153
|
item.set(value);
|
|
1565
2154
|
},
|
|
1566
2155
|
removeItem: item => {
|
|
1567
2156
|
assertBatchScope([item], scope);
|
|
1568
|
-
|
|
2157
|
+
rememberItemRollback(item);
|
|
1569
2158
|
item.delete();
|
|
1570
2159
|
}
|
|
1571
2160
|
};
|
|
1572
2161
|
try {
|
|
1573
2162
|
return transaction(tx);
|
|
1574
2163
|
} catch (error) {
|
|
1575
|
-
|
|
2164
|
+
itemRenameStates.forEach((migrated, item) => {
|
|
2165
|
+
item._setRenameMigrationState(migrated);
|
|
2166
|
+
});
|
|
2167
|
+
const rollbackEntries = Array.from(rollback.values()).reverse();
|
|
1576
2168
|
const rollbackSource = scope === StorageScope.Memory ? "memory" : adapter.changeSource;
|
|
1577
|
-
const
|
|
2169
|
+
const rollbackErrors = [];
|
|
2170
|
+
const readRollbackEventValue = entry => {
|
|
2171
|
+
try {
|
|
2172
|
+
return getEventRawValueForRepresentation(scope, entry.key, entry.representation);
|
|
2173
|
+
} catch (readError) {
|
|
2174
|
+
rollbackErrors.push({
|
|
2175
|
+
label: `rollback read ${entry.representation}:${entry.key}`,
|
|
2176
|
+
error: readError
|
|
2177
|
+
});
|
|
2178
|
+
return undefined;
|
|
2179
|
+
}
|
|
2180
|
+
};
|
|
2181
|
+
const preRollbackValues = rollbackEntries.map(readRollbackEventValue);
|
|
1578
2182
|
if (scope === StorageScope.Memory) {
|
|
1579
|
-
rollbackEntries.forEach(
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
2183
|
+
rollbackEntries.forEach(entry => {
|
|
2184
|
+
try {
|
|
2185
|
+
const record = entry.record;
|
|
2186
|
+
if (record.kind !== "memory") {
|
|
2187
|
+
return;
|
|
2188
|
+
}
|
|
2189
|
+
if (record.value === NOT_SET) {
|
|
2190
|
+
memoryStore.delete(entry.key);
|
|
2191
|
+
} else {
|
|
2192
|
+
memoryStore.set(entry.key, record.value);
|
|
2193
|
+
}
|
|
2194
|
+
if (record.expiresAt === undefined) {
|
|
2195
|
+
memoryExpirationDeadlines.delete(entry.key);
|
|
2196
|
+
} else {
|
|
2197
|
+
memoryExpirationDeadlines.set(entry.key, record.expiresAt);
|
|
2198
|
+
}
|
|
2199
|
+
invalidateMemoryItemCaches(entry.key);
|
|
2200
|
+
notifyKeyListeners(memoryListeners, entry.key);
|
|
2201
|
+
} catch (rollbackError) {
|
|
2202
|
+
rollbackErrors.push({
|
|
2203
|
+
label: `rollback ${entry.representation}:${entry.key}`,
|
|
2204
|
+
error: rollbackError
|
|
2205
|
+
});
|
|
1584
2206
|
}
|
|
1585
|
-
notifyKeyListeners(memoryListeners, key);
|
|
1586
2207
|
});
|
|
1587
2208
|
} else {
|
|
1588
2209
|
const groupedKeysToSet = new Map();
|
|
1589
2210
|
const keysToRemove = [];
|
|
1590
|
-
|
|
2211
|
+
const biometricEntries = [];
|
|
2212
|
+
const biometricValues = new Map();
|
|
2213
|
+
rollbackEntries.forEach(entry => {
|
|
2214
|
+
const {
|
|
2215
|
+
key,
|
|
2216
|
+
record
|
|
2217
|
+
} = entry;
|
|
1591
2218
|
if (record.kind === "biometric") {
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
} else {
|
|
1595
|
-
adapter.backend.setSecureBiometricWithLevel(key, record.value, record.level);
|
|
1596
|
-
}
|
|
2219
|
+
biometricEntries.push(entry);
|
|
2220
|
+
biometricValues.set(key, record.value);
|
|
1597
2221
|
return;
|
|
1598
2222
|
}
|
|
1599
2223
|
if (record.kind !== "raw") {
|
|
@@ -1616,29 +2240,108 @@ export function createStorageCore(buildAdapter) {
|
|
|
1616
2240
|
}
|
|
1617
2241
|
});
|
|
1618
2242
|
if (scope === StorageScope.Disk) {
|
|
1619
|
-
|
|
2243
|
+
try {
|
|
2244
|
+
flushDiskWrites();
|
|
2245
|
+
} catch (flushError) {
|
|
2246
|
+
rollbackErrors.push({
|
|
2247
|
+
label: "rollback flush disk",
|
|
2248
|
+
error: flushError
|
|
2249
|
+
});
|
|
2250
|
+
}
|
|
1620
2251
|
}
|
|
1621
2252
|
if (scope === StorageScope.Secure) {
|
|
1622
|
-
|
|
2253
|
+
try {
|
|
2254
|
+
flushSecureWrites();
|
|
2255
|
+
} catch (flushError) {
|
|
2256
|
+
rollbackErrors.push({
|
|
2257
|
+
label: "rollback flush secure",
|
|
2258
|
+
error: flushError
|
|
2259
|
+
});
|
|
2260
|
+
}
|
|
1623
2261
|
}
|
|
2262
|
+
if (scope === StorageScope.Secure) {
|
|
2263
|
+
rollbackEntries.forEach(entry => {
|
|
2264
|
+
invalidateRawCache(scope, entry.key);
|
|
2265
|
+
});
|
|
2266
|
+
}
|
|
2267
|
+
biometricEntries.forEach(entry => {
|
|
2268
|
+
const {
|
|
2269
|
+
key,
|
|
2270
|
+
record
|
|
2271
|
+
} = entry;
|
|
2272
|
+
if (record.kind !== "biometric" || record.value === undefined) {
|
|
2273
|
+
return;
|
|
2274
|
+
}
|
|
2275
|
+
try {
|
|
2276
|
+
adapter.backend.setSecureBiometricWithLevel(key, record.value, record.level);
|
|
2277
|
+
cacheRawValue(StorageScope.Secure, key, record.value, "biometric");
|
|
2278
|
+
} catch (rollbackError) {
|
|
2279
|
+
rollbackErrors.push({
|
|
2280
|
+
label: "rollback biometric",
|
|
2281
|
+
error: rollbackError
|
|
2282
|
+
});
|
|
2283
|
+
}
|
|
2284
|
+
});
|
|
1624
2285
|
groupedKeysToSet.forEach((group, accessControl) => {
|
|
1625
|
-
|
|
1626
|
-
|
|
2286
|
+
try {
|
|
2287
|
+
if (scope === StorageScope.Secure) {
|
|
2288
|
+
adapter.backend.setSecureAccessControl(accessControl);
|
|
2289
|
+
}
|
|
2290
|
+
adapter.backend.setBatch(group.keys, group.values, scope);
|
|
2291
|
+
group.keys.forEach((key, index) => {
|
|
2292
|
+
cacheRawValue(scope, key, group.values[index], "plain");
|
|
2293
|
+
});
|
|
2294
|
+
} catch (rollbackError) {
|
|
2295
|
+
rollbackErrors.push({
|
|
2296
|
+
label: "rollback plain setBatch",
|
|
2297
|
+
error: rollbackError
|
|
2298
|
+
});
|
|
1627
2299
|
}
|
|
1628
|
-
adapter.backend.setBatch(group.keys, group.values, scope);
|
|
1629
|
-
group.keys.forEach((key, index) => {
|
|
1630
|
-
cacheRawValue(scope, key, group.values[index]);
|
|
1631
|
-
});
|
|
1632
2300
|
});
|
|
1633
2301
|
if (keysToRemove.length > 0) {
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
2302
|
+
const keysToRemoveWithoutBiometric = keysToRemove.filter(key => biometricValues.get(key) === undefined);
|
|
2303
|
+
try {
|
|
2304
|
+
if (keysToRemoveWithoutBiometric.length > 0) {
|
|
2305
|
+
adapter.backend.removeBatch(keysToRemoveWithoutBiometric, scope);
|
|
2306
|
+
}
|
|
2307
|
+
keysToRemove.forEach(key => {
|
|
2308
|
+
cacheRawValue(scope, key, undefined, "plain");
|
|
2309
|
+
});
|
|
2310
|
+
} catch (rollbackError) {
|
|
2311
|
+
rollbackErrors.push({
|
|
2312
|
+
label: "rollback plain removeBatch",
|
|
2313
|
+
error: rollbackError
|
|
2314
|
+
});
|
|
2315
|
+
}
|
|
1638
2316
|
}
|
|
2317
|
+
biometricEntries.forEach(entry => {
|
|
2318
|
+
const {
|
|
2319
|
+
key,
|
|
2320
|
+
record
|
|
2321
|
+
} = entry;
|
|
2322
|
+
if (record.kind !== "biometric") {
|
|
2323
|
+
return;
|
|
2324
|
+
}
|
|
2325
|
+
if (record.value !== undefined) {
|
|
2326
|
+
return;
|
|
2327
|
+
}
|
|
2328
|
+
invalidateRawCache(StorageScope.Secure, key);
|
|
2329
|
+
try {
|
|
2330
|
+
adapter.backend.deleteSecureBiometric(key);
|
|
2331
|
+
cacheRawValue(StorageScope.Secure, key, record.value, "biometric");
|
|
2332
|
+
} catch (rollbackError) {
|
|
2333
|
+
rollbackErrors.push({
|
|
2334
|
+
label: "rollback biometric",
|
|
2335
|
+
error: rollbackError
|
|
2336
|
+
});
|
|
2337
|
+
}
|
|
2338
|
+
});
|
|
1639
2339
|
}
|
|
1640
2340
|
if (rollbackEntries.length > 0) {
|
|
1641
|
-
emitBatchChange(scope, "rollback", rollbackSource, rollbackEntries.map((
|
|
2341
|
+
emitBatchChange(scope, "rollback", rollbackSource, rollbackEntries.map((entry, index) => createKeyChange(scope, entry.key, preRollbackValues[index], readRollbackEventValue(entry), "rollback", rollbackSource)));
|
|
2342
|
+
}
|
|
2343
|
+
if (rollbackErrors.length > 0) {
|
|
2344
|
+
throw createStorageCompositeError("transaction rollback", error, rollbackErrors);
|
|
1642
2345
|
}
|
|
1643
2346
|
throw error;
|
|
1644
2347
|
}
|
|
@@ -1746,8 +2449,16 @@ export function createStorageCore(buildAdapter) {
|
|
|
1746
2449
|
add(id);
|
|
1747
2450
|
return true;
|
|
1748
2451
|
};
|
|
2452
|
+
const getTyped = () => {
|
|
2453
|
+
const typed = {};
|
|
2454
|
+
for (const id of Object.keys(item.get())) {
|
|
2455
|
+
typed[id] = true;
|
|
2456
|
+
}
|
|
2457
|
+
return typed;
|
|
2458
|
+
};
|
|
1749
2459
|
return {
|
|
1750
2460
|
get: item.get,
|
|
2461
|
+
getTyped,
|
|
1751
2462
|
has,
|
|
1752
2463
|
add,
|
|
1753
2464
|
delete: deleteId,
|