react-native-nitro-storage 0.3.2 → 0.4.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/README.md +192 -30
- package/android/src/main/cpp/AndroidStorageAdapterCpp.cpp +22 -2
- package/android/src/main/cpp/AndroidStorageAdapterCpp.hpp +3 -0
- package/android/src/main/cpp/cpp-adapter.cpp +3 -1
- 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 +522 -275
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +614 -270
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/indexeddb-backend.js +130 -0
- package/lib/commonjs/indexeddb-backend.js.map +1 -0
- package/lib/commonjs/internal.js +25 -0
- package/lib/commonjs/internal.js.map +1 -1
- package/lib/module/index.js +516 -277
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +608 -272
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/indexeddb-backend.js +126 -0
- package/lib/module/indexeddb-backend.js.map +1 -0
- 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 +40 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/index.web.d.ts +42 -1
- package/lib/typescript/index.web.d.ts.map +1 -1
- package/lib/typescript/indexeddb-backend.d.ts +29 -0
- package/lib/typescript/indexeddb-backend.d.ts.map +1 -0
- package/lib/typescript/internal.d.ts +1 -0
- package/lib/typescript/internal.d.ts.map +1 -1
- package/nitrogen/generated/android/NitroStorageOnLoad.cpp +22 -17
- package/nitrogen/generated/android/NitroStorageOnLoad.hpp +13 -4
- package/nitrogen/generated/shared/c++/HybridStorageSpec.cpp +2 -0
- package/nitrogen/generated/shared/c++/HybridStorageSpec.hpp +2 -0
- package/package.json +7 -3
- package/src/Storage.nitro.ts +2 -0
- package/src/index.ts +671 -296
- package/src/index.web.ts +776 -288
- package/src/indexeddb-backend.ts +143 -0
- 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,183 @@ 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);
|
|
505
708
|
},
|
|
506
|
-
|
|
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;
|
|
723
|
+
},
|
|
724
|
+
resetMetrics: () => {
|
|
725
|
+
metricsCounters.clear();
|
|
726
|
+
},
|
|
727
|
+
import: (data, scope) => {
|
|
728
|
+
measureOperation("storage:import", scope, () => {
|
|
729
|
+
assertValidScope(scope);
|
|
730
|
+
const keys = Object.keys(data);
|
|
731
|
+
if (keys.length === 0) return;
|
|
732
|
+
const values = keys.map(k => data[k]);
|
|
733
|
+
if (scope === StorageScope.Memory) {
|
|
734
|
+
keys.forEach((key, index) => {
|
|
735
|
+
memoryStore.set(key, values[index]);
|
|
736
|
+
});
|
|
737
|
+
keys.forEach(key => notifyKeyListeners(memoryListeners, key));
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
WebStorage.setBatch(keys, values, scope);
|
|
741
|
+
}, Object.keys(data).length);
|
|
742
|
+
}
|
|
507
743
|
};
|
|
744
|
+
export function setWebSecureStorageBackend(backend) {
|
|
745
|
+
webSecureStorageBackend = backend ?? createLocalStorageWebSecureBackend();
|
|
746
|
+
hydratedWebScopeKeyIndex.delete(StorageScope.Secure);
|
|
747
|
+
clearScopeRawCache(StorageScope.Secure);
|
|
748
|
+
}
|
|
749
|
+
export function getWebSecureStorageBackend() {
|
|
750
|
+
return webSecureStorageBackend;
|
|
751
|
+
}
|
|
508
752
|
function canUseRawBatchPath(item) {
|
|
509
753
|
return item._hasExpiration === false && item._hasValidation === false && item._isBiometric !== true && item._secureAccessControl === undefined;
|
|
510
754
|
}
|
|
@@ -522,7 +766,8 @@ export function createStorageItem(config) {
|
|
|
522
766
|
const serialize = config.serialize ?? defaultSerialize;
|
|
523
767
|
const deserialize = config.deserialize ?? defaultDeserialize;
|
|
524
768
|
const isMemory = config.scope === StorageScope.Memory;
|
|
525
|
-
const
|
|
769
|
+
const resolvedBiometricLevel = config.scope === StorageScope.Secure ? config.biometricLevel ?? (config.biometric === true ? BiometricLevel.BiometryOnly : BiometricLevel.None) : BiometricLevel.None;
|
|
770
|
+
const isBiometric = resolvedBiometricLevel !== BiometricLevel.None;
|
|
526
771
|
const secureAccessControl = config.accessControl;
|
|
527
772
|
const validate = config.validate;
|
|
528
773
|
const onValidationError = config.onValidationError;
|
|
@@ -531,7 +776,7 @@ export function createStorageItem(config) {
|
|
|
531
776
|
const expirationTtlMs = expiration?.ttlMs;
|
|
532
777
|
const memoryExpiration = expiration && isMemory ? new Map() : null;
|
|
533
778
|
const readCache = !isMemory && config.readCache === true;
|
|
534
|
-
const coalesceSecureWrites = config.scope === StorageScope.Secure && config.coalesceSecureWrites === true && !isBiometric
|
|
779
|
+
const coalesceSecureWrites = config.scope === StorageScope.Secure && config.coalesceSecureWrites === true && !isBiometric;
|
|
535
780
|
const defaultValue = config.defaultValue;
|
|
536
781
|
const nonMemoryScope = config.scope === StorageScope.Disk ? StorageScope.Disk : config.scope === StorageScope.Secure ? StorageScope.Secure : null;
|
|
537
782
|
if (expiration && expiration.ttlMs <= 0) {
|
|
@@ -561,6 +806,7 @@ export function createStorageItem(config) {
|
|
|
561
806
|
unsubscribe = addKeyListener(memoryListeners, storageKey, listener);
|
|
562
807
|
return;
|
|
563
808
|
}
|
|
809
|
+
ensureWebStorageEventSubscription();
|
|
564
810
|
unsubscribe = addKeyListener(getScopedListeners(nonMemoryScope), storageKey, listener);
|
|
565
811
|
};
|
|
566
812
|
const readStoredRaw = () => {
|
|
@@ -594,12 +840,12 @@ export function createStorageItem(config) {
|
|
|
594
840
|
};
|
|
595
841
|
const writeStoredRaw = rawValue => {
|
|
596
842
|
if (isBiometric) {
|
|
597
|
-
WebStorage.
|
|
843
|
+
WebStorage.setSecureBiometricWithLevel(storageKey, rawValue, resolvedBiometricLevel);
|
|
598
844
|
return;
|
|
599
845
|
}
|
|
600
846
|
cacheRawValue(nonMemoryScope, storageKey, rawValue);
|
|
601
847
|
if (coalesceSecureWrites) {
|
|
602
|
-
scheduleSecureWrite(storageKey, rawValue);
|
|
848
|
+
scheduleSecureWrite(storageKey, rawValue, secureAccessControl ?? AccessControl.WhenUnlocked);
|
|
603
849
|
return;
|
|
604
850
|
}
|
|
605
851
|
if (nonMemoryScope === StorageScope.Secure) {
|
|
@@ -614,7 +860,7 @@ export function createStorageItem(config) {
|
|
|
614
860
|
}
|
|
615
861
|
cacheRawValue(nonMemoryScope, storageKey, undefined);
|
|
616
862
|
if (coalesceSecureWrites) {
|
|
617
|
-
scheduleSecureWrite(storageKey, undefined);
|
|
863
|
+
scheduleSecureWrite(storageKey, undefined, secureAccessControl ?? AccessControl.WhenUnlocked);
|
|
618
864
|
return;
|
|
619
865
|
}
|
|
620
866
|
if (nonMemoryScope === StorageScope.Secure) {
|
|
@@ -662,7 +908,7 @@ export function createStorageItem(config) {
|
|
|
662
908
|
}
|
|
663
909
|
return resolved;
|
|
664
910
|
};
|
|
665
|
-
const
|
|
911
|
+
const getInternal = () => {
|
|
666
912
|
const raw = readStoredRaw();
|
|
667
913
|
if (!memoryExpiration && raw === lastRaw && hasLastValue) {
|
|
668
914
|
if (!expiration || lastExpiresAt === null) {
|
|
@@ -677,6 +923,7 @@ export function createStorageItem(config) {
|
|
|
677
923
|
onExpired?.(storageKey);
|
|
678
924
|
lastValue = ensureValidatedValue(defaultValue, false);
|
|
679
925
|
hasLastValue = true;
|
|
926
|
+
listeners.forEach(cb => cb());
|
|
680
927
|
return lastValue;
|
|
681
928
|
}
|
|
682
929
|
}
|
|
@@ -712,6 +959,7 @@ export function createStorageItem(config) {
|
|
|
712
959
|
onExpired?.(storageKey);
|
|
713
960
|
lastValue = ensureValidatedValue(defaultValue, false);
|
|
714
961
|
hasLastValue = true;
|
|
962
|
+
listeners.forEach(cb => cb());
|
|
715
963
|
return lastValue;
|
|
716
964
|
}
|
|
717
965
|
deserializableRaw = parsed.payload;
|
|
@@ -727,31 +975,52 @@ export function createStorageItem(config) {
|
|
|
727
975
|
hasLastValue = true;
|
|
728
976
|
return lastValue;
|
|
729
977
|
};
|
|
978
|
+
const getCurrentVersion = () => {
|
|
979
|
+
const raw = readStoredRaw();
|
|
980
|
+
return toVersionToken(raw);
|
|
981
|
+
};
|
|
982
|
+
const get = () => measureOperation("item:get", config.scope, () => getInternal());
|
|
983
|
+
const getWithVersion = () => measureOperation("item:getWithVersion", config.scope, () => ({
|
|
984
|
+
value: getInternal(),
|
|
985
|
+
version: getCurrentVersion()
|
|
986
|
+
}));
|
|
730
987
|
const set = valueOrFn => {
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
988
|
+
measureOperation("item:set", config.scope, () => {
|
|
989
|
+
const newValue = isUpdater(valueOrFn) ? valueOrFn(getInternal()) : valueOrFn;
|
|
990
|
+
invalidateParsedCache();
|
|
991
|
+
if (validate && !validate(newValue)) {
|
|
992
|
+
throw new Error(`Validation failed for key "${storageKey}" in scope "${StorageScope[config.scope]}".`);
|
|
993
|
+
}
|
|
994
|
+
writeValueWithoutValidation(newValue);
|
|
995
|
+
});
|
|
737
996
|
};
|
|
997
|
+
const setIfVersion = (version, valueOrFn) => measureOperation("item:setIfVersion", config.scope, () => {
|
|
998
|
+
const currentVersion = getCurrentVersion();
|
|
999
|
+
if (currentVersion !== version) {
|
|
1000
|
+
return false;
|
|
1001
|
+
}
|
|
1002
|
+
set(valueOrFn);
|
|
1003
|
+
return true;
|
|
1004
|
+
});
|
|
738
1005
|
const deleteItem = () => {
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
if (
|
|
742
|
-
memoryExpiration
|
|
1006
|
+
measureOperation("item:delete", config.scope, () => {
|
|
1007
|
+
invalidateParsedCache();
|
|
1008
|
+
if (isMemory) {
|
|
1009
|
+
if (memoryExpiration) {
|
|
1010
|
+
memoryExpiration.delete(storageKey);
|
|
1011
|
+
}
|
|
1012
|
+
memoryStore.delete(storageKey);
|
|
1013
|
+
notifyKeyListeners(memoryListeners, storageKey);
|
|
1014
|
+
return;
|
|
743
1015
|
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
return;
|
|
747
|
-
}
|
|
748
|
-
removeStoredRaw();
|
|
1016
|
+
removeStoredRaw();
|
|
1017
|
+
});
|
|
749
1018
|
};
|
|
750
|
-
const hasItem = () => {
|
|
1019
|
+
const hasItem = () => measureOperation("item:has", config.scope, () => {
|
|
751
1020
|
if (isMemory) return memoryStore.has(storageKey);
|
|
752
1021
|
if (isBiometric) return WebStorage.hasSecureBiometric(storageKey);
|
|
753
1022
|
return WebStorage.has(storageKey, config.scope);
|
|
754
|
-
};
|
|
1023
|
+
});
|
|
755
1024
|
const subscribe = callback => {
|
|
756
1025
|
ensureSubscription();
|
|
757
1026
|
listeners.add(callback);
|
|
@@ -765,7 +1034,9 @@ export function createStorageItem(config) {
|
|
|
765
1034
|
};
|
|
766
1035
|
const storageItem = {
|
|
767
1036
|
get,
|
|
1037
|
+
getWithVersion,
|
|
768
1038
|
set,
|
|
1039
|
+
setIfVersion,
|
|
769
1040
|
delete: deleteItem,
|
|
770
1041
|
has: hasItem,
|
|
771
1042
|
subscribe,
|
|
@@ -775,10 +1046,14 @@ export function createStorageItem(config) {
|
|
|
775
1046
|
invalidateParsedCache();
|
|
776
1047
|
listeners.forEach(listener => listener());
|
|
777
1048
|
},
|
|
1049
|
+
_invalidateParsedCacheOnly: () => {
|
|
1050
|
+
invalidateParsedCache();
|
|
1051
|
+
},
|
|
778
1052
|
_hasValidation: validate !== undefined,
|
|
779
1053
|
_hasExpiration: expiration !== undefined,
|
|
780
1054
|
_readCacheEnabled: readCache,
|
|
781
1055
|
_isBiometric: isBiometric,
|
|
1056
|
+
_defaultValue: defaultValue,
|
|
782
1057
|
...(secureAccessControl !== undefined ? {
|
|
783
1058
|
_secureAccessControl: secureAccessControl
|
|
784
1059
|
} : {}),
|
|
@@ -788,136 +1063,164 @@ export function createStorageItem(config) {
|
|
|
788
1063
|
return storageItem;
|
|
789
1064
|
}
|
|
790
1065
|
export { useStorage, useStorageSelector, useSetStorage } from "./storage-hooks";
|
|
1066
|
+
export { createIndexedDBBackend } from "./indexeddb-backend";
|
|
791
1067
|
export function getBatch(items, scope) {
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
1068
|
+
return measureOperation("batch:get", scope, () => {
|
|
1069
|
+
assertBatchScope(items, scope);
|
|
1070
|
+
if (scope === StorageScope.Memory) {
|
|
1071
|
+
return items.map(item => item.get());
|
|
1072
|
+
}
|
|
1073
|
+
const useRawBatchPath = items.every(item => scope === StorageScope.Secure ? canUseSecureRawBatchPath(item) : canUseRawBatchPath(item));
|
|
1074
|
+
if (!useRawBatchPath) {
|
|
1075
|
+
return items.map(item => item.get());
|
|
1076
|
+
}
|
|
1077
|
+
const rawValues = new Array(items.length);
|
|
1078
|
+
const keysToFetch = [];
|
|
1079
|
+
const keyIndexes = [];
|
|
1080
|
+
items.forEach((item, index) => {
|
|
1081
|
+
if (scope === StorageScope.Secure) {
|
|
1082
|
+
if (hasPendingSecureWrite(item.key)) {
|
|
1083
|
+
rawValues[index] = readPendingSecureWrite(item.key);
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
if (item._readCacheEnabled === true) {
|
|
1088
|
+
if (hasCachedRawValue(scope, item.key)) {
|
|
1089
|
+
rawValues[index] = readCachedRawValue(scope, item.key);
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
809
1092
|
}
|
|
1093
|
+
keysToFetch.push(item.key);
|
|
1094
|
+
keyIndexes.push(index);
|
|
1095
|
+
});
|
|
1096
|
+
if (keysToFetch.length > 0) {
|
|
1097
|
+
const fetchedValues = WebStorage.getBatch(keysToFetch, scope);
|
|
1098
|
+
fetchedValues.forEach((value, index) => {
|
|
1099
|
+
const key = keysToFetch[index];
|
|
1100
|
+
const targetIndex = keyIndexes[index];
|
|
1101
|
+
if (key === undefined || targetIndex === undefined) {
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1104
|
+
rawValues[targetIndex] = value;
|
|
1105
|
+
cacheRawValue(scope, key, value);
|
|
1106
|
+
});
|
|
810
1107
|
}
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
1108
|
+
return items.map((item, index) => {
|
|
1109
|
+
const raw = rawValues[index];
|
|
1110
|
+
if (raw === undefined) {
|
|
1111
|
+
return asInternal(item)._defaultValue;
|
|
1112
|
+
}
|
|
1113
|
+
return item.deserialize(raw);
|
|
1114
|
+
});
|
|
1115
|
+
}, items.length);
|
|
1116
|
+
}
|
|
1117
|
+
export function setBatch(items, scope) {
|
|
1118
|
+
measureOperation("batch:set", scope, () => {
|
|
1119
|
+
assertBatchScope(items.map(batchEntry => batchEntry.item), scope);
|
|
1120
|
+
if (scope === StorageScope.Memory) {
|
|
1121
|
+
// Determine if any item needs per-item handling (validation or TTL)
|
|
1122
|
+
const needsIndividualSets = items.some(({
|
|
1123
|
+
item
|
|
1124
|
+
}) => {
|
|
1125
|
+
const internal = asInternal(item);
|
|
1126
|
+
return internal._hasValidation || internal._hasExpiration;
|
|
1127
|
+
});
|
|
1128
|
+
if (needsIndividualSets) {
|
|
1129
|
+
items.forEach(({
|
|
1130
|
+
item,
|
|
1131
|
+
value
|
|
1132
|
+
}) => item.set(value));
|
|
814
1133
|
return;
|
|
815
1134
|
}
|
|
1135
|
+
|
|
1136
|
+
// Atomic write: update all values in memoryStore, invalidate caches, then batch-notify
|
|
1137
|
+
items.forEach(({
|
|
1138
|
+
item,
|
|
1139
|
+
value
|
|
1140
|
+
}) => {
|
|
1141
|
+
memoryStore.set(item.key, value);
|
|
1142
|
+
asInternal(item)._invalidateParsedCacheOnly();
|
|
1143
|
+
});
|
|
1144
|
+
items.forEach(({
|
|
1145
|
+
item
|
|
1146
|
+
}) => notifyKeyListeners(memoryListeners, item.key));
|
|
1147
|
+
return;
|
|
816
1148
|
}
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
1149
|
+
if (scope === StorageScope.Secure) {
|
|
1150
|
+
const secureEntries = items.map(({
|
|
1151
|
+
item,
|
|
1152
|
+
value
|
|
1153
|
+
}) => ({
|
|
1154
|
+
item,
|
|
1155
|
+
value,
|
|
1156
|
+
internal: asInternal(item)
|
|
1157
|
+
}));
|
|
1158
|
+
const canUseSecureBatchPath = secureEntries.every(({
|
|
1159
|
+
internal
|
|
1160
|
+
}) => canUseSecureRawBatchPath(internal));
|
|
1161
|
+
if (!canUseSecureBatchPath) {
|
|
1162
|
+
items.forEach(({
|
|
1163
|
+
item,
|
|
1164
|
+
value
|
|
1165
|
+
}) => item.set(value));
|
|
826
1166
|
return;
|
|
827
1167
|
}
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
1168
|
+
flushSecureWrites();
|
|
1169
|
+
const groupedByAccessControl = new Map();
|
|
1170
|
+
secureEntries.forEach(({
|
|
1171
|
+
item,
|
|
1172
|
+
value,
|
|
1173
|
+
internal
|
|
1174
|
+
}) => {
|
|
1175
|
+
const accessControl = internal._secureAccessControl ?? AccessControl.WhenUnlocked;
|
|
1176
|
+
const existingGroup = groupedByAccessControl.get(accessControl);
|
|
1177
|
+
const group = existingGroup ?? {
|
|
1178
|
+
keys: [],
|
|
1179
|
+
values: []
|
|
1180
|
+
};
|
|
1181
|
+
group.keys.push(item.key);
|
|
1182
|
+
group.values.push(item.serialize(value));
|
|
1183
|
+
if (!existingGroup) {
|
|
1184
|
+
groupedByAccessControl.set(accessControl, group);
|
|
1185
|
+
}
|
|
1186
|
+
});
|
|
1187
|
+
groupedByAccessControl.forEach((group, accessControl) => {
|
|
1188
|
+
WebStorage.setSecureAccessControl(accessControl);
|
|
1189
|
+
WebStorage.setBatch(group.keys, group.values, scope);
|
|
1190
|
+
group.keys.forEach((key, index) => cacheRawValue(scope, key, group.values[index]));
|
|
1191
|
+
});
|
|
1192
|
+
return;
|
|
836
1193
|
}
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
assertBatchScope(items.map(batchEntry => batchEntry.item), scope);
|
|
842
|
-
if (scope === StorageScope.Memory) {
|
|
843
|
-
items.forEach(({
|
|
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) {
|
|
1194
|
+
const useRawBatchPath = items.every(({
|
|
1195
|
+
item
|
|
1196
|
+
}) => canUseRawBatchPath(asInternal(item)));
|
|
1197
|
+
if (!useRawBatchPath) {
|
|
862
1198
|
items.forEach(({
|
|
863
1199
|
item,
|
|
864
1200
|
value
|
|
865
1201
|
}) => item.set(value));
|
|
866
1202
|
return;
|
|
867
1203
|
}
|
|
868
|
-
|
|
869
|
-
const
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
internal
|
|
874
|
-
}) => {
|
|
875
|
-
const accessControl = internal._secureAccessControl ?? AccessControl.WhenUnlocked;
|
|
876
|
-
const existingGroup = groupedByAccessControl.get(accessControl);
|
|
877
|
-
const group = existingGroup ?? {
|
|
878
|
-
keys: [],
|
|
879
|
-
values: []
|
|
880
|
-
};
|
|
881
|
-
group.keys.push(item.key);
|
|
882
|
-
group.values.push(item.serialize(value));
|
|
883
|
-
if (!existingGroup) {
|
|
884
|
-
groupedByAccessControl.set(accessControl, group);
|
|
885
|
-
}
|
|
886
|
-
});
|
|
887
|
-
groupedByAccessControl.forEach((group, accessControl) => {
|
|
888
|
-
WebStorage.setSecureAccessControl(accessControl);
|
|
889
|
-
WebStorage.setBatch(group.keys, group.values, scope);
|
|
890
|
-
group.keys.forEach((key, index) => cacheRawValue(scope, key, group.values[index]));
|
|
891
|
-
});
|
|
892
|
-
return;
|
|
893
|
-
}
|
|
894
|
-
const useRawBatchPath = items.every(({
|
|
895
|
-
item
|
|
896
|
-
}) => canUseRawBatchPath(asInternal(item)));
|
|
897
|
-
if (!useRawBatchPath) {
|
|
898
|
-
items.forEach(({
|
|
899
|
-
item,
|
|
900
|
-
value
|
|
901
|
-
}) => item.set(value));
|
|
902
|
-
return;
|
|
903
|
-
}
|
|
904
|
-
const keys = items.map(entry => entry.item.key);
|
|
905
|
-
const values = items.map(entry => entry.item.serialize(entry.value));
|
|
906
|
-
WebStorage.setBatch(keys, values, scope);
|
|
907
|
-
keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
|
|
1204
|
+
const keys = items.map(entry => entry.item.key);
|
|
1205
|
+
const values = items.map(entry => entry.item.serialize(entry.value));
|
|
1206
|
+
WebStorage.setBatch(keys, values, scope);
|
|
1207
|
+
keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
|
|
1208
|
+
}, items.length);
|
|
908
1209
|
}
|
|
909
1210
|
export function removeBatch(items, scope) {
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
1211
|
+
measureOperation("batch:remove", scope, () => {
|
|
1212
|
+
assertBatchScope(items, scope);
|
|
1213
|
+
if (scope === StorageScope.Memory) {
|
|
1214
|
+
items.forEach(item => item.delete());
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1217
|
+
const keys = items.map(item => item.key);
|
|
1218
|
+
if (scope === StorageScope.Secure) {
|
|
1219
|
+
flushSecureWrites();
|
|
1220
|
+
}
|
|
1221
|
+
WebStorage.removeBatch(keys, scope);
|
|
1222
|
+
keys.forEach(key => cacheRawValue(scope, key, undefined));
|
|
1223
|
+
}, items.length);
|
|
921
1224
|
}
|
|
922
1225
|
export function registerMigration(version, migration) {
|
|
923
1226
|
if (!Number.isInteger(version) || version <= 0) {
|
|
@@ -929,77 +1232,107 @@ export function registerMigration(version, migration) {
|
|
|
929
1232
|
registeredMigrations.set(version, migration);
|
|
930
1233
|
}
|
|
931
1234
|
export function migrateToLatest(scope = StorageScope.Disk) {
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
1235
|
+
return measureOperation("migration:run", scope, () => {
|
|
1236
|
+
assertValidScope(scope);
|
|
1237
|
+
const currentVersion = readMigrationVersion(scope);
|
|
1238
|
+
const versions = Array.from(registeredMigrations.keys()).filter(version => version > currentVersion).sort((a, b) => a - b);
|
|
1239
|
+
let appliedVersion = currentVersion;
|
|
1240
|
+
const context = {
|
|
1241
|
+
scope,
|
|
1242
|
+
getRaw: key => getRawValue(key, scope),
|
|
1243
|
+
setRaw: (key, value) => setRawValue(key, value, scope),
|
|
1244
|
+
removeRaw: key => removeRawValue(key, scope)
|
|
1245
|
+
};
|
|
1246
|
+
versions.forEach(version => {
|
|
1247
|
+
const migration = registeredMigrations.get(version);
|
|
1248
|
+
if (!migration) {
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
migration(context);
|
|
1252
|
+
writeMigrationVersion(scope, version);
|
|
1253
|
+
appliedVersion = version;
|
|
1254
|
+
});
|
|
1255
|
+
return appliedVersion;
|
|
950
1256
|
});
|
|
951
|
-
return appliedVersion;
|
|
952
1257
|
}
|
|
953
1258
|
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();
|
|
1259
|
+
return measureOperation("transaction:run", scope, () => {
|
|
1260
|
+
assertValidScope(scope);
|
|
1261
|
+
if (scope === StorageScope.Secure) {
|
|
1262
|
+
flushSecureWrites();
|
|
989
1263
|
}
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
1264
|
+
const rollback = new Map();
|
|
1265
|
+
const rememberRollback = key => {
|
|
1266
|
+
if (rollback.has(key)) {
|
|
1267
|
+
return;
|
|
1268
|
+
}
|
|
1269
|
+
rollback.set(key, getRawValue(key, scope));
|
|
1270
|
+
};
|
|
1271
|
+
const tx = {
|
|
1272
|
+
scope,
|
|
1273
|
+
getRaw: key => getRawValue(key, scope),
|
|
1274
|
+
setRaw: (key, value) => {
|
|
1275
|
+
rememberRollback(key);
|
|
1276
|
+
setRawValue(key, value, scope);
|
|
1277
|
+
},
|
|
1278
|
+
removeRaw: key => {
|
|
1279
|
+
rememberRollback(key);
|
|
996
1280
|
removeRawValue(key, scope);
|
|
1281
|
+
},
|
|
1282
|
+
getItem: item => {
|
|
1283
|
+
assertBatchScope([item], scope);
|
|
1284
|
+
return item.get();
|
|
1285
|
+
},
|
|
1286
|
+
setItem: (item, value) => {
|
|
1287
|
+
assertBatchScope([item], scope);
|
|
1288
|
+
rememberRollback(item.key);
|
|
1289
|
+
item.set(value);
|
|
1290
|
+
},
|
|
1291
|
+
removeItem: item => {
|
|
1292
|
+
assertBatchScope([item], scope);
|
|
1293
|
+
rememberRollback(item.key);
|
|
1294
|
+
item.delete();
|
|
1295
|
+
}
|
|
1296
|
+
};
|
|
1297
|
+
try {
|
|
1298
|
+
return transaction(tx);
|
|
1299
|
+
} catch (error) {
|
|
1300
|
+
const rollbackEntries = Array.from(rollback.entries()).reverse();
|
|
1301
|
+
if (scope === StorageScope.Memory) {
|
|
1302
|
+
rollbackEntries.forEach(([key, previousValue]) => {
|
|
1303
|
+
if (previousValue === undefined) {
|
|
1304
|
+
removeRawValue(key, scope);
|
|
1305
|
+
} else {
|
|
1306
|
+
setRawValue(key, previousValue, scope);
|
|
1307
|
+
}
|
|
1308
|
+
});
|
|
997
1309
|
} else {
|
|
998
|
-
|
|
1310
|
+
const keysToSet = [];
|
|
1311
|
+
const valuesToSet = [];
|
|
1312
|
+
const keysToRemove = [];
|
|
1313
|
+
rollbackEntries.forEach(([key, previousValue]) => {
|
|
1314
|
+
if (previousValue === undefined) {
|
|
1315
|
+
keysToRemove.push(key);
|
|
1316
|
+
} else {
|
|
1317
|
+
keysToSet.push(key);
|
|
1318
|
+
valuesToSet.push(previousValue);
|
|
1319
|
+
}
|
|
1320
|
+
});
|
|
1321
|
+
if (scope === StorageScope.Secure) {
|
|
1322
|
+
flushSecureWrites();
|
|
1323
|
+
}
|
|
1324
|
+
if (keysToSet.length > 0) {
|
|
1325
|
+
WebStorage.setBatch(keysToSet, valuesToSet, scope);
|
|
1326
|
+
keysToSet.forEach((key, index) => cacheRawValue(scope, key, valuesToSet[index]));
|
|
1327
|
+
}
|
|
1328
|
+
if (keysToRemove.length > 0) {
|
|
1329
|
+
WebStorage.removeBatch(keysToRemove, scope);
|
|
1330
|
+
keysToRemove.forEach(key => cacheRawValue(scope, key, undefined));
|
|
1331
|
+
}
|
|
999
1332
|
}
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
}
|
|
1333
|
+
throw error;
|
|
1334
|
+
}
|
|
1335
|
+
});
|
|
1003
1336
|
}
|
|
1004
1337
|
export function createSecureAuthStorage(config, options) {
|
|
1005
1338
|
const ns = options?.namespace ?? "auth";
|
|
@@ -1017,6 +1350,9 @@ export function createSecureAuthStorage(config, options) {
|
|
|
1017
1350
|
...(itemConfig.biometric !== undefined ? {
|
|
1018
1351
|
biometric: itemConfig.biometric
|
|
1019
1352
|
} : {}),
|
|
1353
|
+
...(itemConfig.biometricLevel !== undefined ? {
|
|
1354
|
+
biometricLevel: itemConfig.biometricLevel
|
|
1355
|
+
} : {}),
|
|
1020
1356
|
...(itemConfig.accessControl !== undefined ? {
|
|
1021
1357
|
accessControl: itemConfig.accessControl
|
|
1022
1358
|
} : {}),
|