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 { NitroModules } from "react-native-nitro-modules";
4
4
  import { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
5
5
  import { MIGRATION_VERSION_KEY, isStoredEnvelope, assertBatchScope, assertValidScope, decodeNativeBatchValue, serializeWithPrimitiveFastPath, deserializeWithPrimitiveFastPath, toVersionToken, prefixKey, isNamespaced } from "./internal";
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";
@@ -39,8 +40,11 @@ let diskWritesAsync = false;
39
40
  const pendingSecureWrites = new Map();
40
41
  let secureFlushScheduled = false;
41
42
  let secureDefaultAccessControl = AccessControl.WhenUnlocked;
43
+ const suppressedNativeEvents = new Map([[StorageScope.Disk, new Map()], [StorageScope.Secure, new Map()]]);
42
44
  let metricsObserver;
45
+ let eventObserver;
43
46
  const metricsCounters = new Map();
47
+ const storageEvents = new StorageEventRegistry();
44
48
  const nativeSecureBackend = "platform-secure-storage";
45
49
  function recordMetric(operation, scope, durationMs, keysCount = 1) {
46
50
  const existing = metricsCounters.get(operation);
@@ -91,6 +95,23 @@ function hasCachedRawValue(scope, key) {
91
95
  function clearScopeRawCache(scope) {
92
96
  getScopeRawCache(scope).clear();
93
97
  }
98
+ function suppressNativeEvent(scope, key) {
99
+ const suppressedEvents = suppressedNativeEvents.get(scope);
100
+ suppressedEvents.set(key, (suppressedEvents.get(key) ?? 0) + 1);
101
+ }
102
+ function consumeSuppressedNativeEvent(scope, key) {
103
+ const suppressedEvents = suppressedNativeEvents.get(scope);
104
+ const count = suppressedEvents.get(key);
105
+ if (count === undefined) {
106
+ return false;
107
+ }
108
+ if (count <= 1) {
109
+ suppressedEvents.delete(key);
110
+ } else {
111
+ suppressedEvents.set(key, count - 1);
112
+ }
113
+ return true;
114
+ }
94
115
  function notifyKeyListeners(registry, key) {
95
116
  const listeners = registry.get(key);
96
117
  if (listeners) {
@@ -124,6 +145,52 @@ function addKeyListener(registry, key, listener) {
124
145
  }
125
146
  };
126
147
  }
148
+ function getEventRawValue(scope, key) {
149
+ if (scope === StorageScope.Memory) {
150
+ const value = memoryStore.get(key);
151
+ return typeof value === "string" ? value : undefined;
152
+ }
153
+ return getRawValue(key, scope);
154
+ }
155
+ function createKeyChange(scope, key, oldValue, newValue, operation, source) {
156
+ return {
157
+ type: "key",
158
+ scope,
159
+ key,
160
+ oldValue,
161
+ newValue,
162
+ operation,
163
+ source
164
+ };
165
+ }
166
+ function hasStorageChangeObservers(scope) {
167
+ return storageEvents.hasListeners(scope) || eventObserver !== undefined;
168
+ }
169
+ function emitKeyChange(scope, key, oldValue, newValue, operation, source) {
170
+ if (source === "native" && operation !== "external" && scope !== StorageScope.Memory && scopedUnsubscribers.has(scope)) {
171
+ suppressNativeEvent(scope, key);
172
+ }
173
+ const event = createKeyChange(scope, key, oldValue, newValue, operation, source);
174
+ storageEvents.emitKey(event);
175
+ eventObserver?.(event);
176
+ }
177
+ function emitBatchChange(scope, operation, source, changes) {
178
+ if (changes.length === 0) {
179
+ return;
180
+ }
181
+ if (source === "native" && operation !== "external" && scope !== StorageScope.Memory && scopedUnsubscribers.has(scope)) {
182
+ changes.forEach(change => suppressNativeEvent(scope, change.key));
183
+ }
184
+ const event = {
185
+ type: "batch",
186
+ scope,
187
+ operation,
188
+ source,
189
+ changes
190
+ };
191
+ storageEvents.emitBatch(event);
192
+ eventObserver?.(event);
193
+ }
127
194
  function readPendingSecureWrite(key) {
128
195
  return pendingSecureWrites.get(key)?.value;
129
196
  }
@@ -260,14 +327,19 @@ function ensureNativeScopeSubscription(scope) {
260
327
  notifyAllListeners(getScopedListeners(scope));
261
328
  return;
262
329
  }
330
+ const oldValue = readCachedRawValue(scope, key);
263
331
  cacheRawValue(scope, key, value);
264
332
  notifyKeyListeners(getScopedListeners(scope), key);
333
+ if (consumeSuppressedNativeEvent(scope, key)) {
334
+ return;
335
+ }
336
+ emitKeyChange(scope, key, oldValue, value, "external", "native");
265
337
  });
266
- scopedUnsubscribers.set(scope, unsubscribe);
338
+ scopedUnsubscribers.set(scope, typeof unsubscribe === "function" ? unsubscribe : () => {});
267
339
  }
