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/src/index.web.ts CHANGED
@@ -26,6 +26,15 @@ import {
26
26
  type StorageCapabilities,
27
27
  type StorageErrorCode,
28
28
  } from "./storage-runtime";
29
+ import {
30
+ StorageEventRegistry,
31
+ type StorageBatchChangeEvent,
32
+ type StorageChangeEvent,
33
+ type StorageChangeOperation,
34
+ type StorageChangeSource,
35
+ type StorageEventListener,
36
+ type StorageKeyChangeEvent,
37
+ } from "./storage-events";
29
38
 
30
39
  export { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
31
40
  export { migrateFromMMKV } from "./migration";
@@ -36,6 +45,14 @@ export {
36
45
  type StorageCapabilities,
37
46
  type StorageErrorCode,
38
47
  } from "./storage-runtime";
48
+ export type {
49
+ StorageBatchChangeEvent,
50
+ StorageChangeEvent,
51
+ StorageChangeOperation,
52
+ StorageChangeSource,
53
+ StorageEventListener,
54
+ StorageKeyChangeEvent,
55
+ } from "./storage-events";
39
56
  export type {
40
57
  WebStorageBackend,
41
58
  WebStorageChangeEvent,
@@ -64,6 +81,14 @@ export type StorageMetricSummary = {
64
81
  avgDurationMs: number;
65
82
  maxDurationMs: number;
66
83
  };
84
+ export type StorageSelectorListener<TSelected> = (
85
+ value: TSelected,
86
+ previousValue: TSelected,
87
+ ) => void;
88
+ export type StorageSelectorSubscribeOptions<TSelected> = {
89
+ isEqual?: (previousValue: TSelected, nextValue: TSelected) => boolean;
90
+ fireImmediately?: boolean;
91
+ };
67
92
  export type MigrationContext = {
68
93
  scope: StorageScope;
69
94
  getRaw: (key: string) => string | undefined;
@@ -191,10 +216,12 @@ const BIOMETRIC_WEB_PREFIX = "__bio_";
191
216
  let hasWarnedAboutWebBiometricFallback = false;
192
217
  let hasWindowStorageEventSubscription = false;
193
218
  let metricsObserver: StorageMetricsObserver | undefined;
219
+ let eventObserver: StorageEventListener | undefined;
194
220
  const metricsCounters = new Map<
195
221
  string,
196
222
  { count: number; totalDurationMs: number; maxDurationMs: number }
197
223
  >();
224
+ const storageEvents = new StorageEventRegistry();
198
225
 
199
226
  function recordMetric(
200
227
  operation: string,
@@ -379,6 +406,7 @@ function applyExternalChangeEvent(
379
406
 
380
407
  if (scope === StorageScope.Secure && key.startsWith(SECURE_WEB_PREFIX)) {
381
408
  const plainKey = fromSecureStorageKey(key);
409
+ const oldValue = readCachedRawValue(StorageScope.Secure, plainKey);
382
410
  if (newValue === null) {
383
411
  ensureWebScopeKeyIndex(StorageScope.Secure).delete(plainKey);
384
412
  cacheRawValue(StorageScope.Secure, plainKey, undefined);
@@ -387,11 +415,20 @@ function applyExternalChangeEvent(
387
415
  cacheRawValue(StorageScope.Secure, plainKey, newValue);
388
416
  }
389
417
  notifyKeyListeners(getScopedListeners(StorageScope.Secure), plainKey);
418
+ emitKeyChange(
419
+ StorageScope.Secure,
420
+ plainKey,
421
+ oldValue,
422
+ newValue ?? undefined,
423
+ "external",
424
+ "external",
425
+ );
390
426
  return;
391
427
  }
392
428
 
393
429
  if (scope === StorageScope.Secure && key.startsWith(BIOMETRIC_WEB_PREFIX)) {
394
430
  const plainKey = fromBiometricStorageKey(key);
431
+ const oldValue = readCachedRawValue(StorageScope.Secure, plainKey);
395
432
  if (newValue === null) {
396
433
  if (
397
434
  withWebBackendOperation(
@@ -408,9 +445,18 @@ function applyExternalChangeEvent(
408
445
  cacheRawValue(StorageScope.Secure, plainKey, newValue);
409
446
  }
410
447
  notifyKeyListeners(getScopedListeners(StorageScope.Secure), plainKey);
448
+ emitKeyChange(
449
+ StorageScope.Secure,
450
+ plainKey,
451
+ oldValue,
452
+ newValue ?? undefined,
453
+ "external",
454
+ "external",
455
+ );
411
456
  return;
412
457
  }
413
458
 
459
+ const oldValue = readCachedRawValue(scope, key);
414
460
  if (newValue === null) {
415
461
  ensureWebScopeKeyIndex(scope).delete(key);
416
462
  cacheRawValue(scope, key, undefined);
@@ -419,6 +465,14 @@ function applyExternalChangeEvent(
419
465
  cacheRawValue(scope, key, newValue);
420
466
  }
421
467
  notifyKeyListeners(getScopedListeners(scope), key);
468
+ emitKeyChange(
469
+ scope,
470
+ key,
471
+ oldValue,
472
+ newValue ?? undefined,
473
+ "external",
474
+ "external",
475
+ );
422
476
  }
423
477
 
424
478
  function handleWebStorageEvent(event: StorageEvent): void {
@@ -549,6 +603,82 @@ function addKeyListener(
549
603
  };
550
604
  }
551
605
 
606
+ function getEventRawValue(
607
+ scope: StorageScope,
608
+ key: string,
609
+ ): string | undefined {
610
+ if (scope === StorageScope.Memory) {
611
+ const value = memoryStore.get(key);
612
+ return typeof value === "string" ? value : undefined;
613
+ }
614
+
615
+ return getRawValue(key, scope);
616
+ }
617
+
618
+ function createKeyChange(
619
+ scope: StorageScope,
620
+ key: string,
621
+ oldValue: string | undefined,
622
+ newValue: string | undefined,
623
+ operation: StorageChangeOperation,
624
+ source: StorageChangeSource,
625
+ ): StorageKeyChangeEvent {
626
+ return {
627
+ type: "key",
628
+ scope,
629
+ key,
630
+ oldValue,
631
+ newValue,
632
+ operation,
633
+ source,
634
+ };
635
+ }
636
+
637
+ function hasStorageChangeObservers(scope: StorageScope): boolean {
638
+ return storageEvents.hasListeners(scope) || eventObserver !== undefined;
639
+ }
640
+
641
+ function emitKeyChange(
642
+ scope: StorageScope,
643
+ key: string,
644
+ oldValue: string | undefined,
645
+ newValue: string | undefined,
646
+ operation: StorageChangeOperation,
647
+ source: StorageChangeSource,
648
+ ): void {
649
+ const event = createKeyChange(
650
+ scope,
651
+ key,
652
+ oldValue,
653
+ newValue,
654
+ operation,
655
+ source,
656
+ );
657
+ storageEvents.emitKey(event);
658
+ eventObserver?.(event);
659
+ }
660
+
661
+ function emitBatchChange(
662
+ scope: StorageScope,
663
+ operation: StorageChangeOperation,
664
+ source: StorageChangeSource,
665
+ changes: StorageKeyChangeEvent[],
666
+ ): void {
667
+ if (changes.length === 0) {
668
+ return;
669
+ }
670
+
671
+ const event: StorageBatchChangeEvent = {
672
+ type: "batch",
673
+ scope,
674
+ operation,
675
+ source,
676
+ changes,
677
+ };
678
+ storageEvents.emitBatch(event);
679
+ eventObserver?.(event);
680
+ }
681
+
552
682
  function readPendingSecureWrite(key: string): string | undefined {
553
683
  return pendingSecureWrites.get(key)?.value;
554
684
  }
@@ -833,24 +963,10 @@ const WebStorage: Storage = {
833
963
  return () => {};
834
964
  },
835
965
  has: (key: string, scope: number) => {
836
- if (scope === StorageScope.Secure) {
837
- return (
838
- withWebBackendOperation(scope, "has", (backend) =>
839
- backend.getItem(toSecureStorageKey(key)),
840
- ) !== null ||
841
- withWebBackendOperation(scope, "has", (backend) =>
842
- backend.getItem(toBiometricStorageKey(key)),
843
- ) !== null
844
- );
845
- }
846
- if (scope !== StorageScope.Disk) {
847
- return false;
966
+ if (scope === StorageScope.Disk || scope === StorageScope.Secure) {
967
+ return ensureWebScopeKeyIndex(scope).has(key);
848
968
  }
849
- return (
850
- withWebBackendOperation(scope, "has", (backend) =>
851
- backend.getItem(key),
852
- ) !== null
853
- );
969
+ return false;
854
970
  },
855
971
  getAllKeys: (scope: number) => {
856
972
  if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
@@ -1000,9 +1116,12 @@ function getRawValue(key: string, scope: StorageScope): string | undefined {
1000
1116
 
1001
1117
  function setRawValue(key: string, value: string, scope: StorageScope): void {
1002
1118
  assertValidScope(scope);
1119
+ const oldValue =
1120
+ scope === StorageScope.Memory ? getEventRawValue(scope, key) : undefined;
1003
1121
  if (scope === StorageScope.Memory) {
1004
1122
  memoryStore.set(key, value);
1005
1123
  notifyKeyListeners(memoryListeners, key);
1124
+ emitKeyChange(scope, key, oldValue, value, "set", "memory");
1006
1125
  return;
1007
1126
  }
1008
1127
 
@@ -1010,6 +1129,7 @@ function setRawValue(key: string, value: string, scope: StorageScope): void {
1010
1129
  cacheRawValue(scope, key, value);
1011
1130
  if (diskWritesAsync) {
1012
1131
  scheduleDiskWrite(key, value);
1132
+ emitKeyChange(scope, key, oldValue, value, "set", "web");
1013
1133
  return;
1014
1134
  }
1015
1135
 
@@ -1024,13 +1144,16 @@ function setRawValue(key: string, value: string, scope: StorageScope): void {
1024
1144
 
1025
1145
  WebStorage.set(key, value, scope);
1026
1146
  cacheRawValue(scope, key, value);
1147
+ emitKeyChange(scope, key, oldValue, value, "set", "web");
1027
1148
  }
1028
1149
 
1029
1150
  function removeRawValue(key: string, scope: StorageScope): void {
1030
1151
  assertValidScope(scope);
1152
+ const oldValue = getEventRawValue(scope, key);
1031
1153
  if (scope === StorageScope.Memory) {
1032
1154
  memoryStore.delete(key);
1033
1155
  notifyKeyListeners(memoryListeners, key);
1156
+ emitKeyChange(scope, key, oldValue, undefined, "remove", "memory");
1034
1157
  return;
1035
1158
  }
1036
1159
 
@@ -1038,6 +1161,7 @@ function removeRawValue(key: string, scope: StorageScope): void {
1038
1161
  cacheRawValue(scope, key, undefined);
1039
1162
  if (diskWritesAsync) {
1040
1163
  scheduleDiskWrite(key, undefined);
1164
+ emitKeyChange(scope, key, oldValue, undefined, "remove", "web");
1041
1165
  return;
1042
1166
  }
1043
1167
 
@@ -1052,6 +1176,7 @@ function removeRawValue(key: string, scope: StorageScope): void {
1052
1176
 
1053
1177
  WebStorage.remove(key, scope);
1054
1178
  cacheRawValue(scope, key, undefined);
1179
+ emitKeyChange(scope, key, oldValue, undefined, "remove", "web");
1055
1180
  }
1056
1181
 
1057
1182
  function readMigrationVersion(scope: StorageScope): number {
@@ -1069,11 +1194,74 @@ function writeMigrationVersion(scope: StorageScope, version: number): void {
1069
1194
  }
1070
1195
 
1071
1196
  export const storage = {
1197
+ subscribe: (
1198
+ scope: StorageScope,
1199
+ listener: StorageEventListener,
1200
+ ): (() => void) => {
1201
+ assertValidScope(scope);
1202
+ if (scope !== StorageScope.Memory) {
1203
+ ensureExternalSyncSubscriptions();
1204
+ }
1205
+ return storageEvents.subscribe(scope, listener);
1206
+ },
1207
+ subscribeKey: (
1208
+ scope: StorageScope,
1209
+ key: string,
1210
+ listener: StorageEventListener,
1211
+ ): (() => void) => {
1212
+ assertValidScope(scope);
1213
+ if (scope !== StorageScope.Memory) {
1214
+ ensureExternalSyncSubscriptions();
1215
+ }
1216
+ return storageEvents.subscribeKey(scope, key, listener);
1217
+ },
1218
+ subscribePrefix: (
1219
+ scope: StorageScope,
1220
+ prefix: string,
1221
+ listener: StorageEventListener,
1222
+ ): (() => void) => {
1223
+ assertValidScope(scope);
1224
+ if (scope !== StorageScope.Memory) {
1225
+ ensureExternalSyncSubscriptions();
1226
+ }
1227
+ return storageEvents.subscribePrefix(scope, prefix, listener);
1228
+ },
1229
+ subscribeNamespace: (
1230
+ namespace: string,
1231
+ scope: StorageScope,
1232
+ listener: StorageEventListener,
1233
+ ): (() => void) => {
1234
+ return storage.subscribePrefix(scope, prefixKey(namespace, ""), listener);
1235
+ },
1236
+ setEventObserver: (observer?: StorageEventListener) => {
1237
+ eventObserver = observer;
1238
+ if (observer) {
1239
+ ensureExternalSyncSubscriptions();
1240
+ }
1241
+ },
1072
1242
  clear: (scope: StorageScope) => {
1073
1243
  measureOperation("storage:clear", scope, () => {
1244
+ const previousValues = hasStorageChangeObservers(scope)
1245
+ ? storage.getAll(scope)
1246
+ : {};
1074
1247
  if (scope === StorageScope.Memory) {
1075
1248
  memoryStore.clear();
1076
1249
  notifyAllListeners(memoryListeners);
1250
+ emitBatchChange(
1251
+ scope,
1252
+ "clear",
1253
+ "memory",
1254
+ Object.keys(previousValues).map((key) =>
1255
+ createKeyChange(
1256
+ scope,
1257
+ key,
1258
+ previousValues[key],
1259
+ undefined,
1260
+ "clear",
1261
+ "memory",
1262
+ ),
1263
+ ),
1264
+ );
1077
1265
  return;
1078
1266
  }
1079
1267
 
@@ -1089,6 +1277,21 @@ export const storage = {
1089
1277
 
1090
1278
  clearScopeRawCache(scope);
1091
1279
  WebStorage.clear(scope);
1280
+ emitBatchChange(
1281
+ scope,
1282
+ "clear",
1283
+ "web",
1284
+ Object.keys(previousValues).map((key) =>
1285
+ createKeyChange(
1286
+ scope,
1287
+ key,
1288
+ previousValues[key],
1289
+ undefined,
1290
+ "clear",
1291
+ "web",
1292
+ ),
1293
+ ),
1294
+ );
1092
1295
  });
1093
1296
  },
1094
1297
  clearAll: () => {
@@ -1107,16 +1310,44 @@ export const storage = {
1107
1310
  measureOperation("storage:clearNamespace", scope, () => {
1108
1311
  assertValidScope(scope);
1109
1312
  if (scope === StorageScope.Memory) {
1110
- for (const key of memoryStore.keys()) {
1111
- if (isNamespaced(key, namespace)) {
1112
- memoryStore.delete(key);
1113
- }
1313
+ const affectedKeys = Array.from(memoryStore.keys()).filter((key) =>
1314
+ isNamespaced(key, namespace),
1315
+ );
1316
+ const previousValues = affectedKeys.map((key) => ({
1317
+ key,
1318
+ value: getEventRawValue(scope, key),
1319
+ }));
1320
+
1321
+ if (affectedKeys.length === 0) {
1322
+ return;
1114
1323
  }
1115
- notifyAllListeners(memoryListeners);
1324
+
1325
+ affectedKeys.forEach((key) => {
1326
+ memoryStore.delete(key);
1327
+ });
1328
+ affectedKeys.forEach((key) => notifyKeyListeners(memoryListeners, key));
1329
+ emitBatchChange(
1330
+ scope,
1331
+ "clearNamespace",
1332
+ "memory",
1333
+ previousValues.map(({ key, value }) =>
1334
+ createKeyChange(
1335
+ scope,
1336
+ key,
1337
+ value,
1338
+ undefined,
1339
+ "clearNamespace",
1340
+ "memory",
1341
+ ),
1342
+ ),
1343
+ );
1116
1344
  return;
1117
1345
  }
1118
1346
 
1119
1347
  const keyPrefix = prefixKey(namespace, "");
1348
+ const previousValues = hasStorageChangeObservers(scope)
1349
+ ? storage.getByPrefix(keyPrefix, scope)
1350
+ : {};
1120
1351
  if (scope === StorageScope.Disk) {
1121
1352
  flushDiskWrites();
1122
1353
  }
@@ -1130,6 +1361,21 @@ export const storage = {
1130
1361
  }
1131
1362
  }
1132
1363
  WebStorage.removeByPrefix(keyPrefix, scope);
1364
+ emitBatchChange(
1365
+ scope,
1366
+ "clearNamespace",
1367
+ "web",
1368
+ Object.keys(previousValues).map((key) =>
1369
+ createKeyChange(
1370
+ scope,
1371
+ key,
1372
+ previousValues[key],
1373
+ undefined,
1374
+ "clearNamespace",
1375
+ "web",
1376
+ ),
1377
+ ),
1378
+ );
1133
1379
  });
1134
1380
  },
1135
1381
  clearBiometric: () => {
@@ -1245,6 +1491,11 @@ export const storage = {
1245
1491
  return result;
1246
1492
  });
1247
1493
  },
1494
+ export: (scope: StorageScope): Record<string, string> => {
1495
+ return measureOperation("storage:export", scope, () =>
1496
+ storage.getAll(scope),
1497
+ );
1498
+ },
1248
1499
  size: (scope: StorageScope): number => {
1249
1500
  return measureOperation("storage:size", scope, () => {
1250
1501
  assertValidScope(scope);
@@ -1407,12 +1658,23 @@ export const storage = {
1407
1658
  assertValidScope(scope);
1408
1659
  if (keys.length === 0) return;
1409
1660
  const values = keys.map((k) => data[k]!);
1661
+ const changes = keys.map((key, index) =>
1662
+ createKeyChange(
1663
+ scope,
1664
+ key,
1665
+ getEventRawValue(scope, key),
1666
+ values[index],
1667
+ "import",
1668
+ scope === StorageScope.Memory ? "memory" : "web",
1669
+ ),
1670
+ );
1410
1671
 
1411
1672
  if (scope === StorageScope.Memory) {
1412
1673
  keys.forEach((key, index) => {
1413
1674
  memoryStore.set(key, values[index]);
1414
1675
  });
1415
1676
  keys.forEach((key) => notifyKeyListeners(memoryListeners, key));
1677
+ emitBatchChange(scope, "import", "memory", changes);
1416
1678
  return;
1417
1679
  }
1418
1680
 
@@ -1426,6 +1688,7 @@ export const storage = {
1426
1688
 
1427
1689
  WebStorage.setBatch(keys, values, scope);
1428
1690
  keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
1691
+ emitBatchChange(scope, "import", "web", changes);
1429
1692
  },
1430
1693
  keys.length,
1431
1694
  );
@@ -1512,6 +1775,11 @@ export interface StorageItem<T> {
1512
1775
  delete: () => void;
1513
1776
  has: () => boolean;
1514
1777
  subscribe: (callback: () => void) => () => void;
1778
+ subscribeSelector: <TSelected>(
1779
+ selector: (value: T) => TSelected,
1780
+ listener: StorageSelectorListener<TSelected>,
1781
+ options?: StorageSelectorSubscribeOptions<TSelected>,
1782
+ ) => () => void;
1515
1783
  serialize: (value: T) => string;
1516
1784
  deserialize: (value: string) => T;
1517
1785
  scope: StorageScope;
@@ -1680,12 +1948,17 @@ export function createStorageItem<T = undefined>(
1680
1948
  };
1681
1949
 
1682
1950
  const writeStoredRaw = (rawValue: string): void => {
1951
+ const oldValue =
1952
+ config.scope === StorageScope.Memory
1953
+ ? getEventRawValue(config.scope, storageKey)
1954
+ : undefined;
1683
1955
  if (isBiometric) {
1684
1956
  WebStorage.setSecureBiometricWithLevel(
1685
1957
  storageKey,
1686
1958
  rawValue,
1687
1959
  resolvedBiometricLevel,
1688
1960
  );
1961
+ emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", "web");
1689
1962
  return;
1690
1963
  }
1691
1964
 
@@ -1694,6 +1967,14 @@ export function createStorageItem<T = undefined>(
1694
1967
  if (nonMemoryScope === StorageScope.Disk) {
1695
1968
  if (coalesceDiskWrites || diskWritesAsync) {
1696
1969
  scheduleDiskWrite(storageKey, rawValue);
1970
+ emitKeyChange(
1971
+ config.scope,
1972
+ storageKey,
1973
+ oldValue,
1974
+ rawValue,
1975
+ "set",
1976
+ "web",
1977
+ );
1697
1978
  return;
1698
1979
  }
1699
1980
 
@@ -1706,6 +1987,7 @@ export function createStorageItem<T = undefined>(
1706
1987
  rawValue,
1707
1988
  secureAccessControl ?? secureDefaultAccessControl,
1708
1989
  );
1990
+ emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", "web");
1709
1991
  return;
1710
1992
  }
1711
1993
 
@@ -1714,11 +1996,21 @@ export function createStorageItem<T = undefined>(
1714
1996
  }
1715
1997
 
1716
1998
  WebStorage.set(storageKey, rawValue, config.scope);
1999
+ emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", "web");
1717
2000
  };
1718
2001
 
1719
2002
  const removeStoredRaw = (): void => {
2003
+ const oldValue = getEventRawValue(config.scope, storageKey);
1720
2004
  if (isBiometric) {
1721
2005
  WebStorage.deleteSecureBiometric(storageKey);
2006
+ emitKeyChange(
2007
+ config.scope,
2008
+ storageKey,
2009
+ oldValue,
2010
+ undefined,
2011
+ "remove",
2012
+ "web",
2013
+ );
1722
2014
  return;
1723
2015
  }
1724
2016
 
@@ -1727,6 +2019,14 @@ export function createStorageItem<T = undefined>(
1727
2019
  if (nonMemoryScope === StorageScope.Disk) {
1728
2020
  if (coalesceDiskWrites || diskWritesAsync) {
1729
2021
  scheduleDiskWrite(storageKey, undefined);
2022
+ emitKeyChange(
2023
+ config.scope,
2024
+ storageKey,
2025
+ oldValue,
2026
+ undefined,
2027
+ "remove",
2028
+ "web",
2029
+ );
1730
2030
  return;
1731
2031
  }
1732
2032
 
@@ -1739,6 +2039,14 @@ export function createStorageItem<T = undefined>(
1739
2039
  undefined,
1740
2040
  secureAccessControl ?? secureDefaultAccessControl,
1741
2041
  );
2042
+ emitKeyChange(
2043
+ config.scope,
2044
+ storageKey,
2045
+ oldValue,
2046
+ undefined,
2047
+ "remove",
2048
+ "web",
2049
+ );
1742
2050
  return;
1743
2051
  }
1744
2052
 
@@ -1747,15 +2055,32 @@ export function createStorageItem<T = undefined>(
1747
2055
  }
1748
2056
 
1749
2057
  WebStorage.remove(storageKey, config.scope);
2058
+ emitKeyChange(
2059
+ config.scope,
2060
+ storageKey,
2061
+ oldValue,
2062
+ undefined,
2063
+ "remove",
2064
+ "web",
2065
+ );
1750
2066
  };
1751
2067
 
1752
2068
  const writeValueWithoutValidation = (value: T): void => {
1753
2069
  if (isMemory) {
2070
+ const oldValue = getEventRawValue(config.scope, storageKey);
1754
2071
  if (memoryExpiration) {
1755
2072
  memoryExpiration.set(storageKey, Date.now() + (expirationTtlMs ?? 0));
1756
2073
  }
1757
2074
  memoryStore.set(storageKey, value);
1758
2075
  notifyKeyListeners(memoryListeners, storageKey);
2076
+ emitKeyChange(
2077
+ config.scope,
2078
+ storageKey,
2079
+ oldValue,
2080
+ typeof value === "string" ? value : undefined,
2081
+ "set",
2082
+ "memory",
2083
+ );
1759
2084
  return;
1760
2085
  }
1761
2086
 
@@ -1927,11 +2252,20 @@ export function createStorageItem<T = undefined>(
1927
2252
  invalidateParsedCache();
1928
2253
 
1929
2254
  if (isMemory) {
2255
+ const oldValue = getEventRawValue(config.scope, storageKey);
1930
2256
  if (memoryExpiration) {
1931
2257
  memoryExpiration.delete(storageKey);
1932
2258
  }
1933
2259
  memoryStore.delete(storageKey);
1934
2260
  notifyKeyListeners(memoryListeners, storageKey);
2261
+ emitKeyChange(
2262
+ config.scope,
2263
+ storageKey,
2264
+ oldValue,
2265
+ undefined,
2266
+ "remove",
2267
+ "memory",
2268
+ );
1935
2269
  return;
1936
2270
  }
1937
2271
 
@@ -1970,6 +2304,30 @@ export function createStorageItem<T = undefined>(
1970
2304
  };
1971
2305
  };
1972
2306
 
2307
+ const subscribeSelector = <TSelected>(
2308
+ selector: (value: T) => TSelected,
2309
+ listener: StorageSelectorListener<TSelected>,
2310
+ options: StorageSelectorSubscribeOptions<TSelected> = {},
2311
+ ): (() => void) => {
2312
+ const isEqual = options.isEqual ?? Object.is;
2313
+ let currentValue = selector(getInternal());
2314
+
2315
+ if (options.fireImmediately === true) {
2316
+ listener(currentValue, currentValue);
2317
+ }
2318
+
2319
+ return subscribe(() => {
2320
+ const nextValue = selector(getInternal());
2321
+ if (isEqual(currentValue, nextValue)) {
2322
+ return;
2323
+ }
2324
+
2325
+ const previousValue = currentValue;
2326
+ currentValue = nextValue;
2327
+ listener(nextValue, previousValue);
2328
+ });
2329
+ };
2330
+
1973
2331
  const storageItem: StorageItemInternal<T> = {
1974
2332
  get,
1975
2333
  getWithVersion,
@@ -1978,6 +2336,7 @@ export function createStorageItem<T = undefined>(
1978
2336
  delete: deleteItem,
1979
2337
  has: hasItem,
1980
2338
  subscribe,
2339
+ subscribeSelector,
1981
2340
  serialize,
1982
2341
  deserialize,
1983
2342
  _triggerListeners: () => {
@@ -2130,6 +2489,17 @@ export function setBatch<T>(
2130
2489
  return;
2131
2490
  }
2132
2491
 
2492
+ const changes = items.map(({ item, value }) =>
2493
+ createKeyChange(
2494
+ scope,
2495
+ item.key,
2496
+ getEventRawValue(scope, item.key),
2497
+ typeof value === "string" ? value : undefined,
2498
+ "setBatch",
2499
+ "memory",
2500
+ ),
2501
+ );
2502
+
2133
2503
  // Atomic write: update all values in memoryStore, invalidate caches, then batch-notify
2134
2504
  items.forEach(({ item, value }) => {
2135
2505
  memoryStore.set(item.key, value);
@@ -2138,6 +2508,7 @@ export function setBatch<T>(
2138
2508
  items.forEach(({ item }) =>
2139
2509
  notifyKeyListeners(memoryListeners, item.key),
2140
2510
  );
2511
+ emitBatchChange(scope, "setBatch", "memory", changes);
2141
2512
  return;
2142
2513
  }
2143
2514
 
@@ -2156,6 +2527,10 @@ export function setBatch<T>(
2156
2527
  }
2157
2528
 
2158
2529
  flushSecureWrites();
2530
+ const keys = secureEntries.map(({ item }) => item.key);
2531
+ const oldValues = hasStorageChangeObservers(scope)
2532
+ ? WebStorage.getBatch(keys, scope)
2533
+ : [];
2159
2534
  const groupedByAccessControl = new Map<
2160
2535
  number,
2161
2536
  { keys: string[]; values: string[] }
@@ -2180,6 +2555,21 @@ export function setBatch<T>(
2180
2555
  cacheRawValue(scope, key, group.values[index]),
2181
2556
  );
2182
2557
  });
2558
+ emitBatchChange(
2559
+ scope,
2560
+ "setBatch",
2561
+ "web",
2562
+ secureEntries.map(({ item, value }, index) =>
2563
+ createKeyChange(
2564
+ scope,
2565
+ item.key,
2566
+ oldValues[index],
2567
+ item.serialize(value),
2568
+ "setBatch",
2569
+ "web",
2570
+ ),
2571
+ ),
2572
+ );
2183
2573
  return;
2184
2574
  }
2185
2575
 
@@ -2195,8 +2585,26 @@ export function setBatch<T>(
2195
2585
 
2196
2586
  const keys = items.map((entry) => entry.item.key);
2197
2587
  const values = items.map((entry) => entry.item.serialize(entry.value));
2588
+ const oldValues = hasStorageChangeObservers(scope)
2589
+ ? WebStorage.getBatch(keys, scope)
2590
+ : [];
2198
2591
  WebStorage.setBatch(keys, values, scope);
2199
2592
  keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
2593
+ emitBatchChange(
2594
+ scope,
2595
+ "setBatch",
2596
+ "web",
2597
+ keys.map((key, index) =>
2598
+ createKeyChange(
2599
+ scope,
2600
+ key,
2601
+ oldValues[index],
2602
+ values[index],
2603
+ "setBatch",
2604
+ "web",
2605
+ ),
2606
+ ),
2607
+ );
2200
2608
  },
2201
2609
  items.length,
2202
2610
  );
@@ -2213,7 +2621,18 @@ export function removeBatch(
2213
2621
  assertBatchScope(items, scope);
2214
2622
 
2215
2623
  if (scope === StorageScope.Memory) {
2624
+ const changes = items.map((item) =>
2625
+ createKeyChange(
2626
+ scope,
2627
+ item.key,
2628
+ getEventRawValue(scope, item.key),
2629
+ undefined,
2630
+ "removeBatch",
2631
+ "memory",
2632
+ ),
2633
+ );
2216
2634
  items.forEach((item) => item.delete());
2635
+ emitBatchChange(scope, "removeBatch", "memory", changes);
2217
2636
  return;
2218
2637
  }
2219
2638
 
@@ -2224,8 +2643,26 @@ export function removeBatch(
2224
2643
  if (scope === StorageScope.Secure) {
2225
2644
  flushSecureWrites();
2226
2645
  }
2646
+ const oldValues = hasStorageChangeObservers(scope)
2647
+ ? WebStorage.getBatch(keys, scope)
2648
+ : [];
2227
2649
  WebStorage.removeBatch(keys, scope);
2228
2650
  keys.forEach((key) => cacheRawValue(scope, key, undefined));
2651
+ emitBatchChange(
2652
+ scope,
2653
+ "removeBatch",
2654
+ "web",
2655
+ keys.map((key, index) =>
2656
+ createKeyChange(
2657
+ scope,
2658
+ key,
2659
+ oldValues[index],
2660
+ undefined,
2661
+ "removeBatch",
2662
+ "web",
2663
+ ),
2664
+ ),
2665
+ );
2229
2666
  },
2230
2667
  items.length,
2231
2668
  );