react-native-nitro-storage 0.9.0 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +101 -29
- package/README.md +83 -26
- package/android/build.gradle +3 -1
- package/android/src/main/cpp/AndroidStorageAdapterCpp.cpp +5 -2
- package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +240 -34
- package/cpp/bindings/HybridStorage.cpp +106 -283
- package/cpp/bindings/HybridStorage.hpp +16 -9
- package/docs/api-reference.md +75 -19
- package/docs/benchmarks.md +6 -1
- package/docs/keychain-lifecycle-testing.md +36 -0
- package/docs/recipes.md +1 -2
- package/docs/secure-storage.md +45 -9
- package/ios/IOSStorageAdapterCpp.mm +391 -175
- package/lib/commonjs/capabilities.js +3 -1
- package/lib/commonjs/capabilities.js.map +1 -1
- package/lib/commonjs/core/durability.js +111 -45
- package/lib/commonjs/core/durability.js.map +1 -1
- package/lib/commonjs/index.js +15 -5
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +219 -97
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/internal.js +3 -11
- package/lib/commonjs/internal.js.map +1 -1
- package/lib/commonjs/shared.js +62 -2
- package/lib/commonjs/shared.js.map +1 -1
- package/lib/commonjs/storage-core.js +883 -163
- package/lib/commonjs/storage-core.js.map +1 -1
- package/lib/commonjs/storage-runtime.js +5 -1
- package/lib/commonjs/storage-runtime.js.map +1 -1
- package/lib/commonjs/testing.js +13 -0
- package/lib/commonjs/testing.js.map +1 -1
- package/lib/module/capabilities.js +2 -1
- package/lib/module/capabilities.js.map +1 -1
- package/lib/module/core/durability.js +111 -45
- package/lib/module/core/durability.js.map +1 -1
- package/lib/module/index.js +12 -8
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +215 -99
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/internal.js +2 -9
- package/lib/module/internal.js.map +1 -1
- package/lib/module/shared.js +60 -2
- package/lib/module/shared.js.map +1 -1
- package/lib/module/storage-core.js +884 -164
- package/lib/module/storage-core.js.map +1 -1
- package/lib/module/storage-runtime.js +4 -1
- package/lib/module/storage-runtime.js.map +1 -1
- package/lib/module/testing.js +8 -1
- package/lib/module/testing.js.map +1 -1
- package/lib/typescript/capabilities.d.ts +2 -1
- package/lib/typescript/capabilities.d.ts.map +1 -1
- package/lib/typescript/core/durability.d.ts +8 -4
- package/lib/typescript/core/durability.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +2 -2
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/index.web.d.ts +3 -29
- package/lib/typescript/index.web.d.ts.map +1 -1
- package/lib/typescript/internal.d.ts +0 -2
- package/lib/typescript/internal.d.ts.map +1 -1
- package/lib/typescript/shared.d.ts +19 -0
- package/lib/typescript/shared.d.ts.map +1 -1
- package/lib/typescript/storage-core.d.ts +15 -6
- package/lib/typescript/storage-core.d.ts.map +1 -1
- package/lib/typescript/storage-runtime.d.ts +2 -1
- package/lib/typescript/storage-runtime.d.ts.map +1 -1
- package/lib/typescript/testing.d.ts +1 -1
- package/lib/typescript/testing.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/capabilities.ts +3 -1
- package/src/core/durability.ts +140 -43
- package/src/index.ts +20 -10
- package/src/index.web.ts +328 -166
- package/src/internal.ts +0 -14
- package/src/shared.ts +85 -0
- package/src/storage-core.ts +1198 -232
- package/src/storage-runtime.ts +6 -0
- package/src/testing.ts +8 -1
|
@@ -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;
|
|
@@ -131,18 +158,6 @@ export function createStorageCore(buildAdapter) {
|
|
|
131
158
|
function recordMetric(operation, scope, durationMs, keysCount = 1) {
|
|
132
159
|
metrics.record(operation, scope, durationMs, keysCount);
|
|
133
160
|
}
|
|
134
|
-
function readPendingSecureWrite(key) {
|
|
135
|
-
return durability.readPendingSecureWrite(key);
|
|
136
|
-
}
|
|
137
|
-
function readPendingDiskWrite(key) {
|
|
138
|
-
return durability.readPendingDiskWrite(key);
|
|
139
|
-
}
|
|
140
|
-
function hasPendingDiskWrite(key) {
|
|
141
|
-
return durability.hasPendingDiskWrite(key);
|
|
142
|
-
}
|
|
143
|
-
function hasPendingSecureWrite(key) {
|
|
144
|
-
return durability.hasPendingSecureWrite(key);
|
|
145
|
-
}
|
|
146
161
|
function clearPendingDiskWrite(key) {
|
|
147
162
|
durability.clearPendingDiskWrite(key);
|
|
148
163
|
}
|
|
@@ -155,11 +170,14 @@ export function createStorageCore(buildAdapter) {
|
|
|
155
170
|
function flushSecureWrites() {
|
|
156
171
|
durability.flushSecureWrites();
|
|
157
172
|
}
|
|
173
|
+
function runSecurePromotion(key, promotion) {
|
|
174
|
+
return durability.runSecurePromotion(key, promotion);
|
|
175
|
+
}
|
|
158
176
|
function scheduleDiskWrite(key, value) {
|
|
159
|
-
durability.scheduleDiskWrite(key, value);
|
|
177
|
+
return durability.scheduleDiskWrite(key, value);
|
|
160
178
|
}
|
|
161
179
|
function scheduleSecureWrite(key, value, accessControl) {
|
|
162
|
-
durability.scheduleSecureWrite(key, value, accessControl);
|
|
180
|
+
return durability.scheduleSecureWrite(key, value, accessControl);
|
|
163
181
|
}
|
|
164
182
|
function setDiskWritesAsyncMode(enabled) {
|
|
165
183
|
durability.setDiskWritesAsync(enabled);
|
|
@@ -172,27 +190,43 @@ export function createStorageCore(buildAdapter) {
|
|
|
172
190
|
const value = memoryStore.get(key);
|
|
173
191
|
return typeof value === "string" ? value : undefined;
|
|
174
192
|
}
|
|
175
|
-
if (scope === StorageScope.Disk
|
|
176
|
-
|
|
193
|
+
if (scope === StorageScope.Disk) {
|
|
194
|
+
const pending = durability.getPendingDiskWrite(key);
|
|
195
|
+
if (pending !== undefined) {
|
|
196
|
+
return pending.value;
|
|
197
|
+
}
|
|
177
198
|
}
|
|
178
|
-
if (scope === StorageScope.Secure
|
|
179
|
-
|
|
199
|
+
if (scope === StorageScope.Secure) {
|
|
200
|
+
const pending = durability.getPendingSecureWrite(key);
|
|
201
|
+
if (pending !== undefined) {
|
|
202
|
+
return pending.value;
|
|
203
|
+
}
|
|
180
204
|
}
|
|
181
205
|
return adapter.backend.get(key, scope);
|
|
182
206
|
}
|
|
207
|
+
function getStoredRawValueForRepresentation(key, scope, representation) {
|
|
208
|
+
if (representation === "plain") {
|
|
209
|
+
return getStoredRawValue(key, scope);
|
|
210
|
+
}
|
|
211
|
+
return adapter.backend.getSecureBiometric(key);
|
|
212
|
+
}
|
|
183
213
|
function getRawValue(key, scope) {
|
|
184
214
|
assertValidScope(scope);
|
|
185
215
|
if (scope === StorageScope.Memory) {
|
|
186
216
|
const value = memoryStore.get(key);
|
|
187
217
|
return typeof value === "string" ? unescapeCollidingRawValue(value) : undefined;
|
|
188
218
|
}
|
|
189
|
-
if (scope === StorageScope.Disk
|
|
190
|
-
const pending =
|
|
191
|
-
|
|
219
|
+
if (scope === StorageScope.Disk) {
|
|
220
|
+
const pending = durability.getPendingDiskWrite(key);
|
|
221
|
+
if (pending !== undefined) {
|
|
222
|
+
return pending.value === undefined ? undefined : unescapeCollidingRawValue(pending.value);
|
|
223
|
+
}
|
|
192
224
|
}
|
|
193
|
-
if (scope === StorageScope.Secure
|
|
194
|
-
const pending =
|
|
195
|
-
|
|
225
|
+
if (scope === StorageScope.Secure) {
|
|
226
|
+
const pending = durability.getPendingSecureWrite(key);
|
|
227
|
+
if (pending !== undefined) {
|
|
228
|
+
return pending.value === undefined ? undefined : unescapeCollidingRawValue(pending.value);
|
|
229
|
+
}
|
|
196
230
|
}
|
|
197
231
|
const raw = adapter.backend.get(key, scope);
|
|
198
232
|
return raw === undefined ? undefined : unescapeCollidingRawValue(raw);
|
|
@@ -218,6 +252,7 @@ export function createStorageCore(buildAdapter) {
|
|
|
218
252
|
clearPendingDiskWrite(key);
|
|
219
253
|
}
|
|
220
254
|
if (scope === StorageScope.Secure) {
|
|
255
|
+
invalidateRawCache(scope, key);
|
|
221
256
|
flushSecureWrites();
|
|
222
257
|
clearPendingSecureWrite(key);
|
|
223
258
|
if (adapter.applyAccessControlOnSecureRawWrite) {
|
|
@@ -248,6 +283,7 @@ export function createStorageCore(buildAdapter) {
|
|
|
248
283
|
clearPendingDiskWrite(key);
|
|
249
284
|
}
|
|
250
285
|
if (scope === StorageScope.Secure) {
|
|
286
|
+
invalidateRawCache(scope, key);
|
|
251
287
|
flushSecureWrites();
|
|
252
288
|
clearPendingSecureWrite(key);
|
|
253
289
|
}
|
|
@@ -267,6 +303,7 @@ export function createStorageCore(buildAdapter) {
|
|
|
267
303
|
getScopedListeners,
|
|
268
304
|
cacheRawValue,
|
|
269
305
|
readCachedRawValue,
|
|
306
|
+
invalidateRawCache,
|
|
270
307
|
clearScopeRawCache,
|
|
271
308
|
clearPendingDiskWrite,
|
|
272
309
|
clearPendingSecureWrite,
|
|
@@ -320,6 +357,11 @@ export function createStorageCore(buildAdapter) {
|
|
|
320
357
|
return;
|
|
321
358
|
}
|
|
322
359
|
const previousValues = shouldReadPreviousEventValues(scope) ? adapter.backend.getBatch(removeKeys, scope).map(value => value === undefined ? undefined : unescapeCollidingRawValue(value)) : [];
|
|
360
|
+
if (scope === StorageScope.Secure) {
|
|
361
|
+
removeKeys.forEach(key => {
|
|
362
|
+
invalidateRawCache(scope, key);
|
|
363
|
+
});
|
|
364
|
+
}
|
|
323
365
|
adapter.backend.removeBatch(removeKeys, scope);
|
|
324
366
|
removeKeys.forEach(key => {
|
|
325
367
|
cacheRawValue(scope, key, undefined);
|
|
@@ -387,6 +429,7 @@ export function createStorageCore(buildAdapter) {
|
|
|
387
429
|
const previousValues = shouldReadPreviousEventValues(scope) ? storage.getAll(scope) : {};
|
|
388
430
|
if (scope === StorageScope.Memory) {
|
|
389
431
|
memoryStore.clear();
|
|
432
|
+
memoryExpirationDeadlines.clear();
|
|
390
433
|
notifyAllListeners(memoryListeners);
|
|
391
434
|
emitBatchChange(scope, "clear", "memory", Object.keys(previousValues).map(key => createKeyChange(scope, key, previousValues[key], undefined, "clear", "memory")));
|
|
392
435
|
return;
|
|
@@ -490,6 +533,7 @@ export function createStorageCore(buildAdapter) {
|
|
|
490
533
|
}
|
|
491
534
|
affectedKeys.forEach(key => {
|
|
492
535
|
memoryStore.delete(key);
|
|
536
|
+
memoryExpirationDeadlines.delete(key);
|
|
493
537
|
});
|
|
494
538
|
affectedKeys.forEach(key => {
|
|
495
539
|
notifyKeyListeners(memoryListeners, key);
|
|
@@ -520,7 +564,21 @@ export function createStorageCore(buildAdapter) {
|
|
|
520
564
|
},
|
|
521
565
|
clearBiometric: () => {
|
|
522
566
|
measureOperation("storage:clearBiometric", StorageScope.Secure, () => {
|
|
523
|
-
|
|
567
|
+
flushSecureWrites();
|
|
568
|
+
const readEventValues = shouldReadPreviousEventValues(StorageScope.Secure);
|
|
569
|
+
const shouldEmitChanges = storageEvents.hasListeners(StorageScope.Secure) || eventObserver !== undefined;
|
|
570
|
+
const biometricKeys = shouldEmitChanges ? adapter.backend.getAllKeys(StorageScope.Secure).filter(key => adapter.backend.hasSecureBiometric(key)) : [];
|
|
571
|
+
const previousValues = readEventValues ? biometricKeys.map(key => {
|
|
572
|
+
const raw = adapter.backend.getSecureBiometric(key);
|
|
573
|
+
return raw === undefined ? undefined : unescapeCollidingRawValue(raw);
|
|
574
|
+
}) : [];
|
|
575
|
+
clearScopeRawCache(StorageScope.Secure);
|
|
576
|
+
try {
|
|
577
|
+
adapter.backend.clearSecureBiometric();
|
|
578
|
+
} finally {
|
|
579
|
+
clearScopeRawCache(StorageScope.Secure);
|
|
580
|
+
}
|
|
581
|
+
emitBatchChange(StorageScope.Secure, "clear", adapter.changeSource, biometricKeys.map((key, index) => createKeyChange(StorageScope.Secure, key, readEventValues ? previousValues[index] : undefined, undefined, "clear", adapter.changeSource)));
|
|
524
582
|
});
|
|
525
583
|
},
|
|
526
584
|
has: (key, scope) => {
|
|
@@ -745,6 +803,9 @@ export function createStorageCore(buildAdapter) {
|
|
|
745
803
|
if (scope === StorageScope.Secure) {
|
|
746
804
|
flushSecureWrites();
|
|
747
805
|
adapter.backend.setSecureAccessControl(secureDefaultAccessControl);
|
|
806
|
+
keys.forEach(key => {
|
|
807
|
+
invalidateRawCache(scope, key);
|
|
808
|
+
});
|
|
748
809
|
}
|
|
749
810
|
if (scope === StorageScope.Disk) {
|
|
750
811
|
flushDiskWrites();
|
|
@@ -770,8 +831,9 @@ export function createStorageCore(buildAdapter) {
|
|
|
770
831
|
const expiration = config.expiration;
|
|
771
832
|
const onExpired = config.onExpired;
|
|
772
833
|
const expirationTtlMs = expiration?.ttlMs;
|
|
773
|
-
const memoryExpiration = expiration && isMemory ?
|
|
834
|
+
const memoryExpiration = expiration && isMemory ? memoryExpirationDeadlines : null;
|
|
774
835
|
const readCache = !isMemory && config.readCache === true;
|
|
836
|
+
const rawCacheRepresentation = isBiometric ? "biometric" : "plain";
|
|
775
837
|
const coalesceDiskWrites = config.scope === StorageScope.Disk && config.coalesceDiskWrites === true;
|
|
776
838
|
const coalesceSecureWrites = config.scope === StorageScope.Secure && config.coalesceSecureWrites === true && !isBiometric;
|
|
777
839
|
const defaultValue = config.defaultValue;
|
|
@@ -841,28 +903,44 @@ export function createStorageCore(buildAdapter) {
|
|
|
841
903
|
const memoryStored = memoryStore.get(storageKey);
|
|
842
904
|
return typeof memoryStored === "string" ? unescapeCollidingRawValue(memoryStored) : memoryStored;
|
|
843
905
|
}
|
|
906
|
+
if (nonMemoryScope === StorageScope.Disk) {
|
|
907
|
+
const pending = durability.getPendingDiskWrite(storageKey);
|
|
908
|
+
if (pending !== undefined) {
|
|
909
|
+
return pending.value;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
if (nonMemoryScope === StorageScope.Secure && !isBiometric) {
|
|
913
|
+
const pending = durability.getPendingSecureWrite(storageKey);
|
|
914
|
+
if (pending !== undefined) {
|
|
915
|
+
return pending.value;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
844
918
|
migrateRenamesIfNeeded();
|
|
845
919
|
if (nonMemoryScope === StorageScope.Disk) {
|
|
846
|
-
const pending = durability.
|
|
920
|
+
const pending = durability.getPendingDiskWrite(storageKey);
|
|
847
921
|
if (pending !== undefined) {
|
|
848
|
-
return pending;
|
|
922
|
+
return pending.value;
|
|
849
923
|
}
|
|
850
924
|
}
|
|
851
925
|
if (nonMemoryScope === StorageScope.Secure && !isBiometric) {
|
|
852
|
-
const pending = durability.
|
|
926
|
+
const pending = durability.getPendingSecureWrite(storageKey);
|
|
853
927
|
if (pending !== undefined) {
|
|
854
|
-
return pending;
|
|
928
|
+
return pending.value;
|
|
855
929
|
}
|
|
856
930
|
}
|
|
857
931
|
if (readCache) {
|
|
858
|
-
const
|
|
859
|
-
const
|
|
860
|
-
if (
|
|
861
|
-
return
|
|
932
|
+
const scope = resolveNonMemoryScope();
|
|
933
|
+
const cachedEntry = getCachedRawValueEntry(scope, storageKey);
|
|
934
|
+
if (cachedEntry?.has(rawCacheRepresentation)) {
|
|
935
|
+
return cachedEntry.get(rawCacheRepresentation);
|
|
862
936
|
}
|
|
863
937
|
}
|
|
864
938
|
if (isBiometric) {
|
|
865
|
-
|
|
939
|
+
const raw = readBackendRaw(() => adapter.backend.getSecureBiometric(storageKey));
|
|
940
|
+
if (readCache) {
|
|
941
|
+
cacheRawValue(resolveNonMemoryScope(), storageKey, raw, rawCacheRepresentation);
|
|
942
|
+
}
|
|
943
|
+
return raw;
|
|
866
944
|
}
|
|
867
945
|
const raw = readBackendRaw(() => adapter.backend.get(storageKey, config.scope));
|
|
868
946
|
cacheRawValue(resolveNonMemoryScope(), storageKey, raw);
|
|
@@ -874,7 +952,8 @@ export function createStorageCore(buildAdapter) {
|
|
|
874
952
|
} catch (error) {
|
|
875
953
|
onReadError?.(error);
|
|
876
954
|
if (fallbackToCacheOnReadError) {
|
|
877
|
-
const
|
|
955
|
+
const scope = resolveNonMemoryScope();
|
|
956
|
+
const cached = readCachedRawValue(scope, storageKey, rawCacheRepresentation);
|
|
878
957
|
if (cached !== undefined) {
|
|
879
958
|
return cached;
|
|
880
959
|
}
|
|
@@ -883,26 +962,378 @@ export function createStorageCore(buildAdapter) {
|
|
|
883
962
|
throw error;
|
|
884
963
|
}
|
|
885
964
|
};
|
|
886
|
-
|
|
965
|
+
function getRenameSourceKeys() {
|
|
966
|
+
return renameFromKeys.filter(key => key !== storageKey);
|
|
967
|
+
}
|
|
968
|
+
function getRenameSourceRaw(key) {
|
|
969
|
+
return isBiometric ? adapter.backend.getSecureBiometric(key) : adapter.backend.get(key, config.scope);
|
|
970
|
+
}
|
|
971
|
+
function invalidateRenameSourceCache(key) {
|
|
972
|
+
if (nonMemoryScope !== null) {
|
|
973
|
+
invalidateRawCache(nonMemoryScope, key);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
function removeRenameSource(key) {
|
|
977
|
+
invalidateRenameSourceCache(key);
|
|
978
|
+
if (isBiometric) {
|
|
979
|
+
adapter.backend.deleteSecureBiometric(key);
|
|
980
|
+
invalidateRenameSourceCache(key);
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
adapter.backend.remove(key, config.scope);
|
|
984
|
+
invalidateRenameSourceCache(key);
|
|
985
|
+
}
|
|
986
|
+
function restoreRenameSource(snapshot) {
|
|
987
|
+
invalidateRenameSourceCache(snapshot.key);
|
|
988
|
+
if (nonMemoryScope === StorageScope.Secure) {
|
|
989
|
+
if (snapshot.biometricValue !== undefined) {
|
|
990
|
+
adapter.backend.setSecureBiometricWithLevel(snapshot.key, snapshot.biometricValue, resolvedBiometricLevel === BiometricLevel.None ? BiometricLevel.BiometryOnly : resolvedBiometricLevel);
|
|
991
|
+
} else {
|
|
992
|
+
adapter.backend.deleteSecureBiometric(snapshot.key);
|
|
993
|
+
}
|
|
994
|
+
if (snapshot.plainValue !== undefined) {
|
|
995
|
+
if (adapter.applyAccessControlOnSecureRawWrite) {
|
|
996
|
+
adapter.backend.setSecureAccessControl(secureAccessControl ?? secureDefaultAccessControl);
|
|
997
|
+
}
|
|
998
|
+
adapter.backend.set(snapshot.key, snapshot.plainValue, config.scope);
|
|
999
|
+
} else if (snapshot.biometricValue === undefined) {
|
|
1000
|
+
adapter.backend.remove(snapshot.key, config.scope);
|
|
1001
|
+
}
|
|
1002
|
+
invalidateRenameSourceCache(snapshot.key);
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
if (snapshot.plainValue !== undefined) {
|
|
1006
|
+
adapter.backend.set(snapshot.key, snapshot.plainValue, config.scope);
|
|
1007
|
+
} else {
|
|
1008
|
+
adapter.backend.remove(snapshot.key, config.scope);
|
|
1009
|
+
}
|
|
1010
|
+
invalidateRenameSourceCache(snapshot.key);
|
|
1011
|
+
}
|
|
1012
|
+
function readRenameSnapshots() {
|
|
1013
|
+
return getRenameSourceKeys().flatMap(key => {
|
|
1014
|
+
const plainValue = nonMemoryScope === StorageScope.Secure ? adapter.backend.get(key, config.scope) : getRenameSourceRaw(key);
|
|
1015
|
+
const biometricValue = nonMemoryScope === StorageScope.Secure ? adapter.backend.getSecureBiometric(key) : undefined;
|
|
1016
|
+
if (plainValue === undefined && biometricValue === undefined) {
|
|
1017
|
+
return [];
|
|
1018
|
+
}
|
|
1019
|
+
return [{
|
|
1020
|
+
key,
|
|
1021
|
+
...(plainValue === undefined ? {} : {
|
|
1022
|
+
plainValue
|
|
1023
|
+
}),
|
|
1024
|
+
...(biometricValue === undefined ? {} : {
|
|
1025
|
+
biometricValue
|
|
1026
|
+
})
|
|
1027
|
+
}];
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
function selectedRenameValue(snapshot) {
|
|
1031
|
+
return isBiometric ? snapshot.biometricValue : snapshot.plainValue;
|
|
1032
|
+
}
|
|
1033
|
+
function hasPendingCurrentWrite() {
|
|
1034
|
+
if (nonMemoryScope === StorageScope.Disk) {
|
|
1035
|
+
return durability.hasPendingDiskWrite(storageKey);
|
|
1036
|
+
}
|
|
1037
|
+
if (nonMemoryScope === StorageScope.Secure && !isBiometric) {
|
|
1038
|
+
return durability.hasPendingSecureWrite(storageKey);
|
|
1039
|
+
}
|
|
1040
|
+
return false;
|
|
1041
|
+
}
|
|
1042
|
+
function hasPendingRenameSourceWrite() {
|
|
1043
|
+
if (isBiometric || nonMemoryScope === null) {
|
|
1044
|
+
return false;
|
|
1045
|
+
}
|
|
1046
|
+
return getRenameSourceKeys().some(key => nonMemoryScope === StorageScope.Disk ? durability.hasPendingDiskWrite(key) : durability.hasPendingSecureWrite(key));
|
|
1047
|
+
}
|
|
1048
|
+
function flushPendingRenameSourceWrites() {
|
|
1049
|
+
if (!hasPendingRenameSourceWrite()) {
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
1052
|
+
if (nonMemoryScope === StorageScope.Disk) {
|
|
1053
|
+
flushDiskWrites();
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1056
|
+
flushSecureWrites();
|
|
1057
|
+
}
|
|
1058
|
+
function clearPendingCurrentWriteIf(write) {
|
|
1059
|
+
if (write === undefined) {
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
if (nonMemoryScope === StorageScope.Disk && "generation" in write) {
|
|
1063
|
+
durability.clearPendingDiskWriteIf(write);
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
if (nonMemoryScope === StorageScope.Secure && !isBiometric) {
|
|
1067
|
+
durability.clearPendingSecureWriteIf(write);
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
function flushPendingCurrentWrite() {
|
|
1071
|
+
if (nonMemoryScope === StorageScope.Disk) {
|
|
1072
|
+
flushDiskWrites();
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
1075
|
+
if (nonMemoryScope === StorageScope.Secure && !isBiometric) {
|
|
1076
|
+
flushSecureWrites();
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
function removeCurrentBackendValue() {
|
|
1080
|
+
invalidateRawCache(resolveNonMemoryScope(), storageKey);
|
|
1081
|
+
if (isBiometric) {
|
|
1082
|
+
adapter.backend.deleteSecureBiometric(storageKey);
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1085
|
+
adapter.backend.remove(storageKey, config.scope);
|
|
1086
|
+
}
|
|
1087
|
+
function scheduleRenameSourceCleanup() {
|
|
1088
|
+
const sourceKeys = getRenameSourceKeys();
|
|
1089
|
+
sourceKeys.forEach(invalidateRenameSourceCache);
|
|
1090
|
+
if (isBiometric) {
|
|
1091
|
+
sourceKeys.forEach(removeRenameSource);
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
if (nonMemoryScope === StorageScope.Disk && (coalesceDiskWrites || isDiskWritesAsync())) {
|
|
1095
|
+
sourceKeys.forEach(key => {
|
|
1096
|
+
scheduleDiskWrite(key, undefined);
|
|
1097
|
+
});
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
1100
|
+
if (nonMemoryScope === StorageScope.Secure && coalesceSecureWrites) {
|
|
1101
|
+
sourceKeys.forEach(key => {
|
|
1102
|
+
scheduleSecureWrite(key, undefined, secureAccessControl ?? secureDefaultAccessControl);
|
|
1103
|
+
});
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
1106
|
+
sourceKeys.forEach(removeRenameSource);
|
|
1107
|
+
}
|
|
1108
|
+
let atomicMutationDepth = 0;
|
|
1109
|
+
function getItemStateKeys() {
|
|
1110
|
+
return Array.from(new Set([storageKey, ...getRenameSourceKeys()]));
|
|
1111
|
+
}
|
|
1112
|
+
function captureItemState() {
|
|
1113
|
+
const records = getItemStateKeys().map(key => {
|
|
1114
|
+
let pending;
|
|
1115
|
+
if (nonMemoryScope === StorageScope.Disk) {
|
|
1116
|
+
const pendingWrite = durability.getPendingDiskWrite(key);
|
|
1117
|
+
if (pendingWrite !== undefined) {
|
|
1118
|
+
pending = {
|
|
1119
|
+
value: pendingWrite.value
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
} else if (nonMemoryScope === StorageScope.Secure && !isBiometric) {
|
|
1123
|
+
const pendingWrite = durability.getPendingSecureWrite(key);
|
|
1124
|
+
if (pendingWrite !== undefined) {
|
|
1125
|
+
pending = {
|
|
1126
|
+
value: pendingWrite.value,
|
|
1127
|
+
...(pendingWrite.accessControl === undefined ? {} : {
|
|
1128
|
+
accessControl: pendingWrite.accessControl
|
|
1129
|
+
})
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
return {
|
|
1134
|
+
key,
|
|
1135
|
+
plainValue: nonMemoryScope === null ? undefined : adapter.backend.get(key, config.scope),
|
|
1136
|
+
biometricValue: nonMemoryScope === StorageScope.Secure ? adapter.backend.getSecureBiometric(key) : undefined,
|
|
1137
|
+
...(pending === undefined ? {} : {
|
|
1138
|
+
pending
|
|
1139
|
+
})
|
|
1140
|
+
};
|
|
1141
|
+
});
|
|
1142
|
+
return {
|
|
1143
|
+
records,
|
|
1144
|
+
renamesMigrated
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
function clearPendingItemWrite(key) {
|
|
1148
|
+
if (nonMemoryScope === StorageScope.Disk) {
|
|
1149
|
+
durability.clearPendingDiskWrite(key);
|
|
1150
|
+
} else if (nonMemoryScope === StorageScope.Secure && !isBiometric) {
|
|
1151
|
+
durability.clearPendingSecureWrite(key);
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
function restoreItemState(snapshot) {
|
|
1155
|
+
const rollbackErrors = [];
|
|
1156
|
+
const recordsByKey = new Map(snapshot.records.map(record => [record.key, record]));
|
|
1157
|
+
snapshot.records.forEach(({
|
|
1158
|
+
key
|
|
1159
|
+
}) => {
|
|
1160
|
+
clearPendingItemWrite(key);
|
|
1161
|
+
if (nonMemoryScope !== null) {
|
|
1162
|
+
invalidateRawCache(nonMemoryScope, key);
|
|
1163
|
+
}
|
|
1164
|
+
});
|
|
1165
|
+
const biometricRecords = snapshot.records.filter(({
|
|
1166
|
+
biometricValue
|
|
1167
|
+
}) => biometricValue !== undefined);
|
|
1168
|
+
if (nonMemoryScope === StorageScope.Secure) {
|
|
1169
|
+
biometricRecords.forEach(({
|
|
1170
|
+
key,
|
|
1171
|
+
biometricValue
|
|
1172
|
+
}) => {
|
|
1173
|
+
try {
|
|
1174
|
+
adapter.backend.setSecureBiometricWithLevel(key, biometricValue, resolvedBiometricLevel === BiometricLevel.None ? BiometricLevel.BiometryOnly : resolvedBiometricLevel);
|
|
1175
|
+
cacheRawValue(StorageScope.Secure, key, biometricValue, "biometric");
|
|
1176
|
+
} catch (error) {
|
|
1177
|
+
rollbackErrors.push({
|
|
1178
|
+
label: "rollback biometric",
|
|
1179
|
+
error
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1184
|
+
const plainSets = new Map();
|
|
1185
|
+
const plainRemoves = [];
|
|
1186
|
+
snapshot.records.forEach(({
|
|
1187
|
+
key,
|
|
1188
|
+
plainValue,
|
|
1189
|
+
biometricValue
|
|
1190
|
+
}) => {
|
|
1191
|
+
if (plainValue === undefined) {
|
|
1192
|
+
if (biometricValue === undefined) {
|
|
1193
|
+
plainRemoves.push(key);
|
|
1194
|
+
}
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
const accessControl = secureAccessControl ?? secureDefaultAccessControl;
|
|
1198
|
+
const group = plainSets.get(accessControl) ?? {
|
|
1199
|
+
keys: [],
|
|
1200
|
+
values: []
|
|
1201
|
+
};
|
|
1202
|
+
group.keys.push(key);
|
|
1203
|
+
group.values.push(plainValue);
|
|
1204
|
+
plainSets.set(accessControl, group);
|
|
1205
|
+
});
|
|
1206
|
+
plainSets.forEach((group, accessControl) => {
|
|
1207
|
+
try {
|
|
1208
|
+
if (nonMemoryScope === StorageScope.Secure) {
|
|
1209
|
+
adapter.backend.setSecureAccessControl(accessControl);
|
|
1210
|
+
}
|
|
1211
|
+
adapter.backend.setBatch(group.keys, group.values, config.scope);
|
|
1212
|
+
group.keys.forEach((key, index) => {
|
|
1213
|
+
cacheRawValue(resolveNonMemoryScope(), key, group.values[index], "plain");
|
|
1214
|
+
});
|
|
1215
|
+
} catch (error) {
|
|
1216
|
+
rollbackErrors.push({
|
|
1217
|
+
label: "rollback plain set",
|
|
1218
|
+
error
|
|
1219
|
+
});
|
|
1220
|
+
}
|
|
1221
|
+
});
|
|
1222
|
+
if (plainRemoves.length > 0) {
|
|
1223
|
+
try {
|
|
1224
|
+
adapter.backend.removeBatch(plainRemoves, config.scope);
|
|
1225
|
+
plainRemoves.forEach(key => {
|
|
1226
|
+
cacheRawValue(resolveNonMemoryScope(), key, undefined, "plain");
|
|
1227
|
+
});
|
|
1228
|
+
} catch (error) {
|
|
1229
|
+
rollbackErrors.push({
|
|
1230
|
+
label: "rollback plain remove",
|
|
1231
|
+
error
|
|
1232
|
+
});
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
if (nonMemoryScope === StorageScope.Secure) {
|
|
1236
|
+
snapshot.records.forEach(({
|
|
1237
|
+
key,
|
|
1238
|
+
biometricValue
|
|
1239
|
+
}) => {
|
|
1240
|
+
if (biometricValue !== undefined) {
|
|
1241
|
+
return;
|
|
1242
|
+
}
|
|
1243
|
+
try {
|
|
1244
|
+
adapter.backend.deleteSecureBiometric(key);
|
|
1245
|
+
cacheRawValue(StorageScope.Secure, key, undefined, "biometric");
|
|
1246
|
+
} catch (error) {
|
|
1247
|
+
rollbackErrors.push({
|
|
1248
|
+
label: "rollback biometric remove",
|
|
1249
|
+
error
|
|
1250
|
+
});
|
|
1251
|
+
}
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1254
|
+
snapshot.records.forEach(({
|
|
1255
|
+
key,
|
|
1256
|
+
pending
|
|
1257
|
+
}) => {
|
|
1258
|
+
if (pending === undefined || nonMemoryScope === null) {
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
1261
|
+
if (nonMemoryScope === StorageScope.Disk) {
|
|
1262
|
+
scheduleDiskWrite(key, pending.value);
|
|
1263
|
+
} else if (!isBiometric) {
|
|
1264
|
+
scheduleSecureWrite(key, pending.value, pending.accessControl ?? secureAccessControl ?? secureDefaultAccessControl);
|
|
1265
|
+
}
|
|
1266
|
+
});
|
|
1267
|
+
recordsByKey.forEach(({
|
|
1268
|
+
key
|
|
1269
|
+
}) => {
|
|
1270
|
+
if (nonMemoryScope !== null) {
|
|
1271
|
+
invalidateRawCache(nonMemoryScope, key);
|
|
1272
|
+
}
|
|
1273
|
+
});
|
|
1274
|
+
renamesMigrated = snapshot.renamesMigrated;
|
|
1275
|
+
invalidateParsedCache();
|
|
1276
|
+
return rollbackErrors;
|
|
1277
|
+
}
|
|
1278
|
+
function runAtomicItemMutation(mutation) {
|
|
1279
|
+
if (isMemory || renameFromKeys.length === 0 || atomicMutationDepth > 0) {
|
|
1280
|
+
mutation();
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
const snapshot = captureItemState();
|
|
1284
|
+
atomicMutationDepth += 1;
|
|
1285
|
+
try {
|
|
1286
|
+
mutation();
|
|
1287
|
+
} catch (primaryError) {
|
|
1288
|
+
const rollbackErrors = restoreItemState(snapshot);
|
|
1289
|
+
if (rollbackErrors.length > 0) {
|
|
1290
|
+
throw createStorageCompositeError("item mutation rollback", primaryError, rollbackErrors);
|
|
1291
|
+
}
|
|
1292
|
+
throw primaryError;
|
|
1293
|
+
} finally {
|
|
1294
|
+
atomicMutationDepth -= 1;
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
const writeStoredRaw = (rawValue, options = {}) => {
|
|
1298
|
+
const cleanupRenameSources = options.cleanupRenameSources !== false;
|
|
887
1299
|
const oldValue = undefined;
|
|
888
1300
|
if (isBiometric) {
|
|
889
|
-
|
|
1301
|
+
invalidateRawCache(StorageScope.Secure, storageKey);
|
|
1302
|
+
runSecurePromotion(storageKey, () => {
|
|
1303
|
+
try {
|
|
1304
|
+
adapter.backend.setSecureBiometricWithLevel(storageKey, rawValue, resolvedBiometricLevel);
|
|
1305
|
+
} catch (error) {
|
|
1306
|
+
throw normalizeStorageError(error);
|
|
1307
|
+
}
|
|
1308
|
+
});
|
|
1309
|
+
if (cleanupRenameSources) {
|
|
1310
|
+
scheduleRenameSourceCleanup();
|
|
1311
|
+
}
|
|
890
1312
|
emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", adapter.changeSource);
|
|
891
|
-
return;
|
|
1313
|
+
return undefined;
|
|
1314
|
+
}
|
|
1315
|
+
if (nonMemoryScope === StorageScope.Secure) {
|
|
1316
|
+
invalidateRawCache(StorageScope.Secure, storageKey);
|
|
892
1317
|
}
|
|
893
1318
|
cacheRawValue(resolveNonMemoryScope(), storageKey, rawValue);
|
|
894
1319
|
if (nonMemoryScope === StorageScope.Disk) {
|
|
895
1320
|
if (coalesceDiskWrites || isDiskWritesAsync()) {
|
|
896
|
-
scheduleDiskWrite(storageKey, rawValue);
|
|
1321
|
+
const pendingWrite = scheduleDiskWrite(storageKey, rawValue);
|
|
1322
|
+
if (cleanupRenameSources) {
|
|
1323
|
+
scheduleRenameSourceCleanup();
|
|
1324
|
+
}
|
|
897
1325
|
emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", adapter.changeSource);
|
|
898
|
-
return;
|
|
1326
|
+
return pendingWrite;
|
|
899
1327
|
}
|
|
900
1328
|
clearPendingDiskWrite(storageKey);
|
|
901
1329
|
}
|
|
902
1330
|
if (coalesceSecureWrites) {
|
|
903
|
-
scheduleSecureWrite(storageKey, rawValue, secureAccessControl ?? secureDefaultAccessControl);
|
|
1331
|
+
const pendingWrite = scheduleSecureWrite(storageKey, rawValue, secureAccessControl ?? secureDefaultAccessControl);
|
|
1332
|
+
if (cleanupRenameSources) {
|
|
1333
|
+
scheduleRenameSourceCleanup();
|
|
1334
|
+
}
|
|
904
1335
|
emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", adapter.changeSource);
|
|
905
|
-
return;
|
|
1336
|
+
return pendingWrite;
|
|
906
1337
|
}
|
|
907
1338
|
if (nonMemoryScope === StorageScope.Secure) {
|
|
908
1339
|
clearPendingSecureWrite(storageKey);
|
|
@@ -911,41 +1342,102 @@ export function createStorageCore(buildAdapter) {
|
|
|
911
1342
|
}
|
|
912
1343
|
}
|
|
913
1344
|
adapter.backend.set(storageKey, rawValue, config.scope);
|
|
1345
|
+
if (cleanupRenameSources) {
|
|
1346
|
+
scheduleRenameSourceCleanup();
|
|
1347
|
+
}
|
|
914
1348
|
emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", adapter.changeSource);
|
|
1349
|
+
return undefined;
|
|
915
1350
|
};
|
|
916
1351
|
const migrateRenamesIfNeeded = () => {
|
|
917
1352
|
if (renamesMigrated) {
|
|
918
1353
|
return;
|
|
919
1354
|
}
|
|
920
1355
|
try {
|
|
1356
|
+
flushPendingRenameSourceWrites();
|
|
921
1357
|
const hasCurrent = isBiometric ? adapter.backend.hasSecureBiometric(storageKey) : adapter.backend.has(storageKey, config.scope);
|
|
1358
|
+
const snapshots = readRenameSnapshots();
|
|
922
1359
|
if (hasCurrent) {
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
1360
|
+
try {
|
|
1361
|
+
snapshots.forEach(({
|
|
1362
|
+
key
|
|
1363
|
+
}) => {
|
|
1364
|
+
removeRenameSource(key);
|
|
1365
|
+
});
|
|
1366
|
+
} catch (primaryError) {
|
|
1367
|
+
const rollbackErrors = [];
|
|
1368
|
+
snapshots.forEach(snapshot => {
|
|
1369
|
+
try {
|
|
1370
|
+
restoreRenameSource(snapshot);
|
|
1371
|
+
} catch (error) {
|
|
1372
|
+
rollbackErrors.push({
|
|
1373
|
+
label: "rollback rename source",
|
|
1374
|
+
error
|
|
1375
|
+
});
|
|
927
1376
|
}
|
|
928
|
-
}
|
|
929
|
-
|
|
1377
|
+
});
|
|
1378
|
+
if (rollbackErrors.length > 0) {
|
|
1379
|
+
throw createStorageCompositeError("rename cleanup", primaryError, rollbackErrors);
|
|
930
1380
|
}
|
|
1381
|
+
throw primaryError;
|
|
931
1382
|
}
|
|
932
1383
|
renamesMigrated = true;
|
|
933
1384
|
return;
|
|
934
1385
|
}
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
1386
|
+
const snapshot = snapshots.find(candidate => selectedRenameValue(candidate) !== undefined);
|
|
1387
|
+
if (snapshot === undefined) {
|
|
1388
|
+
renamesMigrated = true;
|
|
1389
|
+
return;
|
|
1390
|
+
}
|
|
1391
|
+
let writeAttempted = false;
|
|
1392
|
+
let pendingCurrentWrite;
|
|
1393
|
+
try {
|
|
1394
|
+
writeAttempted = true;
|
|
1395
|
+
pendingCurrentWrite = writeStoredRaw(selectedRenameValue(snapshot), {
|
|
1396
|
+
cleanupRenameSources: false
|
|
1397
|
+
});
|
|
1398
|
+
if (hasPendingCurrentWrite()) {
|
|
1399
|
+
flushPendingCurrentWrite();
|
|
939
1400
|
}
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
1401
|
+
snapshots.forEach(({
|
|
1402
|
+
key
|
|
1403
|
+
}) => {
|
|
1404
|
+
removeRenameSource(key);
|
|
1405
|
+
});
|
|
1406
|
+
renamesMigrated = true;
|
|
1407
|
+
} catch (primaryError) {
|
|
1408
|
+
const rollbackErrors = [];
|
|
1409
|
+
clearPendingCurrentWriteIf(pendingCurrentWrite);
|
|
1410
|
+
if (writeAttempted) {
|
|
1411
|
+
try {
|
|
1412
|
+
removeCurrentBackendValue();
|
|
1413
|
+
} catch (error) {
|
|
1414
|
+
rollbackErrors.push({
|
|
1415
|
+
label: "rollback current value",
|
|
1416
|
+
error
|
|
1417
|
+
});
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
snapshots.forEach(renameSnapshot => {
|
|
1421
|
+
try {
|
|
1422
|
+
restoreRenameSource(renameSnapshot);
|
|
1423
|
+
} catch (error) {
|
|
1424
|
+
rollbackErrors.push({
|
|
1425
|
+
label: "rollback rename source",
|
|
1426
|
+
error
|
|
1427
|
+
});
|
|
1428
|
+
}
|
|
1429
|
+
});
|
|
1430
|
+
invalidateRawCache(resolveNonMemoryScope(), storageKey);
|
|
1431
|
+
snapshots.forEach(({
|
|
1432
|
+
key
|
|
1433
|
+
}) => {
|
|
1434
|
+
invalidateRenameSourceCache(key);
|
|
1435
|
+
});
|
|
1436
|
+
if (rollbackErrors.length > 0) {
|
|
1437
|
+
throw createStorageCompositeError("rename migration", primaryError, rollbackErrors);
|
|
945
1438
|
}
|
|
946
|
-
|
|
1439
|
+
throw primaryError;
|
|
947
1440
|
}
|
|
948
|
-
renamesMigrated = true;
|
|
949
1441
|
} catch (error) {
|
|
950
1442
|
if (isKeychainLockedError(error)) {
|
|
951
1443
|
onReadError?.(error);
|
|
@@ -955,16 +1447,22 @@ export function createStorageCore(buildAdapter) {
|
|
|
955
1447
|
}
|
|
956
1448
|
};
|
|
957
1449
|
const removeStoredRaw = (operation = "remove") => {
|
|
958
|
-
const oldValue = getEventRawValue(config.scope, storageKey);
|
|
1450
|
+
const oldValue = isBiometric ? getEventRawValueForRepresentation(config.scope, storageKey, "biometric") : getEventRawValue(config.scope, storageKey);
|
|
959
1451
|
if (isBiometric) {
|
|
1452
|
+
invalidateRawCache(StorageScope.Secure, storageKey);
|
|
1453
|
+
scheduleRenameSourceCleanup();
|
|
960
1454
|
adapter.backend.deleteSecureBiometric(storageKey);
|
|
961
1455
|
emitKeyChange(config.scope, storageKey, oldValue, undefined, operation, adapter.changeSource);
|
|
962
1456
|
return;
|
|
963
1457
|
}
|
|
1458
|
+
if (nonMemoryScope === StorageScope.Secure) {
|
|
1459
|
+
invalidateRawCache(StorageScope.Secure, storageKey);
|
|
1460
|
+
}
|
|
964
1461
|
cacheRawValue(resolveNonMemoryScope(), storageKey, undefined);
|
|
965
1462
|
if (nonMemoryScope === StorageScope.Disk) {
|
|
966
1463
|
if (coalesceDiskWrites || isDiskWritesAsync()) {
|
|
967
1464
|
scheduleDiskWrite(storageKey, undefined);
|
|
1465
|
+
scheduleRenameSourceCleanup();
|
|
968
1466
|
emitKeyChange(config.scope, storageKey, oldValue, undefined, operation, adapter.changeSource);
|
|
969
1467
|
return;
|
|
970
1468
|
}
|
|
@@ -972,12 +1470,14 @@ export function createStorageCore(buildAdapter) {
|
|
|
972
1470
|
}
|
|
973
1471
|
if (coalesceSecureWrites) {
|
|
974
1472
|
scheduleSecureWrite(storageKey, undefined, secureAccessControl ?? secureDefaultAccessControl);
|
|
1473
|
+
scheduleRenameSourceCleanup();
|
|
975
1474
|
emitKeyChange(config.scope, storageKey, oldValue, undefined, operation, adapter.changeSource);
|
|
976
1475
|
return;
|
|
977
1476
|
}
|
|
978
1477
|
if (nonMemoryScope === StorageScope.Secure) {
|
|
979
1478
|
clearPendingSecureWrite(storageKey);
|
|
980
1479
|
}
|
|
1480
|
+
scheduleRenameSourceCleanup();
|
|
981
1481
|
adapter.backend.remove(storageKey, config.scope);
|
|
982
1482
|
emitKeyChange(config.scope, storageKey, oldValue, undefined, operation, adapter.changeSource);
|
|
983
1483
|
};
|
|
@@ -986,6 +1486,8 @@ export function createStorageCore(buildAdapter) {
|
|
|
986
1486
|
const oldValue = getEventRawValue(config.scope, storageKey);
|
|
987
1487
|
if (memoryExpiration) {
|
|
988
1488
|
memoryExpiration.set(storageKey, Date.now() + (expirationTtlMs ?? 0));
|
|
1489
|
+
} else {
|
|
1490
|
+
memoryExpirationDeadlines.delete(storageKey);
|
|
989
1491
|
}
|
|
990
1492
|
const storedValue = typeof value === "string" ? escapeCollidingRawValue(value) : value;
|
|
991
1493
|
memoryStore.set(storageKey, storedValue);
|
|
@@ -1034,7 +1536,9 @@ export function createStorageCore(buildAdapter) {
|
|
|
1034
1536
|
if (lastExpiresAt > Date.now()) {
|
|
1035
1537
|
return lastValue;
|
|
1036
1538
|
}
|
|
1037
|
-
|
|
1539
|
+
runAtomicItemMutation(() => {
|
|
1540
|
+
removeStoredRaw("expire");
|
|
1541
|
+
});
|
|
1038
1542
|
invalidateParsedCache();
|
|
1039
1543
|
onExpired?.(storageKey);
|
|
1040
1544
|
lastValue = ensureValidatedValue(defaultValue, false);
|
|
@@ -1072,7 +1576,9 @@ export function createStorageCore(buildAdapter) {
|
|
|
1072
1576
|
if (isStoredEnvelope(parsed)) {
|
|
1073
1577
|
envelopeExpiresAt = parsed.expiresAt;
|
|
1074
1578
|
if (parsed.expiresAt <= Date.now()) {
|
|
1075
|
-
|
|
1579
|
+
runAtomicItemMutation(() => {
|
|
1580
|
+
removeStoredRaw("expire");
|
|
1581
|
+
});
|
|
1076
1582
|
invalidateParsedCache();
|
|
1077
1583
|
onExpired?.(storageKey);
|
|
1078
1584
|
lastValue = ensureValidatedValue(defaultValue, false);
|
|
@@ -1109,36 +1615,45 @@ export function createStorageCore(buildAdapter) {
|
|
|
1109
1615
|
}));
|
|
1110
1616
|
const set = valueOrFn => {
|
|
1111
1617
|
measureOperation("item:set", config.scope, () => {
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1618
|
+
runAtomicItemMutation(() => {
|
|
1619
|
+
const newValue = isUpdater(valueOrFn) ? valueOrFn(getInternal()) : valueOrFn;
|
|
1620
|
+
if (validate && !validate(newValue)) {
|
|
1621
|
+
throw new Error(`Validation failed for key "${storageKey}" in scope "${StorageScope[config.scope]}".`);
|
|
1622
|
+
}
|
|
1623
|
+
invalidateParsedCache();
|
|
1624
|
+
writeValueWithoutValidation(newValue);
|
|
1625
|
+
});
|
|
1118
1626
|
});
|
|
1119
1627
|
};
|
|
1120
1628
|
const setIfVersion = (version, valueOrFn) => measureOperation("item:setIfVersion", config.scope, () => {
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1629
|
+
let didSet = false;
|
|
1630
|
+
runAtomicItemMutation(() => {
|
|
1631
|
+
const currentVersion = getCurrentVersion();
|
|
1632
|
+
if (currentVersion !== version) {
|
|
1633
|
+
return;
|
|
1634
|
+
}
|
|
1635
|
+
set(valueOrFn);
|
|
1636
|
+
didSet = true;
|
|
1637
|
+
});
|
|
1638
|
+
return didSet;
|
|
1127
1639
|
});
|
|
1128
1640
|
const deleteItem = () => {
|
|
1129
1641
|
measureOperation("item:delete", config.scope, () => {
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
memoryExpiration
|
|
1642
|
+
runAtomicItemMutation(() => {
|
|
1643
|
+
invalidateParsedCache();
|
|
1644
|
+
if (isMemory) {
|
|
1645
|
+
const oldValue = getEventRawValue(config.scope, storageKey);
|
|
1646
|
+
if (memoryExpiration) {
|
|
1647
|
+
memoryExpiration.delete(storageKey);
|
|
1648
|
+
}
|
|
1649
|
+
memoryExpirationDeadlines.delete(storageKey);
|
|
1650
|
+
memoryStore.delete(storageKey);
|
|
1651
|
+
notifyKeyListeners(memoryListeners, storageKey);
|
|
1652
|
+
emitKeyChange(config.scope, storageKey, oldValue, undefined, "remove", "memory");
|
|
1653
|
+
return;
|
|
1135
1654
|
}
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
emitKeyChange(config.scope, storageKey, oldValue, undefined, "remove", "memory");
|
|
1139
|
-
return;
|
|
1140
|
-
}
|
|
1141
|
-
removeStoredRaw();
|
|
1655
|
+
removeStoredRaw();
|
|
1656
|
+
});
|
|
1142
1657
|
});
|
|
1143
1658
|
};
|
|
1144
1659
|
const merge = partial => {
|
|
@@ -1166,13 +1681,15 @@ export function createStorageCore(buildAdapter) {
|
|
|
1166
1681
|
if (isMemory) return memoryStore.has(storageKey);
|
|
1167
1682
|
if (isBiometric) return adapter.backend.hasSecureBiometric(storageKey);
|
|
1168
1683
|
if (nonMemoryScope === StorageScope.Disk) {
|
|
1169
|
-
|
|
1170
|
-
|
|
1684
|
+
const pending = durability.getPendingDiskWrite(storageKey);
|
|
1685
|
+
if (pending !== undefined) {
|
|
1686
|
+
return pending.value !== undefined;
|
|
1171
1687
|
}
|
|
1172
1688
|
}
|
|
1173
1689
|
if (nonMemoryScope === StorageScope.Secure) {
|
|
1174
|
-
|
|
1175
|
-
|
|
1690
|
+
const pending = durability.getPendingSecureWrite(storageKey);
|
|
1691
|
+
if (pending !== undefined) {
|
|
1692
|
+
return pending.value !== undefined;
|
|
1176
1693
|
}
|
|
1177
1694
|
}
|
|
1178
1695
|
return adapter.backend.has(storageKey, config.scope);
|
|
@@ -1231,14 +1748,18 @@ export function createStorageCore(buildAdapter) {
|
|
|
1231
1748
|
invalidateParsedCache();
|
|
1232
1749
|
},
|
|
1233
1750
|
_deleteMemoryEntry: () => {
|
|
1234
|
-
|
|
1235
|
-
memoryExpiration.delete(storageKey);
|
|
1236
|
-
}
|
|
1751
|
+
memoryExpirationDeadlines.delete(storageKey);
|
|
1237
1752
|
memoryStore.delete(storageKey);
|
|
1238
1753
|
invalidateParsedCache();
|
|
1239
1754
|
},
|
|
1240
1755
|
_hasValidation: validate !== undefined,
|
|
1241
1756
|
_hasExpiration: expiration !== undefined,
|
|
1757
|
+
_hasRenameFrom: renameFromKeys.length > 0,
|
|
1758
|
+
_renameFromKeys: renameFromKeys,
|
|
1759
|
+
_getRenameMigrationState: () => renamesMigrated,
|
|
1760
|
+
_setRenameMigrationState: migrated => {
|
|
1761
|
+
renamesMigrated = migrated;
|
|
1762
|
+
},
|
|
1242
1763
|
_readCacheEnabled: readCache,
|
|
1243
1764
|
_isBiometric: isBiometric,
|
|
1244
1765
|
_biometricLevel: resolvedBiometricLevel,
|
|
@@ -1260,6 +1781,14 @@ export function createStorageCore(buildAdapter) {
|
|
|
1260
1781
|
}
|
|
1261
1782
|
groupSet.add(storageItem);
|
|
1262
1783
|
}
|
|
1784
|
+
if (isMemory) {
|
|
1785
|
+
let items = memoryItemsByKey.get(storageKey);
|
|
1786
|
+
if (!items) {
|
|
1787
|
+
items = new Set();
|
|
1788
|
+
memoryItemsByKey.set(storageKey, items);
|
|
1789
|
+
}
|
|
1790
|
+
items.add(storageItem);
|
|
1791
|
+
}
|
|
1263
1792
|
const registryKey = `${config.scope}:${storageKey}`;
|
|
1264
1793
|
registeredKeyCounts.set(registryKey, (registeredKeyCounts.get(registryKey) ?? 0) + 1);
|
|
1265
1794
|
return storageItem;
|
|
@@ -1279,24 +1808,23 @@ export function createStorageCore(buildAdapter) {
|
|
|
1279
1808
|
const keyIndexes = [];
|
|
1280
1809
|
items.forEach((item, index) => {
|
|
1281
1810
|
if (scope === StorageScope.Disk) {
|
|
1282
|
-
const pending = durability.
|
|
1811
|
+
const pending = durability.getPendingDiskWrite(item.key);
|
|
1283
1812
|
if (pending !== undefined) {
|
|
1284
|
-
rawValues[index] = pending;
|
|
1813
|
+
rawValues[index] = pending.value;
|
|
1285
1814
|
return;
|
|
1286
1815
|
}
|
|
1287
1816
|
}
|
|
1288
1817
|
if (scope === StorageScope.Secure) {
|
|
1289
|
-
const pending = durability.
|
|
1818
|
+
const pending = durability.getPendingSecureWrite(item.key);
|
|
1290
1819
|
if (pending !== undefined) {
|
|
1291
|
-
rawValues[index] = pending;
|
|
1820
|
+
rawValues[index] = pending.value;
|
|
1292
1821
|
return;
|
|
1293
1822
|
}
|
|
1294
1823
|
}
|
|
1295
1824
|
if (item._readCacheEnabled === true) {
|
|
1296
|
-
const
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
rawValues[index] = cached;
|
|
1825
|
+
const cachedEntry = getCachedRawValueEntry(scope, item.key);
|
|
1826
|
+
if (cachedEntry?.has("plain")) {
|
|
1827
|
+
rawValues[index] = cachedEntry.get("plain");
|
|
1300
1828
|
return;
|
|
1301
1829
|
}
|
|
1302
1830
|
}
|
|
@@ -1391,6 +1919,10 @@ export function createStorageCore(buildAdapter) {
|
|
|
1391
1919
|
const keys = secureEntries.map(({
|
|
1392
1920
|
item
|
|
1393
1921
|
}) => item.key);
|
|
1922
|
+
keys.forEach(key => {
|
|
1923
|
+
invalidateRawCache(scope, key);
|
|
1924
|
+
});
|
|
1925
|
+
const serializedValues = [];
|
|
1394
1926
|
const oldValues = shouldReadPreviousEventValues(scope) ? adapter.backend.getBatch(keys, scope).map(value => value === undefined ? undefined : unescapeCollidingRawValue(value)) : [];
|
|
1395
1927
|
const groupedByAccessControl = new Map();
|
|
1396
1928
|
secureEntries.forEach(({
|
|
@@ -1398,6 +1930,8 @@ export function createStorageCore(buildAdapter) {
|
|
|
1398
1930
|
value,
|
|
1399
1931
|
internal
|
|
1400
1932
|
}) => {
|
|
1933
|
+
const serialized = item.serialize(value);
|
|
1934
|
+
serializedValues.push(serialized);
|
|
1401
1935
|
const accessControl = internal._secureAccessControl ?? secureDefaultAccessControl;
|
|
1402
1936
|
const existingGroup = groupedByAccessControl.get(accessControl);
|
|
1403
1937
|
const group = existingGroup ?? {
|
|
@@ -1405,7 +1939,7 @@ export function createStorageCore(buildAdapter) {
|
|
|
1405
1939
|
values: []
|
|
1406
1940
|
};
|
|
1407
1941
|
group.keys.push(item.key);
|
|
1408
|
-
group.values.push(
|
|
1942
|
+
group.values.push(serialized);
|
|
1409
1943
|
if (!existingGroup) {
|
|
1410
1944
|
groupedByAccessControl.set(accessControl, group);
|
|
1411
1945
|
}
|
|
@@ -1417,10 +1951,8 @@ export function createStorageCore(buildAdapter) {
|
|
|
1417
1951
|
cacheRawValue(scope, key, group.values[index]);
|
|
1418
1952
|
});
|
|
1419
1953
|
});
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
value
|
|
1423
|
-
}, index) => createKeyChange(scope, item.key, oldValues[index], item.serialize(value), "setBatch", adapter.changeSource)));
|
|
1954
|
+
const willEmitChanges = storageEvents.hasListeners(scope) || eventObserver !== undefined;
|
|
1955
|
+
emitBatchChange(scope, "setBatch", adapter.changeSource, willEmitChanges ? keys.map((key, index) => createKeyChange(scope, key, oldValues[index], serializedValues[index], "setBatch", adapter.changeSource)) : []);
|
|
1424
1956
|
return;
|
|
1425
1957
|
}
|
|
1426
1958
|
flushDiskWrites();
|
|
@@ -1460,6 +1992,12 @@ export function createStorageCore(buildAdapter) {
|
|
|
1460
1992
|
emitBatchChange(scope, "removeBatch", "memory", changes);
|
|
1461
1993
|
return;
|
|
1462
1994
|
}
|
|
1995
|
+
if (items.some(item => asInternal(item)._hasRenameFrom)) {
|
|
1996
|
+
items.forEach(item => {
|
|
1997
|
+
asInternal(item).delete();
|
|
1998
|
+
});
|
|
1999
|
+
return;
|
|
2000
|
+
}
|
|
1463
2001
|
const keys = items.map(item => item.key);
|
|
1464
2002
|
if (scope === StorageScope.Disk) {
|
|
1465
2003
|
flushDiskWrites();
|
|
@@ -1467,7 +2005,25 @@ export function createStorageCore(buildAdapter) {
|
|
|
1467
2005
|
if (scope === StorageScope.Secure) {
|
|
1468
2006
|
flushSecureWrites();
|
|
1469
2007
|
}
|
|
1470
|
-
const oldValues = shouldReadPreviousEventValues(scope) ?
|
|
2008
|
+
const oldValues = shouldReadPreviousEventValues(scope) ? (() => {
|
|
2009
|
+
const plainItems = items.filter(item => asInternal(item)._isBiometric !== true);
|
|
2010
|
+
const plainValues = plainItems.length === 0 ? [] : adapter.backend.getBatch(plainItems.map(item => item.key), scope).map(value => value === undefined ? undefined : unescapeCollidingRawValue(value));
|
|
2011
|
+
let plainIndex = 0;
|
|
2012
|
+
return items.map(item => {
|
|
2013
|
+
const internal = asInternal(item);
|
|
2014
|
+
if (internal._isBiometric === true) {
|
|
2015
|
+
return getEventRawValueForRepresentation(scope, item.key, "biometric");
|
|
2016
|
+
}
|
|
2017
|
+
const value = plainValues[plainIndex];
|
|
2018
|
+
plainIndex += 1;
|
|
2019
|
+
return value;
|
|
2020
|
+
});
|
|
2021
|
+
})() : [];
|
|
2022
|
+
if (scope === StorageScope.Secure) {
|
|
2023
|
+
keys.forEach(key => {
|
|
2024
|
+
invalidateRawCache(scope, key);
|
|
2025
|
+
});
|
|
2026
|
+
}
|
|
1471
2027
|
adapter.backend.removeBatch(keys, scope);
|
|
1472
2028
|
keys.forEach(key => {
|
|
1473
2029
|
cacheRawValue(scope, key, undefined);
|
|
@@ -1515,33 +2071,74 @@ export function createStorageCore(buildAdapter) {
|
|
|
1515
2071
|
}
|
|
1516
2072
|
const NOT_SET = Symbol();
|
|
1517
2073
|
const rollback = new Map();
|
|
1518
|
-
const
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
}
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
2074
|
+
const itemRenameStates = new Map();
|
|
2075
|
+
const rememberRollback = (key, item, includeOtherSecureRepresentation = false) => {
|
|
2076
|
+
const internal = item ? item : undefined;
|
|
2077
|
+
const representation = scope === StorageScope.Secure && internal?._isBiometric === true ? "biometric" : "plain";
|
|
2078
|
+
const rememberRepresentation = representationToRemember => {
|
|
2079
|
+
const identity = JSON.stringify([scope, key, representationToRemember]);
|
|
2080
|
+
if (rollback.has(identity)) {
|
|
2081
|
+
return;
|
|
2082
|
+
}
|
|
2083
|
+
if (scope === StorageScope.Memory) {
|
|
2084
|
+
const expiresAt = memoryExpirationDeadlines.get(key);
|
|
2085
|
+
rollback.set(identity, {
|
|
2086
|
+
key,
|
|
2087
|
+
representation: representationToRemember,
|
|
2088
|
+
record: {
|
|
2089
|
+
kind: "memory",
|
|
2090
|
+
value: memoryStore.has(key) ? memoryStore.get(key) : NOT_SET,
|
|
2091
|
+
...(expiresAt === undefined ? {} : {
|
|
2092
|
+
expiresAt
|
|
2093
|
+
})
|
|
2094
|
+
}
|
|
1534
2095
|
});
|
|
1535
2096
|
return;
|
|
1536
2097
|
}
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
2098
|
+
if (representationToRemember === "biometric") {
|
|
2099
|
+
rollback.set(identity, {
|
|
2100
|
+
key,
|
|
2101
|
+
representation: representationToRemember,
|
|
2102
|
+
record: {
|
|
2103
|
+
kind: "biometric",
|
|
2104
|
+
value: getStoredRawValueForRepresentation(key, scope, representationToRemember),
|
|
2105
|
+
level: internal?._biometricLevel !== undefined && internal._biometricLevel !== BiometricLevel.None ? internal._biometricLevel : BiometricLevel.BiometryOnly
|
|
2106
|
+
}
|
|
2107
|
+
});
|
|
2108
|
+
return;
|
|
2109
|
+
}
|
|
2110
|
+
rollback.set(identity, {
|
|
2111
|
+
key,
|
|
2112
|
+
representation: representationToRemember,
|
|
2113
|
+
record: {
|
|
2114
|
+
kind: "raw",
|
|
2115
|
+
value: getStoredRawValueForRepresentation(key, scope, representationToRemember),
|
|
2116
|
+
...(scope === StorageScope.Secure && internal?._secureAccessControl !== undefined ? {
|
|
2117
|
+
accessControl: internal._secureAccessControl
|
|
2118
|
+
} : {})
|
|
2119
|
+
}
|
|
1543
2120
|
});
|
|
2121
|
+
};
|
|
2122
|
+
if (representation === "biometric" || includeOtherSecureRepresentation) {
|
|
2123
|
+
rememberRepresentation("plain");
|
|
2124
|
+
}
|
|
2125
|
+
rememberRepresentation(representation);
|
|
2126
|
+
if (includeOtherSecureRepresentation) {
|
|
2127
|
+
rememberRepresentation("biometric");
|
|
2128
|
+
}
|
|
2129
|
+
};
|
|
2130
|
+
const rememberItemRollback = item => {
|
|
2131
|
+
const internal = item;
|
|
2132
|
+
const includeOtherSecureRepresentation = scope === StorageScope.Secure && (internal._isBiometric === true || internal._hasRenameFrom === true);
|
|
2133
|
+
rememberRollback(item.key, item, includeOtherSecureRepresentation);
|
|
2134
|
+
if (internal._getRenameMigrationState) {
|
|
2135
|
+
if (!itemRenameStates.has(internal)) {
|
|
2136
|
+
itemRenameStates.set(internal, internal._getRenameMigrationState());
|
|
2137
|
+
}
|
|
1544
2138
|
}
|
|
2139
|
+
(internal._renameFromKeys ?? EMPTY_KEYS).forEach(aliasKey => {
|
|
2140
|
+
rememberRollback(aliasKey, item, true);
|
|
2141
|
+
});
|
|
1545
2142
|
};
|
|
1546
2143
|
const tx = {
|
|
1547
2144
|
scope,
|
|
@@ -1551,49 +2148,85 @@ export function createStorageCore(buildAdapter) {
|
|
|
1551
2148
|
setRawValue(key, value, scope);
|
|
1552
2149
|
},
|
|
1553
2150
|
removeRaw: key => {
|
|
1554
|
-
rememberRollback(key);
|
|
2151
|
+
rememberRollback(key, undefined, scope === StorageScope.Secure);
|
|
1555
2152
|
removeRawValue(key, scope);
|
|
1556
2153
|
},
|
|
1557
2154
|
getItem: item => {
|
|
1558
2155
|
assertBatchScope([item], scope);
|
|
2156
|
+
rememberItemRollback(item);
|
|
1559
2157
|
return item.get();
|
|
1560
2158
|
},
|
|
1561
2159
|
setItem: (item, value) => {
|
|
1562
2160
|
assertBatchScope([item], scope);
|
|
1563
|
-
|
|
2161
|
+
rememberItemRollback(item);
|
|
1564
2162
|
item.set(value);
|
|
1565
2163
|
},
|
|
1566
2164
|
removeItem: item => {
|
|
1567
2165
|
assertBatchScope([item], scope);
|
|
1568
|
-
|
|
2166
|
+
rememberItemRollback(item);
|
|
1569
2167
|
item.delete();
|
|
1570
2168
|
}
|
|
1571
2169
|
};
|
|
1572
2170
|
try {
|
|
1573
2171
|
return transaction(tx);
|
|
1574
2172
|
} catch (error) {
|
|
1575
|
-
|
|
2173
|
+
itemRenameStates.forEach((migrated, item) => {
|
|
2174
|
+
item._setRenameMigrationState(migrated);
|
|
2175
|
+
});
|
|
2176
|
+
const rollbackEntries = Array.from(rollback.values()).reverse();
|
|
1576
2177
|
const rollbackSource = scope === StorageScope.Memory ? "memory" : adapter.changeSource;
|
|
1577
|
-
const
|
|
2178
|
+
const rollbackErrors = [];
|
|
2179
|
+
const readRollbackEventValue = entry => {
|
|
2180
|
+
try {
|
|
2181
|
+
return getEventRawValueForRepresentation(scope, entry.key, entry.representation);
|
|
2182
|
+
} catch (readError) {
|
|
2183
|
+
rollbackErrors.push({
|
|
2184
|
+
label: `rollback read ${entry.representation}:${entry.key}`,
|
|
2185
|
+
error: readError
|
|
2186
|
+
});
|
|
2187
|
+
return undefined;
|
|
2188
|
+
}
|
|
2189
|
+
};
|
|
2190
|
+
const preRollbackValues = rollbackEntries.map(readRollbackEventValue);
|
|
1578
2191
|
if (scope === StorageScope.Memory) {
|
|
1579
|
-
rollbackEntries.forEach(
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
2192
|
+
rollbackEntries.forEach(entry => {
|
|
2193
|
+
try {
|
|
2194
|
+
const record = entry.record;
|
|
2195
|
+
if (record.kind !== "memory") {
|
|
2196
|
+
return;
|
|
2197
|
+
}
|
|
2198
|
+
if (record.value === NOT_SET) {
|
|
2199
|
+
memoryStore.delete(entry.key);
|
|
2200
|
+
} else {
|
|
2201
|
+
memoryStore.set(entry.key, record.value);
|
|
2202
|
+
}
|
|
2203
|
+
if (record.expiresAt === undefined) {
|
|
2204
|
+
memoryExpirationDeadlines.delete(entry.key);
|
|
2205
|
+
} else {
|
|
2206
|
+
memoryExpirationDeadlines.set(entry.key, record.expiresAt);
|
|
2207
|
+
}
|
|
2208
|
+
invalidateMemoryItemCaches(entry.key);
|
|
2209
|
+
notifyKeyListeners(memoryListeners, entry.key);
|
|
2210
|
+
} catch (rollbackError) {
|
|
2211
|
+
rollbackErrors.push({
|
|
2212
|
+
label: `rollback ${entry.representation}:${entry.key}`,
|
|
2213
|
+
error: rollbackError
|
|
2214
|
+
});
|
|
1584
2215
|
}
|
|
1585
|
-
notifyKeyListeners(memoryListeners, key);
|
|
1586
2216
|
});
|
|
1587
2217
|
} else {
|
|
1588
2218
|
const groupedKeysToSet = new Map();
|
|
1589
2219
|
const keysToRemove = [];
|
|
1590
|
-
|
|
2220
|
+
const biometricEntries = [];
|
|
2221
|
+
const biometricValues = new Map();
|
|
2222
|
+
rollbackEntries.forEach(entry => {
|
|
2223
|
+
const {
|
|
2224
|
+
key,
|
|
2225
|
+
record
|
|
2226
|
+
} = entry;
|
|
1591
2227
|
if (record.kind === "biometric") {
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
} else {
|
|
1595
|
-
adapter.backend.setSecureBiometricWithLevel(key, record.value, record.level);
|
|
1596
|
-
}
|
|
2228
|
+
biometricEntries.push(entry);
|
|
2229
|
+
biometricValues.set(key, record.value);
|
|
1597
2230
|
return;
|
|
1598
2231
|
}
|
|
1599
2232
|
if (record.kind !== "raw") {
|
|
@@ -1616,29 +2249,108 @@ export function createStorageCore(buildAdapter) {
|
|
|
1616
2249
|
}
|
|
1617
2250
|
});
|
|
1618
2251
|
if (scope === StorageScope.Disk) {
|
|
1619
|
-
|
|
2252
|
+
try {
|
|
2253
|
+
flushDiskWrites();
|
|
2254
|
+
} catch (flushError) {
|
|
2255
|
+
rollbackErrors.push({
|
|
2256
|
+
label: "rollback flush disk",
|
|
2257
|
+
error: flushError
|
|
2258
|
+
});
|
|
2259
|
+
}
|
|
1620
2260
|
}
|
|
1621
2261
|
if (scope === StorageScope.Secure) {
|
|
1622
|
-
|
|
2262
|
+
try {
|
|
2263
|
+
flushSecureWrites();
|
|
2264
|
+
} catch (flushError) {
|
|
2265
|
+
rollbackErrors.push({
|
|
2266
|
+
label: "rollback flush secure",
|
|
2267
|
+
error: flushError
|
|
2268
|
+
});
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
if (scope === StorageScope.Secure) {
|
|
2272
|
+
rollbackEntries.forEach(entry => {
|
|
2273
|
+
invalidateRawCache(scope, entry.key);
|
|
2274
|
+
});
|
|
1623
2275
|
}
|
|
2276
|
+
biometricEntries.forEach(entry => {
|
|
2277
|
+
const {
|
|
2278
|
+
key,
|
|
2279
|
+
record
|
|
2280
|
+
} = entry;
|
|
2281
|
+
if (record.kind !== "biometric" || record.value === undefined) {
|
|
2282
|
+
return;
|
|
2283
|
+
}
|
|
2284
|
+
try {
|
|
2285
|
+
adapter.backend.setSecureBiometricWithLevel(key, record.value, record.level);
|
|
2286
|
+
cacheRawValue(StorageScope.Secure, key, record.value, "biometric");
|
|
2287
|
+
} catch (rollbackError) {
|
|
2288
|
+
rollbackErrors.push({
|
|
2289
|
+
label: "rollback biometric",
|
|
2290
|
+
error: rollbackError
|
|
2291
|
+
});
|
|
2292
|
+
}
|
|
2293
|
+
});
|
|
1624
2294
|
groupedKeysToSet.forEach((group, accessControl) => {
|
|
1625
|
-
|
|
1626
|
-
|
|
2295
|
+
try {
|
|
2296
|
+
if (scope === StorageScope.Secure) {
|
|
2297
|
+
adapter.backend.setSecureAccessControl(accessControl);
|
|
2298
|
+
}
|
|
2299
|
+
adapter.backend.setBatch(group.keys, group.values, scope);
|
|
2300
|
+
group.keys.forEach((key, index) => {
|
|
2301
|
+
cacheRawValue(scope, key, group.values[index], "plain");
|
|
2302
|
+
});
|
|
2303
|
+
} catch (rollbackError) {
|
|
2304
|
+
rollbackErrors.push({
|
|
2305
|
+
label: "rollback plain setBatch",
|
|
2306
|
+
error: rollbackError
|
|
2307
|
+
});
|
|
1627
2308
|
}
|
|
1628
|
-
adapter.backend.setBatch(group.keys, group.values, scope);
|
|
1629
|
-
group.keys.forEach((key, index) => {
|
|
1630
|
-
cacheRawValue(scope, key, group.values[index]);
|
|
1631
|
-
});
|
|
1632
2309
|
});
|
|
1633
2310
|
if (keysToRemove.length > 0) {
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
2311
|
+
const keysToRemoveWithoutBiometric = keysToRemove.filter(key => biometricValues.get(key) === undefined);
|
|
2312
|
+
try {
|
|
2313
|
+
if (keysToRemoveWithoutBiometric.length > 0) {
|
|
2314
|
+
adapter.backend.removeBatch(keysToRemoveWithoutBiometric, scope);
|
|
2315
|
+
}
|
|
2316
|
+
keysToRemove.forEach(key => {
|
|
2317
|
+
cacheRawValue(scope, key, undefined, "plain");
|
|
2318
|
+
});
|
|
2319
|
+
} catch (rollbackError) {
|
|
2320
|
+
rollbackErrors.push({
|
|
2321
|
+
label: "rollback plain removeBatch",
|
|
2322
|
+
error: rollbackError
|
|
2323
|
+
});
|
|
2324
|
+
}
|
|
1638
2325
|
}
|
|
2326
|
+
biometricEntries.forEach(entry => {
|
|
2327
|
+
const {
|
|
2328
|
+
key,
|
|
2329
|
+
record
|
|
2330
|
+
} = entry;
|
|
2331
|
+
if (record.kind !== "biometric") {
|
|
2332
|
+
return;
|
|
2333
|
+
}
|
|
2334
|
+
if (record.value !== undefined) {
|
|
2335
|
+
return;
|
|
2336
|
+
}
|
|
2337
|
+
invalidateRawCache(StorageScope.Secure, key);
|
|
2338
|
+
try {
|
|
2339
|
+
adapter.backend.deleteSecureBiometric(key);
|
|
2340
|
+
cacheRawValue(StorageScope.Secure, key, record.value, "biometric");
|
|
2341
|
+
} catch (rollbackError) {
|
|
2342
|
+
rollbackErrors.push({
|
|
2343
|
+
label: "rollback biometric",
|
|
2344
|
+
error: rollbackError
|
|
2345
|
+
});
|
|
2346
|
+
}
|
|
2347
|
+
});
|
|
1639
2348
|
}
|
|
1640
2349
|
if (rollbackEntries.length > 0) {
|
|
1641
|
-
emitBatchChange(scope, "rollback", rollbackSource, rollbackEntries.map((
|
|
2350
|
+
emitBatchChange(scope, "rollback", rollbackSource, rollbackEntries.map((entry, index) => createKeyChange(scope, entry.key, preRollbackValues[index], readRollbackEventValue(entry), "rollback", rollbackSource)));
|
|
2351
|
+
}
|
|
2352
|
+
if (rollbackErrors.length > 0) {
|
|
2353
|
+
throw createStorageCompositeError("transaction rollback", error, rollbackErrors);
|
|
1642
2354
|
}
|
|
1643
2355
|
throw error;
|
|
1644
2356
|
}
|
|
@@ -1746,8 +2458,16 @@ export function createStorageCore(buildAdapter) {
|
|
|
1746
2458
|
add(id);
|
|
1747
2459
|
return true;
|
|
1748
2460
|
};
|
|
2461
|
+
const getTyped = () => {
|
|
2462
|
+
const typed = {};
|
|
2463
|
+
for (const id of Object.keys(item.get())) {
|
|
2464
|
+
typed[id] = true;
|
|
2465
|
+
}
|
|
2466
|
+
return typed;
|
|
2467
|
+
};
|
|
1749
2468
|
return {
|
|
1750
2469
|
get: item.get,
|
|
2470
|
+
getTyped,
|
|
1751
2471
|
has,
|
|
1752
2472
|
add,
|
|
1753
2473
|
delete: deleteId,
|