268
340
  function maybeCleanupNativeScopeSubscription(scope) {
269
341
  const listeners = getScopedListeners(scope);
270
- if (listeners.size > 0) {
342
+ if (listeners.size > 0 || storageEvents.hasListeners(scope) || eventObserver !== undefined) {
271
343
  return;
272
344
  }
273
345
  const unsubscribe = scopedUnsubscribers.get(scope);
@@ -293,15 +365,18 @@ function getRawValue(key, scope) {
293
365
  }
294
366
  function setRawValue(key, value, scope) {
295
367
  assertValidScope(scope);
368
+ const oldValue = scope === StorageScope.Memory ? getEventRawValue(scope, key) : undefined;
296
369
  if (scope === StorageScope.Memory) {
297
370
  memoryStore.set(key, value);
298
371
  notifyKeyListeners(memoryListeners, key);
372
+ emitKeyChange(scope, key, oldValue, value, "set", "memory");
299
373
  return;
300
374
  }
301
375
  if (scope === StorageScope.Disk) {
302
376
  cacheRawValue(scope, key, value);
303
377
  if (diskWritesAsync) {
304
378
  scheduleDiskWrite(key, value);
379
+ emitKeyChange(scope, key, oldValue, value, "set", "native");
305
380
  return;
306
381
  }
307
382
  flushDiskWrites();
@@ -314,18 +389,22 @@ function setRawValue(key, value, scope) {
314
389
  }
315
390
  getStorageModule().set(key, value, scope);
316
391
  cacheRawValue(scope, key, value);
392
+ emitKeyChange(scope, key, oldValue, value, "set", "native");
317
393
  }
318
394
  function removeRawValue(key, scope) {
319
395
  assertValidScope(scope);
396
+ const oldValue = getEventRawValue(scope, key);
320
397
  if (scope === StorageScope.Memory) {
321
398
  memoryStore.delete(key);
322
399
  notifyKeyListeners(memoryListeners, key);
400
+ emitKeyChange(scope, key, oldValue, undefined, "remove", "memory");
323
401
  return;
324
402
  }
325
403
  if (scope === StorageScope.Disk) {
326
404
  cacheRawValue(scope, key, undefined);
327
405
  if (diskWritesAsync) {
328
406
  scheduleDiskWrite(key, undefined);
407
+ emitKeyChange(scope, key, oldValue, undefined, "remove", "native");
329
408
  return;
330
409
  }
331
410
  flushDiskWrites();
@@ -337,6 +416,7 @@ function removeRawValue(key, scope) {
337
416
  }
338
417
  getStorageModule().remove(key, scope);
339
418
  cacheRawValue(scope, key, undefined);
419
+ emitKeyChange(scope, key, oldValue, undefined, "remove", "native");
340
420
  }
341
421
  function readMigrationVersion(scope) {
342
422
  const raw = getRawValue(MIGRATION_VERSION_KEY, scope);
@@ -350,11 +430,62 @@ function writeMigrationVersion(scope, version) {
350
430
  setRawValue(MIGRATION_VERSION_KEY, String(version), scope);
351
431
  }
352
432
  export const storage = {
433
+ subscribe: (scope, listener) => {
434
+ assertValidScope(scope);
435
+ if (scope !== StorageScope.Memory) {
436
+ ensureNativeScopeSubscription(scope);
437
+ const unsubscribe = storageEvents.subscribe(scope, listener);
438
+ return () => {
439
+ unsubscribe();
440
+ maybeCleanupNativeScopeSubscription(scope);
441
+ };
442
+ }
443
+ return storageEvents.subscribe(scope, listener);
444
+ },
445
+ subscribeKey: (scope, key, listener) => {
446
+ assertValidScope(scope);
447
+ if (scope !== StorageScope.Memory) {
448
+ ensureNativeScopeSubscription(scope);
449
+ const unsubscribe = storageEvents.subscribeKey(scope, key, listener);
450
+ return () => {
451
+ unsubscribe();
452
+ maybeCleanupNativeScopeSubscription(scope);
453
+ };
454
+ }
455
+ return storageEvents.subscribeKey(scope, key, listener);
456
+ },
457
+ subscribePrefix: (scope, prefix, listener) => {
458
+ assertValidScope(scope);
459
+ if (scope !== StorageScope.Memory) {
460
+ ensureNativeScopeSubscription(scope);
461
+ const unsubscribe = storageEvents.subscribePrefix(scope, prefix, listener);
462
+ return () => {
463
+ unsubscribe();
464
+ maybeCleanupNativeScopeSubscription(scope);
465
+ };
466
+ }
467
+ return storageEvents.subscribePrefix(scope, prefix, listener);
468
+ },
469
+ subscribeNamespace: (namespace, scope, listener) => {
470
+ return storage.subscribePrefix(scope, prefixKey(namespace, ""), listener);
471
+ },
472
+ setEventObserver: observer => {
473
+ eventObserver = observer;
474
+ if (observer) {
475
+ ensureNativeScopeSubscription(StorageScope.Disk);
476
+ ensureNativeScopeSubscription(StorageScope.Secure);
477
+ return;
478
+ }
479
+ maybeCleanupNativeScopeSubscription(StorageScope.Disk);
480
+ maybeCleanupNativeScopeSubscription(StorageScope.Secure);
481
+ },
353
482
  clear: scope => {
354
483
  measureOperation("storage:clear", scope, () => {
484
+ const previousValues = hasStorageChangeObservers(scope) ? storage.getAll(scope) : {};
355
485
  if (scope === StorageScope.Memory) {
356
486
  memoryStore.clear();
357
487
  notifyAllListeners(memoryListeners);
488
+ emitBatchChange(scope, "clear", "memory", Object.keys(previousValues).map(key => createKeyChange(scope, key, previousValues[key], undefined, "clear", "memory")));
358
489
  return;
359
490
  }
360
491
  if (scope === StorageScope.Disk) {
@@ -367,6 +498,7 @@ export const storage = {
367
498
  }
368
499
  clearScopeRawCache(scope);
369
500
  getStorageModule().clear(scope);
501
+ emitBatchChange(scope, "clear", "native", Object.keys(previousValues).map(key => createKeyChange(scope, key, previousValues[key], undefined, "clear", "native")));
370
502
  });
371
503
  },
372
504
  clearAll: () => {
@@ -380,15 +512,26 @@ export const storage = {
380
512
  measureOperation("storage:clearNamespace", scope, () => {
381
513
  assertValidScope(scope);
382
514
  if (scope === StorageScope.Memory) {
383
- for (const key of memoryStore.keys()) {
384
- if (isNamespaced(key, namespace)) {
385
- memoryStore.delete(key);
386
- }
515
+ const affectedKeys = Array.from(memoryStore.keys()).filter(key => isNamespaced(key, namespace));
516
+ const previousValues = affectedKeys.map(key => ({
517
+ key,
518
+ value: getEventRawValue(scope, key)
519
+ }));
520
+ if (affectedKeys.length === 0) {
521
+ return;
387
522
  }
388
- notifyAllListeners(memoryListeners);
523
+ affectedKeys.forEach(key => {
524
+ memoryStore.delete(key);
525
+ });
526
+ affectedKeys.forEach(key => notifyKeyListeners(memoryListeners, key));
527
+ emitBatchChange(scope, "clearNamespace", "memory", previousValues.map(({
528
+ key,
529
+ value
530
+ }) => createKeyChange(scope, key, value, undefined, "clearNamespace", "memory")));
389
531
  return;
390
532
  }
391
533
  const keyPrefix = prefixKey(namespace, "");
534
+ const previousValues = hasStorageChangeObservers(scope) ? storage.getByPrefix(keyPrefix, scope) : {};
392
535
  if (scope === StorageScope.Disk) {
393
536
  flushDiskWrites();
394
537
  }
@@ -402,6 +545,7 @@ export const storage = {
402
545
  }
403
546
  }
404
547
  getStorageModule().removeByPrefix(keyPrefix, scope);
548
+ emitBatchChange(scope, "clearNamespace", "native", Object.keys(previousValues).map(key => createKeyChange(scope, key, previousValues[key], undefined, "clearNamespace", "native")));
405
549
  });
406
550
  },
407
551
  clearBiometric: () => {
@@ -451,7 +595,7 @@ export const storage = {
451
595
  if (scope === StorageScope.Secure) {
452
596
  flushSecureWrites();
453
597
  }
454
- return getStorageModule().getKeysByPrefix(prefix, scope);
598
+ return getStorageModule().getKeysByPrefix(prefix, scope) ?? [];
455
599
  });
456
600
  },
457
601
  getByPrefix: (prefix, scope) => {
@@ -476,7 +620,7 @@ export const storage = {
476
620
  if (scope === StorageScope.Secure) {
477
621
  flushSecureWrites();
478
622
  }
479
- const values = getStorageModule().getBatch(keys, scope);
623
+ const values = getStorageModule().getBatch(keys, scope) ?? [];
480
624
  keys.forEach((key, idx) => {
481
625
  const value = decodeNativeBatchValue(values[idx]);
482
626
  if (value !== undefined) {
@@ -491,9 +635,10 @@ export const storage = {
491
635
  assertValidScope(scope);
492
636
  const result = {};
493
637
  if (scope === StorageScope.Memory) {
494
- memoryStore.forEach((value, key) => {
638
+ for (const key of memoryStore.keys()) {
639
+ const value = memoryStore.get(key);
495
640
  if (typeof value === "string") result[key] = value;
496
- });
641
+ }
497
642
  return result;
498
643
  }
499
644
  if (scope === StorageScope.Disk) {
@@ -502,9 +647,9 @@ export const storage = {
502
647
  if (scope === StorageScope.Secure) {
503
648
  flushSecureWrites();
504
649
  }
505
- const keys = getStorageModule().getAllKeys(scope);
650
+ const keys = getStorageModule().getAllKeys(scope) ?? [];
506
651
  if (keys.length === 0) return result;
507
- const values = getStorageModule().getBatch(keys, scope);
652
+ const values = getStorageModule().getBatch(keys, scope) ?? [];
508
653
  keys.forEach((key, idx) => {
509
654
  const val = decodeNativeBatchValue(values[idx]);
510
655
  if (val !== undefined) result[key] = val;
@@ -512,6 +657,9 @@ export const storage = {
512
657
  return result;
513
658
  });
514
659
  },
660
+ export: scope => {
661
+ return measureOperation("storage:export", scope, () => storage.getAll(scope));
662
+ },
515
663
  size: scope => {
516
664
  return measureOperation("storage:size", scope, () => {
517
665
  assertValidScope(scope);
@@ -661,11 +809,13 @@ export const storage = {
661
809
  assertValidScope(scope);
662
810
  if (keys.length === 0) return;
663
811
  const values = keys.map(k => data[k]);
812
+ const changes = keys.map((key, index) => createKeyChange(scope, key, getEventRawValue(scope, key), values[index], "import", scope === StorageScope.Memory ? "memory" : "native"));
664
813
  if (scope === StorageScope.Memory) {
665
814
  keys.forEach((key, index) => {
666
815
  memoryStore.set(key, values[index]);
667
816
  });
668
817
  keys.forEach(key => notifyKeyListeners(memoryListeners, key));
818
+ emitBatchChange(scope, "import", "memory", changes);
669
819
  return;
670
820
  }
671
821
  if (scope === StorageScope.Secure) {
@@ -674,6 +824,7 @@ export const storage = {
674
824
  }
675
825
  getStorageModule().setBatch(keys, values, scope);
676
826
  keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
827
+ emitBatchChange(scope, "import", "native", changes);
677
828
  }, keys.length);
678
829
  }
679
830
  };
@@ -794,20 +945,24 @@ export function createStorageItem(config) {
794
945
  return raw;
795
946
  };
796
947
  const writeStoredRaw = rawValue => {
948
+ const oldValue = undefined;
797
949
  if (isBiometric) {
798
950
  getStorageModule().setSecureBiometricWithLevel(storageKey, rawValue, resolvedBiometricLevel);
951
+ emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", "native");
799
952
  return;
800
953
  }
801
954
  cacheRawValue(nonMemoryScope, storageKey, rawValue);
802
955
  if (nonMemoryScope === StorageScope.Disk) {
803
956
  if (coalesceDiskWrites || diskWritesAsync) {
804
957
  scheduleDiskWrite(storageKey, rawValue);
958
+ emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", "native");
805
959
  return;
806
960
  }
807
961
  clearPendingDiskWrite(storageKey);
808
962
  }
809
963
  if (coalesceSecureWrites) {
810
964
  scheduleSecureWrite(storageKey, rawValue, secureAccessControl ?? secureDefaultAccessControl);
965
+ emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", "native");
811
966
  return;
812
967
  }
813
968
  if (nonMemoryScope === StorageScope.Secure) {
@@ -815,36 +970,44 @@ export function createStorageItem(config) {
815
970
  getStorageModule().setSecureAccessControl(secureAccessControl ?? secureDefaultAccessControl);
816
971
  }
817
972
  getStorageModule().set(storageKey, rawValue, config.scope);
973
+ emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", "native");
818
974
  };
819
975
  const removeStoredRaw = () => {
976
+ const oldValue = getEventRawValue(config.scope, storageKey);
820
977
  if (isBiometric) {
821
978
  getStorageModule().deleteSecureBiometric(storageKey);
979
+ emitKeyChange(config.scope, storageKey, oldValue, undefined, "remove", "native");
822
980
  return;
823
981
  }
824
982
  cacheRawValue(nonMemoryScope, storageKey, undefined);
825
983
  if (nonMemoryScope === StorageScope.Disk) {
826
984
  if (coalesceDiskWrites || diskWritesAsync) {
827
985
  scheduleDiskWrite(storageKey, undefined);
986
+ emitKeyChange(config.scope, storageKey, oldValue, undefined, "remove", "native");
828
987
  return;
829
988
  }
830
989
  clearPendingDiskWrite(storageKey);
831
990
  }
832
991
  if (coalesceSecureWrites) {
833
992
  scheduleSecureWrite(storageKey, undefined, secureAccessControl ?? secureDefaultAccessControl);
993
+ emitKeyChange(config.scope, storageKey, oldValue, undefined, "remove", "native");
834
994
  return;
835
995
  }
836
996
  if (nonMemoryScope === StorageScope.Secure) {
837
997
  clearPendingSecureWrite(storageKey);
838
998
  }
839
999
  getStorageModule().remove(storageKey, config.scope);
1000
+ emitKeyChange(config.scope, storageKey, oldValue, undefined, "remove", "native");
840
1001
  };
841
1002
  const writeValueWithoutValidation = value => {
842
1003
  if (isMemory) {
1004
+ const oldValue = getEventRawValue(config.scope, storageKey);
843
1005
  if (memoryExpiration) {
844
1006
  memoryExpiration.set(storageKey, Date.now() + (expirationTtlMs ?? 0));
845
1007
  }
846
1008
  memoryStore.set(storageKey, value);
847
1009
  notifyKeyListeners(memoryListeners, storageKey);
1010
+ emitKeyChange(config.scope, storageKey, oldValue, typeof value === "string" ? value : undefined, "set", "memory");
848
1011
  return;
849
1012
  }
850
1013
  const serialized = serialize(value);
@@ -976,11 +1139,13 @@ export function createStorageItem(config) {
976
1139
  measureOperation("item:delete", config.scope, () => {
977
1140
  invalidateParsedCache();
978
1141
  if (isMemory) {
1142
+ const oldValue = getEventRawValue(config.scope, storageKey);
979
1143
  if (memoryExpiration) {
980
1144
  memoryExpiration.delete(storageKey);
981
1145
  }
982
1146
  memoryStore.delete(storageKey);
983
1147
  notifyKeyListeners(memoryListeners, storageKey);
1148
+ emitKeyChange(config.scope, storageKey, oldValue, undefined, "remove", "memory");
984
1149
  return;
985
1150
  }
986
1151
  removeStoredRaw();
@@ -1017,6 +1182,22 @@ export function createStorageItem(config) {
1017
1182
  }
1018
1183
  };
1019
1184
  };
1185
+ const subscribeSelector = (selector, listener, options = {}) => {
1186
+ const isEqual = options.isEqual ?? Object.is;
1187
+ let currentValue = selector(getInternal());
1188
+ if (options.fireImmediately === true) {
1189
+ listener(currentValue, currentValue);
1190
+ }
1191
+ return subscribe(() => {
1192
+ const nextValue = selector(getInternal());
1193
+ if (isEqual(currentValue, nextValue)) {
1194
+ return;
1195
+ }
1196
+ const previousValue = currentValue;
1197
+ currentValue = nextValue;
1198
+ listener(nextValue, previousValue);
1199
+ });
1200
+ };
1020
1201
  const storageItem = {
1021
1202
  get,
1022
1203
  getWithVersion,
@@ -1025,6 +1206,7 @@ export function createStorageItem(config) {
1025
1206
  delete: deleteItem,
1026
1207
  has: hasItem,
1027
1208
  subscribe,
1209
+ subscribeSelector,
1028
1210
  serialize,
1029
1211
  deserialize,
1030
1212
  _triggerListeners: () => {
@@ -1128,6 +1310,10 @@ export function setBatch(items, scope) {
1128
1310
  }) => item.set(value));
1129
1311
  return;
1130
1312
  }
1313
+ const changes = items.map(({
1314
+ item,
1315
+ value
1316
+ }) => createKeyChange(scope, item.key, getEventRawValue(scope, item.key), typeof value === "string" ? value : undefined, "setBatch", "memory"));
1131
1317
 
1132
1318
  // Atomic write: update all values in memoryStore, invalidate caches, then batch-notify
1133
1319
  items.forEach(({
@@ -1140,6 +1326,7 @@ export function setBatch(items, scope) {
1140
1326
  items.forEach(({
1141
1327
  item
1142
1328
  }) => notifyKeyListeners(memoryListeners, item.key));
1329
+ emitBatchChange(scope, "setBatch", "memory", changes);
1143
1330
  return;
1144
1331
  }
1145
1332
  if (scope === StorageScope.Secure) {
@@ -1163,6 +1350,10 @@ export function setBatch(items, scope) {
1163
1350
  }
1164
1351
  flushSecureWrites();
1165
1352
  const storageModule = getStorageModule();
1353
+ const keys = secureEntries.map(({
1354
+ item
1355
+ }) => item.key);
1356
+ const oldValues = hasStorageChangeObservers(scope) ? storageModule.getBatch(keys, scope) ?? [] : [];
1166
1357
  const groupedByAccessControl = new Map();
1167
1358
  secureEntries.forEach(({
1168
1359
  item,
@@ -1186,6 +1377,10 @@ export function setBatch(items, scope) {
1186
1377
  storageModule.setBatch(group.keys, group.values, scope);
1187
1378
  group.keys.forEach((key, index) => cacheRawValue(scope, key, group.values[index]));
1188
1379
  });
1380
+ emitBatchChange(scope, "setBatch", "native", secureEntries.map(({
1381
+ item,
1382
+ value
1383
+ }, index) => createKeyChange(scope, item.key, oldValues[index], item.serialize(value), "setBatch", "native")));
1189
1384
  return;
1190
1385
  }
1191
1386
  flushDiskWrites();
@@ -1201,15 +1396,19 @@ export function setBatch(items, scope) {
1201
1396
  }
1202
1397
  const keys = items.map(entry => entry.item.key);
1203
1398
  const values = items.map(entry => entry.item.serialize(entry.value));
1399
+ const oldValues = hasStorageChangeObservers(scope) ? getStorageModule().getBatch(keys, scope) ?? [] : [];
1204
1400
  getStorageModule().setBatch(keys, values, scope);
1205
1401
  keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
1402
+ emitBatchChange(scope, "setBatch", "native", keys.map((key, index) => createKeyChange(scope, key, oldValues[index], values[index], "setBatch", "native")));
1206
1403
  }, items.length);
1207
1404
  }
1208
1405
  export function removeBatch(items, scope) {
1209
1406
  measureOperation("batch:remove", scope, () => {
1210
1407
  assertBatchScope(items, scope);
1211
1408
  if (scope === StorageScope.Memory) {
1409
+ const changes = items.map(item => createKeyChange(scope, item.key, getEventRawValue(scope, item.key), undefined, "removeBatch", "memory"));
1212
1410
  items.forEach(item => item.delete());
1411
+ emitBatchChange(scope, "removeBatch", "memory", changes);
1213
1412
  return;
1214
1413
  }
1215
1414
  const keys = items.map(item => item.key);
@@ -1219,8 +1418,10 @@ export function removeBatch(items, scope) {
1219
1418
  if (scope === StorageScope.Secure) {
1220
1419
  flushSecureWrites();
1221
1420
  }
1421
+ const oldValues = hasStorageChangeObservers(scope) ? getStorageModule().getBatch(keys, scope) ?? [] : [];
1222
1422
  getStorageModule().removeBatch(keys, scope);
1223
1423
  keys.forEach(key => cacheRawValue(scope, key, undefined));
1424
+ emitBatchChange(scope, "removeBatch", "native", keys.map((key, index) => createKeyChange(scope, key, oldValues[index], undefined, "removeBatch", "native")));
1224
1425
  }, items.length);
1225
1426
  }
1226
1427
  export function registerMigration(version, migration) {