react-native-nitro-storage 0.5.0 → 0.5.2

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.
@@ -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 withWebBackendOperation(scope, "has", backend => backend.getItem(toSecureStorageKey(key))) !== null || withWebBackendOperation(scope, "has", backend => backend.getItem(toBiometricStorageKey(key))) !== null;
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 withWebBackendOperation(scope, "has", backend => backend.getItem(key)) !== null;
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
- for (const key of memoryStore.keys()) {
721
- if (isNamespaced(key, namespace)) {
722
- memoryStore.delete(key);
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
- notifyAllListeners(memoryListeners);
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) {