react-native-nitro-storage 0.4.5 → 0.5.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 +254 -945
- package/SECURITY.md +26 -0
- package/docs/api-reference.md +281 -0
- package/docs/batch-transactions-migrations.md +200 -0
- package/docs/benchmarks.md +37 -0
- package/docs/mmkv-migration.md +80 -0
- package/docs/react-hooks.md +113 -0
- package/docs/recipes.md +302 -0
- package/docs/secure-storage.md +190 -0
- package/docs/web-backends.md +141 -0
- package/lib/commonjs/index.js +265 -14
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +220 -11
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/storage-events.js +117 -0
- package/lib/commonjs/storage-events.js.map +1 -0
- package/lib/commonjs/storage-runtime.js.map +1 -1
- package/lib/module/index.js +265 -14
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +220 -11
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/storage-events.js +112 -0
- package/lib/module/storage-events.js.map +1 -0
- package/lib/module/storage-runtime.js.map +1 -1
- package/lib/typescript/index.d.ts +19 -2
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/index.web.d.ts +19 -2
- package/lib/typescript/index.web.d.ts.map +1 -1
- package/lib/typescript/storage-events.d.ts +37 -0
- package/lib/typescript/storage-events.d.ts.map +1 -0
- package/lib/typescript/storage-runtime.d.ts +32 -0
- package/lib/typescript/storage-runtime.d.ts.map +1 -1
- package/package.json +25 -11
- package/src/index.ts +601 -14
- package/src/index.web.ts +535 -22
- package/src/storage-events.ts +184 -0
- package/src/storage-runtime.ts +35 -0
package/src/index.web.ts
CHANGED
|
@@ -21,17 +21,38 @@ import {
|
|
|
21
21
|
import {
|
|
22
22
|
getStorageErrorCode,
|
|
23
23
|
isLockedStorageErrorCode,
|
|
24
|
+
type SecureStorageMetadata,
|
|
25
|
+
type SecurityCapabilities,
|
|
24
26
|
type StorageCapabilities,
|
|
25
27
|
type StorageErrorCode,
|
|
26
28
|
} from "./storage-runtime";
|
|
29
|
+
import {
|
|
30
|
+
StorageEventRegistry,
|
|
31
|
+
type StorageBatchChangeEvent,
|
|
32
|
+
type StorageChangeEvent,
|
|
33
|
+
type StorageChangeOperation,
|
|
34
|
+
type StorageChangeSource,
|
|
35
|
+
type StorageEventListener,
|
|
36
|
+
type StorageKeyChangeEvent,
|
|
37
|
+
} from "./storage-events";
|
|
27
38
|
|
|
28
39
|
export { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
|
|
29
40
|
export { migrateFromMMKV } from "./migration";
|
|
30
41
|
export {
|
|
31
42
|
getStorageErrorCode,
|
|
43
|
+
type SecureStorageMetadata,
|
|
44
|
+
type SecurityCapabilities,
|
|
32
45
|
type StorageCapabilities,
|
|
33
46
|
type StorageErrorCode,
|
|
34
47
|
} from "./storage-runtime";
|
|
48
|
+
export type {
|
|
49
|
+
StorageBatchChangeEvent,
|
|
50
|
+
StorageChangeEvent,
|
|
51
|
+
StorageChangeOperation,
|
|
52
|
+
StorageChangeSource,
|
|
53
|
+
StorageEventListener,
|
|
54
|
+
StorageKeyChangeEvent,
|
|
55
|
+
} from "./storage-events";
|
|
35
56
|
export type {
|
|
36
57
|
WebStorageBackend,
|
|
37
58
|
WebStorageChangeEvent,
|
|
@@ -60,6 +81,14 @@ export type StorageMetricSummary = {
|
|
|
60
81
|
avgDurationMs: number;
|
|
61
82
|
maxDurationMs: number;
|
|
62
83
|
};
|
|
84
|
+
export type StorageSelectorListener<TSelected> = (
|
|
85
|
+
value: TSelected,
|
|
86
|
+
previousValue: TSelected,
|
|
87
|
+
) => void;
|
|
88
|
+
export type StorageSelectorSubscribeOptions<TSelected> = {
|
|
89
|
+
isEqual?: (previousValue: TSelected, nextValue: TSelected) => boolean;
|
|
90
|
+
fireImmediately?: boolean;
|
|
91
|
+
};
|
|
63
92
|
export type MigrationContext = {
|
|
64
93
|
scope: StorageScope;
|
|
65
94
|
getRaw: (key: string) => string | undefined;
|
|
@@ -187,10 +216,12 @@ const BIOMETRIC_WEB_PREFIX = "__bio_";
|
|
|
187
216
|
let hasWarnedAboutWebBiometricFallback = false;
|
|
188
217
|
let hasWindowStorageEventSubscription = false;
|
|
189
218
|
let metricsObserver: StorageMetricsObserver | undefined;
|
|
219
|
+
let eventObserver: StorageEventListener | undefined;
|
|
190
220
|
const metricsCounters = new Map<
|
|
191
221
|
string,
|
|
192
222
|
{ count: number; totalDurationMs: number; maxDurationMs: number }
|
|
193
223
|
>();
|
|
224
|
+
const storageEvents = new StorageEventRegistry();
|
|
194
225
|
|
|
195
226
|
function recordMetric(
|
|
196
227
|
operation: string,
|
|
@@ -261,6 +292,12 @@ function getBackendName(
|
|
|
261
292
|
return backend?.name ?? `web:${scopeName}`;
|
|
262
293
|
}
|
|
263
294
|
|
|
295
|
+
function getWebSecureEncryptionStatus(
|
|
296
|
+
backend: WebSecureStorageBackend | undefined,
|
|
297
|
+
): "unavailable" | "unknown" {
|
|
298
|
+
return backend?.name === "localStorage:secure" ? "unavailable" : "unknown";
|
|
299
|
+
}
|
|
300
|
+
|
|
264
301
|
function createWebStorageError(
|
|
265
302
|
scope: NonMemoryScope,
|
|
266
303
|
operation: string,
|
|
@@ -369,6 +406,7 @@ function applyExternalChangeEvent(
|
|
|
369
406
|
|
|
370
407
|
if (scope === StorageScope.Secure && key.startsWith(SECURE_WEB_PREFIX)) {
|
|
371
408
|
const plainKey = fromSecureStorageKey(key);
|
|
409
|
+
const oldValue = readCachedRawValue(StorageScope.Secure, plainKey);
|
|
372
410
|
if (newValue === null) {
|
|
373
411
|
ensureWebScopeKeyIndex(StorageScope.Secure).delete(plainKey);
|
|
374
412
|
cacheRawValue(StorageScope.Secure, plainKey, undefined);
|
|
@@ -377,11 +415,20 @@ function applyExternalChangeEvent(
|
|
|
377
415
|
cacheRawValue(StorageScope.Secure, plainKey, newValue);
|
|
378
416
|
}
|
|
379
417
|
notifyKeyListeners(getScopedListeners(StorageScope.Secure), plainKey);
|
|
418
|
+
emitKeyChange(
|
|
419
|
+
StorageScope.Secure,
|
|
420
|
+
plainKey,
|
|
421
|
+
oldValue,
|
|
422
|
+
newValue ?? undefined,
|
|
423
|
+
"external",
|
|
424
|
+
"external",
|
|
425
|
+
);
|
|
380
426
|
return;
|
|
381
427
|
}
|
|
382
428
|
|
|
383
429
|
if (scope === StorageScope.Secure && key.startsWith(BIOMETRIC_WEB_PREFIX)) {
|
|
384
430
|
const plainKey = fromBiometricStorageKey(key);
|
|
431
|
+
const oldValue = readCachedRawValue(StorageScope.Secure, plainKey);
|
|
385
432
|
if (newValue === null) {
|
|
386
433
|
if (
|
|
387
434
|
withWebBackendOperation(
|
|
@@ -398,9 +445,18 @@ function applyExternalChangeEvent(
|
|
|
398
445
|
cacheRawValue(StorageScope.Secure, plainKey, newValue);
|
|
399
446
|
}
|
|
400
447
|
notifyKeyListeners(getScopedListeners(StorageScope.Secure), plainKey);
|
|
448
|
+
emitKeyChange(
|
|
449
|
+
StorageScope.Secure,
|
|
450
|
+
plainKey,
|
|
451
|
+
oldValue,
|
|
452
|
+
newValue ?? undefined,
|
|
453
|
+
"external",
|
|
454
|
+
"external",
|
|
455
|
+
);
|
|
401
456
|
return;
|
|
402
457
|
}
|
|
403
458
|
|
|
459
|
+
const oldValue = readCachedRawValue(scope, key);
|
|
404
460
|
if (newValue === null) {
|
|
405
461
|
ensureWebScopeKeyIndex(scope).delete(key);
|
|
406
462
|
cacheRawValue(scope, key, undefined);
|
|
@@ -409,6 +465,14 @@ function applyExternalChangeEvent(
|
|
|
409
465
|
cacheRawValue(scope, key, newValue);
|
|
410
466
|
}
|
|
411
467
|
notifyKeyListeners(getScopedListeners(scope), key);
|
|
468
|
+
emitKeyChange(
|
|
469
|
+
scope,
|
|
470
|
+
key,
|
|
471
|
+
oldValue,
|
|
472
|
+
newValue ?? undefined,
|
|
473
|
+
"external",
|
|
474
|
+
"external",
|
|
475
|
+
);
|
|
412
476
|
}
|
|
413
477
|
|
|
414
478
|
function handleWebStorageEvent(event: StorageEvent): void {
|
|
@@ -539,6 +603,82 @@ function addKeyListener(
|
|
|
539
603
|
};
|
|
540
604
|
}
|
|
541
605
|
|
|
606
|
+
function getEventRawValue(
|
|
607
|
+
scope: StorageScope,
|
|
608
|
+
key: string,
|
|
609
|
+
): string | undefined {
|
|
610
|
+
if (scope === StorageScope.Memory) {
|
|
611
|
+
const value = memoryStore.get(key);
|
|
612
|
+
return typeof value === "string" ? value : undefined;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
return getRawValue(key, scope);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function createKeyChange(
|
|
619
|
+
scope: StorageScope,
|
|
620
|
+
key: string,
|
|
621
|
+
oldValue: string | undefined,
|
|
622
|
+
newValue: string | undefined,
|
|
623
|
+
operation: StorageChangeOperation,
|
|
624
|
+
source: StorageChangeSource,
|
|
625
|
+
): StorageKeyChangeEvent {
|
|
626
|
+
return {
|
|
627
|
+
type: "key",
|
|
628
|
+
scope,
|
|
629
|
+
key,
|
|
630
|
+
oldValue,
|
|
631
|
+
newValue,
|
|
632
|
+
operation,
|
|
633
|
+
source,
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function hasStorageChangeObservers(scope: StorageScope): boolean {
|
|
638
|
+
return storageEvents.hasListeners(scope) || eventObserver !== undefined;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function emitKeyChange(
|
|
642
|
+
scope: StorageScope,
|
|
643
|
+
key: string,
|
|
644
|
+
oldValue: string | undefined,
|
|
645
|
+
newValue: string | undefined,
|
|
646
|
+
operation: StorageChangeOperation,
|
|
647
|
+
source: StorageChangeSource,
|
|
648
|
+
): void {
|
|
649
|
+
const event = createKeyChange(
|
|
650
|
+
scope,
|
|
651
|
+
key,
|
|
652
|
+
oldValue,
|
|
653
|
+
newValue,
|
|
654
|
+
operation,
|
|
655
|
+
source,
|
|
656
|
+
);
|
|
657
|
+
storageEvents.emitKey(event);
|
|
658
|
+
eventObserver?.(event);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function emitBatchChange(
|
|
662
|
+
scope: StorageScope,
|
|
663
|
+
operation: StorageChangeOperation,
|
|
664
|
+
source: StorageChangeSource,
|
|
665
|
+
changes: StorageKeyChangeEvent[],
|
|
666
|
+
): void {
|
|
667
|
+
if (changes.length === 0) {
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
const event: StorageBatchChangeEvent = {
|
|
672
|
+
type: "batch",
|
|
673
|
+
scope,
|
|
674
|
+
operation,
|
|
675
|
+
source,
|
|
676
|
+
changes,
|
|
677
|
+
};
|
|
678
|
+
storageEvents.emitBatch(event);
|
|
679
|
+
eventObserver?.(event);
|
|
680
|
+
}
|
|
681
|
+
|
|
542
682
|
function readPendingSecureWrite(key: string): string | undefined {
|
|
543
683
|
return pendingSecureWrites.get(key)?.value;
|
|
544
684
|
}
|
|
@@ -823,24 +963,10 @@ const WebStorage: Storage = {
|
|
|
823
963
|
return () => {};
|
|
824
964
|
},
|
|
825
965
|
has: (key: string, scope: number) => {
|
|
826
|
-
if (scope === StorageScope.Secure) {
|
|
827
|
-
return (
|
|
828
|
-
withWebBackendOperation(scope, "has", (backend) =>
|
|
829
|
-
backend.getItem(toSecureStorageKey(key)),
|
|
830
|
-
) !== null ||
|
|
831
|
-
withWebBackendOperation(scope, "has", (backend) =>
|
|
832
|
-
backend.getItem(toBiometricStorageKey(key)),
|
|
833
|
-
) !== null
|
|
834
|
-
);
|
|
835
|
-
}
|
|
836
|
-
if (scope !== StorageScope.Disk) {
|
|
837
|
-
return false;
|
|
966
|
+
if (scope === StorageScope.Disk || scope === StorageScope.Secure) {
|
|
967
|
+
return ensureWebScopeKeyIndex(scope).has(key);
|
|
838
968
|
}
|
|
839
|
-
return
|
|
840
|
-
withWebBackendOperation(scope, "has", (backend) =>
|
|
841
|
-
backend.getItem(key),
|
|
842
|
-
) !== null
|
|
843
|
-
);
|
|
969
|
+
return false;
|
|
844
970
|
},
|
|
845
971
|
getAllKeys: (scope: number) => {
|
|
846
972
|
if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
|
|
@@ -990,9 +1116,12 @@ function getRawValue(key: string, scope: StorageScope): string | undefined {
|
|
|
990
1116
|
|
|
991
1117
|
function setRawValue(key: string, value: string, scope: StorageScope): void {
|
|
992
1118
|
assertValidScope(scope);
|
|
1119
|
+
const oldValue =
|
|
1120
|
+
scope === StorageScope.Memory ? getEventRawValue(scope, key) : undefined;
|
|
993
1121
|
if (scope === StorageScope.Memory) {
|
|
994
1122
|
memoryStore.set(key, value);
|
|
995
1123
|
notifyKeyListeners(memoryListeners, key);
|
|
1124
|
+
emitKeyChange(scope, key, oldValue, value, "set", "memory");
|
|
996
1125
|
return;
|
|
997
1126
|
}
|
|
998
1127
|
|
|
@@ -1000,6 +1129,7 @@ function setRawValue(key: string, value: string, scope: StorageScope): void {
|
|
|
1000
1129
|
cacheRawValue(scope, key, value);
|
|
1001
1130
|
if (diskWritesAsync) {
|
|
1002
1131
|
scheduleDiskWrite(key, value);
|
|
1132
|
+
emitKeyChange(scope, key, oldValue, value, "set", "web");
|
|
1003
1133
|
return;
|
|
1004
1134
|
}
|
|
1005
1135
|
|
|
@@ -1014,13 +1144,16 @@ function setRawValue(key: string, value: string, scope: StorageScope): void {
|
|
|
1014
1144
|
|
|
1015
1145
|
WebStorage.set(key, value, scope);
|
|
1016
1146
|
cacheRawValue(scope, key, value);
|
|
1147
|
+
emitKeyChange(scope, key, oldValue, value, "set", "web");
|
|
1017
1148
|
}
|
|
1018
1149
|
|
|
1019
1150
|
function removeRawValue(key: string, scope: StorageScope): void {
|
|
1020
1151
|
assertValidScope(scope);
|
|
1152
|
+
const oldValue = getEventRawValue(scope, key);
|
|
1021
1153
|
if (scope === StorageScope.Memory) {
|
|
1022
1154
|
memoryStore.delete(key);
|
|
1023
1155
|
notifyKeyListeners(memoryListeners, key);
|
|
1156
|
+
emitKeyChange(scope, key, oldValue, undefined, "remove", "memory");
|
|
1024
1157
|
return;
|
|
1025
1158
|
}
|
|
1026
1159
|
|
|
@@ -1028,6 +1161,7 @@ function removeRawValue(key: string, scope: StorageScope): void {
|
|
|
1028
1161
|
cacheRawValue(scope, key, undefined);
|
|
1029
1162
|
if (diskWritesAsync) {
|
|
1030
1163
|
scheduleDiskWrite(key, undefined);
|
|
1164
|
+
emitKeyChange(scope, key, oldValue, undefined, "remove", "web");
|
|
1031
1165
|
return;
|
|
1032
1166
|
}
|
|
1033
1167
|
|
|
@@ -1042,6 +1176,7 @@ function removeRawValue(key: string, scope: StorageScope): void {
|
|
|
1042
1176
|
|
|
1043
1177
|
WebStorage.remove(key, scope);
|
|
1044
1178
|
cacheRawValue(scope, key, undefined);
|
|
1179
|
+
emitKeyChange(scope, key, oldValue, undefined, "remove", "web");
|
|
1045
1180
|
}
|
|
1046
1181
|
|
|
1047
1182
|
function readMigrationVersion(scope: StorageScope): number {
|
|
@@ -1059,11 +1194,74 @@ function writeMigrationVersion(scope: StorageScope, version: number): void {
|
|
|
1059
1194
|
}
|
|
1060
1195
|
|
|
1061
1196
|
export const storage = {
|
|
1197
|
+
subscribe: (
|
|
1198
|
+
scope: StorageScope,
|
|
1199
|
+
listener: StorageEventListener,
|
|
1200
|
+
): (() => void) => {
|
|
1201
|
+
assertValidScope(scope);
|
|
1202
|
+
if (scope !== StorageScope.Memory) {
|
|
1203
|
+
ensureExternalSyncSubscriptions();
|
|
1204
|
+
}
|
|
1205
|
+
return storageEvents.subscribe(scope, listener);
|
|
1206
|
+
},
|
|
1207
|
+
subscribeKey: (
|
|
1208
|
+
scope: StorageScope,
|
|
1209
|
+
key: string,
|
|
1210
|
+
listener: StorageEventListener,
|
|
1211
|
+
): (() => void) => {
|
|
1212
|
+
assertValidScope(scope);
|
|
1213
|
+
if (scope !== StorageScope.Memory) {
|
|
1214
|
+
ensureExternalSyncSubscriptions();
|
|
1215
|
+
}
|
|
1216
|
+
return storageEvents.subscribeKey(scope, key, listener);
|
|
1217
|
+
},
|
|
1218
|
+
subscribePrefix: (
|
|
1219
|
+
scope: StorageScope,
|
|
1220
|
+
prefix: string,
|
|
1221
|
+
listener: StorageEventListener,
|
|
1222
|
+
): (() => void) => {
|
|
1223
|
+
assertValidScope(scope);
|
|
1224
|
+
if (scope !== StorageScope.Memory) {
|
|
1225
|
+
ensureExternalSyncSubscriptions();
|
|
1226
|
+
}
|
|
1227
|
+
return storageEvents.subscribePrefix(scope, prefix, listener);
|
|
1228
|
+
},
|
|
1229
|
+
subscribeNamespace: (
|
|
1230
|
+
namespace: string,
|
|
1231
|
+
scope: StorageScope,
|
|
1232
|
+
listener: StorageEventListener,
|
|
1233
|
+
): (() => void) => {
|
|
1234
|
+
return storage.subscribePrefix(scope, prefixKey(namespace, ""), listener);
|
|
1235
|
+
},
|
|
1236
|
+
setEventObserver: (observer?: StorageEventListener) => {
|
|
1237
|
+
eventObserver = observer;
|
|
1238
|
+
if (observer) {
|
|
1239
|
+
ensureExternalSyncSubscriptions();
|
|
1240
|
+
}
|
|
1241
|
+
},
|
|
1062
1242
|
clear: (scope: StorageScope) => {
|
|
1063
1243
|
measureOperation("storage:clear", scope, () => {
|
|
1244
|
+
const previousValues = hasStorageChangeObservers(scope)
|
|
1245
|
+
? storage.getAll(scope)
|
|
1246
|
+
: {};
|
|
1064
1247
|
if (scope === StorageScope.Memory) {
|
|
1065
1248
|
memoryStore.clear();
|
|
1066
1249
|
notifyAllListeners(memoryListeners);
|
|
1250
|
+
emitBatchChange(
|
|
1251
|
+
scope,
|
|
1252
|
+
"clear",
|
|
1253
|
+
"memory",
|
|
1254
|
+
Object.keys(previousValues).map((key) =>
|
|
1255
|
+
createKeyChange(
|
|
1256
|
+
scope,
|
|
1257
|
+
key,
|
|
1258
|
+
previousValues[key],
|
|
1259
|
+
undefined,
|
|
1260
|
+
"clear",
|
|
1261
|
+
"memory",
|
|
1262
|
+
),
|
|
1263
|
+
),
|
|
1264
|
+
);
|
|
1067
1265
|
return;
|
|
1068
1266
|
}
|
|
1069
1267
|
|
|
@@ -1079,6 +1277,21 @@ export const storage = {
|
|
|
1079
1277
|
|
|
1080
1278
|
clearScopeRawCache(scope);
|
|
1081
1279
|
WebStorage.clear(scope);
|
|
1280
|
+
emitBatchChange(
|
|
1281
|
+
scope,
|
|
1282
|
+
"clear",
|
|
1283
|
+
"web",
|
|
1284
|
+
Object.keys(previousValues).map((key) =>
|
|
1285
|
+
createKeyChange(
|
|
1286
|
+
scope,
|
|
1287
|
+
key,
|
|
1288
|
+
previousValues[key],
|
|
1289
|
+
undefined,
|
|
1290
|
+
"clear",
|
|
1291
|
+
"web",
|
|
1292
|
+
),
|
|
1293
|
+
),
|
|
1294
|
+
);
|
|
1082
1295
|
});
|
|
1083
1296
|
},
|
|
1084
1297
|
clearAll: () => {
|
|
@@ -1097,16 +1310,44 @@ export const storage = {
|
|
|
1097
1310
|
measureOperation("storage:clearNamespace", scope, () => {
|
|
1098
1311
|
assertValidScope(scope);
|
|
1099
1312
|
if (scope === StorageScope.Memory) {
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1313
|
+
const affectedKeys = Array.from(memoryStore.keys()).filter((key) =>
|
|
1314
|
+
isNamespaced(key, namespace),
|
|
1315
|
+
);
|
|
1316
|
+
const previousValues = affectedKeys.map((key) => ({
|
|
1317
|
+
key,
|
|
1318
|
+
value: getEventRawValue(scope, key),
|
|
1319
|
+
}));
|
|
1320
|
+
|
|
1321
|
+
if (affectedKeys.length === 0) {
|
|
1322
|
+
return;
|
|
1104
1323
|
}
|
|
1105
|
-
|
|
1324
|
+
|
|
1325
|
+
affectedKeys.forEach((key) => {
|
|
1326
|
+
memoryStore.delete(key);
|
|
1327
|
+
});
|
|
1328
|
+
affectedKeys.forEach((key) => notifyKeyListeners(memoryListeners, key));
|
|
1329
|
+
emitBatchChange(
|
|
1330
|
+
scope,
|
|
1331
|
+
"clearNamespace",
|
|
1332
|
+
"memory",
|
|
1333
|
+
previousValues.map(({ key, value }) =>
|
|
1334
|
+
createKeyChange(
|
|
1335
|
+
scope,
|
|
1336
|
+
key,
|
|
1337
|
+
value,
|
|
1338
|
+
undefined,
|
|
1339
|
+
"clearNamespace",
|
|
1340
|
+
"memory",
|
|
1341
|
+
),
|
|
1342
|
+
),
|
|
1343
|
+
);
|
|
1106
1344
|
return;
|
|
1107
1345
|
}
|
|
1108
1346
|
|
|
1109
1347
|
const keyPrefix = prefixKey(namespace, "");
|
|
1348
|
+
const previousValues = hasStorageChangeObservers(scope)
|
|
1349
|
+
? storage.getByPrefix(keyPrefix, scope)
|
|
1350
|
+
: {};
|
|
1110
1351
|
if (scope === StorageScope.Disk) {
|
|
1111
1352
|
flushDiskWrites();
|
|
1112
1353
|
}
|
|
@@ -1120,6 +1361,21 @@ export const storage = {
|
|
|
1120
1361
|
}
|
|
1121
1362
|
}
|
|
1122
1363
|
WebStorage.removeByPrefix(keyPrefix, scope);
|
|
1364
|
+
emitBatchChange(
|
|
1365
|
+
scope,
|
|
1366
|
+
"clearNamespace",
|
|
1367
|
+
"web",
|
|
1368
|
+
Object.keys(previousValues).map((key) =>
|
|
1369
|
+
createKeyChange(
|
|
1370
|
+
scope,
|
|
1371
|
+
key,
|
|
1372
|
+
previousValues[key],
|
|
1373
|
+
undefined,
|
|
1374
|
+
"clearNamespace",
|
|
1375
|
+
"web",
|
|
1376
|
+
),
|
|
1377
|
+
),
|
|
1378
|
+
);
|
|
1123
1379
|
});
|
|
1124
1380
|
},
|
|
1125
1381
|
clearBiometric: () => {
|
|
@@ -1235,6 +1491,11 @@ export const storage = {
|
|
|
1235
1491
|
return result;
|
|
1236
1492
|
});
|
|
1237
1493
|
},
|
|
1494
|
+
export: (scope: StorageScope): Record<string, string> => {
|
|
1495
|
+
return measureOperation("storage:export", scope, () =>
|
|
1496
|
+
storage.getAll(scope),
|
|
1497
|
+
);
|
|
1498
|
+
},
|
|
1238
1499
|
size: (scope: StorageScope): number => {
|
|
1239
1500
|
return measureOperation("storage:size", scope, () => {
|
|
1240
1501
|
assertValidScope(scope);
|
|
@@ -1307,6 +1568,72 @@ export const storage = {
|
|
|
1307
1568
|
},
|
|
1308
1569
|
errorClassification: true,
|
|
1309
1570
|
}),
|
|
1571
|
+
getSecurityCapabilities: (): SecurityCapabilities => {
|
|
1572
|
+
const secureBackend = getBackendName(
|
|
1573
|
+
StorageScope.Secure,
|
|
1574
|
+
webSecureStorageBackend,
|
|
1575
|
+
);
|
|
1576
|
+
return {
|
|
1577
|
+
platform: "web",
|
|
1578
|
+
secureStorage: {
|
|
1579
|
+
backend: secureBackend,
|
|
1580
|
+
encrypted: getWebSecureEncryptionStatus(webSecureStorageBackend),
|
|
1581
|
+
accessControl: "unavailable",
|
|
1582
|
+
keychainAccessGroup: "unavailable",
|
|
1583
|
+
hardwareBacked: "unavailable",
|
|
1584
|
+
},
|
|
1585
|
+
biometric: {
|
|
1586
|
+
storage: "unavailable",
|
|
1587
|
+
prompt: "unavailable",
|
|
1588
|
+
biometryOnly: "unavailable",
|
|
1589
|
+
biometryOrPasscode: "unavailable",
|
|
1590
|
+
},
|
|
1591
|
+
metadata: {
|
|
1592
|
+
perKey: true,
|
|
1593
|
+
listsWithoutValues: true,
|
|
1594
|
+
persistsTimestamps: false,
|
|
1595
|
+
},
|
|
1596
|
+
};
|
|
1597
|
+
},
|
|
1598
|
+
getSecureMetadata: (key: string): SecureStorageMetadata => {
|
|
1599
|
+
return measureOperation(
|
|
1600
|
+
"storage:getSecureMetadata",
|
|
1601
|
+
StorageScope.Secure,
|
|
1602
|
+
() => {
|
|
1603
|
+
flushSecureWrites();
|
|
1604
|
+
const biometricProtected = WebStorage.hasSecureBiometric(key);
|
|
1605
|
+
const exists =
|
|
1606
|
+
biometricProtected || WebStorage.has(key, StorageScope.Secure);
|
|
1607
|
+
let kind: SecureStorageMetadata["kind"] = "missing";
|
|
1608
|
+
if (exists) {
|
|
1609
|
+
kind = biometricProtected ? "biometric" : "secure";
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
return {
|
|
1613
|
+
key,
|
|
1614
|
+
exists,
|
|
1615
|
+
kind,
|
|
1616
|
+
backend: getBackendName(StorageScope.Secure, webSecureStorageBackend),
|
|
1617
|
+
encrypted: getWebSecureEncryptionStatus(webSecureStorageBackend),
|
|
1618
|
+
hardwareBacked: "unavailable",
|
|
1619
|
+
biometricProtected,
|
|
1620
|
+
valueExposed: false,
|
|
1621
|
+
};
|
|
1622
|
+
},
|
|
1623
|
+
);
|
|
1624
|
+
},
|
|
1625
|
+
getAllSecureMetadata: (): SecureStorageMetadata[] => {
|
|
1626
|
+
return measureOperation(
|
|
1627
|
+
"storage:getAllSecureMetadata",
|
|
1628
|
+
StorageScope.Secure,
|
|
1629
|
+
() => {
|
|
1630
|
+
flushSecureWrites();
|
|
1631
|
+
return WebStorage.getAllKeys(StorageScope.Secure).map((key) =>
|
|
1632
|
+
storage.getSecureMetadata(key),
|
|
1633
|
+
);
|
|
1634
|
+
},
|
|
1635
|
+
);
|
|
1636
|
+
},
|
|
1310
1637
|
getString: (key: string, scope: StorageScope): string | undefined => {
|
|
1311
1638
|
return measureOperation("storage:getString", scope, () => {
|
|
1312
1639
|
return getRawValue(key, scope);
|
|
@@ -1331,12 +1658,23 @@ export const storage = {
|
|
|
1331
1658
|
assertValidScope(scope);
|
|
1332
1659
|
if (keys.length === 0) return;
|
|
1333
1660
|
const values = keys.map((k) => data[k]!);
|
|
1661
|
+
const changes = keys.map((key, index) =>
|
|
1662
|
+
createKeyChange(
|
|
1663
|
+
scope,
|
|
1664
|
+
key,
|
|
1665
|
+
getEventRawValue(scope, key),
|
|
1666
|
+
values[index],
|
|
1667
|
+
"import",
|
|
1668
|
+
scope === StorageScope.Memory ? "memory" : "web",
|
|
1669
|
+
),
|
|
1670
|
+
);
|
|
1334
1671
|
|
|
1335
1672
|
if (scope === StorageScope.Memory) {
|
|
1336
1673
|
keys.forEach((key, index) => {
|
|
1337
1674
|
memoryStore.set(key, values[index]);
|
|
1338
1675
|
});
|
|
1339
1676
|
keys.forEach((key) => notifyKeyListeners(memoryListeners, key));
|
|
1677
|
+
emitBatchChange(scope, "import", "memory", changes);
|
|
1340
1678
|
return;
|
|
1341
1679
|
}
|
|
1342
1680
|
|
|
@@ -1350,6 +1688,7 @@ export const storage = {
|
|
|
1350
1688
|
|
|
1351
1689
|
WebStorage.setBatch(keys, values, scope);
|
|
1352
1690
|
keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
|
|
1691
|
+
emitBatchChange(scope, "import", "web", changes);
|
|
1353
1692
|
},
|
|
1354
1693
|
keys.length,
|
|
1355
1694
|
);
|
|
@@ -1436,6 +1775,11 @@ export interface StorageItem<T> {
|
|
|
1436
1775
|
delete: () => void;
|
|
1437
1776
|
has: () => boolean;
|
|
1438
1777
|
subscribe: (callback: () => void) => () => void;
|
|
1778
|
+
subscribeSelector: <TSelected>(
|
|
1779
|
+
selector: (value: T) => TSelected,
|
|
1780
|
+
listener: StorageSelectorListener<TSelected>,
|
|
1781
|
+
options?: StorageSelectorSubscribeOptions<TSelected>,
|
|
1782
|
+
) => () => void;
|
|
1439
1783
|
serialize: (value: T) => string;
|
|
1440
1784
|
deserialize: (value: string) => T;
|
|
1441
1785
|
scope: StorageScope;
|
|
@@ -1604,12 +1948,17 @@ export function createStorageItem<T = undefined>(
|
|
|
1604
1948
|
};
|
|
1605
1949
|
|
|
1606
1950
|
const writeStoredRaw = (rawValue: string): void => {
|
|
1951
|
+
const oldValue =
|
|
1952
|
+
config.scope === StorageScope.Memory
|
|
1953
|
+
? getEventRawValue(config.scope, storageKey)
|
|
1954
|
+
: undefined;
|
|
1607
1955
|
if (isBiometric) {
|
|
1608
1956
|
WebStorage.setSecureBiometricWithLevel(
|
|
1609
1957
|
storageKey,
|
|
1610
1958
|
rawValue,
|
|
1611
1959
|
resolvedBiometricLevel,
|
|
1612
1960
|
);
|
|
1961
|
+
emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", "web");
|
|
1613
1962
|
return;
|
|
1614
1963
|
}
|
|
1615
1964
|
|
|
@@ -1618,6 +1967,14 @@ export function createStorageItem<T = undefined>(
|
|
|
1618
1967
|
if (nonMemoryScope === StorageScope.Disk) {
|
|
1619
1968
|
if (coalesceDiskWrites || diskWritesAsync) {
|
|
1620
1969
|
scheduleDiskWrite(storageKey, rawValue);
|
|
1970
|
+
emitKeyChange(
|
|
1971
|
+
config.scope,
|
|
1972
|
+
storageKey,
|
|
1973
|
+
oldValue,
|
|
1974
|
+
rawValue,
|
|
1975
|
+
"set",
|
|
1976
|
+
"web",
|
|
1977
|
+
);
|
|
1621
1978
|
return;
|
|
1622
1979
|
}
|
|
1623
1980
|
|
|
@@ -1630,6 +1987,7 @@ export function createStorageItem<T = undefined>(
|
|
|
1630
1987
|
rawValue,
|
|
1631
1988
|
secureAccessControl ?? secureDefaultAccessControl,
|
|
1632
1989
|
);
|
|
1990
|
+
emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", "web");
|
|
1633
1991
|
return;
|
|
1634
1992
|
}
|
|
1635
1993
|
|
|
@@ -1638,11 +1996,21 @@ export function createStorageItem<T = undefined>(
|
|
|
1638
1996
|
}
|
|
1639
1997
|
|
|
1640
1998
|
WebStorage.set(storageKey, rawValue, config.scope);
|
|
1999
|
+
emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", "web");
|
|
1641
2000
|
};
|
|
1642
2001
|
|
|
1643
2002
|
const removeStoredRaw = (): void => {
|
|
2003
|
+
const oldValue = getEventRawValue(config.scope, storageKey);
|
|
1644
2004
|
if (isBiometric) {
|
|
1645
2005
|
WebStorage.deleteSecureBiometric(storageKey);
|
|
2006
|
+
emitKeyChange(
|
|
2007
|
+
config.scope,
|
|
2008
|
+
storageKey,
|
|
2009
|
+
oldValue,
|
|
2010
|
+
undefined,
|
|
2011
|
+
"remove",
|
|
2012
|
+
"web",
|
|
2013
|
+
);
|
|
1646
2014
|
return;
|
|
1647
2015
|
}
|
|
1648
2016
|
|
|
@@ -1651,6 +2019,14 @@ export function createStorageItem<T = undefined>(
|
|
|
1651
2019
|
if (nonMemoryScope === StorageScope.Disk) {
|
|
1652
2020
|
if (coalesceDiskWrites || diskWritesAsync) {
|
|
1653
2021
|
scheduleDiskWrite(storageKey, undefined);
|
|
2022
|
+
emitKeyChange(
|
|
2023
|
+
config.scope,
|
|
2024
|
+
storageKey,
|
|
2025
|
+
oldValue,
|
|
2026
|
+
undefined,
|
|
2027
|
+
"remove",
|
|
2028
|
+
"web",
|
|
2029
|
+
);
|
|
1654
2030
|
return;
|
|
1655
2031
|
}
|
|
1656
2032
|
|
|
@@ -1663,6 +2039,14 @@ export function createStorageItem<T = undefined>(
|
|
|
1663
2039
|
undefined,
|
|
1664
2040
|
secureAccessControl ?? secureDefaultAccessControl,
|
|
1665
2041
|
);
|
|
2042
|
+
emitKeyChange(
|
|
2043
|
+
config.scope,
|
|
2044
|
+
storageKey,
|
|
2045
|
+
oldValue,
|
|
2046
|
+
undefined,
|
|
2047
|
+
"remove",
|
|
2048
|
+
"web",
|
|
2049
|
+
);
|
|
1666
2050
|
return;
|
|
1667
2051
|
}
|
|
1668
2052
|
|
|
@@ -1671,15 +2055,32 @@ export function createStorageItem<T = undefined>(
|
|
|
1671
2055
|
}
|
|
1672
2056
|
|
|
1673
2057
|
WebStorage.remove(storageKey, config.scope);
|
|
2058
|
+
emitKeyChange(
|
|
2059
|
+
config.scope,
|
|
2060
|
+
storageKey,
|
|
2061
|
+
oldValue,
|
|
2062
|
+
undefined,
|
|
2063
|
+
"remove",
|
|
2064
|
+
"web",
|
|
2065
|
+
);
|
|
1674
2066
|
};
|
|
1675
2067
|
|
|
1676
2068
|
const writeValueWithoutValidation = (value: T): void => {
|
|
1677
2069
|
if (isMemory) {
|
|
2070
|
+
const oldValue = getEventRawValue(config.scope, storageKey);
|
|
1678
2071
|
if (memoryExpiration) {
|
|
1679
2072
|
memoryExpiration.set(storageKey, Date.now() + (expirationTtlMs ?? 0));
|
|
1680
2073
|
}
|
|
1681
2074
|
memoryStore.set(storageKey, value);
|
|
1682
2075
|
notifyKeyListeners(memoryListeners, storageKey);
|
|
2076
|
+
emitKeyChange(
|
|
2077
|
+
config.scope,
|
|
2078
|
+
storageKey,
|
|
2079
|
+
oldValue,
|
|
2080
|
+
typeof value === "string" ? value : undefined,
|
|
2081
|
+
"set",
|
|
2082
|
+
"memory",
|
|
2083
|
+
);
|
|
1683
2084
|
return;
|
|
1684
2085
|
}
|
|
1685
2086
|
|
|
@@ -1851,11 +2252,20 @@ export function createStorageItem<T = undefined>(
|
|
|
1851
2252
|
invalidateParsedCache();
|
|
1852
2253
|
|
|
1853
2254
|
if (isMemory) {
|
|
2255
|
+
const oldValue = getEventRawValue(config.scope, storageKey);
|
|
1854
2256
|
if (memoryExpiration) {
|
|
1855
2257
|
memoryExpiration.delete(storageKey);
|
|
1856
2258
|
}
|
|
1857
2259
|
memoryStore.delete(storageKey);
|
|
1858
2260
|
notifyKeyListeners(memoryListeners, storageKey);
|
|
2261
|
+
emitKeyChange(
|
|
2262
|
+
config.scope,
|
|
2263
|
+
storageKey,
|
|
2264
|
+
oldValue,
|
|
2265
|
+
undefined,
|
|
2266
|
+
"remove",
|
|
2267
|
+
"memory",
|
|
2268
|
+
);
|
|
1859
2269
|
return;
|
|
1860
2270
|
}
|
|
1861
2271
|
|
|
@@ -1894,6 +2304,30 @@ export function createStorageItem<T = undefined>(
|
|
|
1894
2304
|
};
|
|
1895
2305
|
};
|
|
1896
2306
|
|
|
2307
|
+
const subscribeSelector = <TSelected>(
|
|
2308
|
+
selector: (value: T) => TSelected,
|
|
2309
|
+
listener: StorageSelectorListener<TSelected>,
|
|
2310
|
+
options: StorageSelectorSubscribeOptions<TSelected> = {},
|
|
2311
|
+
): (() => void) => {
|
|
2312
|
+
const isEqual = options.isEqual ?? Object.is;
|
|
2313
|
+
let currentValue = selector(getInternal());
|
|
2314
|
+
|
|
2315
|
+
if (options.fireImmediately === true) {
|
|
2316
|
+
listener(currentValue, currentValue);
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
return subscribe(() => {
|
|
2320
|
+
const nextValue = selector(getInternal());
|
|
2321
|
+
if (isEqual(currentValue, nextValue)) {
|
|
2322
|
+
return;
|
|
2323
|
+
}
|
|
2324
|
+
|
|
2325
|
+
const previousValue = currentValue;
|
|
2326
|
+
currentValue = nextValue;
|
|
2327
|
+
listener(nextValue, previousValue);
|
|
2328
|
+
});
|
|
2329
|
+
};
|
|
2330
|
+
|
|
1897
2331
|
const storageItem: StorageItemInternal<T> = {
|
|
1898
2332
|
get,
|
|
1899
2333
|
getWithVersion,
|
|
@@ -1902,6 +2336,7 @@ export function createStorageItem<T = undefined>(
|
|
|
1902
2336
|
delete: deleteItem,
|
|
1903
2337
|
has: hasItem,
|
|
1904
2338
|
subscribe,
|
|
2339
|
+
subscribeSelector,
|
|
1905
2340
|
serialize,
|
|
1906
2341
|
deserialize,
|
|
1907
2342
|
_triggerListeners: () => {
|
|
@@ -2054,6 +2489,17 @@ export function setBatch<T>(
|
|
|
2054
2489
|
return;
|
|
2055
2490
|
}
|
|
2056
2491
|
|
|
2492
|
+
const changes = items.map(({ item, value }) =>
|
|
2493
|
+
createKeyChange(
|
|
2494
|
+
scope,
|
|
2495
|
+
item.key,
|
|
2496
|
+
getEventRawValue(scope, item.key),
|
|
2497
|
+
typeof value === "string" ? value : undefined,
|
|
2498
|
+
"setBatch",
|
|
2499
|
+
"memory",
|
|
2500
|
+
),
|
|
2501
|
+
);
|
|
2502
|
+
|
|
2057
2503
|
// Atomic write: update all values in memoryStore, invalidate caches, then batch-notify
|
|
2058
2504
|
items.forEach(({ item, value }) => {
|
|
2059
2505
|
memoryStore.set(item.key, value);
|
|
@@ -2062,6 +2508,7 @@ export function setBatch<T>(
|
|
|
2062
2508
|
items.forEach(({ item }) =>
|
|
2063
2509
|
notifyKeyListeners(memoryListeners, item.key),
|
|
2064
2510
|
);
|
|
2511
|
+
emitBatchChange(scope, "setBatch", "memory", changes);
|
|
2065
2512
|
return;
|
|
2066
2513
|
}
|
|
2067
2514
|
|
|
@@ -2080,6 +2527,10 @@ export function setBatch<T>(
|
|
|
2080
2527
|
}
|
|
2081
2528
|
|
|
2082
2529
|
flushSecureWrites();
|
|
2530
|
+
const keys = secureEntries.map(({ item }) => item.key);
|
|
2531
|
+
const oldValues = hasStorageChangeObservers(scope)
|
|
2532
|
+
? WebStorage.getBatch(keys, scope)
|
|
2533
|
+
: [];
|
|
2083
2534
|
const groupedByAccessControl = new Map<
|
|
2084
2535
|
number,
|
|
2085
2536
|
{ keys: string[]; values: string[] }
|
|
@@ -2104,6 +2555,21 @@ export function setBatch<T>(
|
|
|
2104
2555
|
cacheRawValue(scope, key, group.values[index]),
|
|
2105
2556
|
);
|
|
2106
2557
|
});
|
|
2558
|
+
emitBatchChange(
|
|
2559
|
+
scope,
|
|
2560
|
+
"setBatch",
|
|
2561
|
+
"web",
|
|
2562
|
+
secureEntries.map(({ item, value }, index) =>
|
|
2563
|
+
createKeyChange(
|
|
2564
|
+
scope,
|
|
2565
|
+
item.key,
|
|
2566
|
+
oldValues[index],
|
|
2567
|
+
item.serialize(value),
|
|
2568
|
+
"setBatch",
|
|
2569
|
+
"web",
|
|
2570
|
+
),
|
|
2571
|
+
),
|
|
2572
|
+
);
|
|
2107
2573
|
return;
|
|
2108
2574
|
}
|
|
2109
2575
|
|
|
@@ -2119,8 +2585,26 @@ export function setBatch<T>(
|
|
|
2119
2585
|
|
|
2120
2586
|
const keys = items.map((entry) => entry.item.key);
|
|
2121
2587
|
const values = items.map((entry) => entry.item.serialize(entry.value));
|
|
2588
|
+
const oldValues = hasStorageChangeObservers(scope)
|
|
2589
|
+
? WebStorage.getBatch(keys, scope)
|
|
2590
|
+
: [];
|
|
2122
2591
|
WebStorage.setBatch(keys, values, scope);
|
|
2123
2592
|
keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
|
|
2593
|
+
emitBatchChange(
|
|
2594
|
+
scope,
|
|
2595
|
+
"setBatch",
|
|
2596
|
+
"web",
|
|
2597
|
+
keys.map((key, index) =>
|
|
2598
|
+
createKeyChange(
|
|
2599
|
+
scope,
|
|
2600
|
+
key,
|
|
2601
|
+
oldValues[index],
|
|
2602
|
+
values[index],
|
|
2603
|
+
"setBatch",
|
|
2604
|
+
"web",
|
|
2605
|
+
),
|
|
2606
|
+
),
|
|
2607
|
+
);
|
|
2124
2608
|
},
|
|
2125
2609
|
items.length,
|
|
2126
2610
|
);
|
|
@@ -2137,7 +2621,18 @@ export function removeBatch(
|
|
|
2137
2621
|
assertBatchScope(items, scope);
|
|
2138
2622
|
|
|
2139
2623
|
if (scope === StorageScope.Memory) {
|
|
2624
|
+
const changes = items.map((item) =>
|
|
2625
|
+
createKeyChange(
|
|
2626
|
+
scope,
|
|
2627
|
+
item.key,
|
|
2628
|
+
getEventRawValue(scope, item.key),
|
|
2629
|
+
undefined,
|
|
2630
|
+
"removeBatch",
|
|
2631
|
+
"memory",
|
|
2632
|
+
),
|
|
2633
|
+
);
|
|
2140
2634
|
items.forEach((item) => item.delete());
|
|
2635
|
+
emitBatchChange(scope, "removeBatch", "memory", changes);
|
|
2141
2636
|
return;
|
|
2142
2637
|
}
|
|
2143
2638
|
|
|
@@ -2148,8 +2643,26 @@ export function removeBatch(
|
|
|
2148
2643
|
if (scope === StorageScope.Secure) {
|
|
2149
2644
|
flushSecureWrites();
|
|
2150
2645
|
}
|
|
2646
|
+
const oldValues = hasStorageChangeObservers(scope)
|
|
2647
|
+
? WebStorage.getBatch(keys, scope)
|
|
2648
|
+
: [];
|
|
2151
2649
|
WebStorage.removeBatch(keys, scope);
|
|
2152
2650
|
keys.forEach((key) => cacheRawValue(scope, key, undefined));
|
|
2651
|
+
emitBatchChange(
|
|
2652
|
+
scope,
|
|
2653
|
+
"removeBatch",
|
|
2654
|
+
"web",
|
|
2655
|
+
keys.map((key, index) =>
|
|
2656
|
+
createKeyChange(
|
|
2657
|
+
scope,
|
|
2658
|
+
key,
|
|
2659
|
+
oldValues[index],
|
|
2660
|
+
undefined,
|
|
2661
|
+
"removeBatch",
|
|
2662
|
+
"web",
|
|
2663
|
+
),
|
|
2664
|
+
),
|
|
2665
|
+
);
|
|
2153
2666
|
},
|
|
2154
2667
|
items.length,
|
|
2155
2668
|
);
|