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/lib/module/index.web.js
CHANGED
|
@@ -4,6 +4,7 @@ import { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
|
|
|
4
4
|
import { MIGRATION_VERSION_KEY, isStoredEnvelope, assertBatchScope, assertValidScope, serializeWithPrimitiveFastPath, deserializeWithPrimitiveFastPath, toVersionToken, prefixKey, isNamespaced } from "./internal";
|
|
5
5
|
import { createLocalStorageWebBackend } from "./web-storage-backend";
|
|
6
6
|
import { getStorageErrorCode, isLockedStorageErrorCode } from "./storage-runtime";
|
|
7
|
+
import { StorageEventRegistry } from "./storage-events";
|
|
7
8
|
export { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
|
|
8
9
|
export { migrateFromMMKV } from "./migration";
|
|
9
10
|
export { getStorageErrorCode } from "./storage-runtime";
|
|
@@ -38,7 +39,9 @@ const BIOMETRIC_WEB_PREFIX = "__bio_";
|
|
|
38
39
|
let hasWarnedAboutWebBiometricFallback = false;
|
|
39
40
|
let hasWindowStorageEventSubscription = false;
|
|
40
41
|
let metricsObserver;
|
|
42
|
+
let eventObserver;
|
|
41
43
|
const metricsCounters = new Map();
|
|
44
|
+
const storageEvents = new StorageEventRegistry();
|
|
42
45
|
function recordMetric(operation, scope, durationMs, keysCount = 1) {
|
|
43
46
|
const existing = metricsCounters.get(operation);
|
|
44
47
|
if (!existing) {
|
|
@@ -89,6 +92,9 @@ function getBackendName(scope, backend) {
|
|
|
89
92
|
const scopeName = scope === StorageScope.Disk ? "disk" : "secure";
|
|
90
93
|
return backend?.name ?? `web:${scopeName}`;
|
|
91
94
|
}
|
|
95
|
+
function getWebSecureEncryptionStatus(backend) {
|
|
96
|
+
return backend?.name === "localStorage:secure" ? "unavailable" : "unknown";
|
|
97
|
+
}
|
|
92
98
|
function createWebStorageError(scope, operation, error, backend) {
|
|
93
99
|
const backendName = getBackendName(scope, backend);
|
|
94
100
|
const message = error instanceof Error ? error.message : String(error ?? "Unknown error");
|
|
@@ -160,6 +166,7 @@ function applyExternalChangeEvent(scope, key, newValue) {
|
|
|
160
166
|
}
|
|
161
167
|
if (scope === StorageScope.Secure && key.startsWith(SECURE_WEB_PREFIX)) {
|
|
162
168
|
const plainKey = fromSecureStorageKey(key);
|
|
169
|
+
const oldValue = readCachedRawValue(StorageScope.Secure, plainKey);
|
|
163
170
|
if (newValue === null) {
|
|
164
171
|
ensureWebScopeKeyIndex(StorageScope.Secure).delete(plainKey);
|
|
165
172
|
cacheRawValue(StorageScope.Secure, plainKey, undefined);
|
|
@@ -168,10 +175,12 @@ function applyExternalChangeEvent(scope, key, newValue) {
|
|
|
168
175
|
cacheRawValue(StorageScope.Secure, plainKey, newValue);
|
|
169
176
|
}
|
|
170
177
|
notifyKeyListeners(getScopedListeners(StorageScope.Secure), plainKey);
|
|
178
|
+
emitKeyChange(StorageScope.Secure, plainKey, oldValue, newValue ?? undefined, "external", "external");
|
|
171
179
|
return;
|
|
172
180
|
}
|
|
173
181
|
if (scope === StorageScope.Secure && key.startsWith(BIOMETRIC_WEB_PREFIX)) {
|
|
174
182
|
const plainKey = fromBiometricStorageKey(key);
|
|
183
|
+
const oldValue = readCachedRawValue(StorageScope.Secure, plainKey);
|
|
175
184
|
if (newValue === null) {
|
|
176
185
|
if (withWebBackendOperation(StorageScope.Secure, "external-sync:getItem", backend => backend.getItem(toSecureStorageKey(plainKey))) === null) {
|
|
177
186
|
ensureWebScopeKeyIndex(StorageScope.Secure).delete(plainKey);
|
|
@@ -182,8 +191,10 @@ function applyExternalChangeEvent(scope, key, newValue) {
|
|
|
182
191
|
cacheRawValue(StorageScope.Secure, plainKey, newValue);
|
|
183
192
|
}
|
|
184
193
|
notifyKeyListeners(getScopedListeners(StorageScope.Secure), plainKey);
|
|
194
|
+
emitKeyChange(StorageScope.Secure, plainKey, oldValue, newValue ?? undefined, "external", "external");
|
|
185
195
|
return;
|
|
186
196
|
}
|
|
197
|
+
const oldValue = readCachedRawValue(scope, key);
|
|
187
198
|
if (newValue === null) {
|
|
188
199
|
ensureWebScopeKeyIndex(scope).delete(key);
|
|
189
200
|
cacheRawValue(scope, key, undefined);
|
|
@@ -192,6 +203,7 @@ function applyExternalChangeEvent(scope, key, newValue) {
|
|
|
192
203
|
cacheRawValue(scope, key, newValue);
|
|
193
204
|
}
|
|
194
205
|
notifyKeyListeners(getScopedListeners(scope), key);
|
|
206
|
+
emitKeyChange(scope, key, oldValue, newValue ?? undefined, "external", "external");
|
|
195
207
|
}
|
|
196
208
|
function handleWebStorageEvent(event) {
|
|
197
209
|
const key = event.key;
|
|
@@ -282,6 +294,46 @@ function addKeyListener(registry, key, listener) {
|
|
|
282
294
|
}
|
|
283
295
|
};
|
|
284
296
|
}
|
|
297
|
+
function getEventRawValue(scope, key) {
|
|
298
|
+
if (scope === StorageScope.Memory) {
|
|
299
|
+
const value = memoryStore.get(key);
|
|
300
|
+
return typeof value === "string" ? value : undefined;
|
|
301
|
+
}
|
|
302
|
+
return getRawValue(key, scope);
|
|
303
|
+
}
|
|
304
|
+
function createKeyChange(scope, key, oldValue, newValue, operation, source) {
|
|
305
|
+
return {
|
|
306
|
+
type: "key",
|
|
307
|
+
scope,
|
|
308
|
+
key,
|
|
309
|
+
oldValue,
|
|
310
|
+
newValue,
|
|
311
|
+
operation,
|
|
312
|
+
source
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
function hasStorageChangeObservers(scope) {
|
|
316
|
+
return storageEvents.hasListeners(scope) || eventObserver !== undefined;
|
|
317
|
+
}
|
|
318
|
+
function emitKeyChange(scope, key, oldValue, newValue, operation, source) {
|
|
319
|
+
const event = createKeyChange(scope, key, oldValue, newValue, operation, source);
|
|
320
|
+
storageEvents.emitKey(event);
|
|
321
|
+
eventObserver?.(event);
|
|
322
|
+
}
|
|
323
|
+
function emitBatchChange(scope, operation, source, changes) {
|
|
324
|
+
if (changes.length === 0) {
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
const event = {
|
|
328
|
+
type: "batch",
|
|
329
|
+
scope,
|
|
330
|
+
operation,
|
|
331
|
+
source,
|
|
332
|
+
changes
|
|
333
|
+
};
|
|
334
|
+
storageEvents.emitBatch(event);
|
|
335
|
+
eventObserver?.(event);
|
|
336
|
+
}
|
|
285
337
|
function readPendingSecureWrite(key) {
|
|
286
338
|
return pendingSecureWrites.get(key)?.value;
|
|
287
339
|
}
|
|
@@ -531,13 +583,10 @@ const WebStorage = {
|
|
|
531
583
|
return () => {};
|
|
532
584
|
},
|
|
533
585
|
has: (key, scope) => {
|
|
534
|
-
if (scope === StorageScope.Secure) {
|
|
535
|
-
return
|
|
536
|
-
}
|
|
537
|
-
if (scope !== StorageScope.Disk) {
|
|
538
|
-
return false;
|
|
586
|
+
if (scope === StorageScope.Disk || scope === StorageScope.Secure) {
|
|
587
|
+
return ensureWebScopeKeyIndex(scope).has(key);
|
|
539
588
|
}
|
|
540
|
-
return
|
|
589
|
+
return false;
|
|
541
590
|
},
|
|
542
591
|
getAllKeys: scope => {
|
|
543
592
|
if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
|
|
@@ -628,15 +677,18 @@ function getRawValue(key, scope) {
|
|
|
628
677
|
}
|
|
629
678
|
function setRawValue(key, value, scope) {
|
|
630
679
|
assertValidScope(scope);
|
|
680
|
+
const oldValue = scope === StorageScope.Memory ? getEventRawValue(scope, key) : undefined;
|
|
631
681
|
if (scope === StorageScope.Memory) {
|
|
632
682
|
memoryStore.set(key, value);
|
|
633
683
|
notifyKeyListeners(memoryListeners, key);
|
|
684
|
+
emitKeyChange(scope, key, oldValue, value, "set", "memory");
|
|
634
685
|
return;
|
|
635
686
|
}
|
|
636
687
|
if (scope === StorageScope.Disk) {
|
|
637
688
|
cacheRawValue(scope, key, value);
|
|
638
689
|
if (diskWritesAsync) {
|
|
639
690
|
scheduleDiskWrite(key, value);
|
|
691
|
+
emitKeyChange(scope, key, oldValue, value, "set", "web");
|
|
640
692
|
return;
|
|
641
693
|
}
|
|
642
694
|
flushDiskWrites();
|
|
@@ -648,18 +700,22 @@ function setRawValue(key, value, scope) {
|
|
|
648
700
|
}
|
|
649
701
|
WebStorage.set(key, value, scope);
|
|
650
702
|
cacheRawValue(scope, key, value);
|
|
703
|
+
emitKeyChange(scope, key, oldValue, value, "set", "web");
|
|
651
704
|
}
|
|
652
705
|
function removeRawValue(key, scope) {
|
|
653
706
|
assertValidScope(scope);
|
|
707
|
+
const oldValue = getEventRawValue(scope, key);
|
|
654
708
|
if (scope === StorageScope.Memory) {
|
|
655
709
|
memoryStore.delete(key);
|
|
656
710
|
notifyKeyListeners(memoryListeners, key);
|
|
711
|
+
emitKeyChange(scope, key, oldValue, undefined, "remove", "memory");
|
|
657
712
|
return;
|
|
658
713
|
}
|
|
659
714
|
if (scope === StorageScope.Disk) {
|
|
660
715
|
cacheRawValue(scope, key, undefined);
|
|
661
716
|
if (diskWritesAsync) {
|
|
662
717
|
scheduleDiskWrite(key, undefined);
|
|
718
|
+
emitKeyChange(scope, key, oldValue, undefined, "remove", "web");
|
|
663
719
|
return;
|
|
664
720
|
}
|
|
665
721
|
flushDiskWrites();
|
|
@@ -671,6 +727,7 @@ function removeRawValue(key, scope) {
|
|
|
671
727
|
}
|
|
672
728
|
WebStorage.remove(key, scope);
|
|
673
729
|
cacheRawValue(scope, key, undefined);
|
|
730
|
+
emitKeyChange(scope, key, oldValue, undefined, "remove", "web");
|
|
674
731
|
}
|
|
675
732
|
function readMigrationVersion(scope) {
|
|
676
733
|
const raw = getRawValue(MIGRATION_VERSION_KEY, scope);
|
|
@@ -684,11 +741,43 @@ function writeMigrationVersion(scope, version) {
|
|
|
684
741
|
setRawValue(MIGRATION_VERSION_KEY, String(version), scope);
|
|
685
742
|
}
|
|
686
743
|
export const storage = {
|
|
744
|
+
subscribe: (scope, listener) => {
|
|
745
|
+
assertValidScope(scope);
|
|
746
|
+
if (scope !== StorageScope.Memory) {
|
|
747
|
+
ensureExternalSyncSubscriptions();
|
|
748
|
+
}
|
|
749
|
+
return storageEvents.subscribe(scope, listener);
|
|
750
|
+
},
|
|
751
|
+
subscribeKey: (scope, key, listener) => {
|
|
752
|
+
assertValidScope(scope);
|
|
753
|
+
if (scope !== StorageScope.Memory) {
|
|
754
|
+
ensureExternalSyncSubscriptions();
|
|
755
|
+
}
|
|
756
|
+
return storageEvents.subscribeKey(scope, key, listener);
|
|
757
|
+
},
|
|
758
|
+
subscribePrefix: (scope, prefix, listener) => {
|
|
759
|
+
assertValidScope(scope);
|
|
760
|
+
if (scope !== StorageScope.Memory) {
|
|
761
|
+
ensureExternalSyncSubscriptions();
|
|
762
|
+
}
|
|
763
|
+
return storageEvents.subscribePrefix(scope, prefix, listener);
|
|
764
|
+
},
|
|
765
|
+
subscribeNamespace: (namespace, scope, listener) => {
|
|
766
|
+
return storage.subscribePrefix(scope, prefixKey(namespace, ""), listener);
|
|
767
|
+
},
|
|
768
|
+
setEventObserver: observer => {
|
|
769
|
+
eventObserver = observer;
|
|
770
|
+
if (observer) {
|
|
771
|
+
ensureExternalSyncSubscriptions();
|
|
772
|
+
}
|
|
773
|
+
},
|
|
687
774
|
clear: scope => {
|
|
688
775
|
measureOperation("storage:clear", scope, () => {
|
|
776
|
+
const previousValues = hasStorageChangeObservers(scope) ? storage.getAll(scope) : {};
|
|
689
777
|
if (scope === StorageScope.Memory) {
|
|
690
778
|
memoryStore.clear();
|
|
691
779
|
notifyAllListeners(memoryListeners);
|
|
780
|
+
emitBatchChange(scope, "clear", "memory", Object.keys(previousValues).map(key => createKeyChange(scope, key, previousValues[key], undefined, "clear", "memory")));
|
|
692
781
|
return;
|
|
693
782
|
}
|
|
694
783
|
if (scope === StorageScope.Disk) {
|
|
@@ -701,6 +790,7 @@ export const storage = {
|
|
|
701
790
|
}
|
|
702
791
|
clearScopeRawCache(scope);
|
|
703
792
|
WebStorage.clear(scope);
|
|
793
|
+
emitBatchChange(scope, "clear", "web", Object.keys(previousValues).map(key => createKeyChange(scope, key, previousValues[key], undefined, "clear", "web")));
|
|
704
794
|
});
|
|
705
795
|
},
|
|
706
796
|
clearAll: () => {
|
|
@@ -714,15 +804,26 @@ export const storage = {
|
|
|
714
804
|
measureOperation("storage:clearNamespace", scope, () => {
|
|
715
805
|
assertValidScope(scope);
|
|
716
806
|
if (scope === StorageScope.Memory) {
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
807
|
+
const affectedKeys = Array.from(memoryStore.keys()).filter(key => isNamespaced(key, namespace));
|
|
808
|
+
const previousValues = affectedKeys.map(key => ({
|
|
809
|
+
key,
|
|
810
|
+
value: getEventRawValue(scope, key)
|
|
811
|
+
}));
|
|
812
|
+
if (affectedKeys.length === 0) {
|
|
813
|
+
return;
|
|
721
814
|
}
|
|
722
|
-
|
|
815
|
+
affectedKeys.forEach(key => {
|
|
816
|
+
memoryStore.delete(key);
|
|
817
|
+
});
|
|
818
|
+
affectedKeys.forEach(key => notifyKeyListeners(memoryListeners, key));
|
|
819
|
+
emitBatchChange(scope, "clearNamespace", "memory", previousValues.map(({
|
|
820
|
+
key,
|
|
821
|
+
value
|
|
822
|
+
}) => createKeyChange(scope, key, value, undefined, "clearNamespace", "memory")));
|
|
723
823
|
return;
|
|
724
824
|
}
|
|
725
825
|
const keyPrefix = prefixKey(namespace, "");
|
|
826
|
+
const previousValues = hasStorageChangeObservers(scope) ? storage.getByPrefix(keyPrefix, scope) : {};
|
|
726
827
|
if (scope === StorageScope.Disk) {
|
|
727
828
|
flushDiskWrites();
|
|
728
829
|
}
|
|
@@ -736,6 +837,7 @@ export const storage = {
|
|
|
736
837
|
}
|
|
737
838
|
}
|
|
738
839
|
WebStorage.removeByPrefix(keyPrefix, scope);
|
|
840
|
+
emitBatchChange(scope, "clearNamespace", "web", Object.keys(previousValues).map(key => createKeyChange(scope, key, previousValues[key], undefined, "clearNamespace", "web")));
|
|
739
841
|
});
|
|
740
842
|
},
|
|
741
843
|
clearBiometric: () => {
|
|
@@ -844,6 +946,9 @@ export const storage = {
|
|
|
844
946
|
return result;
|
|
845
947
|
});
|
|
846
948
|
},
|
|
949
|
+
export: scope => {
|
|
950
|
+
return measureOperation("storage:export", scope, () => storage.getAll(scope));
|
|
951
|
+
},
|
|
847
952
|
size: scope => {
|
|
848
953
|
return measureOperation("storage:size", scope, () => {
|
|
849
954
|
assertValidScope(scope);
|
|
@@ -915,6 +1020,57 @@ export const storage = {
|
|
|
915
1020
|
},
|
|
916
1021
|
errorClassification: true
|
|
917
1022
|
}),
|
|
1023
|
+
getSecurityCapabilities: () => {
|
|
1024
|
+
const secureBackend = getBackendName(StorageScope.Secure, webSecureStorageBackend);
|
|
1025
|
+
return {
|
|
1026
|
+
platform: "web",
|
|
1027
|
+
secureStorage: {
|
|
1028
|
+
backend: secureBackend,
|
|
1029
|
+
encrypted: getWebSecureEncryptionStatus(webSecureStorageBackend),
|
|
1030
|
+
accessControl: "unavailable",
|
|
1031
|
+
keychainAccessGroup: "unavailable",
|
|
1032
|
+
hardwareBacked: "unavailable"
|
|
1033
|
+
},
|
|
1034
|
+
biometric: {
|
|
1035
|
+
storage: "unavailable",
|
|
1036
|
+
prompt: "unavailable",
|
|
1037
|
+
biometryOnly: "unavailable",
|
|
1038
|
+
biometryOrPasscode: "unavailable"
|
|
1039
|
+
},
|
|
1040
|
+
metadata: {
|
|
1041
|
+
perKey: true,
|
|
1042
|
+
listsWithoutValues: true,
|
|
1043
|
+
persistsTimestamps: false
|
|
1044
|
+
}
|
|
1045
|
+
};
|
|
1046
|
+
},
|
|
1047
|
+
getSecureMetadata: key => {
|
|
1048
|
+
return measureOperation("storage:getSecureMetadata", StorageScope.Secure, () => {
|
|
1049
|
+
flushSecureWrites();
|
|
1050
|
+
const biometricProtected = WebStorage.hasSecureBiometric(key);
|
|
1051
|
+
const exists = biometricProtected || WebStorage.has(key, StorageScope.Secure);
|
|
1052
|
+
let kind = "missing";
|
|
1053
|
+
if (exists) {
|
|
1054
|
+
kind = biometricProtected ? "biometric" : "secure";
|
|
1055
|
+
}
|
|
1056
|
+
return {
|
|
1057
|
+
key,
|
|
1058
|
+
exists,
|
|
1059
|
+
kind,
|
|
1060
|
+
backend: getBackendName(StorageScope.Secure, webSecureStorageBackend),
|
|
1061
|
+
encrypted: getWebSecureEncryptionStatus(webSecureStorageBackend),
|
|
1062
|
+
hardwareBacked: "unavailable",
|
|
1063
|
+
biometricProtected,
|
|
1064
|
+
valueExposed: false
|
|
1065
|
+
};
|
|
1066
|
+
});
|
|
1067
|
+
},
|
|
1068
|
+
getAllSecureMetadata: () => {
|
|
1069
|
+
return measureOperation("storage:getAllSecureMetadata", StorageScope.Secure, () => {
|
|
1070
|
+
flushSecureWrites();
|
|
1071
|
+
return WebStorage.getAllKeys(StorageScope.Secure).map(key => storage.getSecureMetadata(key));
|
|
1072
|
+
});
|
|
1073
|
+
},
|
|
918
1074
|
getString: (key, scope) => {
|
|
919
1075
|
return measureOperation("storage:getString", scope, () => {
|
|
920
1076
|
return getRawValue(key, scope);
|
|
@@ -936,11 +1092,13 @@ export const storage = {
|
|
|
936
1092
|
assertValidScope(scope);
|
|
937
1093
|
if (keys.length === 0) return;
|
|
938
1094
|
const values = keys.map(k => data[k]);
|
|
1095
|
+
const changes = keys.map((key, index) => createKeyChange(scope, key, getEventRawValue(scope, key), values[index], "import", scope === StorageScope.Memory ? "memory" : "web"));
|
|
939
1096
|
if (scope === StorageScope.Memory) {
|
|
940
1097
|
keys.forEach((key, index) => {
|
|
941
1098
|
memoryStore.set(key, values[index]);
|
|
942
1099
|
});
|
|
943
1100
|
keys.forEach(key => notifyKeyListeners(memoryListeners, key));
|
|
1101
|
+
emitBatchChange(scope, "import", "memory", changes);
|
|
944
1102
|
return;
|
|
945
1103
|
}
|
|
946
1104
|
if (scope === StorageScope.Secure) {
|
|
@@ -952,6 +1110,7 @@ export const storage = {
|
|
|
952
1110
|
}
|
|
953
1111
|
WebStorage.setBatch(keys, values, scope);
|
|
954
1112
|
keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
|
|
1113
|
+
emitBatchChange(scope, "import", "web", changes);
|
|
955
1114
|
}, keys.length);
|
|
956
1115
|
}
|
|
957
1116
|
};
|
|
@@ -1093,56 +1252,68 @@ export function createStorageItem(config) {
|
|
|
1093
1252
|
return raw;
|
|
1094
1253
|
};
|
|
1095
1254
|
const writeStoredRaw = rawValue => {
|
|
1255
|
+
const oldValue = config.scope === StorageScope.Memory ? getEventRawValue(config.scope, storageKey) : undefined;
|
|
1096
1256
|
if (isBiometric) {
|
|
1097
1257
|
WebStorage.setSecureBiometricWithLevel(storageKey, rawValue, resolvedBiometricLevel);
|
|
1258
|
+
emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", "web");
|
|
1098
1259
|
return;
|
|
1099
1260
|
}
|
|
1100
1261
|
cacheRawValue(nonMemoryScope, storageKey, rawValue);
|
|
1101
1262
|
if (nonMemoryScope === StorageScope.Disk) {
|
|
1102
1263
|
if (coalesceDiskWrites || diskWritesAsync) {
|
|
1103
1264
|
scheduleDiskWrite(storageKey, rawValue);
|
|
1265
|
+
emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", "web");
|
|
1104
1266
|
return;
|
|
1105
1267
|
}
|
|
1106
1268
|
clearPendingDiskWrite(storageKey);
|
|
1107
1269
|
}
|
|
1108
1270
|
if (coalesceSecureWrites) {
|
|
1109
1271
|
scheduleSecureWrite(storageKey, rawValue, secureAccessControl ?? secureDefaultAccessControl);
|
|
1272
|
+
emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", "web");
|
|
1110
1273
|
return;
|
|
1111
1274
|
}
|
|
1112
1275
|
if (nonMemoryScope === StorageScope.Secure) {
|
|
1113
1276
|
clearPendingSecureWrite(storageKey);
|
|
1114
1277
|
}
|
|
1115
1278
|
WebStorage.set(storageKey, rawValue, config.scope);
|
|
1279
|
+
emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", "web");
|
|
1116
1280
|
};
|
|
1117
1281
|
const removeStoredRaw = () => {
|
|
1282
|
+
const oldValue = getEventRawValue(config.scope, storageKey);
|
|
1118
1283
|
if (isBiometric) {
|
|
1119
1284
|
WebStorage.deleteSecureBiometric(storageKey);
|
|
1285
|
+
emitKeyChange(config.scope, storageKey, oldValue, undefined, "remove", "web");
|
|
1120
1286
|
return;
|
|
1121
1287
|
}
|
|
1122
1288
|
cacheRawValue(nonMemoryScope, storageKey, undefined);
|
|
1123
1289
|
if (nonMemoryScope === StorageScope.Disk) {
|
|
1124
1290
|
if (coalesceDiskWrites || diskWritesAsync) {
|
|
1125
1291
|
scheduleDiskWrite(storageKey, undefined);
|
|
1292
|
+
emitKeyChange(config.scope, storageKey, oldValue, undefined, "remove", "web");
|
|
1126
1293
|
return;
|
|
1127
1294
|
}
|
|
1128
1295
|
clearPendingDiskWrite(storageKey);
|
|
1129
1296
|
}
|
|
1130
1297
|
if (coalesceSecureWrites) {
|
|
1131
1298
|
scheduleSecureWrite(storageKey, undefined, secureAccessControl ?? secureDefaultAccessControl);
|
|
1299
|
+
emitKeyChange(config.scope, storageKey, oldValue, undefined, "remove", "web");
|
|
1132
1300
|
return;
|
|
1133
1301
|
}
|
|
1134
1302
|
if (nonMemoryScope === StorageScope.Secure) {
|
|
1135
1303
|
clearPendingSecureWrite(storageKey);
|
|
1136
1304
|
}
|
|
1137
1305
|
WebStorage.remove(storageKey, config.scope);
|
|
1306
|
+
emitKeyChange(config.scope, storageKey, oldValue, undefined, "remove", "web");
|
|
1138
1307
|
};
|
|
1139
1308
|
const writeValueWithoutValidation = value => {
|
|
1140
1309
|
if (isMemory) {
|
|
1310
|
+
const oldValue = getEventRawValue(config.scope, storageKey);
|
|
1141
1311
|
if (memoryExpiration) {
|
|
1142
1312
|
memoryExpiration.set(storageKey, Date.now() + (expirationTtlMs ?? 0));
|
|
1143
1313
|
}
|
|
1144
1314
|
memoryStore.set(storageKey, value);
|
|
1145
1315
|
notifyKeyListeners(memoryListeners, storageKey);
|
|
1316
|
+
emitKeyChange(config.scope, storageKey, oldValue, typeof value === "string" ? value : undefined, "set", "memory");
|
|
1146
1317
|
return;
|
|
1147
1318
|
}
|
|
1148
1319
|
const serialized = serialize(value);
|
|
@@ -1274,11 +1445,13 @@ export function createStorageItem(config) {
|
|
|
1274
1445
|
measureOperation("item:delete", config.scope, () => {
|
|
1275
1446
|
invalidateParsedCache();
|
|
1276
1447
|
if (isMemory) {
|
|
1448
|
+
const oldValue = getEventRawValue(config.scope, storageKey);
|
|
1277
1449
|
if (memoryExpiration) {
|
|
1278
1450
|
memoryExpiration.delete(storageKey);
|
|
1279
1451
|
}
|
|
1280
1452
|
memoryStore.delete(storageKey);
|
|
1281
1453
|
notifyKeyListeners(memoryListeners, storageKey);
|
|
1454
|
+
emitKeyChange(config.scope, storageKey, oldValue, undefined, "remove", "memory");
|
|
1282
1455
|
return;
|
|
1283
1456
|
}
|
|
1284
1457
|
removeStoredRaw();
|
|
@@ -1312,6 +1485,22 @@ export function createStorageItem(config) {
|
|
|
1312
1485
|
}
|
|
1313
1486
|
};
|
|
1314
1487
|
};
|
|
1488
|
+
const subscribeSelector = (selector, listener, options = {}) => {
|
|
1489
|
+
const isEqual = options.isEqual ?? Object.is;
|
|
1490
|
+
let currentValue = selector(getInternal());
|
|
1491
|
+
if (options.fireImmediately === true) {
|
|
1492
|
+
listener(currentValue, currentValue);
|
|
1493
|
+
}
|
|
1494
|
+
return subscribe(() => {
|
|
1495
|
+
const nextValue = selector(getInternal());
|
|
1496
|
+
if (isEqual(currentValue, nextValue)) {
|
|
1497
|
+
return;
|
|
1498
|
+
}
|
|
1499
|
+
const previousValue = currentValue;
|
|
1500
|
+
currentValue = nextValue;
|
|
1501
|
+
listener(nextValue, previousValue);
|
|
1502
|
+
});
|
|
1503
|
+
};
|
|
1315
1504
|
const storageItem = {
|
|
1316
1505
|
get,
|
|
1317
1506
|
getWithVersion,
|
|
@@ -1320,6 +1509,7 @@ export function createStorageItem(config) {
|
|
|
1320
1509
|
delete: deleteItem,
|
|
1321
1510
|
has: hasItem,
|
|
1322
1511
|
subscribe,
|
|
1512
|
+
subscribeSelector,
|
|
1323
1513
|
serialize,
|
|
1324
1514
|
deserialize,
|
|
1325
1515
|
_triggerListeners: () => {
|
|
@@ -1422,6 +1612,10 @@ export function setBatch(items, scope) {
|
|
|
1422
1612
|
}) => item.set(value));
|
|
1423
1613
|
return;
|
|
1424
1614
|
}
|
|
1615
|
+
const changes = items.map(({
|
|
1616
|
+
item,
|
|
1617
|
+
value
|
|
1618
|
+
}) => createKeyChange(scope, item.key, getEventRawValue(scope, item.key), typeof value === "string" ? value : undefined, "setBatch", "memory"));
|
|
1425
1619
|
|
|
1426
1620
|
// Atomic write: update all values in memoryStore, invalidate caches, then batch-notify
|
|
1427
1621
|
items.forEach(({
|
|
@@ -1434,6 +1628,7 @@ export function setBatch(items, scope) {
|
|
|
1434
1628
|
items.forEach(({
|
|
1435
1629
|
item
|
|
1436
1630
|
}) => notifyKeyListeners(memoryListeners, item.key));
|
|
1631
|
+
emitBatchChange(scope, "setBatch", "memory", changes);
|
|
1437
1632
|
return;
|
|
1438
1633
|
}
|
|
1439
1634
|
if (scope === StorageScope.Secure) {
|
|
@@ -1456,6 +1651,10 @@ export function setBatch(items, scope) {
|
|
|
1456
1651
|
return;
|
|
1457
1652
|
}
|
|
1458
1653
|
flushSecureWrites();
|
|
1654
|
+
const keys = secureEntries.map(({
|
|
1655
|
+
item
|
|
1656
|
+
}) => item.key);
|
|
1657
|
+
const oldValues = hasStorageChangeObservers(scope) ? WebStorage.getBatch(keys, scope) : [];
|
|
1459
1658
|
const groupedByAccessControl = new Map();
|
|
1460
1659
|
secureEntries.forEach(({
|
|
1461
1660
|
item,
|
|
@@ -1479,6 +1678,10 @@ export function setBatch(items, scope) {
|
|
|
1479
1678
|
WebStorage.setBatch(group.keys, group.values, scope);
|
|
1480
1679
|
group.keys.forEach((key, index) => cacheRawValue(scope, key, group.values[index]));
|
|
1481
1680
|
});
|
|
1681
|
+
emitBatchChange(scope, "setBatch", "web", secureEntries.map(({
|
|
1682
|
+
item,
|
|
1683
|
+
value
|
|
1684
|
+
}, index) => createKeyChange(scope, item.key, oldValues[index], item.serialize(value), "setBatch", "web")));
|
|
1482
1685
|
return;
|
|
1483
1686
|
}
|
|
1484
1687
|
flushDiskWrites();
|
|
@@ -1494,15 +1697,19 @@ export function setBatch(items, scope) {
|
|
|
1494
1697
|
}
|
|
1495
1698
|
const keys = items.map(entry => entry.item.key);
|
|
1496
1699
|
const values = items.map(entry => entry.item.serialize(entry.value));
|
|
1700
|
+
const oldValues = hasStorageChangeObservers(scope) ? WebStorage.getBatch(keys, scope) : [];
|
|
1497
1701
|
WebStorage.setBatch(keys, values, scope);
|
|
1498
1702
|
keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
|
|
1703
|
+
emitBatchChange(scope, "setBatch", "web", keys.map((key, index) => createKeyChange(scope, key, oldValues[index], values[index], "setBatch", "web")));
|
|
1499
1704
|
}, items.length);
|
|
1500
1705
|
}
|
|
1501
1706
|
export function removeBatch(items, scope) {
|
|
1502
1707
|
measureOperation("batch:remove", scope, () => {
|
|
1503
1708
|
assertBatchScope(items, scope);
|
|
1504
1709
|
if (scope === StorageScope.Memory) {
|
|
1710
|
+
const changes = items.map(item => createKeyChange(scope, item.key, getEventRawValue(scope, item.key), undefined, "removeBatch", "memory"));
|
|
1505
1711
|
items.forEach(item => item.delete());
|
|
1712
|
+
emitBatchChange(scope, "removeBatch", "memory", changes);
|
|
1506
1713
|
return;
|
|
1507
1714
|
}
|
|
1508
1715
|
const keys = items.map(item => item.key);
|
|
@@ -1512,8 +1719,10 @@ export function removeBatch(items, scope) {
|
|
|
1512
1719
|
if (scope === StorageScope.Secure) {
|
|
1513
1720
|
flushSecureWrites();
|
|
1514
1721
|
}
|
|
1722
|
+
const oldValues = hasStorageChangeObservers(scope) ? WebStorage.getBatch(keys, scope) : [];
|
|
1515
1723
|
WebStorage.removeBatch(keys, scope);
|
|
1516
1724
|
keys.forEach(key => cacheRawValue(scope, key, undefined));
|
|
1725
|
+
emitBatchChange(scope, "removeBatch", "web", keys.map((key, index) => createKeyChange(scope, key, oldValues[index], undefined, "removeBatch", "web")));
|
|
1517
1726
|
}, items.length);
|
|
1518
1727
|
}
|
|
1519
1728
|
export function registerMigration(version, migration) {
|