react-native-nitro-storage 0.4.5 → 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.
Files changed (37) hide show
  1. package/README.md +254 -945
  2. package/SECURITY.md +26 -0
  3. package/docs/api-reference.md +281 -0
  4. package/docs/batch-transactions-migrations.md +200 -0
  5. package/docs/benchmarks.md +37 -0
  6. package/docs/mmkv-migration.md +80 -0
  7. package/docs/react-hooks.md +113 -0
  8. package/docs/recipes.md +302 -0
  9. package/docs/secure-storage.md +190 -0
  10. package/docs/web-backends.md +141 -0
  11. package/lib/commonjs/index.js +265 -14
  12. package/lib/commonjs/index.js.map +1 -1
  13. package/lib/commonjs/index.web.js +220 -11
  14. package/lib/commonjs/index.web.js.map +1 -1
  15. package/lib/commonjs/storage-events.js +117 -0
  16. package/lib/commonjs/storage-events.js.map +1 -0
  17. package/lib/commonjs/storage-runtime.js.map +1 -1
  18. package/lib/module/index.js +265 -14
  19. package/lib/module/index.js.map +1 -1
  20. package/lib/module/index.web.js +220 -11
  21. package/lib/module/index.web.js.map +1 -1
  22. package/lib/module/storage-events.js +112 -0
  23. package/lib/module/storage-events.js.map +1 -0
  24. package/lib/module/storage-runtime.js.map +1 -1
  25. package/lib/typescript/index.d.ts +19 -2
  26. package/lib/typescript/index.d.ts.map +1 -1
  27. package/lib/typescript/index.web.d.ts +19 -2
  28. package/lib/typescript/index.web.d.ts.map +1 -1
  29. package/lib/typescript/storage-events.d.ts +37 -0
  30. package/lib/typescript/storage-events.d.ts.map +1 -0
  31. package/lib/typescript/storage-runtime.d.ts +32 -0
  32. package/lib/typescript/storage-runtime.d.ts.map +1 -1
  33. package/package.json +25 -11
  34. package/src/index.ts +601 -14
  35. package/src/index.web.ts +535 -22
  36. package/src/storage-events.ts +184 -0
  37. package/src/storage-runtime.ts +35 -0
