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/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) {
|
|
@@ -163,6 +166,7 @@ function applyExternalChangeEvent(scope, key, newValue) {
|
|
|
163
166
|
}
|
|
164
167
|
if (scope === StorageScope.Secure && key.startsWith(SECURE_WEB_PREFIX)) {
|
|
165
168
|
const plainKey = fromSecureStorageKey(key);
|
|
169
|
+
const oldValue = readCachedRawValue(StorageScope.Secure, plainKey);
|
|
166
170
|
if (newValue === null) {
|
|
167
171
|
ensureWebScopeKeyIndex(StorageScope.Secure).delete(plainKey);
|
|
168
172
|
cacheRawValue(StorageScope.Secure, plainKey, undefined);
|
|
@@ -171,10 +175,12 @@ function applyExternalChangeEvent(scope, key, newValue) {
|
|
|
171
175
|
cacheRawValue(StorageScope.Secure, plainKey, newValue);
|
|
172
176
|
}
|
|
173
177
|
notifyKeyListeners(getScopedListeners(StorageScope.Secure), plainKey);
|
|
178
|
+
emitKeyChange(StorageScope.Secure, plainKey, oldValue, newValue ?? undefined, "external", "external");
|
|
174
179
|
return;
|
|
175
180
|
}
|
|
176
181
|
if (scope === StorageScope.Secure && key.startsWith(BIOMETRIC_WEB_PREFIX)) {
|
|
177
182
|
const plainKey = fromBiometricStorageKey(key);
|
|
183
|
+
const oldValue = readCachedRawValue(StorageScope.Secure, plainKey);
|
|
178
184
|
if (newValue === null) {
|
|
179
185
|
if (withWebBackendOperation(StorageScope.Secure, "external-sync:getItem", backend => backend.getItem(toSecureStorageKey(plainKey))) === null) {
|
|
180
186
|
ensureWebScopeKeyIndex(StorageScope.Secure).delete(plainKey);
|
|
@@ -185,8 +191,10 @@ function applyExternalChangeEvent(scope, key, newValue) {
|
|
|
185
191
|
cacheRawValue(StorageScope.Secure, plainKey, newValue);
|
|
186
192
|
}
|
|
187
193
|
notifyKeyListeners(getScopedListeners(StorageScope.Secure), plainKey);
|
|
194
|
+
emitKeyChange(StorageScope.Secure, plainKey, oldValue, newValue ?? undefined, "external", "external");
|
|
188
195
|
return;
|
|
189
196
|
}
|
|
197
|
+
const oldValue = readCachedRawValue(scope, key);
|
|
190
198
|
if (newValue === null) {
|
|
191
199
|
ensureWebScopeKeyIndex(scope).delete(key);
|
|
192
200
|
cacheRawValue(scope, key, undefined);
|
|
@@ -195,6 +203,7 @@ function applyExternalChangeEvent(scope, key, newValue) {
|
|
|
195
203
|
cacheRawValue(scope, key, newValue);
|
|
196
204
|
}
|
|
197
205
|
notifyKeyListeners(getScopedListeners(scope), key);
|
|
206
|
+
emitKeyChange(scope, key, oldValue, newValue ?? undefined, "external", "external");
|
|
198
207
|
}
|
|
199
208
|
function handleWebStorageEvent(event) {
|
|
200
209
|
const key = event.key;
|
|
@@ -285,6 +294,46 @@ function addKeyListener(registry, key, listener) {
|
|
|
285
294
|
}
|
|
286
295
|
};
|
|
287
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
|
+
}
|
|
288
337
|
function readPendingSecureWrite(key) {
|
|
289
338
|
return pendingSecureWrites.get(key)?.value;
|
|
290
339
|
}
|
|
@@ -534,13 +583,10 @@ const WebStorage = {
|
|
|
534
583
|
return () => {};
|
|
535
584
|
},
|
|
536
585
|
has: (key, scope) => {
|
|
537
|
-
if (scope === StorageScope.Secure) {
|
|
538
|
-
return
|
|
539
|
-
}
|
|
540
|
-
if (scope !== StorageScope.Disk) {
|
|
541
|
-
return false;
|
|
586
|
+
if (scope === StorageScope.Disk || scope === StorageScope.Secure) {
|
|
587
|
+
return ensureWebScopeKeyIndex(scope).has(key);
|
|
542
588
|
}
|
|
543
|
-
return
|
|
589
|
+
return false;
|
|
544
590
|
},
|
|
545
591
|
getAllKeys: scope => {
|
|
546
592
|
if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
|
|
@@ -631,15 +677,18 @@ function getRawValue(key, scope) {
|
|
|
631
677
|
}
|
|
632
678
|
function setRawValue(key, value, scope) {
|
|
633
679
|
assertValidScope(scope);
|
|
680
|
+
const oldValue = scope === StorageScope.Memory ? getEventRawValue(scope, key) : undefined;
|
|
634
681
|
if (scope === StorageScope.Memory) {
|
|
635
682
|
memoryStore.set(key, value);
|
|
636
683
|
notifyKeyListeners(memoryListeners, key);
|
|
684
|
+
emitKeyChange(scope, key, oldValue, value, "set", "memory");
|
|
637
685
|
return;
|
|
638
686
|
}
|
|
639
687
|
if (scope === StorageScope.Disk) {
|
|
640
688
|
cacheRawValue(scope, key, value);
|
|
641
689
|
if (diskWritesAsync) {
|
|
642
690
|
scheduleDiskWrite(key, value);
|
|
691
|
+
emitKeyChange(scope, key, oldValue, value, "set", "web");
|
|
643
692
|
return;
|
|
644
693
|
}
|
|
645
694
|
flushDiskWrites();
|
|
@@ -651,18 +700,22 @@ function setRawValue(key, value, scope) {
|
|
|
651
700
|
}
|
|
652
701
|
WebStorage.set(key, value, scope);
|
|
653
702
|
cacheRawValue(scope, key, value);
|
|
703
|
+
emitKeyChange(scope, key, oldValue, value, "set", "web");
|
|
654
704
|
}
|
|
655
705
|
function removeRawValue(key, scope) {
|
|
656
706
|
assertValidScope(scope);
|
|
707
|
+
const oldValue = getEventRawValue(scope, key);
|
|
657
708
|
if (scope === StorageScope.Memory) {
|
|
658
709
|
memoryStore.delete(key);
|
|
659
710
|
notifyKeyListeners(memoryListeners, key);
|
|
711
|
+
emitKeyChange(scope, key, oldValue, undefined, "remove", "memory");
|
|
660
712
|
return;
|
|
661
713
|
}
|
|
662
714
|
if (scope === StorageScope.Disk) {
|
|
663
715
|
cacheRawValue(scope, key, undefined);
|
|
664
716
|
if (diskWritesAsync) {
|
|
665
717
|
scheduleDiskWrite(key, undefined);
|
|
718
|
+
emitKeyChange(scope, key, oldValue, undefined, "remove", "web");
|
|
666
719
|
return;
|
|
667
720
|
}
|
|
668
721
|
flushDiskWrites();
|
|
@@ -674,6 +727,7 @@ function removeRawValue(key, scope) {
|
|
|
674
727
|
}
|
|
675
728
|
WebStorage.remove(key, scope);
|
|
676
729
|
cacheRawValue(scope, key, undefined);
|
|
730
|
+
emitKeyChange(scope, key, oldValue, undefined, "remove", "web");
|
|
677
731
|
}
|
|
678
732
|
function readMigrationVersion(scope) {
|
|
679
733
|
const raw = getRawValue(MIGRATION_VERSION_KEY, scope);
|
|
@@ -687,11 +741,43 @@ function writeMigrationVersion(scope, version) {
|
|
|
687
741
|
setRawValue(MIGRATION_VERSION_KEY, String(version), scope);
|
|
688
742
|
}
|
|
689
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
|
+
},
|
|
690
774
|
clear: scope => {
|
|
691
775
|
measureOperation("storage:clear", scope, () => {
|
|
776
|
+
const previousValues = hasStorageChangeObservers(scope) ? storage.getAll(scope) : {};
|
|
692
777
|
if (scope === StorageScope.Memory) {
|
|
693
778
|
memoryStore.clear();
|
|
694
779
|
notifyAllListeners(memoryListeners);
|
|
780
|
+
emitBatchChange(scope, "clear", "memory", Object.keys(previousValues).map(key => createKeyChange(scope, key, previousValues[key], undefined, "clear", "memory")));
|
|
695
781
|
return;
|
|
696
782
|
}
|
|
697
783
|
if (scope === StorageScope.Disk) {
|
|
@@ -704,6 +790,7 @@ export const storage = {
|
|
|
704
790
|
}
|
|
705
791
|
clearScopeRawCache(scope);
|
|
706
792
|
WebStorage.clear(scope);
|
|
793
|
+
emitBatchChange(scope, "clear", "web", Object.keys(previousValues).map(key => createKeyChange(scope, key, previousValues[key], undefined, "clear", "web")));
|
|
707
794
|
});
|
|
708
795
|
},
|
|
709
796
|
clearAll: () => {
|
|
@@ -717,15 +804,26 @@ export const storage = {
|
|
|
717
804
|
measureOperation("storage:clearNamespace", scope, () => {
|
|
718
805
|
assertValidScope(scope);
|
|
719
806
|
if (scope === StorageScope.Memory) {
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
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;
|
|
724
814
|
}
|
|
725
|
-
|
|
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")));
|
|
726
823
|
return;
|
|
727
824
|
}
|
|
728
825
|
const keyPrefix = prefixKey(namespace, "");
|
|
826
|
+
const previousValues = hasStorageChangeObservers(scope) ? storage.getByPrefix(keyPrefix, scope) : {};
|
|
729
827
|
if (scope === StorageScope.Disk) {
|
|
730
828
|
flushDiskWrites();
|
|
731
829
|
}
|
|
@@ -739,6 +837,7 @@ export const storage = {
|
|
|
739
837
|
}
|
|
740
838
|
}
|
|
741
839
|
WebStorage.removeByPrefix(keyPrefix, scope);
|
|
840
|
+
emitBatchChange(scope, "clearNamespace", "web", Object.keys(previousValues).map(key => createKeyChange(scope, key, previousValues[key], undefined, "clearNamespace", "web")));
|
|
742
841
|
});
|
|
743
842
|
},
|
|
744
843
|
clearBiometric: () => {
|
|
@@ -847,6 +946,9 @@ export const storage = {
|
|
|
847
946
|
return result;
|
|
848
947
|
});
|
|
849
948
|
},
|
|
949
|
+
export: scope => {
|
|
950
|
+
return measureOperation("storage:export", scope, () => storage.getAll(scope));
|
|
951
|
+
},
|
|
850
952
|
size: scope => {
|
|
851
953
|
return measureOperation("storage:size", scope, () => {
|
|
852
954
|
assertValidScope(scope);
|
|
@@ -990,11 +1092,13 @@ export const storage = {
|
|
|
990
1092
|
assertValidScope(scope);
|
|
991
1093
|
if (keys.length === 0) return;
|
|
992
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"));
|
|
993
1096
|
if (scope === StorageScope.Memory) {
|
|
994
1097
|
keys.forEach((key, index) => {
|
|
995
1098
|
memoryStore.set(key, values[index]);
|
|
996
1099
|
});
|
|
997
1100
|
keys.forEach(key => notifyKeyListeners(memoryListeners, key));
|
|
1101
|
+
emitBatchChange(scope, "import", "memory", changes);
|
|
998
1102
|
return;
|
|
999
1103
|
}
|
|
1000
1104
|
if (scope === StorageScope.Secure) {
|
|
@@ -1006,6 +1110,7 @@ export const storage = {
|
|
|
1006
1110
|
}
|
|
1007
1111
|
WebStorage.setBatch(keys, values, scope);
|
|
1008
1112
|
keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
|
|
1113
|
+
emitBatchChange(scope, "import", "web", changes);
|
|
1009
1114
|
}, keys.length);
|
|
1010
1115
|
}
|
|
1011
1116
|
};
|
|
@@ -1147,56 +1252,68 @@ export function createStorageItem(config) {
|
|
|
1147
1252
|
return raw;
|
|
1148
1253
|
};
|
|
1149
1254
|
const writeStoredRaw = rawValue => {
|
|
1255
|
+
const oldValue = config.scope === StorageScope.Memory ? getEventRawValue(config.scope, storageKey) : undefined;
|
|
1150
1256
|
if (isBiometric) {
|
|
1151
1257
|
WebStorage.setSecureBiometricWithLevel(storageKey, rawValue, resolvedBiometricLevel);
|
|
1258
|
+
emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", "web");
|
|
1152
1259
|
return;
|
|
1153
1260
|
}
|
|
1154
1261
|
cacheRawValue(nonMemoryScope, storageKey, rawValue);
|
|
1155
1262
|
if (nonMemoryScope === StorageScope.Disk) {
|
|
1156
1263
|
if (coalesceDiskWrites || diskWritesAsync) {
|
|
1157
1264
|
scheduleDiskWrite(storageKey, rawValue);
|
|
1265
|
+
emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", "web");
|
|
1158
1266
|
return;
|
|
1159
1267
|
}
|
|
1160
1268
|
clearPendingDiskWrite(storageKey);
|
|
1161
1269
|
}
|
|
1162
1270
|
if (coalesceSecureWrites) {
|
|
1163
1271
|
scheduleSecureWrite(storageKey, rawValue, secureAccessControl ?? secureDefaultAccessControl);
|
|
1272
|
+
emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", "web");
|
|
1164
1273
|
return;
|
|
1165
1274
|
}
|
|
1166
1275
|
if (nonMemoryScope === StorageScope.Secure) {
|
|
1167
1276
|
clearPendingSecureWrite(storageKey);
|
|
1168
1277
|
}
|
|
1169
1278
|
WebStorage.set(storageKey, rawValue, config.scope);
|
|
1279
|
+
emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", "web");
|
|
1170
1280
|
};
|
|
1171
1281
|
const removeStoredRaw = () => {
|
|
1282
|
+
const oldValue = getEventRawValue(config.scope, storageKey);
|
|
1172
1283
|
if (isBiometric) {
|
|
1173
1284
|
WebStorage.deleteSecureBiometric(storageKey);
|
|
1285
|
+
emitKeyChange(config.scope, storageKey, oldValue, undefined, "remove", "web");
|
|
1174
1286
|
return;
|
|
1175
1287
|
}
|
|
1176
1288
|
cacheRawValue(nonMemoryScope, storageKey, undefined);
|
|
1177
1289
|
if (nonMemoryScope === StorageScope.Disk) {
|
|
1178
1290
|
if (coalesceDiskWrites || diskWritesAsync) {
|
|
1179
1291
|
scheduleDiskWrite(storageKey, undefined);
|
|
1292
|
+
emitKeyChange(config.scope, storageKey, oldValue, undefined, "remove", "web");
|
|
1180
1293
|
return;
|
|
1181
1294
|
}
|
|
1182
1295
|
clearPendingDiskWrite(storageKey);
|
|
1183
1296
|
}
|
|
1184
1297
|
if (coalesceSecureWrites) {
|
|
1185
1298
|
scheduleSecureWrite(storageKey, undefined, secureAccessControl ?? secureDefaultAccessControl);
|
|
1299
|
+
emitKeyChange(config.scope, storageKey, oldValue, undefined, "remove", "web");
|
|
1186
1300
|
return;
|
|
1187
1301
|
}
|
|
1188
1302
|
if (nonMemoryScope === StorageScope.Secure) {
|
|
1189
1303
|
clearPendingSecureWrite(storageKey);
|
|
1190
1304
|
}
|
|
1191
1305
|
WebStorage.remove(storageKey, config.scope);
|
|
1306
|
+
emitKeyChange(config.scope, storageKey, oldValue, undefined, "remove", "web");
|
|
1192
1307
|
};
|
|
1193
1308
|
const writeValueWithoutValidation = value => {
|
|
1194
1309
|
if (isMemory) {
|
|
1310
|
+
const oldValue = getEventRawValue(config.scope, storageKey);
|
|
1195
1311
|
if (memoryExpiration) {
|
|
1196
1312
|
memoryExpiration.set(storageKey, Date.now() + (expirationTtlMs ?? 0));
|
|
1197
1313
|
}
|
|
1198
1314
|
memoryStore.set(storageKey, value);
|
|
1199
1315
|
notifyKeyListeners(memoryListeners, storageKey);
|
|
1316
|
+
emitKeyChange(config.scope, storageKey, oldValue, typeof value === "string" ? value : undefined, "set", "memory");
|
|
1200
1317
|
return;
|
|
1201
1318
|
}
|
|
1202
1319
|
const serialized = serialize(value);
|
|
@@ -1328,11 +1445,13 @@ export function createStorageItem(config) {
|
|
|
1328
1445
|
measureOperation("item:delete", config.scope, () => {
|
|
1329
1446
|
invalidateParsedCache();
|
|
1330
1447
|
if (isMemory) {
|
|
1448
|
+
const oldValue = getEventRawValue(config.scope, storageKey);
|
|
1331
1449
|
if (memoryExpiration) {
|
|
1332
1450
|
memoryExpiration.delete(storageKey);
|
|
1333
1451
|
}
|
|
1334
1452
|
memoryStore.delete(storageKey);
|
|
1335
1453
|
notifyKeyListeners(memoryListeners, storageKey);
|
|
1454
|
+
emitKeyChange(config.scope, storageKey, oldValue, undefined, "remove", "memory");
|
|
1336
1455
|
return;
|
|
1337
1456
|
}
|
|
1338
1457
|
removeStoredRaw();
|
|
@@ -1366,6 +1485,22 @@ export function createStorageItem(config) {
|
|
|
1366
1485
|
}
|
|
1367
1486
|
};
|
|
1368
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
|
+
};
|
|
1369
1504
|
const storageItem = {
|
|
1370
1505
|
get,
|
|
1371
1506
|
getWithVersion,
|
|
@@ -1374,6 +1509,7 @@ export function createStorageItem(config) {
|
|
|
1374
1509
|
delete: deleteItem,
|
|
1375
1510
|
has: hasItem,
|
|
1376
1511
|
subscribe,
|
|
1512
|
+
subscribeSelector,
|
|
1377
1513
|
serialize,
|
|
1378
1514
|
deserialize,
|
|
1379
1515
|
_triggerListeners: () => {
|
|
@@ -1476,6 +1612,10 @@ export function setBatch(items, scope) {
|
|
|
1476
1612
|
}) => item.set(value));
|
|
1477
1613
|
return;
|
|
1478
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"));
|
|
1479
1619
|
|
|
1480
1620
|
// Atomic write: update all values in memoryStore, invalidate caches, then batch-notify
|
|
1481
1621
|
items.forEach(({
|
|
@@ -1488,6 +1628,7 @@ export function setBatch(items, scope) {
|
|
|
1488
1628
|
items.forEach(({
|
|
1489
1629
|
item
|
|
1490
1630
|
}) => notifyKeyListeners(memoryListeners, item.key));
|
|
1631
|
+
emitBatchChange(scope, "setBatch", "memory", changes);
|
|
1491
1632
|
return;
|
|
1492
1633
|
}
|
|
1493
1634
|
if (scope === StorageScope.Secure) {
|
|
@@ -1510,6 +1651,10 @@ export function setBatch(items, scope) {
|
|
|
1510
1651
|
return;
|
|
1511
1652
|
}
|
|
1512
1653
|
flushSecureWrites();
|
|
1654
|
+
const keys = secureEntries.map(({
|
|
1655
|
+
item
|
|
1656
|
+
}) => item.key);
|
|
1657
|
+
const oldValues = hasStorageChangeObservers(scope) ? WebStorage.getBatch(keys, scope) : [];
|
|
1513
1658
|
const groupedByAccessControl = new Map();
|
|
1514
1659
|
secureEntries.forEach(({
|
|
1515
1660
|
item,
|
|
@@ -1533,6 +1678,10 @@ export function setBatch(items, scope) {
|
|
|
1533
1678
|
WebStorage.setBatch(group.keys, group.values, scope);
|
|
1534
1679
|
group.keys.forEach((key, index) => cacheRawValue(scope, key, group.values[index]));
|
|
1535
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")));
|
|
1536
1685
|
return;
|
|
1537
1686
|
}
|
|
1538
1687
|
flushDiskWrites();
|
|
@@ -1548,15 +1697,19 @@ export function setBatch(items, scope) {
|
|
|
1548
1697
|
}
|
|
1549
1698
|
const keys = items.map(entry => entry.item.key);
|
|
1550
1699
|
const values = items.map(entry => entry.item.serialize(entry.value));
|
|
1700
|
+
const oldValues = hasStorageChangeObservers(scope) ? WebStorage.getBatch(keys, scope) : [];
|
|
1551
1701
|
WebStorage.setBatch(keys, values, scope);
|
|
1552
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")));
|
|
1553
1704
|
}, items.length);
|
|
1554
1705
|
}
|
|
1555
1706
|
export function removeBatch(items, scope) {
|
|
1556
1707
|
measureOperation("batch:remove", scope, () => {
|
|
1557
1708
|
assertBatchScope(items, scope);
|
|
1558
1709
|
if (scope === StorageScope.Memory) {
|
|
1710
|
+
const changes = items.map(item => createKeyChange(scope, item.key, getEventRawValue(scope, item.key), undefined, "removeBatch", "memory"));
|
|
1559
1711
|
items.forEach(item => item.delete());
|
|
1712
|
+
emitBatchChange(scope, "removeBatch", "memory", changes);
|
|
1560
1713
|
return;
|
|
1561
1714
|
}
|
|
1562
1715
|
const keys = items.map(item => item.key);
|
|
@@ -1566,8 +1719,10 @@ export function removeBatch(items, scope) {
|
|
|
1566
1719
|
if (scope === StorageScope.Secure) {
|
|
1567
1720
|
flushSecureWrites();
|
|
1568
1721
|
}
|
|
1722
|
+
const oldValues = hasStorageChangeObservers(scope) ? WebStorage.getBatch(keys, scope) : [];
|
|
1569
1723
|
WebStorage.removeBatch(keys, scope);
|
|
1570
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")));
|
|
1571
1726
|
}, items.length);
|
|
1572
1727
|
}
|
|
1573
1728
|
export function registerMigration(version, migration) {
|