react-native-nitro-storage 0.3.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +141 -30
- package/android/src/main/cpp/AndroidStorageAdapterCpp.cpp +22 -2
- package/android/src/main/cpp/AndroidStorageAdapterCpp.hpp +3 -0
- package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +54 -5
- package/cpp/bindings/HybridStorage.cpp +167 -22
- package/cpp/bindings/HybridStorage.hpp +12 -1
- package/cpp/core/NativeStorageAdapter.hpp +3 -0
- package/ios/IOSStorageAdapterCpp.hpp +16 -0
- package/ios/IOSStorageAdapterCpp.mm +135 -11
- package/lib/commonjs/index.js +466 -275
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +564 -270
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/internal.js +25 -0
- package/lib/commonjs/internal.js.map +1 -1
- package/lib/module/index.js +466 -277
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +564 -272
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/internal.js +24 -0
- package/lib/module/internal.js.map +1 -1
- package/lib/typescript/Storage.nitro.d.ts +2 -0
- package/lib/typescript/Storage.nitro.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +38 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/index.web.d.ts +40 -1
- package/lib/typescript/index.web.d.ts.map +1 -1
- package/lib/typescript/internal.d.ts +1 -0
- package/lib/typescript/internal.d.ts.map +1 -1
- package/nitrogen/generated/shared/c++/HybridStorageSpec.cpp +2 -0
- package/nitrogen/generated/shared/c++/HybridStorageSpec.hpp +2 -0
- package/package.json +1 -1
- package/src/Storage.nitro.ts +2 -0
- package/src/index.ts +616 -296
- package/src/index.web.ts +728 -288
- package/src/internal.ts +28 -0
package/lib/module/index.web.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
import { StorageScope, AccessControl } from "./Storage.types";
|
|
4
|
-
import { MIGRATION_VERSION_KEY, isStoredEnvelope, assertBatchScope, assertValidScope, serializeWithPrimitiveFastPath, deserializeWithPrimitiveFastPath, prefixKey, isNamespaced } from "./internal";
|
|
3
|
+
import { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
|
|
4
|
+
import { MIGRATION_VERSION_KEY, isStoredEnvelope, assertBatchScope, assertValidScope, serializeWithPrimitiveFastPath, deserializeWithPrimitiveFastPath, toVersionToken, prefixKey, isNamespaced } from "./internal";
|
|
5
5
|
export { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
|
|
6
6
|
export { migrateFromMMKV } from "./migration";
|
|
7
7
|
function asInternal(item) {
|
|
@@ -28,12 +28,76 @@ let secureFlushScheduled = false;
|
|
|
28
28
|
const SECURE_WEB_PREFIX = "__secure_";
|
|
29
29
|
const BIOMETRIC_WEB_PREFIX = "__bio_";
|
|
30
30
|
let hasWarnedAboutWebBiometricFallback = false;
|
|
31
|
+
let hasWebStorageEventSubscription = false;
|
|
32
|
+
let metricsObserver;
|
|
33
|
+
const metricsCounters = new Map();
|
|
34
|
+
function recordMetric(operation, scope, durationMs, keysCount = 1) {
|
|
35
|
+
const existing = metricsCounters.get(operation);
|
|
36
|
+
if (!existing) {
|
|
37
|
+
metricsCounters.set(operation, {
|
|
38
|
+
count: 1,
|
|
39
|
+
totalDurationMs: durationMs,
|
|
40
|
+
maxDurationMs: durationMs
|
|
41
|
+
});
|
|
42
|
+
} else {
|
|
43
|
+
existing.count += 1;
|
|
44
|
+
existing.totalDurationMs += durationMs;
|
|
45
|
+
existing.maxDurationMs = Math.max(existing.maxDurationMs, durationMs);
|
|
46
|
+
}
|
|
47
|
+
metricsObserver?.({
|
|
48
|
+
operation,
|
|
49
|
+
scope,
|
|
50
|
+
durationMs,
|
|
51
|
+
keysCount
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
function measureOperation(operation, scope, fn, keysCount = 1) {
|
|
55
|
+
const start = Date.now();
|
|
56
|
+
try {
|
|
57
|
+
return fn();
|
|
58
|
+
} finally {
|
|
59
|
+
recordMetric(operation, scope, Date.now() - start, keysCount);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function createLocalStorageWebSecureBackend() {
|
|
63
|
+
return {
|
|
64
|
+
getItem: key => globalThis.localStorage?.getItem(key) ?? null,
|
|
65
|
+
setItem: (key, value) => globalThis.localStorage?.setItem(key, value),
|
|
66
|
+
removeItem: key => globalThis.localStorage?.removeItem(key),
|
|
67
|
+
clear: () => globalThis.localStorage?.clear(),
|
|
68
|
+
getAllKeys: () => {
|
|
69
|
+
const storage = globalThis.localStorage;
|
|
70
|
+
if (!storage) return [];
|
|
71
|
+
const keys = [];
|
|
72
|
+
for (let index = 0; index < storage.length; index += 1) {
|
|
73
|
+
const key = storage.key(index);
|
|
74
|
+
if (key) {
|
|
75
|
+
keys.push(key);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return keys;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
let webSecureStorageBackend = createLocalStorageWebSecureBackend();
|
|
31
83
|
function getBrowserStorage(scope) {
|
|
32
84
|
if (scope === StorageScope.Disk) {
|
|
33
85
|
return globalThis.localStorage;
|
|
34
86
|
}
|
|
35
87
|
if (scope === StorageScope.Secure) {
|
|
36
|
-
|
|
88
|
+
if (!webSecureStorageBackend) {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
setItem: (key, value) => webSecureStorageBackend?.setItem(key, value),
|
|
93
|
+
getItem: key => webSecureStorageBackend?.getItem(key) ?? null,
|
|
94
|
+
removeItem: key => webSecureStorageBackend?.removeItem(key),
|
|
95
|
+
clear: () => webSecureStorageBackend?.clear(),
|
|
96
|
+
key: index => webSecureStorageBackend?.getAllKeys()[index] ?? null,
|
|
97
|
+
get length() {
|
|
98
|
+
return webSecureStorageBackend?.getAllKeys().length ?? 0;
|
|
99
|
+
}
|
|
100
|
+
};
|
|
37
101
|
}
|
|
38
102
|
return undefined;
|
|
39
103
|
}
|
|
@@ -86,6 +150,62 @@ function ensureWebScopeKeyIndex(scope) {
|
|
|
86
150
|
hydrateWebScopeKeyIndex(scope);
|
|
87
151
|
return getWebScopeKeyIndex(scope);
|
|
88
152
|
}
|
|
153
|
+
function handleWebStorageEvent(event) {
|
|
154
|
+
const key = event.key;
|
|
155
|
+
if (key === null) {
|
|
156
|
+
clearScopeRawCache(StorageScope.Disk);
|
|
157
|
+
clearScopeRawCache(StorageScope.Secure);
|
|
158
|
+
ensureWebScopeKeyIndex(StorageScope.Disk).clear();
|
|
159
|
+
ensureWebScopeKeyIndex(StorageScope.Secure).clear();
|
|
160
|
+
notifyAllListeners(getScopedListeners(StorageScope.Disk));
|
|
161
|
+
notifyAllListeners(getScopedListeners(StorageScope.Secure));
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (key.startsWith(SECURE_WEB_PREFIX)) {
|
|
165
|
+
const plainKey = fromSecureStorageKey(key);
|
|
166
|
+
if (event.newValue === null) {
|
|
167
|
+
ensureWebScopeKeyIndex(StorageScope.Secure).delete(plainKey);
|
|
168
|
+
cacheRawValue(StorageScope.Secure, plainKey, undefined);
|
|
169
|
+
} else {
|
|
170
|
+
ensureWebScopeKeyIndex(StorageScope.Secure).add(plainKey);
|
|
171
|
+
cacheRawValue(StorageScope.Secure, plainKey, event.newValue);
|
|
172
|
+
}
|
|
173
|
+
notifyKeyListeners(getScopedListeners(StorageScope.Secure), plainKey);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (key.startsWith(BIOMETRIC_WEB_PREFIX)) {
|
|
177
|
+
const plainKey = fromBiometricStorageKey(key);
|
|
178
|
+
if (event.newValue === null) {
|
|
179
|
+
if (getBrowserStorage(StorageScope.Secure)?.getItem(toSecureStorageKey(plainKey)) === null) {
|
|
180
|
+
ensureWebScopeKeyIndex(StorageScope.Secure).delete(plainKey);
|
|
181
|
+
}
|
|
182
|
+
cacheRawValue(StorageScope.Secure, plainKey, undefined);
|
|
183
|
+
} else {
|
|
184
|
+
ensureWebScopeKeyIndex(StorageScope.Secure).add(plainKey);
|
|
185
|
+
cacheRawValue(StorageScope.Secure, plainKey, event.newValue);
|
|
186
|
+
}
|
|
187
|
+
notifyKeyListeners(getScopedListeners(StorageScope.Secure), plainKey);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (event.newValue === null) {
|
|
191
|
+
ensureWebScopeKeyIndex(StorageScope.Disk).delete(key);
|
|
192
|
+
cacheRawValue(StorageScope.Disk, key, undefined);
|
|
193
|
+
} else {
|
|
194
|
+
ensureWebScopeKeyIndex(StorageScope.Disk).add(key);
|
|
195
|
+
cacheRawValue(StorageScope.Disk, key, event.newValue);
|
|
196
|
+
}
|
|
197
|
+
notifyKeyListeners(getScopedListeners(StorageScope.Disk), key);
|
|
198
|
+
}
|
|
199
|
+
function ensureWebStorageEventSubscription() {
|
|
200
|
+
if (hasWebStorageEventSubscription) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (typeof window === "undefined" || typeof window.addEventListener !== "function") {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
window.addEventListener("storage", handleWebStorageEvent);
|
|
207
|
+
hasWebStorageEventSubscription = true;
|
|
208
|
+
}
|
|
89
209
|
function getScopedListeners(scope) {
|
|
90
210
|
return webScopeListeners.get(scope);
|
|
91
211
|
}
|
|
@@ -146,32 +266,46 @@ function flushSecureWrites() {
|
|
|
146
266
|
}
|
|
147
267
|
const writes = Array.from(pendingSecureWrites.values());
|
|
148
268
|
pendingSecureWrites.clear();
|
|
149
|
-
const
|
|
150
|
-
const valuesToSet = [];
|
|
269
|
+
const groupedSetWrites = new Map();
|
|
151
270
|
const keysToRemove = [];
|
|
152
271
|
writes.forEach(({
|
|
153
272
|
key,
|
|
154
|
-
value
|
|
273
|
+
value,
|
|
274
|
+
accessControl
|
|
155
275
|
}) => {
|
|
156
276
|
if (value === undefined) {
|
|
157
277
|
keysToRemove.push(key);
|
|
158
278
|
} else {
|
|
159
|
-
|
|
160
|
-
|
|
279
|
+
const resolvedAccessControl = accessControl ?? AccessControl.WhenUnlocked;
|
|
280
|
+
const existingGroup = groupedSetWrites.get(resolvedAccessControl);
|
|
281
|
+
const group = existingGroup ?? {
|
|
282
|
+
keys: [],
|
|
283
|
+
values: []
|
|
284
|
+
};
|
|
285
|
+
group.keys.push(key);
|
|
286
|
+
group.values.push(value);
|
|
287
|
+
if (!existingGroup) {
|
|
288
|
+
groupedSetWrites.set(resolvedAccessControl, group);
|
|
289
|
+
}
|
|
161
290
|
}
|
|
162
291
|
});
|
|
163
|
-
|
|
164
|
-
WebStorage.
|
|
165
|
-
|
|
292
|
+
groupedSetWrites.forEach((group, accessControl) => {
|
|
293
|
+
WebStorage.setSecureAccessControl(accessControl);
|
|
294
|
+
WebStorage.setBatch(group.keys, group.values, StorageScope.Secure);
|
|
295
|
+
});
|
|
166
296
|
if (keysToRemove.length > 0) {
|
|
167
297
|
WebStorage.removeBatch(keysToRemove, StorageScope.Secure);
|
|
168
298
|
}
|
|
169
299
|
}
|
|
170
|
-
function scheduleSecureWrite(key, value) {
|
|
171
|
-
|
|
300
|
+
function scheduleSecureWrite(key, value, accessControl) {
|
|
301
|
+
const pendingWrite = {
|
|
172
302
|
key,
|
|
173
303
|
value
|
|
174
|
-
}
|
|
304
|
+
};
|
|
305
|
+
if (accessControl !== undefined) {
|
|
306
|
+
pendingWrite.accessControl = accessControl;
|
|
307
|
+
}
|
|
308
|
+
pendingSecureWrites.set(key, pendingWrite);
|
|
175
309
|
if (secureFlushScheduled) {
|
|
176
310
|
return;
|
|
177
311
|
}
|
|
@@ -322,6 +456,12 @@ const WebStorage = {
|
|
|
322
456
|
}
|
|
323
457
|
return Array.from(ensureWebScopeKeyIndex(scope));
|
|
324
458
|
},
|
|
459
|
+
getKeysByPrefix: (prefix, scope) => {
|
|
460
|
+
if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
|
|
461
|
+
return [];
|
|
462
|
+
}
|
|
463
|
+
return Array.from(ensureWebScopeKeyIndex(scope)).filter(key => key.startsWith(prefix));
|
|
464
|
+
},
|
|
325
465
|
size: scope => {
|
|
326
466
|
if (scope === StorageScope.Disk || scope === StorageScope.Secure) {
|
|
327
467
|
return ensureWebScopeKeyIndex(scope).size;
|
|
@@ -332,19 +472,22 @@ const WebStorage = {
|
|
|
332
472
|
setSecureWritesAsync: _enabled => {},
|
|
333
473
|
setKeychainAccessGroup: () => {},
|
|
334
474
|
setSecureBiometric: (key, value) => {
|
|
475
|
+
WebStorage.setSecureBiometricWithLevel(key, value, BiometricLevel.BiometryOnly);
|
|
476
|
+
},
|
|
477
|
+
setSecureBiometricWithLevel: (key, value, _level) => {
|
|
335
478
|
if (typeof __DEV__ !== "undefined" && __DEV__ && !hasWarnedAboutWebBiometricFallback) {
|
|
336
479
|
hasWarnedAboutWebBiometricFallback = true;
|
|
337
480
|
console.warn("[NitroStorage] Biometric storage is not supported on web. Using localStorage.");
|
|
338
481
|
}
|
|
339
|
-
|
|
482
|
+
getBrowserStorage(StorageScope.Secure)?.setItem(toBiometricStorageKey(key), value);
|
|
340
483
|
ensureWebScopeKeyIndex(StorageScope.Secure).add(key);
|
|
341
484
|
notifyKeyListeners(getScopedListeners(StorageScope.Secure), key);
|
|
342
485
|
},
|
|
343
486
|
getSecureBiometric: key => {
|
|
344
|
-
return
|
|
487
|
+
return getBrowserStorage(StorageScope.Secure)?.getItem(toBiometricStorageKey(key)) ?? undefined;
|
|
345
488
|
},
|
|
346
489
|
deleteSecureBiometric: key => {
|
|
347
|
-
const storage =
|
|
490
|
+
const storage = getBrowserStorage(StorageScope.Secure);
|
|
348
491
|
storage?.removeItem(toBiometricStorageKey(key));
|
|
349
492
|
if (storage?.getItem(toSecureStorageKey(key)) === null) {
|
|
350
493
|
ensureWebScopeKeyIndex(StorageScope.Secure).delete(key);
|
|
@@ -352,10 +495,10 @@ const WebStorage = {
|
|
|
352
495
|
notifyKeyListeners(getScopedListeners(StorageScope.Secure), key);
|
|
353
496
|
},
|
|
354
497
|
hasSecureBiometric: key => {
|
|
355
|
-
return
|
|
498
|
+
return getBrowserStorage(StorageScope.Secure)?.getItem(toBiometricStorageKey(key)) !== null;
|
|
356
499
|
},
|
|
357
500
|
clearSecureBiometric: () => {
|
|
358
|
-
const storage =
|
|
501
|
+
const storage = getBrowserStorage(StorageScope.Secure);
|
|
359
502
|
if (!storage) return;
|
|
360
503
|
const keysToNotify = [];
|
|
361
504
|
const toRemove = [];
|
|
@@ -429,82 +572,167 @@ function writeMigrationVersion(scope, version) {
|
|
|
429
572
|
}
|
|
430
573
|
export const storage = {
|
|
431
574
|
clear: scope => {
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
575
|
+
measureOperation("storage:clear", scope, () => {
|
|
576
|
+
if (scope === StorageScope.Memory) {
|
|
577
|
+
memoryStore.clear();
|
|
578
|
+
notifyAllListeners(memoryListeners);
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
if (scope === StorageScope.Secure) {
|
|
582
|
+
flushSecureWrites();
|
|
583
|
+
pendingSecureWrites.clear();
|
|
584
|
+
}
|
|
585
|
+
clearScopeRawCache(scope);
|
|
586
|
+
WebStorage.clear(scope);
|
|
587
|
+
});
|
|
443
588
|
},
|
|
444
589
|
clearAll: () => {
|
|
445
|
-
storage
|
|
446
|
-
|
|
447
|
-
|
|
590
|
+
measureOperation("storage:clearAll", StorageScope.Memory, () => {
|
|
591
|
+
storage.clear(StorageScope.Memory);
|
|
592
|
+
storage.clear(StorageScope.Disk);
|
|
593
|
+
storage.clear(StorageScope.Secure);
|
|
594
|
+
}, 3);
|
|
448
595
|
},
|
|
449
596
|
clearNamespace: (namespace, scope) => {
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
597
|
+
measureOperation("storage:clearNamespace", scope, () => {
|
|
598
|
+
assertValidScope(scope);
|
|
599
|
+
if (scope === StorageScope.Memory) {
|
|
600
|
+
for (const key of memoryStore.keys()) {
|
|
601
|
+
if (isNamespaced(key, namespace)) {
|
|
602
|
+
memoryStore.delete(key);
|
|
603
|
+
}
|
|
455
604
|
}
|
|
605
|
+
notifyAllListeners(memoryListeners);
|
|
606
|
+
return;
|
|
456
607
|
}
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
}
|
|
464
|
-
clearScopeRawCache(scope);
|
|
465
|
-
WebStorage.removeByPrefix(keyPrefix, scope);
|
|
608
|
+
const keyPrefix = prefixKey(namespace, "");
|
|
609
|
+
if (scope === StorageScope.Secure) {
|
|
610
|
+
flushSecureWrites();
|
|
611
|
+
}
|
|
612
|
+
clearScopeRawCache(scope);
|
|
613
|
+
WebStorage.removeByPrefix(keyPrefix, scope);
|
|
614
|
+
});
|
|
466
615
|
},
|
|
467
616
|
clearBiometric: () => {
|
|
468
|
-
|
|
617
|
+
measureOperation("storage:clearBiometric", StorageScope.Secure, () => {
|
|
618
|
+
WebStorage.clearSecureBiometric();
|
|
619
|
+
});
|
|
469
620
|
},
|
|
470
621
|
has: (key, scope) => {
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
622
|
+
return measureOperation("storage:has", scope, () => {
|
|
623
|
+
assertValidScope(scope);
|
|
624
|
+
if (scope === StorageScope.Memory) return memoryStore.has(key);
|
|
625
|
+
return WebStorage.has(key, scope);
|
|
626
|
+
});
|
|
474
627
|
},
|
|
475
628
|
getAllKeys: scope => {
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
629
|
+
return measureOperation("storage:getAllKeys", scope, () => {
|
|
630
|
+
assertValidScope(scope);
|
|
631
|
+
if (scope === StorageScope.Memory) return Array.from(memoryStore.keys());
|
|
632
|
+
return WebStorage.getAllKeys(scope);
|
|
633
|
+
});
|
|
634
|
+
},
|
|
635
|
+
getKeysByPrefix: (prefix, scope) => {
|
|
636
|
+
return measureOperation("storage:getKeysByPrefix", scope, () => {
|
|
637
|
+
assertValidScope(scope);
|
|
638
|
+
if (scope === StorageScope.Memory) {
|
|
639
|
+
return Array.from(memoryStore.keys()).filter(key => key.startsWith(prefix));
|
|
640
|
+
}
|
|
641
|
+
return WebStorage.getKeysByPrefix(prefix, scope);
|
|
642
|
+
});
|
|
643
|
+
},
|
|
644
|
+
getByPrefix: (prefix, scope) => {
|
|
645
|
+
return measureOperation("storage:getByPrefix", scope, () => {
|
|
646
|
+
const result = {};
|
|
647
|
+
const keys = storage.getKeysByPrefix(prefix, scope);
|
|
648
|
+
if (keys.length === 0) {
|
|
649
|
+
return result;
|
|
650
|
+
}
|
|
651
|
+
if (scope === StorageScope.Memory) {
|
|
652
|
+
keys.forEach(key => {
|
|
653
|
+
const value = memoryStore.get(key);
|
|
654
|
+
if (typeof value === "string") {
|
|
655
|
+
result[key] = value;
|
|
656
|
+
}
|
|
657
|
+
});
|
|
658
|
+
return result;
|
|
659
|
+
}
|
|
660
|
+
const values = WebStorage.getBatch(keys, scope);
|
|
661
|
+
keys.forEach((key, index) => {
|
|
662
|
+
const value = values[index];
|
|
663
|
+
if (value !== undefined) {
|
|
664
|
+
result[key] = value;
|
|
665
|
+
}
|
|
666
|
+
});
|
|
667
|
+
return result;
|
|
668
|
+
});
|
|
479
669
|
},
|
|
480
670
|
getAll: scope => {
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
671
|
+
return measureOperation("storage:getAll", scope, () => {
|
|
672
|
+
assertValidScope(scope);
|
|
673
|
+
const result = {};
|
|
674
|
+
if (scope === StorageScope.Memory) {
|
|
675
|
+
memoryStore.forEach((value, key) => {
|
|
676
|
+
if (typeof value === "string") result[key] = value;
|
|
677
|
+
});
|
|
678
|
+
return result;
|
|
679
|
+
}
|
|
680
|
+
const keys = WebStorage.getAllKeys(scope);
|
|
681
|
+
keys.forEach(key => {
|
|
682
|
+
const val = WebStorage.get(key, scope);
|
|
683
|
+
if (val !== undefined) result[key] = val;
|
|
486
684
|
});
|
|
487
685
|
return result;
|
|
488
|
-
}
|
|
489
|
-
const keys = WebStorage.getAllKeys(scope);
|
|
490
|
-
keys.forEach(key => {
|
|
491
|
-
const val = WebStorage.get(key, scope);
|
|
492
|
-
if (val !== undefined) result[key] = val;
|
|
493
686
|
});
|
|
494
|
-
return result;
|
|
495
687
|
},
|
|
496
688
|
size: scope => {
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
689
|
+
return measureOperation("storage:size", scope, () => {
|
|
690
|
+
assertValidScope(scope);
|
|
691
|
+
if (scope === StorageScope.Memory) return memoryStore.size;
|
|
692
|
+
return WebStorage.size(scope);
|
|
693
|
+
});
|
|
694
|
+
},
|
|
695
|
+
setAccessControl: _level => {
|
|
696
|
+
recordMetric("storage:setAccessControl", StorageScope.Secure, 0);
|
|
697
|
+
},
|
|
698
|
+
setSecureWritesAsync: _enabled => {
|
|
699
|
+
recordMetric("storage:setSecureWritesAsync", StorageScope.Secure, 0);
|
|
500
700
|
},
|
|
501
|
-
setAccessControl: _level => {},
|
|
502
|
-
setSecureWritesAsync: _enabled => {},
|
|
503
701
|
flushSecureWrites: () => {
|
|
504
|
-
flushSecureWrites()
|
|
702
|
+
measureOperation("storage:flushSecureWrites", StorageScope.Secure, () => {
|
|
703
|
+
flushSecureWrites();
|
|
704
|
+
});
|
|
705
|
+
},
|
|
706
|
+
setKeychainAccessGroup: _group => {
|
|
707
|
+
recordMetric("storage:setKeychainAccessGroup", StorageScope.Secure, 0);
|
|
708
|
+
},
|
|
709
|
+
setMetricsObserver: observer => {
|
|
710
|
+
metricsObserver = observer;
|
|
711
|
+
},
|
|
712
|
+
getMetricsSnapshot: () => {
|
|
713
|
+
const snapshot = {};
|
|
714
|
+
metricsCounters.forEach((value, key) => {
|
|
715
|
+
snapshot[key] = {
|
|
716
|
+
count: value.count,
|
|
717
|
+
totalDurationMs: value.totalDurationMs,
|
|
718
|
+
avgDurationMs: value.count === 0 ? 0 : value.totalDurationMs / value.count,
|
|
719
|
+
maxDurationMs: value.maxDurationMs
|
|
720
|
+
};
|
|
721
|
+
});
|
|
722
|
+
return snapshot;
|
|
505
723
|
},
|
|
506
|
-
|
|
724
|
+
resetMetrics: () => {
|
|
725
|
+
metricsCounters.clear();
|
|
726
|
+
}
|
|
507
727
|
};
|
|
728
|
+
export function setWebSecureStorageBackend(backend) {
|
|
729
|
+
webSecureStorageBackend = backend ?? createLocalStorageWebSecureBackend();
|
|
730
|
+
hydratedWebScopeKeyIndex.delete(StorageScope.Secure);
|
|
731
|
+
clearScopeRawCache(StorageScope.Secure);
|
|
732
|
+
}
|
|
733
|
+
export function getWebSecureStorageBackend() {
|
|
734
|
+
return webSecureStorageBackend;
|
|
735
|
+
}
|
|
508
736
|
function canUseRawBatchPath(item) {
|
|
509
737
|
return item._hasExpiration === false && item._hasValidation === false && item._isBiometric !== true && item._secureAccessControl === undefined;
|
|
510
738
|
}
|
|
@@ -522,7 +750,8 @@ export function createStorageItem(config) {
|
|
|
522
750
|
const serialize = config.serialize ?? defaultSerialize;
|
|
523
751
|
const deserialize = config.deserialize ?? defaultDeserialize;
|
|
524
752
|
const isMemory = config.scope === StorageScope.Memory;
|
|
525
|
-
const
|
|
753
|
+
const resolvedBiometricLevel = config.scope === StorageScope.Secure ? config.biometricLevel ?? (config.biometric === true ? BiometricLevel.BiometryOnly : BiometricLevel.None) : BiometricLevel.None;
|
|
754
|
+
const isBiometric = resolvedBiometricLevel !== BiometricLevel.None;
|
|
526
755
|
const secureAccessControl = config.accessControl;
|
|
527
756
|
const validate = config.validate;
|
|
528
757
|
const onValidationError = config.onValidationError;
|
|
@@ -531,7 +760,7 @@ export function createStorageItem(config) {
|
|
|
531
760
|
const expirationTtlMs = expiration?.ttlMs;
|
|
532
761
|
const memoryExpiration = expiration && isMemory ? new Map() : null;
|
|
533
762
|
const readCache = !isMemory && config.readCache === true;
|
|
534
|
-
const coalesceSecureWrites = config.scope === StorageScope.Secure && config.coalesceSecureWrites === true && !isBiometric
|
|
763
|
+
const coalesceSecureWrites = config.scope === StorageScope.Secure && config.coalesceSecureWrites === true && !isBiometric;
|
|
535
764
|
const defaultValue = config.defaultValue;
|
|
536
765
|
const nonMemoryScope = config.scope === StorageScope.Disk ? StorageScope.Disk : config.scope === StorageScope.Secure ? StorageScope.Secure : null;
|
|
537
766
|
if (expiration && expiration.ttlMs <= 0) {
|
|
@@ -561,6 +790,7 @@ export function createStorageItem(config) {
|
|
|
561
790
|
unsubscribe = addKeyListener(memoryListeners, storageKey, listener);
|
|
562
791
|
return;
|
|
563
792
|
}
|
|
793
|
+
ensureWebStorageEventSubscription();
|
|
564
794
|
unsubscribe = addKeyListener(getScopedListeners(nonMemoryScope), storageKey, listener);
|
|
565
795
|
};
|
|
566
796
|
const readStoredRaw = () => {
|
|
@@ -594,12 +824,12 @@ export function createStorageItem(config) {
|
|
|
594
824
|
};
|
|
595
825
|
const writeStoredRaw = rawValue => {
|
|
596
826
|
if (isBiometric) {
|
|
597
|
-
WebStorage.
|
|
827
|
+
WebStorage.setSecureBiometricWithLevel(storageKey, rawValue, resolvedBiometricLevel);
|
|
598
828
|
return;
|
|
599
829
|
}
|
|
600
830
|
cacheRawValue(nonMemoryScope, storageKey, rawValue);
|
|
601
831
|
if (coalesceSecureWrites) {
|
|
602
|
-
scheduleSecureWrite(storageKey, rawValue);
|
|
832
|
+
scheduleSecureWrite(storageKey, rawValue, secureAccessControl ?? AccessControl.WhenUnlocked);
|
|
603
833
|
return;
|
|
604
834
|
}
|
|
605
835
|
if (nonMemoryScope === StorageScope.Secure) {
|
|
@@ -614,7 +844,7 @@ export function createStorageItem(config) {
|
|
|
614
844
|
}
|
|
615
845
|
cacheRawValue(nonMemoryScope, storageKey, undefined);
|
|
616
846
|
if (coalesceSecureWrites) {
|
|
617
|
-
scheduleSecureWrite(storageKey, undefined);
|
|
847
|
+
scheduleSecureWrite(storageKey, undefined, secureAccessControl ?? AccessControl.WhenUnlocked);
|
|
618
848
|
return;
|
|
619
849
|
}
|
|
620
850
|
if (nonMemoryScope === StorageScope.Secure) {
|
|
@@ -662,7 +892,7 @@ export function createStorageItem(config) {
|
|
|
662
892
|
}
|
|
663
893
|
return resolved;
|
|
664
894
|
};
|
|
665
|
-
const
|
|
895
|
+
const getInternal = () => {
|
|
666
896
|
const raw = readStoredRaw();
|
|
667
897
|
if (!memoryExpiration && raw === lastRaw && hasLastValue) {
|
|
668
898
|
if (!expiration || lastExpiresAt === null) {
|
|
@@ -727,31 +957,52 @@ export function createStorageItem(config) {
|
|
|
727
957
|
hasLastValue = true;
|
|
728
958
|
return lastValue;
|
|
729
959
|
};
|
|
960
|
+
const getCurrentVersion = () => {
|
|
961
|
+
const raw = readStoredRaw();
|
|
962
|
+
return toVersionToken(raw);
|
|
963
|
+
};
|
|
964
|
+
const get = () => measureOperation("item:get", config.scope, () => getInternal());
|
|
965
|
+
const getWithVersion = () => measureOperation("item:getWithVersion", config.scope, () => ({
|
|
966
|
+
value: getInternal(),
|
|
967
|
+
version: getCurrentVersion()
|
|
968
|
+
}));
|
|
730
969
|
const set = valueOrFn => {
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
970
|
+
measureOperation("item:set", config.scope, () => {
|
|
971
|
+
const newValue = isUpdater(valueOrFn) ? valueOrFn(getInternal()) : valueOrFn;
|
|
972
|
+
invalidateParsedCache();
|
|
973
|
+
if (validate && !validate(newValue)) {
|
|
974
|
+
throw new Error(`Validation failed for key "${storageKey}" in scope "${StorageScope[config.scope]}".`);
|
|
975
|
+
}
|
|
976
|
+
writeValueWithoutValidation(newValue);
|
|
977
|
+
});
|
|
737
978
|
};
|
|
979
|
+
const setIfVersion = (version, valueOrFn) => measureOperation("item:setIfVersion", config.scope, () => {
|
|
980
|
+
const currentVersion = getCurrentVersion();
|
|
981
|
+
if (currentVersion !== version) {
|
|
982
|
+
return false;
|
|
983
|
+
}
|
|
984
|
+
set(valueOrFn);
|
|
985
|
+
return true;
|
|
986
|
+
});
|
|
738
987
|
const deleteItem = () => {
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
if (
|
|
742
|
-
memoryExpiration
|
|
988
|
+
measureOperation("item:delete", config.scope, () => {
|
|
989
|
+
invalidateParsedCache();
|
|
990
|
+
if (isMemory) {
|
|
991
|
+
if (memoryExpiration) {
|
|
992
|
+
memoryExpiration.delete(storageKey);
|
|
993
|
+
}
|
|
994
|
+
memoryStore.delete(storageKey);
|
|
995
|
+
notifyKeyListeners(memoryListeners, storageKey);
|
|
996
|
+
return;
|
|
743
997
|
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
return;
|
|
747
|
-
}
|
|
748
|
-
removeStoredRaw();
|
|
998
|
+
removeStoredRaw();
|
|
999
|
+
});
|
|
749
1000
|
};
|
|
750
|
-
const hasItem = () => {
|
|
1001
|
+
const hasItem = () => measureOperation("item:has", config.scope, () => {
|
|
751
1002
|
if (isMemory) return memoryStore.has(storageKey);
|
|
752
1003
|
if (isBiometric) return WebStorage.hasSecureBiometric(storageKey);
|
|
753
1004
|
return WebStorage.has(storageKey, config.scope);
|
|
754
|
-
};
|
|
1005
|
+
});
|
|
755
1006
|
const subscribe = callback => {
|
|
756
1007
|
ensureSubscription();
|
|
757
1008
|
listeners.add(callback);
|
|
@@ -765,7 +1016,9 @@ export function createStorageItem(config) {
|
|
|
765
1016
|
};
|
|
766
1017
|
const storageItem = {
|
|
767
1018
|
get,
|
|
1019
|
+
getWithVersion,
|
|
768
1020
|
set,
|
|
1021
|
+
setIfVersion,
|
|
769
1022
|
delete: deleteItem,
|
|
770
1023
|
has: hasItem,
|
|
771
1024
|
subscribe,
|
|
@@ -779,6 +1032,7 @@ export function createStorageItem(config) {
|
|
|
779
1032
|
_hasExpiration: expiration !== undefined,
|
|
780
1033
|
_readCacheEnabled: readCache,
|
|
781
1034
|
_isBiometric: isBiometric,
|
|
1035
|
+
_defaultValue: defaultValue,
|
|
782
1036
|
...(secureAccessControl !== undefined ? {
|
|
783
1037
|
_secureAccessControl: secureAccessControl
|
|
784
1038
|
} : {}),
|
|
@@ -789,135 +1043,140 @@ export function createStorageItem(config) {
|
|
|
789
1043
|
}
|
|
790
1044
|
export { useStorage, useStorageSelector, useSetStorage } from "./storage-hooks";
|
|
791
1045
|
export function getBatch(items, scope) {
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
const useRawBatchPath = items.every(item => scope === StorageScope.Secure ? canUseSecureRawBatchPath(item) : canUseRawBatchPath(item));
|
|
797
|
-
if (!useRawBatchPath) {
|
|
798
|
-
return items.map(item => item.get());
|
|
799
|
-
}
|
|
800
|
-
const useBatchCache = items.every(item => item._readCacheEnabled === true);
|
|
801
|
-
const rawValues = new Array(items.length);
|
|
802
|
-
const keysToFetch = [];
|
|
803
|
-
const keyIndexes = [];
|
|
804
|
-
items.forEach((item, index) => {
|
|
805
|
-
if (scope === StorageScope.Secure) {
|
|
806
|
-
if (hasPendingSecureWrite(item.key)) {
|
|
807
|
-
rawValues[index] = readPendingSecureWrite(item.key);
|
|
808
|
-
return;
|
|
809
|
-
}
|
|
1046
|
+
return measureOperation("batch:get", scope, () => {
|
|
1047
|
+
assertBatchScope(items, scope);
|
|
1048
|
+
if (scope === StorageScope.Memory) {
|
|
1049
|
+
return items.map(item => item.get());
|
|
810
1050
|
}
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
return;
|
|
815
|
-
}
|
|
1051
|
+
const useRawBatchPath = items.every(item => scope === StorageScope.Secure ? canUseSecureRawBatchPath(item) : canUseRawBatchPath(item));
|
|
1052
|
+
if (!useRawBatchPath) {
|
|
1053
|
+
return items.map(item => item.get());
|
|
816
1054
|
}
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
1055
|
+
const rawValues = new Array(items.length);
|
|
1056
|
+
const keysToFetch = [];
|
|
1057
|
+
const keyIndexes = [];
|
|
1058
|
+
items.forEach((item, index) => {
|
|
1059
|
+
if (scope === StorageScope.Secure) {
|
|
1060
|
+
if (hasPendingSecureWrite(item.key)) {
|
|
1061
|
+
rawValues[index] = readPendingSecureWrite(item.key);
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
if (item._readCacheEnabled === true) {
|
|
1066
|
+
if (hasCachedRawValue(scope, item.key)) {
|
|
1067
|
+
rawValues[index] = readCachedRawValue(scope, item.key);
|
|
1068
|
+
return;
|
|
1069
|
+
}
|
|
827
1070
|
}
|
|
828
|
-
|
|
829
|
-
|
|
1071
|
+
keysToFetch.push(item.key);
|
|
1072
|
+
keyIndexes.push(index);
|
|
830
1073
|
});
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
1074
|
+
if (keysToFetch.length > 0) {
|
|
1075
|
+
const fetchedValues = WebStorage.getBatch(keysToFetch, scope);
|
|
1076
|
+
fetchedValues.forEach((value, index) => {
|
|
1077
|
+
const key = keysToFetch[index];
|
|
1078
|
+
const targetIndex = keyIndexes[index];
|
|
1079
|
+
if (key === undefined || targetIndex === undefined) {
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
rawValues[targetIndex] = value;
|
|
1083
|
+
cacheRawValue(scope, key, value);
|
|
1084
|
+
});
|
|
836
1085
|
}
|
|
837
|
-
return
|
|
838
|
-
|
|
1086
|
+
return items.map((item, index) => {
|
|
1087
|
+
const raw = rawValues[index];
|
|
1088
|
+
if (raw === undefined) {
|
|
1089
|
+
return asInternal(item)._defaultValue;
|
|
1090
|
+
}
|
|
1091
|
+
return item.deserialize(raw);
|
|
1092
|
+
});
|
|
1093
|
+
}, items.length);
|
|
839
1094
|
}
|
|
840
1095
|
export function setBatch(items, scope) {
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
item,
|
|
845
|
-
value
|
|
846
|
-
}) => item.set(value));
|
|
847
|
-
return;
|
|
848
|
-
}
|
|
849
|
-
if (scope === StorageScope.Secure) {
|
|
850
|
-
const secureEntries = items.map(({
|
|
851
|
-
item,
|
|
852
|
-
value
|
|
853
|
-
}) => ({
|
|
854
|
-
item,
|
|
855
|
-
value,
|
|
856
|
-
internal: asInternal(item)
|
|
857
|
-
}));
|
|
858
|
-
const canUseSecureBatchPath = secureEntries.every(({
|
|
859
|
-
internal
|
|
860
|
-
}) => canUseSecureRawBatchPath(internal));
|
|
861
|
-
if (!canUseSecureBatchPath) {
|
|
1096
|
+
measureOperation("batch:set", scope, () => {
|
|
1097
|
+
assertBatchScope(items.map(batchEntry => batchEntry.item), scope);
|
|
1098
|
+
if (scope === StorageScope.Memory) {
|
|
862
1099
|
items.forEach(({
|
|
863
1100
|
item,
|
|
864
1101
|
value
|
|
865
1102
|
}) => item.set(value));
|
|
866
1103
|
return;
|
|
867
1104
|
}
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
const
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
1105
|
+
if (scope === StorageScope.Secure) {
|
|
1106
|
+
const secureEntries = items.map(({
|
|
1107
|
+
item,
|
|
1108
|
+
value
|
|
1109
|
+
}) => ({
|
|
1110
|
+
item,
|
|
1111
|
+
value,
|
|
1112
|
+
internal: asInternal(item)
|
|
1113
|
+
}));
|
|
1114
|
+
const canUseSecureBatchPath = secureEntries.every(({
|
|
1115
|
+
internal
|
|
1116
|
+
}) => canUseSecureRawBatchPath(internal));
|
|
1117
|
+
if (!canUseSecureBatchPath) {
|
|
1118
|
+
items.forEach(({
|
|
1119
|
+
item,
|
|
1120
|
+
value
|
|
1121
|
+
}) => item.set(value));
|
|
1122
|
+
return;
|
|
885
1123
|
}
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
1124
|
+
flushSecureWrites();
|
|
1125
|
+
const groupedByAccessControl = new Map();
|
|
1126
|
+
secureEntries.forEach(({
|
|
1127
|
+
item,
|
|
1128
|
+
value,
|
|
1129
|
+
internal
|
|
1130
|
+
}) => {
|
|
1131
|
+
const accessControl = internal._secureAccessControl ?? AccessControl.WhenUnlocked;
|
|
1132
|
+
const existingGroup = groupedByAccessControl.get(accessControl);
|
|
1133
|
+
const group = existingGroup ?? {
|
|
1134
|
+
keys: [],
|
|
1135
|
+
values: []
|
|
1136
|
+
};
|
|
1137
|
+
group.keys.push(item.key);
|
|
1138
|
+
group.values.push(item.serialize(value));
|
|
1139
|
+
if (!existingGroup) {
|
|
1140
|
+
groupedByAccessControl.set(accessControl, group);
|
|
1141
|
+
}
|
|
1142
|
+
});
|
|
1143
|
+
groupedByAccessControl.forEach((group, accessControl) => {
|
|
1144
|
+
WebStorage.setSecureAccessControl(accessControl);
|
|
1145
|
+
WebStorage.setBatch(group.keys, group.values, scope);
|
|
1146
|
+
group.keys.forEach((key, index) => cacheRawValue(scope, key, group.values[index]));
|
|
1147
|
+
});
|
|
1148
|
+
return;
|
|
1149
|
+
}
|
|
1150
|
+
const useRawBatchPath = items.every(({
|
|
1151
|
+
item
|
|
1152
|
+
}) => canUseRawBatchPath(asInternal(item)));
|
|
1153
|
+
if (!useRawBatchPath) {
|
|
1154
|
+
items.forEach(({
|
|
1155
|
+
item,
|
|
1156
|
+
value
|
|
1157
|
+
}) => item.set(value));
|
|
1158
|
+
return;
|
|
1159
|
+
}
|
|
1160
|
+
const keys = items.map(entry => entry.item.key);
|
|
1161
|
+
const values = items.map(entry => entry.item.serialize(entry.value));
|
|
1162
|
+
WebStorage.setBatch(keys, values, scope);
|
|
1163
|
+
keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
|
|
1164
|
+
}, items.length);
|
|
908
1165
|
}
|
|
909
1166
|
export function removeBatch(items, scope) {
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
1167
|
+
measureOperation("batch:remove", scope, () => {
|
|
1168
|
+
assertBatchScope(items, scope);
|
|
1169
|
+
if (scope === StorageScope.Memory) {
|
|
1170
|
+
items.forEach(item => item.delete());
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
const keys = items.map(item => item.key);
|
|
1174
|
+
if (scope === StorageScope.Secure) {
|
|
1175
|
+
flushSecureWrites();
|
|
1176
|
+
}
|
|
1177
|
+
WebStorage.removeBatch(keys, scope);
|
|
1178
|
+
keys.forEach(key => cacheRawValue(scope, key, undefined));
|
|
1179
|
+
}, items.length);
|
|
921
1180
|
}
|
|
922
1181
|
export function registerMigration(version, migration) {
|
|
923
1182
|
if (!Number.isInteger(version) || version <= 0) {
|
|
@@ -929,77 +1188,107 @@ export function registerMigration(version, migration) {
|
|
|
929
1188
|
registeredMigrations.set(version, migration);
|
|
930
1189
|
}
|
|
931
1190
|
export function migrateToLatest(scope = StorageScope.Disk) {
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
1191
|
+
return measureOperation("migration:run", scope, () => {
|
|
1192
|
+
assertValidScope(scope);
|
|
1193
|
+
const currentVersion = readMigrationVersion(scope);
|
|
1194
|
+
const versions = Array.from(registeredMigrations.keys()).filter(version => version > currentVersion).sort((a, b) => a - b);
|
|
1195
|
+
let appliedVersion = currentVersion;
|
|
1196
|
+
const context = {
|
|
1197
|
+
scope,
|
|
1198
|
+
getRaw: key => getRawValue(key, scope),
|
|
1199
|
+
setRaw: (key, value) => setRawValue(key, value, scope),
|
|
1200
|
+
removeRaw: key => removeRawValue(key, scope)
|
|
1201
|
+
};
|
|
1202
|
+
versions.forEach(version => {
|
|
1203
|
+
const migration = registeredMigrations.get(version);
|
|
1204
|
+
if (!migration) {
|
|
1205
|
+
return;
|
|
1206
|
+
}
|
|
1207
|
+
migration(context);
|
|
1208
|
+
writeMigrationVersion(scope, version);
|
|
1209
|
+
appliedVersion = version;
|
|
1210
|
+
});
|
|
1211
|
+
return appliedVersion;
|
|
950
1212
|
});
|
|
951
|
-
return appliedVersion;
|
|
952
1213
|
}
|
|
953
1214
|
export function runTransaction(scope, transaction) {
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
const rollback = new Map();
|
|
959
|
-
const rememberRollback = key => {
|
|
960
|
-
if (rollback.has(key)) {
|
|
961
|
-
return;
|
|
962
|
-
}
|
|
963
|
-
rollback.set(key, getRawValue(key, scope));
|
|
964
|
-
};
|
|
965
|
-
const tx = {
|
|
966
|
-
scope,
|
|
967
|
-
getRaw: key => getRawValue(key, scope),
|
|
968
|
-
setRaw: (key, value) => {
|
|
969
|
-
rememberRollback(key);
|
|
970
|
-
setRawValue(key, value, scope);
|
|
971
|
-
},
|
|
972
|
-
removeRaw: key => {
|
|
973
|
-
rememberRollback(key);
|
|
974
|
-
removeRawValue(key, scope);
|
|
975
|
-
},
|
|
976
|
-
getItem: item => {
|
|
977
|
-
assertBatchScope([item], scope);
|
|
978
|
-
return item.get();
|
|
979
|
-
},
|
|
980
|
-
setItem: (item, value) => {
|
|
981
|
-
assertBatchScope([item], scope);
|
|
982
|
-
rememberRollback(item.key);
|
|
983
|
-
item.set(value);
|
|
984
|
-
},
|
|
985
|
-
removeItem: item => {
|
|
986
|
-
assertBatchScope([item], scope);
|
|
987
|
-
rememberRollback(item.key);
|
|
988
|
-
item.delete();
|
|
1215
|
+
return measureOperation("transaction:run", scope, () => {
|
|
1216
|
+
assertValidScope(scope);
|
|
1217
|
+
if (scope === StorageScope.Secure) {
|
|
1218
|
+
flushSecureWrites();
|
|
989
1219
|
}
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
1220
|
+
const rollback = new Map();
|
|
1221
|
+
const rememberRollback = key => {
|
|
1222
|
+
if (rollback.has(key)) {
|
|
1223
|
+
return;
|
|
1224
|
+
}
|
|
1225
|
+
rollback.set(key, getRawValue(key, scope));
|
|
1226
|
+
};
|
|
1227
|
+
const tx = {
|
|
1228
|
+
scope,
|
|
1229
|
+
getRaw: key => getRawValue(key, scope),
|
|
1230
|
+
setRaw: (key, value) => {
|
|
1231
|
+
rememberRollback(key);
|
|
1232
|
+
setRawValue(key, value, scope);
|
|
1233
|
+
},
|
|
1234
|
+
removeRaw: key => {
|
|
1235
|
+
rememberRollback(key);
|
|
996
1236
|
removeRawValue(key, scope);
|
|
1237
|
+
},
|
|
1238
|
+
getItem: item => {
|
|
1239
|
+
assertBatchScope([item], scope);
|
|
1240
|
+
return item.get();
|
|
1241
|
+
},
|
|
1242
|
+
setItem: (item, value) => {
|
|
1243
|
+
assertBatchScope([item], scope);
|
|
1244
|
+
rememberRollback(item.key);
|
|
1245
|
+
item.set(value);
|
|
1246
|
+
},
|
|
1247
|
+
removeItem: item => {
|
|
1248
|
+
assertBatchScope([item], scope);
|
|
1249
|
+
rememberRollback(item.key);
|
|
1250
|
+
item.delete();
|
|
1251
|
+
}
|
|
1252
|
+
};
|
|
1253
|
+
try {
|
|
1254
|
+
return transaction(tx);
|
|
1255
|
+
} catch (error) {
|
|
1256
|
+
const rollbackEntries = Array.from(rollback.entries()).reverse();
|
|
1257
|
+
if (scope === StorageScope.Memory) {
|
|
1258
|
+
rollbackEntries.forEach(([key, previousValue]) => {
|
|
1259
|
+
if (previousValue === undefined) {
|
|
1260
|
+
removeRawValue(key, scope);
|
|
1261
|
+
} else {
|
|
1262
|
+
setRawValue(key, previousValue, scope);
|
|
1263
|
+
}
|
|
1264
|
+
});
|
|
997
1265
|
} else {
|
|
998
|
-
|
|
1266
|
+
const keysToSet = [];
|
|
1267
|
+
const valuesToSet = [];
|
|
1268
|
+
const keysToRemove = [];
|
|
1269
|
+
rollbackEntries.forEach(([key, previousValue]) => {
|
|
1270
|
+
if (previousValue === undefined) {
|
|
1271
|
+
keysToRemove.push(key);
|
|
1272
|
+
} else {
|
|
1273
|
+
keysToSet.push(key);
|
|
1274
|
+
valuesToSet.push(previousValue);
|
|
1275
|
+
}
|
|
1276
|
+
});
|
|
1277
|
+
if (scope === StorageScope.Secure) {
|
|
1278
|
+
flushSecureWrites();
|
|
1279
|
+
}
|
|
1280
|
+
if (keysToSet.length > 0) {
|
|
1281
|
+
WebStorage.setBatch(keysToSet, valuesToSet, scope);
|
|
1282
|
+
keysToSet.forEach((key, index) => cacheRawValue(scope, key, valuesToSet[index]));
|
|
1283
|
+
}
|
|
1284
|
+
if (keysToRemove.length > 0) {
|
|
1285
|
+
WebStorage.removeBatch(keysToRemove, scope);
|
|
1286
|
+
keysToRemove.forEach(key => cacheRawValue(scope, key, undefined));
|
|
1287
|
+
}
|
|
999
1288
|
}
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
}
|
|
1289
|
+
throw error;
|
|
1290
|
+
}
|
|
1291
|
+
});
|
|
1003
1292
|
}
|
|
1004
1293
|
export function createSecureAuthStorage(config, options) {
|
|
1005
1294
|
const ns = options?.namespace ?? "auth";
|
|
@@ -1017,6 +1306,9 @@ export function createSecureAuthStorage(config, options) {
|
|
|
1017
1306
|
...(itemConfig.biometric !== undefined ? {
|
|
1018
1307
|
biometric: itemConfig.biometric
|
|
1019
1308
|
} : {}),
|
|
1309
|
+
...(itemConfig.biometricLevel !== undefined ? {
|
|
1310
|
+
biometricLevel: itemConfig.biometricLevel
|
|
1311
|
+
} : {}),
|
|
1020
1312
|
...(itemConfig.accessControl !== undefined ? {
|
|
1021
1313
|
accessControl: itemConfig.accessControl
|
|
1022
1314
|
} : {}),
|