@@ -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) {
@@ -161,6 +164,9 @@ function getBackendName(scope, backend) {
161
164
  const scopeName = scope === _Storage.StorageScope.Disk ? "disk" : "secure";
162
165
  return backend?.name ?? `web:${scopeName}`;
163
166
  }
167
+ function getWebSecureEncryptionStatus(backend) {
168
+ return backend?.name === "localStorage:secure" ? "unavailable" : "unknown";
169
+ }
164
170
  function createWebStorageError(scope, operation, error, backend) {
165
171
  const backendName = getBackendName(scope, backend);
166
172
  const message = error instanceof Error ? error.message : String(error ?? "Unknown error");
@@ -232,6 +238,7 @@ function applyExternalChangeEvent(scope, key, newValue) {
232
238
  }
233
239
  if (scope === _Storage.StorageScope.Secure && key.startsWith(SECURE_WEB_PREFIX)) {
234
240
  const plainKey = fromSecureStorageKey(key);
241
+ const oldValue = readCachedRawValue(_Storage.StorageScope.Secure, plainKey);
235
242
  if (newValue === null) {
236
243
  ensureWebScopeKeyIndex(_Storage.StorageScope.Secure).delete(plainKey);
237
244
  cacheRawValue(_Storage.StorageScope.Secure, plainKey, undefined);
@@ -240,10 +247,12 @@ function applyExternalChangeEvent(scope, key, newValue) {
240
247
  cacheRawValue(_Storage.StorageScope.Secure, plainKey, newValue);
241
248
  }
242
249
  notifyKeyListeners(getScopedListeners(_Storage.StorageScope.Secure), plainKey);
250
+ emitKeyChange(_Storage.StorageScope.Secure, plainKey, oldValue, newValue ?? undefined, "external", "external");
243
251
  return;
244
252
  }
245
253
  if (scope === _Storage.StorageScope.Secure && key.startsWith(BIOMETRIC_WEB_PREFIX)) {
246
254
  const plainKey = fromBiometricStorageKey(key);
255
+ const oldValue = readCachedRawValue(_Storage.StorageScope.Secure, plainKey);
247
256
  if (newValue === null) {
248
257
  if (withWebBackendOperation(_Storage.StorageScope.Secure, "external-sync:getItem", backend => backend.getItem(toSecureStorageKey(plainKey))) === null) {
249
258
  ensureWebScopeKeyIndex(_Storage.StorageScope.Secure).delete(plainKey);
@@ -254,8 +263,10 @@ function applyExternalChangeEvent(scope, key, newValue) {
254
263
  cacheRawValue(_Storage.StorageScope.Secure, plainKey, newValue);
255
264
  }
256
265
  notifyKeyListeners(getScopedListeners(_Storage.StorageScope.Secure), plainKey);
266
+ emitKeyChange(_Storage.StorageScope.Secure, plainKey, oldValue, newValue ?? undefined, "external", "external");
257
267
  return;
258
268
  }
269
+ const oldValue = readCachedRawValue(scope, key);
259
270
  if (newValue === null) {
260
271
  ensureWebScopeKeyIndex(scope).delete(key);
261
272
  cacheRawValue(scope, key, undefined);
@@ -264,6 +275,7 @@ function applyExternalChangeEvent(scope, key, newValue) {
264
275
  cacheRawValue(scope, key, newValue);
265
276
  }
266
277
  notifyKeyListeners(getScopedListeners(scope), key);
278
+ emitKeyChange(scope, key, oldValue, newValue ?? undefined, "external", "external");
267
279
  }
268
280
  function handleWebStorageEvent(event) {
269
281
  const key = event.key;
@@ -354,6 +366,46 @@ function addKeyListener(registry, key, listener) {
354
366
  }
355
367
  };
356
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
+ }
357
409
  function readPendingSecureWrite(key) {
358
410
  return pendingSecureWrites.get(key)?.value;
359
411
  }
@@ -603,13 +655,10 @@ const WebStorage = {
603
655
  return () => {};
604
656
  },
605
657
  has: (key, scope) => {
606
- if (scope === _Storage.StorageScope.Secure) {
607
- return withWebBackendOperation(scope, "has", backend => backend.getItem(toSecureStorageKey(key))) !== null || withWebBackendOperation(scope, "has", backend => backend.getItem(toBiometricStorageKey(key))) !== null;
608
- }
609
- if (scope !== _Storage.StorageScope.Disk) {
610
- return false;
658
+ if (scope === _Storage.StorageScope.Disk || scope === _Storage.StorageScope.Secure) {
659
+ return ensureWebScopeKeyIndex(scope).has(key);
611
660
  }
612
- return withWebBackendOperation(scope, "has", backend => backend.getItem(key)) !== null;
661
+ return false;
613
662
  },
614
663
  getAllKeys: scope => {
615
664
  if (scope !== _Storage.StorageScope.Disk && scope !== _Storage.StorageScope.Secure) {
@@ -700,15 +749,18 @@ function getRawValue(key, scope) {
700
749
  }
701
750
  function setRawValue(key, value, scope) {
702
751
  (0, _internal.assertValidScope)(scope);
752
+ const oldValue = scope === _Storage.StorageScope.Memory ? getEventRawValue(scope, key) : undefined;
703
753
  if (scope === _Storage.StorageScope.Memory) {
704
754
  memoryStore.set(key, value);
705
755
  notifyKeyListeners(memoryListeners, key);
756
+ emitKeyChange(scope, key, oldValue, value, "set", "memory");
706
757
  return;
707
758
  }
708
759
  if (scope === _Storage.StorageScope.Disk) {
709
760
  cacheRawValue(scope, key, value);
710
761
  if (diskWritesAsync) {
711
762
  scheduleDiskWrite(key, value);
763
+ emitKeyChange(scope, key, oldValue, value, "set", "web");
712
764
  return;
713
765
  }
714
766
  flushDiskWrites();
@@ -720,18 +772,22 @@ function setRawValue(key, value, scope) {
720
772
  }
721
773
  WebStorage.set(key, value, scope);
722
774
  cacheRawValue(scope, key, value);
775
+ emitKeyChange(scope, key, oldValue, value, "set", "web");
723
776
  }
724
777
  function removeRawValue(key, scope) {
725
778
  (0, _internal.assertValidScope)(scope);
779
+ const oldValue = getEventRawValue(scope, key);
726
780
  if (scope === _Storage.StorageScope.Memory) {
727
781
  memoryStore.delete(key);
728
782
  notifyKeyListeners(memoryListeners, key);
783
+ emitKeyChange(scope, key, oldValue, undefined, "remove", "memory");
729
784
  return;
730
785
  }
731
786
  if (scope === _Storage.StorageScope.Disk) {
732
787
  cacheRawValue(scope, key, undefined);
733
788
  if (diskWritesAsync) {
734
789
  scheduleDiskWrite(key, undefined);
790
+ emitKeyChange(scope, key, oldValue, undefined, "remove", "web");
735
791
  return;
736
792
  }
737
793
  flushDiskWrites();
@@ -743,6 +799,7 @@ function removeRawValue(key, scope) {
743
799
  }
744
800
  WebStorage.remove(key, scope);
745
801
  cacheRawValue(scope, key, undefined);
802
+ emitKeyChange(scope, key, oldValue, undefined, "remove", "web");
746
803
  }
747
804
  function readMigrationVersion(scope) {
748
805
  const raw = getRawValue(_internal.MIGRATION_VERSION_KEY, scope);
@@ -756,11 +813,43 @@ function writeMigrationVersion(scope, version) {
756
813
  setRawValue(_internal.MIGRATION_VERSION_KEY, String(version), scope);
757
814
  }
758
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
+ },
759
846
  clear: scope => {
760
847
  measureOperation("storage:clear", scope, () => {
848
+ const previousValues = hasStorageChangeObservers(scope) ? storage.getAll(scope) : {};
761
849
  if (scope === _Storage.StorageScope.Memory) {
762
850
  memoryStore.clear();
763
851
  notifyAllListeners(memoryListeners);
852
+ emitBatchChange(scope, "clear", "memory", Object.keys(previousValues).map(key => createKeyChange(scope, key, previousValues[key], undefined, "clear", "memory")));
764
853
  return;
765
854
  }
766
855
  if (scope === _Storage.StorageScope.Disk) {
@@ -773,6 +862,7 @@ const storage = exports.storage = {
773
862
  }
774
863
  clearScopeRawCache(scope);
775
864
  WebStorage.clear(scope);
865
+ emitBatchChange(scope, "clear", "web", Object.keys(previousValues).map(key => createKeyChange(scope, key, previousValues[key], undefined, "clear", "web")));
776
866
  });
777
867
  },
778
868
  clearAll: () => {
@@ -786,15 +876,26 @@ const storage = exports.storage = {
786
876
  measureOperation("storage:clearNamespace", scope, () => {
787
877
  (0, _internal.assertValidScope)(scope);
788
878
  if (scope === _Storage.StorageScope.Memory) {
789
- for (const key of memoryStore.keys()) {
790
- if ((0, _internal.isNamespaced)(key, namespace)) {
791
- memoryStore.delete(key);
792
- }
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;
793
886
  }
794
- 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")));
795
895
  return;
796
896
  }
797
897
  const keyPrefix = (0, _internal.prefixKey)(namespace, "");
898
+ const previousValues = hasStorageChangeObservers(scope) ? storage.getByPrefix(keyPrefix, scope) : {};
798
899
  if (scope === _Storage.StorageScope.Disk) {
799
900
  flushDiskWrites();
800
901
  }
@@ -808,6 +909,7 @@ const storage = exports.storage = {
808
909
  }
809
910
  }
810
911
  WebStorage.removeByPrefix(keyPrefix, scope);
912
+ emitBatchChange(scope, "clearNamespace", "web", Object.keys(previousValues).map(key => createKeyChange(scope, key, previousValues[key], undefined, "clearNamespace", "web")));
811
913
  });
812
914
  },
813
915
  clearBiometric: () => {
@@ -916,6 +1018,9 @@ const storage = exports.storage = {
916
1018
  return result;
917
1019
  });
918
1020
  },
1021
+ export: scope => {
1022
+ return measureOperation("storage:export", scope, () => storage.getAll(scope));
1023
+ },
919
1024
  size: scope => {
920
1025
  return measureOperation("storage:size", scope, () => {
921
1026
  (0, _internal.assertValidScope)(scope);
@@ -987,6 +1092,57 @@ const storage = exports.storage = {
987
1092
  },
988
1093
  errorClassification: true
989
1094
  }),
1095
+ getSecurityCapabilities: () => {
1096
+ const secureBackend = getBackendName(_Storage.StorageScope.Secure, webSecureStorageBackend);
1097
+ return {
1098
+ platform: "web",
1099
+ secureStorage: {
1100
+ backend: secureBackend,
1101
+ encrypted: getWebSecureEncryptionStatus(webSecureStorageBackend),
1102
+ accessControl: "unavailable",
1103
+ keychainAccessGroup: "unavailable",
1104
+ hardwareBacked: "unavailable"
1105
+ },
1106
+ biometric: {
1107
+ storage: "unavailable",
1108
+ prompt: "unavailable",
1109
+ biometryOnly: "unavailable",
1110
+ biometryOrPasscode: "unavailable"
1111
+ },
1112
+ metadata: {
1113
+ perKey: true,
1114
+ listsWithoutValues: true,
1115
+ persistsTimestamps: false
1116
+ }
1117
+ };
1118
+ },
1119
+ getSecureMetadata: key => {
1120
+ return measureOperation("storage:getSecureMetadata", _Storage.StorageScope.Secure, () => {
1121
+ flushSecureWrites();
1122
+ const biometricProtected = WebStorage.hasSecureBiometric(key);
1123
+ const exists = biometricProtected || WebStorage.has(key, _Storage.StorageScope.Secure);
1124
+ let kind = "missing";
1125
+ if (exists) {
1126
+ kind = biometricProtected ? "biometric" : "secure";
1127
+ }
1128
+ return {
1129
+ key,
1130
+ exists,
1131
+ kind,
1132
+ backend: getBackendName(_Storage.StorageScope.Secure, webSecureStorageBackend),
1133
+ encrypted: getWebSecureEncryptionStatus(webSecureStorageBackend),
1134
+ hardwareBacked: "unavailable",
1135
+ biometricProtected,
1136
+ valueExposed: false
1137
+ };
1138
+ });
1139
+ },
1140
+ getAllSecureMetadata: () => {
1141
+ return measureOperation("storage:getAllSecureMetadata", _Storage.StorageScope.Secure, () => {
1142
+ flushSecureWrites();
1143
+ return WebStorage.getAllKeys(_Storage.StorageScope.Secure).map(key => storage.getSecureMetadata(key));
1144
+ });
1145
+ },
990
1146
  getString: (key, scope) => {
991
1147
  return measureOperation("storage:getString", scope, () => {
992
1148
  return getRawValue(key, scope);
@@ -1008,11 +1164,13 @@ const storage = exports.storage = {
1008
1164
  (0, _internal.assertValidScope)(scope);
1009
1165
  if (keys.length === 0) return;
1010
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"));
1011
1168
  if (scope === _Storage.StorageScope.Memory) {
1012
1169
  keys.forEach((key, index) => {
1013
1170
  memoryStore.set(key, values[index]);
1014
1171
  });
1015
1172
  keys.forEach(key => notifyKeyListeners(memoryListeners, key));
1173
+ emitBatchChange(scope, "import", "memory", changes);
1016
1174
  return;
1017
1175
  }
1018
1176
  if (scope === _Storage.StorageScope.Secure) {
@@ -1024,6 +1182,7 @@ const storage = exports.storage = {
1024
1182
  }
1025
1183
  WebStorage.setBatch(keys, values, scope);
1026
1184
  keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
1185
+ emitBatchChange(scope, "import", "web", changes);
1027
1186
  }, keys.length);
1028
1187
  }
1029
1188
  };
@@ -1165,56 +1324,68 @@ function createStorageItem(config) {
1165
1324
  return raw;
1166
1325
  };
1167
1326
  const writeStoredRaw = rawValue => {
1327
+ const oldValue = config.scope === _Storage.StorageScope.Memory ? getEventRawValue(config.scope, storageKey) : undefined;
1168
1328
  if (isBiometric) {
1169
1329
  WebStorage.setSecureBiometricWithLevel(storageKey, rawValue, resolvedBiometricLevel);
1330
+ emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", "web");
1170
1331
  return;
1171
1332
  }
1172
1333
  cacheRawValue(nonMemoryScope, storageKey, rawValue);
1173
1334
  if (nonMemoryScope === _Storage.StorageScope.Disk) {
1174
1335
  if (coalesceDiskWrites || diskWritesAsync) {
1175
1336
  scheduleDiskWrite(storageKey, rawValue);
1337
+ emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", "web");
1176
1338
  return;
1177
1339
  }
1178
1340
  clearPendingDiskWrite(storageKey);
1179
1341
  }
1180
1342
  if (coalesceSecureWrites) {
1181
1343
  scheduleSecureWrite(storageKey, rawValue, secureAccessControl ?? secureDefaultAccessControl);
1344
+ emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", "web");
1182
1345
  return;
1183
1346
  }
1184
1347
  if (nonMemoryScope === _Storage.StorageScope.Secure) {
1185
1348
  clearPendingSecureWrite(storageKey);
1186
1349
  }
1187
1350
  WebStorage.set(storageKey, rawValue, config.scope);
1351
+ emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", "web");
1188
1352
  };
1189
1353
  const removeStoredRaw = () => {
1354
+ const oldValue = getEventRawValue(config.scope, storageKey);
1190
1355
  if (isBiometric) {
1191
1356
  WebStorage.deleteSecureBiometric(storageKey);
1357
+ emitKeyChange(config.scope, storageKey, oldValue, undefined, "remove", "web");
1192
1358
  return;
1193
1359
  }
1194
1360
  cacheRawValue(nonMemoryScope, storageKey, undefined);
1195
1361
  if (nonMemoryScope === _Storage.StorageScope.Disk) {
1196
1362
  if (coalesceDiskWrites || diskWritesAsync) {
1197
1363
  scheduleDiskWrite(storageKey, undefined);
1364
+ emitKeyChange(config.scope, storageKey, oldValue, undefined, "remove", "web");
1198
1365
  return;
1199
1366
  }
1200
1367
  clearPendingDiskWrite(storageKey);
1201
1368
  }
1202
1369
  if (coalesceSecureWrites) {
1203
1370
  scheduleSecureWrite(storageKey, undefined, secureAccessControl ?? secureDefaultAccessControl);
1371
+ emitKeyChange(config.scope, storageKey, oldValue, undefined, "remove", "web");
1204
1372
  return;
1205
1373
  }
1206
1374
  if (nonMemoryScope === _Storage.StorageScope.Secure) {
1207
1375
  clearPendingSecureWrite(storageKey);
1208
1376
  }
1209
1377
  WebStorage.remove(storageKey, config.scope);
1378
+ emitKeyChange(config.scope, storageKey, oldValue, undefined, "remove", "web");
1210
1379
  };
1211
1380
  const writeValueWithoutValidation = value => {
1212
1381
  if (isMemory) {
1382
+ const oldValue = getEventRawValue(config.scope, storageKey);
1213
1383
  if (memoryExpiration) {
1214
1384
  memoryExpiration.set(storageKey, Date.now() + (expirationTtlMs ?? 0));
1215
1385
  }
1216
1386
  memoryStore.set(storageKey, value);
1217
1387
  notifyKeyListeners(memoryListeners, storageKey);
1388
+ emitKeyChange(config.scope, storageKey, oldValue, typeof value === "string" ? value : undefined, "set", "memory");
1218
1389
  return;
1219
1390
  }
1220
1391
  const serialized = serialize(value);
@@ -1346,11 +1517,13 @@ function createStorageItem(config) {
1346
1517
  measureOperation("item:delete", config.scope, () => {
1347
1518
  invalidateParsedCache();
1348
1519
  if (isMemory) {
1520
+ const oldValue = getEventRawValue(config.scope, storageKey);
1349
1521
  if (memoryExpiration) {
1350
1522
  memoryExpiration.delete(storageKey);
1351
1523
  }
1352
1524
  memoryStore.delete(storageKey);
1353
1525
  notifyKeyListeners(memoryListeners, storageKey);
1526
+ emitKeyChange(config.scope, storageKey, oldValue, undefined, "remove", "memory");
1354
1527
  return;
1355
1528
  }
1356
1529
  removeStoredRaw();
@@ -1384,6 +1557,22 @@ function createStorageItem(config) {
1384
1557
  }
1385
1558
  };
1386
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
+ };
1387
1576
  const storageItem = {
1388
1577
  get,
1389
1578
  getWithVersion,
@@ -1392,6 +1581,7 @@ function createStorageItem(config) {
1392
1581
  delete: deleteItem,
1393
1582
  has: hasItem,
1394
1583
  subscribe,
1584
+ subscribeSelector,
1395
1585
  serialize,
1396
1586
  deserialize,
1397
1587
  _triggerListeners: () => {
@@ -1492,6 +1682,10 @@ function setBatch(items, scope) {
1492
1682
  }) => item.set(value));
1493
1683
  return;
1494
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"));
1495
1689
 
1496
1690
  // Atomic write: update all values in memoryStore, invalidate caches, then batch-notify
1497
1691
  items.forEach(({
@@ -1504,6 +1698,7 @@ function setBatch(items, scope) {
1504
1698
  items.forEach(({
1505
1699
  item
1506
1700
  }) => notifyKeyListeners(memoryListeners, item.key));
1701
+ emitBatchChange(scope, "setBatch", "memory", changes);
1507
1702
  return;
1508
1703
  }
1509
1704
  if (scope === _Storage.StorageScope.Secure) {
@@ -1526,6 +1721,10 @@ function setBatch(items, scope) {
1526
1721
  return;
1527
1722
  }
1528
1723
  flushSecureWrites();
1724
+ const keys = secureEntries.map(({
1725
+ item
1726
+ }) => item.key);
1727
+ const oldValues = hasStorageChangeObservers(scope) ? WebStorage.getBatch(keys, scope) : [];
1529
1728
  const groupedByAccessControl = new Map();
1530
1729
  secureEntries.forEach(({
1531
1730
  item,
@@ -1549,6 +1748,10 @@ function setBatch(items, scope) {
1549
1748
  WebStorage.setBatch(group.keys, group.values, scope);
1550
1749
  group.keys.forEach((key, index) => cacheRawValue(scope, key, group.values[index]));
1551
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")));
1552
1755
  return;
1553
1756
  }
1554
1757
  flushDiskWrites();
@@ -1564,15 +1767,19 @@ function setBatch(items, scope) {
1564
1767
  }
1565
1768
  const keys = items.map(entry => entry.item.key);
1566
1769
  const values = items.map(entry => entry.item.serialize(entry.value));
1770
+ const oldValues = hasStorageChangeObservers(scope) ? WebStorage.getBatch(keys, scope) : [];
1567
1771
  WebStorage.setBatch(keys, values, scope);
1568
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")));
1569
1774
  }, items.length);
1570
1775
  }
1571
1776
  function removeBatch(items, scope) {
1572
1777
  measureOperation("batch:remove", scope, () => {
1573
1778
  (0, _internal.assertBatchScope)(items, scope);
1574
1779
  if (scope === _Storage.StorageScope.Memory) {
1780
+ const changes = items.map(item => createKeyChange(scope, item.key, getEventRawValue(scope, item.key), undefined, "removeBatch", "memory"));
1575
1781
  items.forEach(item => item.delete());
1782
+ emitBatchChange(scope, "removeBatch", "memory", changes);
1576
1783
  return;
1577
1784
  }
1578
1785
  const keys = items.map(item => item.key);
@@ -1582,8 +1789,10 @@ function removeBatch(items, scope) {
1582
1789
  if (scope === _Storage.StorageScope.Secure) {
1583
1790
  flushSecureWrites();
1584
1791
  }
1792
+ const oldValues = hasStorageChangeObservers(scope) ? WebStorage.getBatch(keys, scope) : [];
1585
1793
  WebStorage.removeBatch(keys, scope);
1586
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")));
1587
1796
  }, items.length);
1588
1797
  }
1589
1798
  function registerMigration(version, migration) {