react-native-nitro-storage 0.5.0 → 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 +40 -6
- package/docs/api-reference.md +95 -31
- package/docs/batch-transactions-migrations.md +23 -9
- package/docs/recipes.md +30 -9
- package/docs/secure-storage.md +19 -0
- package/lib/commonjs/index.js +214 -13
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +166 -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/module/index.js +214 -13
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +166 -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/typescript/index.d.ts +14 -0
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/index.web.d.ts +14 -0
- 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/package.json +5 -4
- package/src/index.ts +534 -13
- package/src/index.web.ts +459 -22
- package/src/storage-events.ts +184 -0
package/src/index.web.ts
CHANGED
|
@@ -26,6 +26,15 @@ import {
|
|
|
26
26
|
type StorageCapabilities,
|
|
27
27
|
type StorageErrorCode,
|
|
28
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";
|
|
29
38
|
|
|
30
39
|
export { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
|
|
31
40
|
export { migrateFromMMKV } from "./migration";
|
|
@@ -36,6 +45,14 @@ export {
|
|
|
36
45
|
type StorageCapabilities,
|
|
37
46
|
type StorageErrorCode,
|
|
38
47
|
} from "./storage-runtime";
|
|
48
|
+
export type {
|
|
49
|
+
StorageBatchChangeEvent,
|
|
50
|
+
StorageChangeEvent,
|
|
51
|
+
StorageChangeOperation,
|
|
52
|
+
StorageChangeSource,
|
|
53
|
+
StorageEventListener,
|
|
54
|
+
StorageKeyChangeEvent,
|
|
55
|
+
} from "./storage-events";
|
|
39
56
|
export type {
|
|
40
57
|
WebStorageBackend,
|
|
41
58
|
WebStorageChangeEvent,
|
|
@@ -64,6 +81,14 @@ export type StorageMetricSummary = {
|
|
|
64
81
|
avgDurationMs: number;
|
|
65
82
|
maxDurationMs: number;
|
|
66
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
|
+
};
|
|
67
92
|
export type MigrationContext = {
|
|
68
93
|
scope: StorageScope;
|
|
69
94
|
getRaw: (key: string) => string | undefined;
|
|
@@ -191,10 +216,12 @@ const BIOMETRIC_WEB_PREFIX = "__bio_";
|
|
|
191
216
|
let hasWarnedAboutWebBiometricFallback = false;
|
|
192
217
|
let hasWindowStorageEventSubscription = false;
|
|
193
218
|
let metricsObserver: StorageMetricsObserver | undefined;
|
|
219
|
+
let eventObserver: StorageEventListener | undefined;
|
|
194
220
|
const metricsCounters = new Map<
|
|
195
221
|
string,
|
|
196
222
|
{ count: number; totalDurationMs: number; maxDurationMs: number }
|
|
197
223
|
>();
|
|
224
|
+
const storageEvents = new StorageEventRegistry();
|
|
198
225
|
|
|
199
226
|
function recordMetric(
|
|
200
227
|
operation: string,
|
|
@@ -379,6 +406,7 @@ function applyExternalChangeEvent(
|
|
|
379
406
|
|
|
380
407
|
if (scope === StorageScope.Secure && key.startsWith(SECURE_WEB_PREFIX)) {
|
|
381
408
|
const plainKey = fromSecureStorageKey(key);
|
|
409
|
+
const oldValue = readCachedRawValue(StorageScope.Secure, plainKey);
|
|
382
410
|
if (newValue === null) {
|
|
383
411
|
ensureWebScopeKeyIndex(StorageScope.Secure).delete(plainKey);
|
|
384
412
|
cacheRawValue(StorageScope.Secure, plainKey, undefined);
|
|
@@ -387,11 +415,20 @@ function applyExternalChangeEvent(
|
|
|
387
415
|
cacheRawValue(StorageScope.Secure, plainKey, newValue);
|
|
388
416
|
}
|
|
389
417
|
notifyKeyListeners(getScopedListeners(StorageScope.Secure), plainKey);
|
|
418
|
+
emitKeyChange(
|
|
419
|
+
StorageScope.Secure,
|
|
420
|
+
plainKey,
|
|
421
|
+
oldValue,
|
|
422
|
+
newValue ?? undefined,
|
|
423
|
+
"external",
|
|
424
|
+
"external",
|
|
425
|
+
);
|
|
390
426
|
return;
|
|
391
427
|
}
|
|
392
428
|
|
|
393
429
|
if (scope === StorageScope.Secure && key.startsWith(BIOMETRIC_WEB_PREFIX)) {
|
|
394
430
|
const plainKey = fromBiometricStorageKey(key);
|
|
431
|
+
const oldValue = readCachedRawValue(StorageScope.Secure, plainKey);
|
|
395
432
|
if (newValue === null) {
|
|
396
433
|
if (
|
|
397
434
|
withWebBackendOperation(
|
|
@@ -408,9 +445,18 @@ function applyExternalChangeEvent(
|
|
|
408
445
|
cacheRawValue(StorageScope.Secure, plainKey, newValue);
|
|
409
446
|
}
|
|
410
447
|
notifyKeyListeners(getScopedListeners(StorageScope.Secure), plainKey);
|
|
448
|
+
emitKeyChange(
|
|
449
|
+
StorageScope.Secure,
|
|
450
|
+
plainKey,
|
|
451
|
+
oldValue,
|
|
452
|
+
newValue ?? undefined,
|
|
453
|
+
"external",
|
|
454
|
+
"external",
|
|
455
|
+
);
|
|
411
456
|
return;
|
|
412
457
|
}
|
|
413
458
|
|
|
459
|
+
const oldValue = readCachedRawValue(scope, key);
|
|
414
460
|
if (newValue === null) {
|
|
415
461
|
ensureWebScopeKeyIndex(scope).delete(key);
|
|
416
462
|
cacheRawValue(scope, key, undefined);
|
|
@@ -419,6 +465,14 @@ function applyExternalChangeEvent(
|
|
|
419
465
|
cacheRawValue(scope, key, newValue);
|
|
420
466
|
}
|
|
421
467
|
notifyKeyListeners(getScopedListeners(scope), key);
|
|
468
|
+
emitKeyChange(
|
|
469
|
+
scope,
|
|
470
|
+
key,
|
|
471
|
+
oldValue,
|
|
472
|
+
newValue ?? undefined,
|
|
473
|
+
"external",
|
|
474
|
+
"external",
|
|
475
|
+
);
|
|
422
476
|
}
|
|
423
477
|
|
|
424
478
|
function handleWebStorageEvent(event: StorageEvent): void {
|
|
@@ -549,6 +603,82 @@ function addKeyListener(
|
|
|
549
603
|
};
|
|
550
604
|
}
|
|
551
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
|
+
|
|
552
682
|
function readPendingSecureWrite(key: string): string | undefined {
|
|
553
683
|
return pendingSecureWrites.get(key)?.value;
|
|
554
684
|
}
|
|
@@ -833,24 +963,10 @@ const WebStorage: Storage = {
|
|
|
833
963
|
return () => {};
|
|
834
964
|
},
|
|
835
965
|
has: (key: string, scope: number) => {
|
|
836
|
-
if (scope === StorageScope.Secure) {
|
|
837
|
-
return (
|
|
838
|
-
withWebBackendOperation(scope, "has", (backend) =>
|
|
839
|
-
backend.getItem(toSecureStorageKey(key)),
|
|
840
|
-
) !== null ||
|
|
841
|
-
withWebBackendOperation(scope, "has", (backend) =>
|
|
842
|
-
backend.getItem(toBiometricStorageKey(key)),
|
|
843
|
-
) !== null
|
|
844
|
-
);
|
|
845
|
-
}
|
|
846
|
-
if (scope !== StorageScope.Disk) {
|
|
847
|
-
return false;
|
|
966
|
+
if (scope === StorageScope.Disk || scope === StorageScope.Secure) {
|
|
967
|
+
return ensureWebScopeKeyIndex(scope).has(key);
|
|
848
968
|
}
|
|
849
|
-
return
|
|
850
|
-
withWebBackendOperation(scope, "has", (backend) =>
|
|
851
|
-
backend.getItem(key),
|
|
852
|
-
) !== null
|
|
853
|
-
);
|
|
969
|
+
return false;
|
|
854
970
|
},
|
|
855
971
|
getAllKeys: (scope: number) => {
|
|
856
972
|
if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
|
|
@@ -1000,9 +1116,12 @@ function getRawValue(key: string, scope: StorageScope): string | undefined {
|
|
|
1000
1116
|
|
|
1001
1117
|
function setRawValue(key: string, value: string, scope: StorageScope): void {
|
|
1002
1118
|
assertValidScope(scope);
|
|
1119
|
+
const oldValue =
|
|
1120
|
+
scope === StorageScope.Memory ? getEventRawValue(scope, key) : undefined;
|
|
1003
1121
|
if (scope === StorageScope.Memory) {
|
|
1004
1122
|
memoryStore.set(key, value);
|
|
1005
1123
|
notifyKeyListeners(memoryListeners, key);
|
|
1124
|
+
emitKeyChange(scope, key, oldValue, value, "set", "memory");
|
|
1006
1125
|
return;
|
|
1007
1126
|
}
|
|
1008
1127
|
|
|
@@ -1010,6 +1129,7 @@ function setRawValue(key: string, value: string, scope: StorageScope): void {
|
|
|
1010
1129
|
cacheRawValue(scope, key, value);
|
|
1011
1130
|
if (diskWritesAsync) {
|
|
1012
1131
|
scheduleDiskWrite(key, value);
|
|
1132
|
+
emitKeyChange(scope, key, oldValue, value, "set", "web");
|
|
1013
1133
|
return;
|
|
1014
1134
|
}
|
|
1015
1135
|
|
|
@@ -1024,13 +1144,16 @@ function setRawValue(key: string, value: string, scope: StorageScope): void {
|
|
|
1024
1144
|
|
|
1025
1145
|
WebStorage.set(key, value, scope);
|
|
1026
1146
|
cacheRawValue(scope, key, value);
|
|
1147
|
+
emitKeyChange(scope, key, oldValue, value, "set", "web");
|
|
1027
1148
|
}
|
|
1028
1149
|
|
|
1029
1150
|
function removeRawValue(key: string, scope: StorageScope): void {
|
|
1030
1151
|
assertValidScope(scope);
|
|
1152
|
+
const oldValue = getEventRawValue(scope, key);
|
|
1031
1153
|
if (scope === StorageScope.Memory) {
|
|
1032
1154
|
memoryStore.delete(key);
|
|
1033
1155
|
notifyKeyListeners(memoryListeners, key);
|
|
1156
|
+
emitKeyChange(scope, key, oldValue, undefined, "remove", "memory");
|
|
1034
1157
|
return;
|
|
1035
1158
|
}
|
|
1036
1159
|
|
|
@@ -1038,6 +1161,7 @@ function removeRawValue(key: string, scope: StorageScope): void {
|
|
|
1038
1161
|
cacheRawValue(scope, key, undefined);
|
|
1039
1162
|
if (diskWritesAsync) {
|
|
1040
1163
|
scheduleDiskWrite(key, undefined);
|
|
1164
|
+
emitKeyChange(scope, key, oldValue, undefined, "remove", "web");
|
|
1041
1165
|
return;
|
|
1042
1166
|
}
|
|
1043
1167
|
|
|
@@ -1052,6 +1176,7 @@ function removeRawValue(key: string, scope: StorageScope): void {
|
|
|
1052
1176
|
|
|
1053
1177
|
WebStorage.remove(key, scope);
|
|
1054
1178
|
cacheRawValue(scope, key, undefined);
|
|
1179
|
+
emitKeyChange(scope, key, oldValue, undefined, "remove", "web");
|
|
1055
1180
|
}
|
|
1056
1181
|
|
|
1057
1182
|
function readMigrationVersion(scope: StorageScope): number {
|
|
@@ -1069,11 +1194,74 @@ function writeMigrationVersion(scope: StorageScope, version: number): void {
|
|
|
1069
1194
|
}
|
|
1070
1195
|
|
|
1071
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
|
+
},
|
|
1072
1242
|
clear: (scope: StorageScope) => {
|
|
1073
1243
|
measureOperation("storage:clear", scope, () => {
|
|
1244
|
+
const previousValues = hasStorageChangeObservers(scope)
|
|
1245
|
+
? storage.getAll(scope)
|
|
1246
|
+
: {};
|
|
1074
1247
|
if (scope === StorageScope.Memory) {
|
|
1075
1248
|
memoryStore.clear();
|
|
1076
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
|
+
);
|
|
1077
1265
|
return;
|
|
1078
1266
|
}
|
|
1079
1267
|
|
|
@@ -1089,6 +1277,21 @@ export const storage = {
|
|
|
1089
1277
|
|
|
1090
1278
|
clearScopeRawCache(scope);
|
|
1091
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
|
+
);
|
|
1092
1295
|
});
|
|
1093
1296
|
},
|
|
1094
1297
|
clearAll: () => {
|
|
@@ -1107,16 +1310,44 @@ export const storage = {
|
|
|
1107
1310
|
measureOperation("storage:clearNamespace", scope, () => {
|
|
1108
1311
|
assertValidScope(scope);
|
|
1109
1312
|
if (scope === StorageScope.Memory) {
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
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;
|
|
1114
1323
|
}
|
|
1115
|
-
|
|
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
|
+
);
|
|
1116
1344
|
return;
|
|
1117
1345
|
}
|
|
1118
1346
|
|
|
1119
1347
|
const keyPrefix = prefixKey(namespace, "");
|
|
1348
|
+
const previousValues = hasStorageChangeObservers(scope)
|
|
1349
|
+
? storage.getByPrefix(keyPrefix, scope)
|
|
1350
|
+
: {};
|
|
1120
1351
|
if (scope === StorageScope.Disk) {
|
|
1121
1352
|
flushDiskWrites();
|
|
1122
1353
|
}
|
|
@@ -1130,6 +1361,21 @@ export const storage = {
|
|
|
1130
1361
|
}
|
|
1131
1362
|
}
|
|
1132
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
|
+
);
|
|
1133
1379
|
});
|
|
1134
1380
|
},
|
|
1135
1381
|
clearBiometric: () => {
|
|
@@ -1245,6 +1491,11 @@ export const storage = {
|
|
|
1245
1491
|
return result;
|
|
1246
1492
|
});
|
|
1247
1493
|
},
|
|
1494
|
+
export: (scope: StorageScope): Record<string, string> => {
|
|
1495
|
+
return measureOperation("storage:export", scope, () =>
|
|
1496
|
+
storage.getAll(scope),
|
|
1497
|
+
);
|
|
1498
|
+
},
|
|
1248
1499
|
size: (scope: StorageScope): number => {
|
|
1249
1500
|
return measureOperation("storage:size", scope, () => {
|
|
1250
1501
|
assertValidScope(scope);
|
|
@@ -1407,12 +1658,23 @@ export const storage = {
|
|
|
1407
1658
|
assertValidScope(scope);
|
|
1408
1659
|
if (keys.length === 0) return;
|
|
1409
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
|
+
);
|
|
1410
1671
|
|
|
1411
1672
|
if (scope === StorageScope.Memory) {
|
|
1412
1673
|
keys.forEach((key, index) => {
|
|
1413
1674
|
memoryStore.set(key, values[index]);
|
|
1414
1675
|
});
|
|
1415
1676
|
keys.forEach((key) => notifyKeyListeners(memoryListeners, key));
|
|
1677
|
+
emitBatchChange(scope, "import", "memory", changes);
|
|
1416
1678
|
return;
|
|
1417
1679
|
}
|
|
1418
1680
|
|
|
@@ -1426,6 +1688,7 @@ export const storage = {
|
|
|
1426
1688
|
|
|
1427
1689
|
WebStorage.setBatch(keys, values, scope);
|
|
1428
1690
|
keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
|
|
1691
|
+
emitBatchChange(scope, "import", "web", changes);
|
|
1429
1692
|
},
|
|
1430
1693
|
keys.length,
|
|
1431
1694
|
);
|
|
@@ -1512,6 +1775,11 @@ export interface StorageItem<T> {
|
|
|
1512
1775
|
delete: () => void;
|
|
1513
1776
|
has: () => boolean;
|
|
1514
1777
|
subscribe: (callback: () => void) => () => void;
|
|
1778
|
+
subscribeSelector: <TSelected>(
|
|
1779
|
+
selector: (value: T) => TSelected,
|
|
1780
|
+
listener: StorageSelectorListener<TSelected>,
|
|
1781
|
+
options?: StorageSelectorSubscribeOptions<TSelected>,
|
|
1782
|
+
) => () => void;
|
|
1515
1783
|
serialize: (value: T) => string;
|
|
1516
1784
|
deserialize: (value: string) => T;
|
|
1517
1785
|
scope: StorageScope;
|
|
@@ -1680,12 +1948,17 @@ export function createStorageItem<T = undefined>(
|
|
|
1680
1948
|
};
|
|
1681
1949
|
|
|
1682
1950
|
const writeStoredRaw = (rawValue: string): void => {
|
|
1951
|
+
const oldValue =
|
|
1952
|
+
config.scope === StorageScope.Memory
|
|
1953
|
+
? getEventRawValue(config.scope, storageKey)
|
|
1954
|
+
: undefined;
|
|
1683
1955
|
if (isBiometric) {
|
|
1684
1956
|
WebStorage.setSecureBiometricWithLevel(
|
|
1685
1957
|
storageKey,
|
|
1686
1958
|
rawValue,
|
|
1687
1959
|
resolvedBiometricLevel,
|
|
1688
1960
|
);
|
|
1961
|
+
emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", "web");
|
|
1689
1962
|
return;
|
|
1690
1963
|
}
|
|
1691
1964
|
|
|
@@ -1694,6 +1967,14 @@ export function createStorageItem<T = undefined>(
|
|
|
1694
1967
|
if (nonMemoryScope === StorageScope.Disk) {
|
|
1695
1968
|
if (coalesceDiskWrites || diskWritesAsync) {
|
|
1696
1969
|
scheduleDiskWrite(storageKey, rawValue);
|
|
1970
|
+
emitKeyChange(
|
|
1971
|
+
config.scope,
|
|
1972
|
+
storageKey,
|
|
1973
|
+
oldValue,
|
|
1974
|
+
rawValue,
|
|
1975
|
+
"set",
|
|
1976
|
+
"web",
|
|
1977
|
+
);
|
|
1697
1978
|
return;
|
|
1698
1979
|
}
|
|
1699
1980
|
|
|
@@ -1706,6 +1987,7 @@ export function createStorageItem<T = undefined>(
|
|
|
1706
1987
|
rawValue,
|
|
1707
1988
|
secureAccessControl ?? secureDefaultAccessControl,
|
|
1708
1989
|
);
|
|
1990
|
+
emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", "web");
|
|
1709
1991
|
return;
|
|
1710
1992
|
}
|
|
1711
1993
|
|
|
@@ -1714,11 +1996,21 @@ export function createStorageItem<T = undefined>(
|
|
|
1714
1996
|
}
|
|
1715
1997
|
|
|
1716
1998
|
WebStorage.set(storageKey, rawValue, config.scope);
|
|
1999
|
+
emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", "web");
|
|
1717
2000
|
};
|
|
1718
2001
|
|
|
1719
2002
|
const removeStoredRaw = (): void => {
|
|
2003
|
+
const oldValue = getEventRawValue(config.scope, storageKey);
|
|
1720
2004
|
if (isBiometric) {
|
|
1721
2005
|
WebStorage.deleteSecureBiometric(storageKey);
|
|
2006
|
+
emitKeyChange(
|
|
2007
|
+
config.scope,
|
|
2008
|
+
storageKey,
|
|
2009
|
+
oldValue,
|
|
2010
|
+
undefined,
|
|
2011
|
+
"remove",
|
|
2012
|
+
"web",
|
|
2013
|
+
);
|
|
1722
2014
|
return;
|
|
1723
2015
|
}
|
|
1724
2016
|
|
|
@@ -1727,6 +2019,14 @@ export function createStorageItem<T = undefined>(
|
|
|
1727
2019
|
if (nonMemoryScope === StorageScope.Disk) {
|
|
1728
2020
|
if (coalesceDiskWrites || diskWritesAsync) {
|
|
1729
2021
|
scheduleDiskWrite(storageKey, undefined);
|
|
2022
|
+
emitKeyChange(
|
|
2023
|
+
config.scope,
|
|
2024
|
+
storageKey,
|
|
2025
|
+
oldValue,
|
|
2026
|
+
undefined,
|
|
2027
|
+
"remove",
|
|
2028
|
+
"web",
|
|
2029
|
+
);
|
|
1730
2030
|
return;
|
|
1731
2031
|
}
|
|
1732
2032
|
|
|
@@ -1739,6 +2039,14 @@ export function createStorageItem<T = undefined>(
|
|
|
1739
2039
|
undefined,
|
|
1740
2040
|
secureAccessControl ?? secureDefaultAccessControl,
|
|
1741
2041
|
);
|
|
2042
|
+
emitKeyChange(
|
|
2043
|
+
config.scope,
|
|
2044
|
+
storageKey,
|
|
2045
|
+
oldValue,
|
|
2046
|
+
undefined,
|
|
2047
|
+
"remove",
|
|
2048
|
+
"web",
|
|
2049
|
+
);
|
|
1742
2050
|
return;
|
|
1743
2051
|
}
|
|
1744
2052
|
|
|
@@ -1747,15 +2055,32 @@ export function createStorageItem<T = undefined>(
|
|
|
1747
2055
|
}
|
|
1748
2056
|
|
|
1749
2057
|
WebStorage.remove(storageKey, config.scope);
|
|
2058
|
+
emitKeyChange(
|
|
2059
|
+
config.scope,
|
|
2060
|
+
storageKey,
|
|
2061
|
+
oldValue,
|
|
2062
|
+
undefined,
|
|
2063
|
+
"remove",
|
|
2064
|
+
"web",
|
|
2065
|
+
);
|
|
1750
2066
|
};
|
|
1751
2067
|
|
|
1752
2068
|
const writeValueWithoutValidation = (value: T): void => {
|
|
1753
2069
|
if (isMemory) {
|
|
2070
|
+
const oldValue = getEventRawValue(config.scope, storageKey);
|
|
1754
2071
|
if (memoryExpiration) {
|
|
1755
2072
|
memoryExpiration.set(storageKey, Date.now() + (expirationTtlMs ?? 0));
|
|
1756
2073
|
}
|
|
1757
2074
|
memoryStore.set(storageKey, value);
|
|
1758
2075
|
notifyKeyListeners(memoryListeners, storageKey);
|
|
2076
|
+
emitKeyChange(
|
|
2077
|
+
config.scope,
|
|
2078
|
+
storageKey,
|
|
2079
|
+
oldValue,
|
|
2080
|
+
typeof value === "string" ? value : undefined,
|
|
2081
|
+
"set",
|
|
2082
|
+
"memory",
|
|
2083
|
+
);
|
|
1759
2084
|
return;
|
|
1760
2085
|
}
|
|
1761
2086
|
|
|
@@ -1927,11 +2252,20 @@ export function createStorageItem<T = undefined>(
|
|
|
1927
2252
|
invalidateParsedCache();
|
|
1928
2253
|
|
|
1929
2254
|
if (isMemory) {
|
|
2255
|
+
const oldValue = getEventRawValue(config.scope, storageKey);
|
|
1930
2256
|
if (memoryExpiration) {
|
|
1931
2257
|
memoryExpiration.delete(storageKey);
|
|
1932
2258
|
}
|
|
1933
2259
|
memoryStore.delete(storageKey);
|
|
1934
2260
|
notifyKeyListeners(memoryListeners, storageKey);
|
|
2261
|
+
emitKeyChange(
|
|
2262
|
+
config.scope,
|
|
2263
|
+
storageKey,
|
|
2264
|
+
oldValue,
|
|
2265
|
+
undefined,
|
|
2266
|
+
"remove",
|
|
2267
|
+
"memory",
|
|
2268
|
+
);
|
|
1935
2269
|
return;
|
|
1936
2270
|
}
|
|
1937
2271
|
|
|
@@ -1970,6 +2304,30 @@ export function createStorageItem<T = undefined>(
|
|
|
1970
2304
|
};
|
|
1971
2305
|
};
|
|
1972
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
|
+
|
|
1973
2331
|
const storageItem: StorageItemInternal<T> = {
|
|
1974
2332
|
get,
|
|
1975
2333
|
getWithVersion,
|
|
@@ -1978,6 +2336,7 @@ export function createStorageItem<T = undefined>(
|
|
|
1978
2336
|
delete: deleteItem,
|
|
1979
2337
|
has: hasItem,
|
|
1980
2338
|
subscribe,
|
|
2339
|
+
subscribeSelector,
|
|
1981
2340
|
serialize,
|
|
1982
2341
|
deserialize,
|
|
1983
2342
|
_triggerListeners: () => {
|
|
@@ -2130,6 +2489,17 @@ export function setBatch<T>(
|
|
|
2130
2489
|
return;
|
|
2131
2490
|
}
|
|
2132
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
|
+
|
|
2133
2503
|
// Atomic write: update all values in memoryStore, invalidate caches, then batch-notify
|
|
2134
2504
|
items.forEach(({ item, value }) => {
|
|
2135
2505
|
memoryStore.set(item.key, value);
|
|
@@ -2138,6 +2508,7 @@ export function setBatch<T>(
|
|
|
2138
2508
|
items.forEach(({ item }) =>
|
|
2139
2509
|
notifyKeyListeners(memoryListeners, item.key),
|
|
2140
2510
|
);
|
|
2511
|
+
emitBatchChange(scope, "setBatch", "memory", changes);
|
|
2141
2512
|
return;
|
|
2142
2513
|
}
|
|
2143
2514
|
|
|
@@ -2156,6 +2527,10 @@ export function setBatch<T>(
|
|
|
2156
2527
|
}
|
|
2157
2528
|
|
|
2158
2529
|
flushSecureWrites();
|
|
2530
|
+
const keys = secureEntries.map(({ item }) => item.key);
|
|
2531
|
+
const oldValues = hasStorageChangeObservers(scope)
|
|
2532
|
+
? WebStorage.getBatch(keys, scope)
|
|
2533
|
+
: [];
|
|
2159
2534
|
const groupedByAccessControl = new Map<
|
|
2160
2535
|
number,
|
|
2161
2536
|
{ keys: string[]; values: string[] }
|
|
@@ -2180,6 +2555,21 @@ export function setBatch<T>(
|
|
|
2180
2555
|
cacheRawValue(scope, key, group.values[index]),
|
|
2181
2556
|
);
|
|
2182
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
|
+
);
|
|
2183
2573
|
return;
|
|
2184
2574
|
}
|
|
2185
2575
|
|
|
@@ -2195,8 +2585,26 @@ export function setBatch<T>(
|
|
|
2195
2585
|
|
|
2196
2586
|
const keys = items.map((entry) => entry.item.key);
|
|
2197
2587
|
const values = items.map((entry) => entry.item.serialize(entry.value));
|
|
2588
|
+
const oldValues = hasStorageChangeObservers(scope)
|
|
2589
|
+
? WebStorage.getBatch(keys, scope)
|
|
2590
|
+
: [];
|
|
2198
2591
|
WebStorage.setBatch(keys, values, scope);
|
|
2199
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
|
+
);
|
|
2200
2608
|
},
|
|
2201
2609
|
items.length,
|
|
2202
2610
|
);
|
|
@@ -2213,7 +2621,18 @@ export function removeBatch(
|
|
|
2213
2621
|
assertBatchScope(items, scope);
|
|
2214
2622
|
|
|
2215
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
|
+
);
|
|
2216
2634
|
items.forEach((item) => item.delete());
|
|
2635
|
+
emitBatchChange(scope, "removeBatch", "memory", changes);
|
|
2217
2636
|
return;
|
|
2218
2637
|
}
|
|
2219
2638
|
|
|
@@ -2224,8 +2643,26 @@ export function removeBatch(
|
|
|
2224
2643
|
if (scope === StorageScope.Secure) {
|
|
2225
2644
|
flushSecureWrites();
|
|
2226
2645
|
}
|
|
2646
|
+
const oldValues = hasStorageChangeObservers(scope)
|
|
2647
|
+
? WebStorage.getBatch(keys, scope)
|
|
2648
|
+
: [];
|
|
2227
2649
|
WebStorage.removeBatch(keys, scope);
|
|
2228
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
|
+
);
|
|
2229
2666
|
},
|
|
2230
2667
|
items.length,
|
|
2231
2668
|
);
|