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.
@@ -76,6 +76,7 @@ var _Storage = require("./Storage.types");
76
76
  var _internal = require("./internal");
77
77
  var _webStorageBackend = require("./web-storage-backend");
78
78
  var _storageRuntime = require("./storage-runtime");
79
+ var _storageEvents = require("./storage-events");
79
80
  var _migration = require("./migration");
80
81
  var _storageHooks = require("./storage-hooks");
81
82
  var _indexeddbBackend = require("./indexeddb-backend");
@@ -110,7 +111,9 @@ const BIOMETRIC_WEB_PREFIX = "__bio_";
110
111
  let hasWarnedAboutWebBiometricFallback = false;
111
112
  let hasWindowStorageEventSubscription = false;
112
113
  let metricsObserver;
114
+ let eventObserver;
113
115
  const metricsCounters = new Map();
116
+ const storageEvents = new _storageEvents.StorageEventRegistry();
114
117
  function recordMetric(operation, scope, durationMs, keysCount = 1) {
115
118
  const existing = metricsCounters.get(operation);
116
119
  if (!existing) {
@@ -235,6 +238,7 @@ function applyExternalChangeEvent(scope, key, newValue) {
235
238
  }
236
239
  if (scope === _Storage.StorageScope.Secure && key.startsWith(SECURE_WEB_PREFIX)) {
237
240
  const plainKey = fromSecureStorageKey(key);
241
+ const oldValue = readCachedRawValue(_Storage.StorageScope.Secure, plainKey);
238
242
  if (newValue === null) {
239
243
  ensureWebScopeKeyIndex(_Storage.StorageScope.Secure).delete(plainKey);
240
244
  cacheRawValue(_Storage.StorageScope.Secure, plainKey, undefined);
@@ -243,10 +247,12 @@ function applyExternalChangeEvent(scope, key, newValue) {
243
247
  cacheRawValue(_Storage.StorageScope.Secure, plainKey, newValue);
244
248
  }
245
249
  notifyKeyListeners(getScopedListeners(_Storage.StorageScope.Secure), plainKey);
250
+ emitKeyChange(_Storage.StorageScope.Secure, plainKey, oldValue, newValue ?? undefined, "external", "external");
246
251
  return;
247
252
  }
248
253
  if (scope === _Storage.StorageScope.Secure && key.startsWith(BIOMETRIC_WEB_PREFIX)) {
249
254
  const plainKey = fromBiometricStorageKey(key);
255
+ const oldValue = readCachedRawValue(_Storage.StorageScope.Secure, plainKey);
250
256
  if (newValue === null) {
251
257
  if (withWebBackendOperation(_Storage.StorageScope.Secure, "external-sync:getItem", backend => backend.getItem(toSecureStorageKey(plainKey))) === null) {
252
258
  ensureWebScopeKeyIndex(_Storage.StorageScope.Secure).delete(plainKey);
@@ -257,8 +263,10 @@ function applyExternalChangeEvent(scope, key, newValue) {
257
263
  cacheRawValue(_Storage.StorageScope.Secure, plainKey, newValue);
258
264
  }
259
265
  notifyKeyListeners(getScopedListeners(_Storage.StorageScope.Secure), plainKey);
266
+ emitKeyChange(_Storage.StorageScope.Secure, plainKey, oldValue, newValue ?? undefined, "external", "external");
260
267
  return;
261
268
  }
269
+ const oldValue = readCachedRawValue(scope, key);
262
270
  if (newValue === null) {
263
271
  ensureWebScopeKeyIndex(scope).delete(key);
264
272
  cacheRawValue(scope, key, undefined);
@@ -267,6 +275,7 @@ function applyExternalChangeEvent(scope, key, newValue) {
267
275
  cacheRawValue(scope, key, newValue);
268
276
  }
269
277
  notifyKeyListeners(getScopedListeners(scope), key);
278
+ emitKeyChange(scope, key, oldValue, newValue ?? undefined, "external", "external");
270
279
  }
271
280
  function handleWebStorageEvent(event) {
272
281
  const key = event.key;
@@ -357,6 +366,46 @@ function addKeyListener(registry, key, listener) {
357
366
  }
358
367
  };
359
368
  }
369
+ function getEventRawValue(scope, key) {
370
+ if (scope === _Storage.StorageScope.Memory) {
371
+ const value = memoryStore.get(key);
372
+ return typeof value === "string" ? value : undefined;
373
+ }
374
+ return getRawValue(key, scope);
375
+ }
376
+ function createKeyChange(scope, key, oldValue, newValue, operation, source) {
377
+ return {
378
+ type: "key",
379
+ scope,
380
+ key,
381
+ oldValue,
382
+ newValue,
383
+ operation,
384
+ source
385
+ };
386
+ }
387
+ function hasStorageChangeObservers(scope) {
388
+ return storageEvents.hasListeners(scope) || eventObserver !== undefined;
389
+ }
390
+ function emitKeyChange(scope, key, oldValue, newValue, operation, source) {
391
+ const event = createKeyChange(scope, key, oldValue, newValue, operation, source);
392
+ storageEvents.emitKey(event);
393
+ eventObserver?.(event);
394
+ }
395
+ function emitBatchChange(scope, operation, source, changes) {
396
+ if (changes.length === 0) {
397
+ return;
398
+ }
399
+ const event = {
400
+ type: "batch",
401
+ scope,
402
+ operation,
403
+ source,
404
+ changes
405
+ };
406
+ storageEvents.emitBatch(event);
407
+ eventObserver?.(event);
408
+ }
360
409
  function readPendingSecureWrite(key) {
361
410
  return pendingSecureWrites.get(key)?.value;
362
411
  }
@@ -606,13 +655,10 @@ const WebStorage = {
606
655
  return () => {};
607
656
  },
608
657
  has: (key, scope) => {
609
- if (scope === _Storage.StorageScope.Secure) {
610
- return withWebBackendOperation(scope, "has", backend => backend.getItem(toSecureStorageKey(key))) !== null || withWebBackendOperation(scope, "has", backend => backend.getItem(toBiometricStorageKey(key))) !== null;
611
- }
612
- if (scope !== _Storage.StorageScope.Disk) {
613
- return false;
658
+ if (scope === _Storage.StorageScope.Disk || scope === _Storage.StorageScope.Secure) {
659
+ return ensureWebScopeKeyIndex(scope).has(key);
614
660
  }
615
- return withWebBackendOperation(scope, "has", backend => backend.getItem(key)) !== null;
661
+ return false;
616
662
  },
617
663
  getAllKeys: scope => {
618
664
  if (scope !== _Storage.StorageScope.Disk && scope !== _Storage.StorageScope.Secure) {
@@ -703,15 +749,18 @@ function getRawValue(key, scope) {
703
749
  }
704
750
  function setRawValue(key, value, scope) {
705
751
  (0, _internal.assertValidScope)(scope);
752
+ const oldValue = scope === _Storage.StorageScope.Memory ? getEventRawValue(scope, key) : undefined;
706
753
  if (scope === _Storage.StorageScope.Memory) {
707
754
  memoryStore.set(key, value);
708
755
  notifyKeyListeners(memoryListeners, key);
756
+ emitKeyChange(scope, key, oldValue, value, "set", "memory");
709
757
  return;
710
758
  }
711
759
  if (scope === _Storage.StorageScope.Disk) {
712
760
  cacheRawValue(scope, key, value);
713
761
  if (diskWritesAsync) {
714
762
  scheduleDiskWrite(key, value);
763
+ emitKeyChange(scope, key, oldValue, value, "set", "web");
715
764
  return;
716
765
  }
717
766
  flushDiskWrites();
@@ -723,18 +772,22 @@ function setRawValue(key, value, scope) {
723
772
  }
724
773
  WebStorage.set(key, value, scope);
725
774
  cacheRawValue(scope, key, value);
775
+ emitKeyChange(scope, key, oldValue, value, "set", "web");
726
776
  }
727
777
  function removeRawValue(key, scope) {
728
778
  (0, _internal.assertValidScope)(scope);
779
+ const oldValue = getEventRawValue(scope, key);
729
780
  if (scope === _Storage.StorageScope.Memory) {
730
781
  memoryStore.delete(key);
731
782
  notifyKeyListeners(memoryListeners, key);
783
+ emitKeyChange(scope, key, oldValue, undefined, "remove", "memory");
732
784
  return;
733
785
  }
734
786
  if (scope === _Storage.StorageScope.Disk) {
735
787
  cacheRawValue(scope, key, undefined);
736
788
  if (diskWritesAsync) {
737
789
  scheduleDiskWrite(key, undefined);
790
+ emitKeyChange(scope, key, oldValue, undefined, "remove", "web");
738
791
  return;
739
792
  }
740
793
  flushDiskWrites();
@@ -746,6 +799,7 @@ function removeRawValue(key, scope) {
746
799
  }
747
800
  WebStorage.remove(key, scope);
748
801
  cacheRawValue(scope, key, undefined);
802
+ emitKeyChange(scope, key, oldValue, undefined, "remove", "web");
749
803
  }
750
804
  function readMigrationVersion(scope) {
751
805
  const raw = getRawValue(_internal.MIGRATION_VERSION_KEY, scope);
@@ -759,11 +813,43 @@ function writeMigrationVersion(scope, version) {
759
813
  setRawValue(_internal.MIGRATION_VERSION_KEY, String(version), scope);
760
814
  }
761
815
  const storage = exports.storage = {
816
+ subscribe: (scope, listener) => {
817
+ (0, _internal.assertValidScope)(scope);
818
+ if (scope !== _Storage.StorageScope.Memory) {
819
+ ensureExternalSyncSubscriptions();
820
+ }
821
+ return storageEvents.subscribe(scope, listener);
822
+ },
823
+ subscribeKey: (scope, key, listener) => {
824
+ (0, _internal.assertValidScope)(scope);
825
+ if (scope !== _Storage.StorageScope.Memory) {
826
+ ensureExternalSyncSubscriptions();
827
+ }
828
+ return storageEvents.subscribeKey(scope, key, listener);
829
+ },
830
+ subscribePrefix: (scope, prefix, listener) => {
831
+ (0, _internal.assertValidScope)(scope);
832
+ if (scope !== _Storage.StorageScope.Memory) {
833
+ ensureExternalSyncSubscriptions();
834
+ }
835
+ return storageEvents.subscribePrefix(scope, prefix, listener);
836
+ },
837
+ subscribeNamespace: (namespace, scope, listener) => {
838
+ return storage.subscribePrefix(scope, (0, _internal.prefixKey)(namespace, ""), listener);
839
+ },
840
+ setEventObserver: observer => {
841
+ eventObserver = observer;
842
+ if (observer) {
843
+ ensureExternalSyncSubscriptions();
844
+ }
845
+ },
762
846
  clear: scope => {
763
847
  measureOperation("storage:clear", scope, () => {
848
+ const previousValues = hasStorageChangeObservers(scope) ? storage.getAll(scope) : {};
764
849
  if (scope === _Storage.StorageScope.Memory) {
765
850
  memoryStore.clear();
766
851
  notifyAllListeners(memoryListeners);
852
+ emitBatchChange(scope, "clear", "memory", Object.keys(previousValues).map(key => createKeyChange(scope, key, previousValues[key], undefined, "clear", "memory")));
767
853
  return;
768
854
  }
769
855
  if (scope === _Storage.StorageScope.Disk) {
@@ -776,6 +862,7 @@ const storage = exports.storage = {
776
862
  }
777
863
  clearScopeRawCache(scope);
778
864
  WebStorage.clear(scope);
865
+ emitBatchChange(scope, "clear", "web", Object.keys(previousValues).map(key => createKeyChange(scope, key, previousValues[key], undefined, "clear", "web")));
779
866
  });
780
867
  },
781
868
  clearAll: () => {
@@ -789,15 +876,26 @@ const storage = exports.storage = {
789
876
  measureOperation("storage:clearNamespace", scope, () => {
790
877
  (0, _internal.assertValidScope)(scope);
791
878
  if (scope === _Storage.StorageScope.Memory) {
792
- for (const key of memoryStore.keys()) {
793
- if ((0, _internal.isNamespaced)(key, namespace)) {
794
- memoryStore.delete(key);
795
- }
879
+ const affectedKeys = Array.from(memoryStore.keys()).filter(key => (0, _internal.isNamespaced)(key, namespace));
880
+ const previousValues = affectedKeys.map(key => ({
881
+ key,
882
+ value: getEventRawValue(scope, key)
883
+ }));
884
+ if (affectedKeys.length === 0) {
885
+ return;
796
886
  }
797
- notifyAllListeners(memoryListeners);
887
+ affectedKeys.forEach(key => {
888
+ memoryStore.delete(key);
889
+ });
890
+ affectedKeys.forEach(key => notifyKeyListeners(memoryListeners, key));
891
+ emitBatchChange(scope, "clearNamespace", "memory", previousValues.map(({
892
+ key,
893
+ value
894
+ }) => createKeyChange(scope, key, value, undefined, "clearNamespace", "memory")));
798
895
  return;
799
896
  }
800
897
  const keyPrefix = (0, _internal.prefixKey)(namespace, "");
898
+ const previousValues = hasStorageChangeObservers(scope) ? storage.getByPrefix(keyPrefix, scope) : {};
801
899
  if (scope === _Storage.StorageScope.Disk) {
802
900
  flushDiskWrites();
803
901
  }
@@ -811,6 +909,7 @@ const storage = exports.storage = {
811
909
  }
812
910
  }
813
911
  WebStorage.removeByPrefix(keyPrefix, scope);
912
+ emitBatchChange(scope, "clearNamespace", "web", Object.keys(previousValues).map(key => createKeyChange(scope, key, previousValues[key], undefined, "clearNamespace", "web")));
814
913
  });
815
914
  },
816
915
  clearBiometric: () => {
@@ -919,6 +1018,9 @@ const storage = exports.storage = {
919
1018
  return result;
920
1019
  });
921
1020
  },
1021
+ export: scope => {
1022
+ return measureOperation("storage:export", scope, () => storage.getAll(scope));
1023
+ },
922
1024
  size: scope => {
923
1025
  return measureOperation("storage:size", scope, () => {
924
1026
  (0, _internal.assertValidScope)(scope);
@@ -1062,11 +1164,13 @@ const storage = exports.storage = {
1062
1164
  (0, _internal.assertValidScope)(scope);
1063
1165
  if (keys.length === 0) return;
1064
1166
  const values = keys.map(k => data[k]);
1167
+ const changes = keys.map((key, index) => createKeyChange(scope, key, getEventRawValue(scope, key), values[index], "import", scope === _Storage.StorageScope.Memory ? "memory" : "web"));
1065
1168
  if (scope === _Storage.StorageScope.Memory) {
1066
1169
  keys.forEach((key, index) => {
1067
1170
  memoryStore.set(key, values[index]);
1068
1171
  });
1069
1172
  keys.forEach(key => notifyKeyListeners(memoryListeners, key));
1173
+ emitBatchChange(scope, "import", "memory", changes);
1070
1174
  return;
1071
1175
  }
1072
1176
  if (scope === _Storage.StorageScope.Secure) {
@@ -1078,6 +1182,7 @@ const storage = exports.storage = {
1078
1182
  }
1079
1183
  WebStorage.setBatch(keys, values, scope);
1080
1184
  keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
1185
+ emitBatchChange(scope, "import", "web", changes);
1081
1186
  }, keys.length);
1082
1187
  }
1083
1188
  };
@@ -1219,56 +1324,68 @@ function createStorageItem(config) {
1219
1324
  return raw;
1220
1325
  };
1221
1326
  const writeStoredRaw = rawValue => {
1327
+ const oldValue = config.scope === _Storage.StorageScope.Memory ? getEventRawValue(config.scope, storageKey) : undefined;
1222
1328
  if (isBiometric) {
1223
1329
  WebStorage.setSecureBiometricWithLevel(storageKey, rawValue, resolvedBiometricLevel);
1330
+ emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", "web");
1224
1331
  return;
1225
1332
  }
1226
1333
  cacheRawValue(nonMemoryScope, storageKey, rawValue);
1227
1334
  if (nonMemoryScope === _Storage.StorageScope.Disk) {
1228
1335
  if (coalesceDiskWrites || diskWritesAsync) {
1229
1336
  scheduleDiskWrite(storageKey, rawValue);
1337
+ emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", "web");
1230
1338
  return;
1231
1339
  }
1232
1340
  clearPendingDiskWrite(storageKey);
1233
1341
  }
1234
1342
  if (coalesceSecureWrites) {
1235
1343
  scheduleSecureWrite(storageKey, rawValue, secureAccessControl ?? secureDefaultAccessControl);
1344
+ emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", "web");
1236
1345
  return;
1237
1346
  }
1238
1347
  if (nonMemoryScope === _Storage.StorageScope.Secure) {
1239
1348
  clearPendingSecureWrite(storageKey);
1240
1349
  }
1241
1350
  WebStorage.set(storageKey, rawValue, config.scope);
1351
+ emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", "web");
1242
1352
  };
1243
1353
  const removeStoredRaw = () => {
1354
+ const oldValue = getEventRawValue(config.scope, storageKey);
1244
1355
  if (isBiometric) {
1245
1356
  WebStorage.deleteSecureBiometric(storageKey);
1357
+ emitKeyChange(config.scope, storageKey, oldValue, undefined, "remove", "web");
1246
1358
  return;
1247
1359
  }
1248
1360
  cacheRawValue(nonMemoryScope, storageKey, undefined);
1249
1361
  if (nonMemoryScope === _Storage.StorageScope.Disk) {
1250
1362
  if (coalesceDiskWrites || diskWritesAsync) {
1251
1363
  scheduleDiskWrite(storageKey, undefined);
1364
+ emitKeyChange(config.scope, storageKey, oldValue, undefined, "remove", "web");
1252
1365
  return;
1253
1366
  }
1254
1367
  clearPendingDiskWrite(storageKey);
1255
1368
  }
1256
1369
  if (coalesceSecureWrites) {
1257
1370
  scheduleSecureWrite(storageKey, undefined, secureAccessControl ?? secureDefaultAccessControl);
1371
+ emitKeyChange(config.scope, storageKey, oldValue, undefined, "remove", "web");
1258
1372
  return;
1259
1373
  }
1260
1374
  if (nonMemoryScope === _Storage.StorageScope.Secure) {
1261
1375
  clearPendingSecureWrite(storageKey);
1262
1376
  }
1263
1377
  WebStorage.remove(storageKey, config.scope);
1378
+ emitKeyChange(config.scope, storageKey, oldValue, undefined, "remove", "web");
1264
1379
  };
1265
1380
  const writeValueWithoutValidation = value => {
1266
1381
  if (isMemory) {
1382
+ const oldValue = getEventRawValue(config.scope, storageKey);
1267
1383
  if (memoryExpiration) {
1268
1384
  memoryExpiration.set(storageKey, Date.now() + (expirationTtlMs ?? 0));
1269
1385
  }
1270
1386
  memoryStore.set(storageKey, value);
1271
1387
  notifyKeyListeners(memoryListeners, storageKey);
1388
+ emitKeyChange(config.scope, storageKey, oldValue, typeof value === "string" ? value : undefined, "set", "memory");
1272
1389
  return;
1273
1390
  }
1274
1391
  const serialized = serialize(value);
@@ -1400,11 +1517,13 @@ function createStorageItem(config) {
1400
1517
  measureOperation("item:delete", config.scope, () => {
1401
1518
  invalidateParsedCache();
1402
1519
  if (isMemory) {
1520
+ const oldValue = getEventRawValue(config.scope, storageKey);
1403
1521
  if (memoryExpiration) {
1404
1522
  memoryExpiration.delete(storageKey);
1405
1523
  }
1406
1524
  memoryStore.delete(storageKey);
1407
1525
  notifyKeyListeners(memoryListeners, storageKey);
1526
+ emitKeyChange(config.scope, storageKey, oldValue, undefined, "remove", "memory");
1408
1527
  return;
1409
1528
  }
1410
1529
  removeStoredRaw();
@@ -1438,6 +1557,22 @@ function createStorageItem(config) {
1438
1557
  }
1439
1558
  };
1440
1559
  };
1560
+ const subscribeSelector = (selector, listener, options = {}) => {
1561
+ const isEqual = options.isEqual ?? Object.is;
1562
+ let currentValue = selector(getInternal());
1563
+ if (options.fireImmediately === true) {
1564
+ listener(currentValue, currentValue);
1565
+ }
1566
+ return subscribe(() => {
1567
+ const nextValue = selector(getInternal());
1568
+ if (isEqual(currentValue, nextValue)) {
1569
+ return;
1570
+ }
1571
+ const previousValue = currentValue;
1572
+ currentValue = nextValue;
1573
+ listener(nextValue, previousValue);
1574
+ });
1575
+ };
1441
1576
  const storageItem = {
1442
1577
  get,
1443
1578
  getWithVersion,
@@ -1446,6 +1581,7 @@ function createStorageItem(config) {
1446
1581
  delete: deleteItem,
1447
1582
  has: hasItem,
1448
1583
  subscribe,
1584
+ subscribeSelector,
1449
1585
  serialize,
1450
1586
  deserialize,
1451
1587
  _triggerListeners: () => {
@@ -1546,6 +1682,10 @@ function setBatch(items, scope) {
1546
1682
  }) => item.set(value));
1547
1683
  return;
1548
1684
  }
1685
+ const changes = items.map(({
1686
+ item,
1687
+ value
1688
+ }) => createKeyChange(scope, item.key, getEventRawValue(scope, item.key), typeof value === "string" ? value : undefined, "setBatch", "memory"));
1549
1689
 
1550
1690
  // Atomic write: update all values in memoryStore, invalidate caches, then batch-notify
1551
1691
  items.forEach(({
@@ -1558,6 +1698,7 @@ function setBatch(items, scope) {
1558
1698
  items.forEach(({
1559
1699
  item
1560
1700
  }) => notifyKeyListeners(memoryListeners, item.key));
1701
+ emitBatchChange(scope, "setBatch", "memory", changes);
1561
1702
  return;
1562
1703
  }
1563
1704
  if (scope === _Storage.StorageScope.Secure) {
@@ -1580,6 +1721,10 @@ function setBatch(items, scope) {
1580
1721
  return;
1581
1722
  }
1582
1723
  flushSecureWrites();
1724
+ const keys = secureEntries.map(({
1725
+ item
1726
+ }) => item.key);
1727
+ const oldValues = hasStorageChangeObservers(scope) ? WebStorage.getBatch(keys, scope) : [];
1583
1728
  const groupedByAccessControl = new Map();
1584
1729
  secureEntries.forEach(({
1585
1730
  item,
@@ -1603,6 +1748,10 @@ function setBatch(items, scope) {
1603
1748
  WebStorage.setBatch(group.keys, group.values, scope);
1604
1749
  group.keys.forEach((key, index) => cacheRawValue(scope, key, group.values[index]));
1605
1750
  });
1751
+ emitBatchChange(scope, "setBatch", "web", secureEntries.map(({
1752
+ item,
1753
+ value
1754
+ }, index) => createKeyChange(scope, item.key, oldValues[index], item.serialize(value), "setBatch", "web")));
1606
1755
  return;
1607
1756
  }
1608
1757
  flushDiskWrites();
@@ -1618,15 +1767,19 @@ function setBatch(items, scope) {
1618
1767
  }
1619
1768
  const keys = items.map(entry => entry.item.key);
1620
1769
  const values = items.map(entry => entry.item.serialize(entry.value));
1770
+ const oldValues = hasStorageChangeObservers(scope) ? WebStorage.getBatch(keys, scope) : [];
1621
1771
  WebStorage.setBatch(keys, values, scope);
1622
1772
  keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
1773
+ emitBatchChange(scope, "setBatch", "web", keys.map((key, index) => createKeyChange(scope, key, oldValues[index], values[index], "setBatch", "web")));
1623
1774
  }, items.length);
1624
1775
  }
1625
1776
  function removeBatch(items, scope) {
1626
1777
  measureOperation("batch:remove", scope, () => {
1627
1778
  (0, _internal.assertBatchScope)(items, scope);
1628
1779
  if (scope === _Storage.StorageScope.Memory) {
1780
+ const changes = items.map(item => createKeyChange(scope, item.key, getEventRawValue(scope, item.key), undefined, "removeBatch", "memory"));
1629
1781
  items.forEach(item => item.delete());
1782
+ emitBatchChange(scope, "removeBatch", "memory", changes);
1630
1783
  return;
1631
1784
  }
1632
1785
  const keys = items.map(item => item.key);
@@ -1636,8 +1789,10 @@ function removeBatch(items, scope) {
1636
1789
  if (scope === _Storage.StorageScope.Secure) {
1637
1790
  flushSecureWrites();
1638
1791
  }
1792
+ const oldValues = hasStorageChangeObservers(scope) ? WebStorage.getBatch(keys, scope) : [];
1639
1793
  WebStorage.removeBatch(keys, scope);
1640
1794
  keys.forEach(key => cacheRawValue(scope, key, undefined));
1795
+ emitBatchChange(scope, "removeBatch", "web", keys.map((key, index) => createKeyChange(scope, key, oldValues[index], undefined, "removeBatch", "web")));
1641
1796
  }, items.length);
1642
1797
  }
1643
1798
  function registerMigration(version, migration) {