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 _reactNativeNitroModules = require("react-native-nitro-modules");
76
76
  var _Storage = require("./Storage.types");
77
77
  var _internal = require("./internal");
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");
@@ -111,8 +112,12 @@ let diskWritesAsync = false;
111
112
  const pendingSecureWrites = new Map();
112
113
  let secureFlushScheduled = false;
113
114
  let secureDefaultAccessControl = _Storage.AccessControl.WhenUnlocked;
115
+ const suppressedNativeEvents = new Map([[_Storage.StorageScope.Disk, new Map()], [_Storage.StorageScope.Secure, new Map()]]);
114
116
  let metricsObserver;
117
+ let eventObserver;
115
118
  const metricsCounters = new Map();
119
+ const storageEvents = new _storageEvents.StorageEventRegistry();
120
+ const nativeSecureBackend = "platform-secure-storage";
116
121
  function recordMetric(operation, scope, durationMs, keysCount = 1) {
117
122
  const existing = metricsCounters.get(operation);
118
123
  if (!existing) {
@@ -162,6 +167,23 @@ function hasCachedRawValue(scope, key) {
162
167
  function clearScopeRawCache(scope) {
163
168
  getScopeRawCache(scope).clear();
164
169
  }
170
+ function suppressNativeEvent(scope, key) {
171
+ const suppressedEvents = suppressedNativeEvents.get(scope);
172
+ suppressedEvents.set(key, (suppressedEvents.get(key) ?? 0) + 1);
173
+ }
174
+ function consumeSuppressedNativeEvent(scope, key) {
175
+ const suppressedEvents = suppressedNativeEvents.get(scope);
176
+ const count = suppressedEvents.get(key);
177
+ if (count === undefined) {
178
+ return false;
179
+ }
180
+ if (count <= 1) {
181
+ suppressedEvents.delete(key);
182
+ } else {
183
+ suppressedEvents.set(key, count - 1);
184
+ }
185
+ return true;
186
+ }
165
187
  function notifyKeyListeners(registry, key) {
166
188
  const listeners = registry.get(key);
167
189
  if (listeners) {
@@ -195,6 +217,52 @@ function addKeyListener(registry, key, listener) {
195
217
  }
196
218
  };
197
219
  }
220
+ function getEventRawValue(scope, key) {
221
+ if (scope === _Storage.StorageScope.Memory) {
222
+ const value = memoryStore.get(key);
223
+ return typeof value === "string" ? value : undefined;
224
+ }
225
+ return getRawValue(key, scope);
226
+ }
227
+ function createKeyChange(scope, key, oldValue, newValue, operation, source) {
228
+ return {
229
+ type: "key",
230
+ scope,
231
+ key,
232
+ oldValue,
233
+ newValue,
234
+ operation,
235
+ source
236
+ };
237
+ }
238
+ function hasStorageChangeObservers(scope) {
239
+ return storageEvents.hasListeners(scope) || eventObserver !== undefined;
240
+ }
241
+ function emitKeyChange(scope, key, oldValue, newValue, operation, source) {
242
+ if (source === "native" && operation !== "external" && scope !== _Storage.StorageScope.Memory && scopedUnsubscribers.has(scope)) {
243
+ suppressNativeEvent(scope, key);
244
+ }
245
+ const event = createKeyChange(scope, key, oldValue, newValue, operation, source);
246
+ storageEvents.emitKey(event);
247
+ eventObserver?.(event);
248
+ }
249
+ function emitBatchChange(scope, operation, source, changes) {
250
+ if (changes.length === 0) {
251
+ return;
252
+ }
253
+ if (source === "native" && operation !== "external" && scope !== _Storage.StorageScope.Memory && scopedUnsubscribers.has(scope)) {
254
+ changes.forEach(change => suppressNativeEvent(scope, change.key));
255
+ }
256
+ const event = {
257
+ type: "batch",
258
+ scope,
259
+ operation,
260
+ source,
261
+ changes
262
+ };
263
+ storageEvents.emitBatch(event);
264
+ eventObserver?.(event);
265
+ }
198
266
  function readPendingSecureWrite(key) {
199
267
  return pendingSecureWrites.get(key)?.value;
200
268
  }
@@ -331,14 +399,19 @@ function ensureNativeScopeSubscription(scope) {
331
399
  notifyAllListeners(getScopedListeners(scope));
332
400
  return;
333
401
  }
402
+ const oldValue = readCachedRawValue(scope, key);
334
403
  cacheRawValue(scope, key, value);
335
404
  notifyKeyListeners(getScopedListeners(scope), key);
405
+ if (consumeSuppressedNativeEvent(scope, key)) {
406
+ return;
407
+ }
408
+ emitKeyChange(scope, key, oldValue, value, "external", "native");
336
409
  });
337
- scopedUnsubscribers.set(scope, unsubscribe);
410
+ scopedUnsubscribers.set(scope, typeof unsubscribe === "function" ? unsubscribe : () => {});
338
411
  }
339
412
  function maybeCleanupNativeScopeSubscription(scope) {
340
413
  const listeners = getScopedListeners(scope);
341
- if (listeners.size > 0) {
414
+ if (listeners.size > 0 || storageEvents.hasListeners(scope) || eventObserver !== undefined) {
342
415
  return;
343
416
  }
344
417
  const unsubscribe = scopedUnsubscribers.get(scope);
@@ -364,15 +437,18 @@ function getRawValue(key, scope) {
364
437
  }
365
438
  function setRawValue(key, value, scope) {
366
439
  (0, _internal.assertValidScope)(scope);
440
+ const oldValue = scope === _Storage.StorageScope.Memory ? getEventRawValue(scope, key) : undefined;
367
441
  if (scope === _Storage.StorageScope.Memory) {
368
442
  memoryStore.set(key, value);
369
443
  notifyKeyListeners(memoryListeners, key);
444
+ emitKeyChange(scope, key, oldValue, value, "set", "memory");
370
445
  return;
371
446
  }
372
447
  if (scope === _Storage.StorageScope.Disk) {
373
448
  cacheRawValue(scope, key, value);
374
449
  if (diskWritesAsync) {
375
450
  scheduleDiskWrite(key, value);
451
+ emitKeyChange(scope, key, oldValue, value, "set", "native");
376
452
  return;
377
453
  }
378
454
  flushDiskWrites();
@@ -385,18 +461,22 @@ function setRawValue(key, value, scope) {
385
461
  }
386
462
  getStorageModule().set(key, value, scope);
387
463
  cacheRawValue(scope, key, value);
464
+ emitKeyChange(scope, key, oldValue, value, "set", "native");
388
465
  }
389
466
  function removeRawValue(key, scope) {
390
467
  (0, _internal.assertValidScope)(scope);
468
+ const oldValue = getEventRawValue(scope, key);
391
469
  if (scope === _Storage.StorageScope.Memory) {
392
470
  memoryStore.delete(key);
393
471
  notifyKeyListeners(memoryListeners, key);
472
+ emitKeyChange(scope, key, oldValue, undefined, "remove", "memory");
394
473
  return;
395
474
  }
396
475
  if (scope === _Storage.StorageScope.Disk) {
397
476
  cacheRawValue(scope, key, undefined);
398
477
  if (diskWritesAsync) {
399
478
  scheduleDiskWrite(key, undefined);
479
+ emitKeyChange(scope, key, oldValue, undefined, "remove", "native");
400
480
  return;
401
481
  }
402
482
  flushDiskWrites();
@@ -408,6 +488,7 @@ function removeRawValue(key, scope) {
408
488
  }
409
489
  getStorageModule().remove(key, scope);
410
490
  cacheRawValue(scope, key, undefined);
491
+ emitKeyChange(scope, key, oldValue, undefined, "remove", "native");
411
492
  }
412
493
  function readMigrationVersion(scope) {
413
494
  const raw = getRawValue(_internal.MIGRATION_VERSION_KEY, scope);
@@ -421,11 +502,62 @@ function writeMigrationVersion(scope, version) {
421
502
  setRawValue(_internal.MIGRATION_VERSION_KEY, String(version), scope);
422
503
  }
423
504
  const storage = exports.storage = {
505
+ subscribe: (scope, listener) => {
506
+ (0, _internal.assertValidScope)(scope);
507
+ if (scope !== _Storage.StorageScope.Memory) {
508
+ ensureNativeScopeSubscription(scope);
509
+ const unsubscribe = storageEvents.subscribe(scope, listener);
510
+ return () => {
511
+ unsubscribe();
512
+ maybeCleanupNativeScopeSubscription(scope);
513
+ };
514
+ }
515
+ return storageEvents.subscribe(scope, listener);
516
+ },
517
+ subscribeKey: (scope, key, listener) => {
518
+ (0, _internal.assertValidScope)(scope);
519
+ if (scope !== _Storage.StorageScope.Memory) {
520
+ ensureNativeScopeSubscription(scope);
521
+ const unsubscribe = storageEvents.subscribeKey(scope, key, listener);
522
+ return () => {
523
+ unsubscribe();
524
+ maybeCleanupNativeScopeSubscription(scope);
525
+ };
526
+ }
527
+ return storageEvents.subscribeKey(scope, key, listener);
528
+ },
529
+ subscribePrefix: (scope, prefix, listener) => {
530
+ (0, _internal.assertValidScope)(scope);
531
+ if (scope !== _Storage.StorageScope.Memory) {
532
+ ensureNativeScopeSubscription(scope);
533
+ const unsubscribe = storageEvents.subscribePrefix(scope, prefix, listener);
534
+ return () => {
535
+ unsubscribe();
536
+ maybeCleanupNativeScopeSubscription(scope);
537
+ };
538
+ }
539
+ return storageEvents.subscribePrefix(scope, prefix, listener);
540
+ },
541
+ subscribeNamespace: (namespace, scope, listener) => {
542
+ return storage.subscribePrefix(scope, (0, _internal.prefixKey)(namespace, ""), listener);
543
+ },
544
+ setEventObserver: observer => {
545
+ eventObserver = observer;
546
+ if (observer) {
547
+ ensureNativeScopeSubscription(_Storage.StorageScope.Disk);
548
+ ensureNativeScopeSubscription(_Storage.StorageScope.Secure);
549
+ return;
550
+ }
551
+ maybeCleanupNativeScopeSubscription(_Storage.StorageScope.Disk);
552
+ maybeCleanupNativeScopeSubscription(_Storage.StorageScope.Secure);
553
+ },
424
554
  clear: scope => {
425
555
  measureOperation("storage:clear", scope, () => {
556
+ const previousValues = hasStorageChangeObservers(scope) ? storage.getAll(scope) : {};
426
557
  if (scope === _Storage.StorageScope.Memory) {
427
558
  memoryStore.clear();
428
559
  notifyAllListeners(memoryListeners);
560
+ emitBatchChange(scope, "clear", "memory", Object.keys(previousValues).map(key => createKeyChange(scope, key, previousValues[key], undefined, "clear", "memory")));
429
561
  return;
430
562
  }
431
563
  if (scope === _Storage.StorageScope.Disk) {
@@ -438,6 +570,7 @@ const storage = exports.storage = {
438
570
  }
439
571
  clearScopeRawCache(scope);
440
572
  getStorageModule().clear(scope);
573
+ emitBatchChange(scope, "clear", "native", Object.keys(previousValues).map(key => createKeyChange(scope, key, previousValues[key], undefined, "clear", "native")));
441
574
  });
442
575
  },
443
576
  clearAll: () => {
@@ -451,15 +584,26 @@ const storage = exports.storage = {
451
584
  measureOperation("storage:clearNamespace", scope, () => {
452
585
  (0, _internal.assertValidScope)(scope);
453
586
  if (scope === _Storage.StorageScope.Memory) {
454
- for (const key of memoryStore.keys()) {
455
- if ((0, _internal.isNamespaced)(key, namespace)) {
456
- memoryStore.delete(key);
457
- }
587
+ const affectedKeys = Array.from(memoryStore.keys()).filter(key => (0, _internal.isNamespaced)(key, namespace));
588
+ const previousValues = affectedKeys.map(key => ({
589
+ key,
590
+ value: getEventRawValue(scope, key)
591
+ }));
592
+ if (affectedKeys.length === 0) {
593
+ return;
458
594
  }
459
- notifyAllListeners(memoryListeners);
595
+ affectedKeys.forEach(key => {
596
+ memoryStore.delete(key);
597
+ });
598
+ affectedKeys.forEach(key => notifyKeyListeners(memoryListeners, key));
599
+ emitBatchChange(scope, "clearNamespace", "memory", previousValues.map(({
600
+ key,
601
+ value
602
+ }) => createKeyChange(scope, key, value, undefined, "clearNamespace", "memory")));
460
603
  return;
461
604
  }
462
605
  const keyPrefix = (0, _internal.prefixKey)(namespace, "");
606
+ const previousValues = hasStorageChangeObservers(scope) ? storage.getByPrefix(keyPrefix, scope) : {};
463
607
  if (scope === _Storage.StorageScope.Disk) {
464
608
  flushDiskWrites();
465
609
  }
@@ -473,6 +617,7 @@ const storage = exports.storage = {
473
617
  }
474
618
  }
475
619
  getStorageModule().removeByPrefix(keyPrefix, scope);
620
+ emitBatchChange(scope, "clearNamespace", "native", Object.keys(previousValues).map(key => createKeyChange(scope, key, previousValues[key], undefined, "clearNamespace", "native")));
476
621
  });
477
622
  },
478
623
  clearBiometric: () => {
@@ -522,7 +667,7 @@ const storage = exports.storage = {
522
667
  if (scope === _Storage.StorageScope.Secure) {
523
668
  flushSecureWrites();
524
669
  }
525
- return getStorageModule().getKeysByPrefix(prefix, scope);
670
+ return getStorageModule().getKeysByPrefix(prefix, scope) ?? [];
526
671
  });
527
672
  },
528
673
  getByPrefix: (prefix, scope) => {
@@ -547,7 +692,7 @@ const storage = exports.storage = {
547
692
  if (scope === _Storage.StorageScope.Secure) {
548
693
  flushSecureWrites();
549
694
  }
550
- const values = getStorageModule().getBatch(keys, scope);
695
+ const values = getStorageModule().getBatch(keys, scope) ?? [];
551
696
  keys.forEach((key, idx) => {
552
697
  const value = (0, _internal.decodeNativeBatchValue)(values[idx]);
553
698
  if (value !== undefined) {
@@ -562,9 +707,10 @@ const storage = exports.storage = {
562
707
  (0, _internal.assertValidScope)(scope);
563
708
  const result = {};
564
709
  if (scope === _Storage.StorageScope.Memory) {
565
- memoryStore.forEach((value, key) => {
710
+ for (const key of memoryStore.keys()) {
711
+ const value = memoryStore.get(key);
566
712
  if (typeof value === "string") result[key] = value;
567
- });
713
+ }
568
714
  return result;
569
715
  }
570
716
  if (scope === _Storage.StorageScope.Disk) {
@@ -573,9 +719,9 @@ const storage = exports.storage = {
573
719
  if (scope === _Storage.StorageScope.Secure) {
574
720
  flushSecureWrites();
575
721
  }
576
- const keys = getStorageModule().getAllKeys(scope);
722
+ const keys = getStorageModule().getAllKeys(scope) ?? [];
577
723
  if (keys.length === 0) return result;
578
- const values = getStorageModule().getBatch(keys, scope);
724
+ const values = getStorageModule().getBatch(keys, scope) ?? [];
579
725
  keys.forEach((key, idx) => {
580
726
  const val = (0, _internal.decodeNativeBatchValue)(values[idx]);
581
727
  if (val !== undefined) result[key] = val;
@@ -583,6 +729,9 @@ const storage = exports.storage = {
583
729
  return result;
584
730
  });
585
731
  },
732
+ export: scope => {
733
+ return measureOperation("storage:export", scope, () => storage.getAll(scope));
734
+ },
586
735
  size: scope => {
587
736
  return measureOperation("storage:size", scope, () => {
588
737
  (0, _internal.assertValidScope)(scope);
@@ -654,7 +803,7 @@ const storage = exports.storage = {
654
803
  platform: "native",
655
804
  backend: {
656
805
  disk: "platform-preferences",
657
- secure: "platform-secure-storage"
806
+ secure: nativeSecureBackend
658
807
  },
659
808
  writeBuffering: {
660
809
  disk: true,
@@ -662,6 +811,55 @@ const storage = exports.storage = {
662
811
  },
663
812
  errorClassification: true
664
813
  }),
814
+ getSecurityCapabilities: () => ({
815
+ platform: "native",
816
+ secureStorage: {
817
+ backend: nativeSecureBackend,
818
+ encrypted: "available",
819
+ accessControl: "unknown",
820
+ keychainAccessGroup: "unknown",
821
+ hardwareBacked: "unknown"
822
+ },
823
+ biometric: {
824
+ storage: "unknown",
825
+ prompt: "unknown",
826
+ biometryOnly: "unknown",
827
+ biometryOrPasscode: "unknown"
828
+ },
829
+ metadata: {
830
+ perKey: true,
831
+ listsWithoutValues: true,
832
+ persistsTimestamps: false
833
+ }
834
+ }),
835
+ getSecureMetadata: key => {
836
+ return measureOperation("storage:getSecureMetadata", _Storage.StorageScope.Secure, () => {
837
+ flushSecureWrites();
838
+ const storageModule = getStorageModule();
839
+ const biometricProtected = storageModule.hasSecureBiometric(key);
840
+ const exists = biometricProtected || storageModule.has(key, _Storage.StorageScope.Secure);
841
+ let kind = "missing";
842
+ if (exists) {
843
+ kind = biometricProtected ? "biometric" : "secure";
844
+ }
845
+ return {
846
+ key,
847
+ exists,
848
+ kind,
849
+ backend: nativeSecureBackend,
850
+ encrypted: "available",
851
+ hardwareBacked: "unknown",
852
+ biometricProtected,
853
+ valueExposed: false
854
+ };
855
+ });
856
+ },
857
+ getAllSecureMetadata: () => {
858
+ return measureOperation("storage:getAllSecureMetadata", _Storage.StorageScope.Secure, () => {
859
+ flushSecureWrites();
860
+ return getStorageModule().getAllKeys(_Storage.StorageScope.Secure).map(key => storage.getSecureMetadata(key));
861
+ });
862
+ },
665
863
  getString: (key, scope) => {
666
864
  return measureOperation("storage:getString", scope, () => {
667
865
  return getRawValue(key, scope);
@@ -683,11 +881,13 @@ const storage = exports.storage = {
683
881
  (0, _internal.assertValidScope)(scope);
684
882
  if (keys.length === 0) return;
685
883
  const values = keys.map(k => data[k]);
884
+ const changes = keys.map((key, index) => createKeyChange(scope, key, getEventRawValue(scope, key), values[index], "import", scope === _Storage.StorageScope.Memory ? "memory" : "native"));
686
885
  if (scope === _Storage.StorageScope.Memory) {
687
886
  keys.forEach((key, index) => {
688
887
  memoryStore.set(key, values[index]);
689
888
  });
690
889
  keys.forEach(key => notifyKeyListeners(memoryListeners, key));
890
+ emitBatchChange(scope, "import", "memory", changes);
691
891
  return;
692
892
  }
693
893
  if (scope === _Storage.StorageScope.Secure) {
@@ -696,6 +896,7 @@ const storage = exports.storage = {
696
896
  }
697
897
  getStorageModule().setBatch(keys, values, scope);
698
898
  keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
899
+ emitBatchChange(scope, "import", "native", changes);
699
900
  }, keys.length);
700
901
  }
701
902
  };
@@ -816,20 +1017,24 @@ function createStorageItem(config) {
816
1017
  return raw;
817
1018
  };
818
1019
  const writeStoredRaw = rawValue => {
1020
+ const oldValue = undefined;
819
1021
  if (isBiometric) {
820
1022
  getStorageModule().setSecureBiometricWithLevel(storageKey, rawValue, resolvedBiometricLevel);
1023
+ emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", "native");
821
1024
  return;
822
1025
  }
823
1026
  cacheRawValue(nonMemoryScope, storageKey, rawValue);
824
1027
  if (nonMemoryScope === _Storage.StorageScope.Disk) {
825
1028
  if (coalesceDiskWrites || diskWritesAsync) {
826
1029
  scheduleDiskWrite(storageKey, rawValue);
1030
+ emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", "native");
827
1031
  return;
828
1032
  }
829
1033
  clearPendingDiskWrite(storageKey);
830
1034
  }
831
1035
  if (coalesceSecureWrites) {
832
1036
  scheduleSecureWrite(storageKey, rawValue, secureAccessControl ?? secureDefaultAccessControl);
1037
+ emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", "native");
833
1038
  return;
834
1039
  }
835
1040
  if (nonMemoryScope === _Storage.StorageScope.Secure) {
@@ -837,36 +1042,44 @@ function createStorageItem(config) {
837
1042
  getStorageModule().setSecureAccessControl(secureAccessControl ?? secureDefaultAccessControl);
838
1043
  }
839
1044
  getStorageModule().set(storageKey, rawValue, config.scope);
1045
+ emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", "native");
840
1046
  };
841
1047
  const removeStoredRaw = () => {
1048
+ const oldValue = getEventRawValue(config.scope, storageKey);
842
1049
  if (isBiometric) {
843
1050
  getStorageModule().deleteSecureBiometric(storageKey);
1051
+ emitKeyChange(config.scope, storageKey, oldValue, undefined, "remove", "native");
844
1052
  return;
845
1053
  }
846
1054
  cacheRawValue(nonMemoryScope, storageKey, undefined);
847
1055
  if (nonMemoryScope === _Storage.StorageScope.Disk) {
848
1056
  if (coalesceDiskWrites || diskWritesAsync) {
849
1057
  scheduleDiskWrite(storageKey, undefined);
1058
+ emitKeyChange(config.scope, storageKey, oldValue, undefined, "remove", "native");
850
1059
  return;
851
1060
  }
852
1061
  clearPendingDiskWrite(storageKey);
853
1062
  }
854
1063
  if (coalesceSecureWrites) {
855
1064
  scheduleSecureWrite(storageKey, undefined, secureAccessControl ?? secureDefaultAccessControl);
1065
+ emitKeyChange(config.scope, storageKey, oldValue, undefined, "remove", "native");
856
1066
  return;
857
1067
  }
858
1068
  if (nonMemoryScope === _Storage.StorageScope.Secure) {
859
1069
  clearPendingSecureWrite(storageKey);
860
1070
  }
861
1071
  getStorageModule().remove(storageKey, config.scope);
1072
+ emitKeyChange(config.scope, storageKey, oldValue, undefined, "remove", "native");
862
1073
  };
863
1074
  const writeValueWithoutValidation = value => {
864
1075
  if (isMemory) {
1076
+ const oldValue = getEventRawValue(config.scope, storageKey);
865
1077
  if (memoryExpiration) {
866
1078
  memoryExpiration.set(storageKey, Date.now() + (expirationTtlMs ?? 0));
867
1079
  }
868
1080
  memoryStore.set(storageKey, value);
869
1081
  notifyKeyListeners(memoryListeners, storageKey);
1082
+ emitKeyChange(config.scope, storageKey, oldValue, typeof value === "string" ? value : undefined, "set", "memory");
870
1083
  return;
871
1084
  }
872
1085
  const serialized = serialize(value);
@@ -998,11 +1211,13 @@ function createStorageItem(config) {
998
1211
  measureOperation("item:delete", config.scope, () => {
999
1212
  invalidateParsedCache();
1000
1213
  if (isMemory) {
1214
+ const oldValue = getEventRawValue(config.scope, storageKey);
1001
1215
  if (memoryExpiration) {
1002
1216
  memoryExpiration.delete(storageKey);
1003
1217
  }
1004
1218
  memoryStore.delete(storageKey);
1005
1219
  notifyKeyListeners(memoryListeners, storageKey);
1220
+ emitKeyChange(config.scope, storageKey, oldValue, undefined, "remove", "memory");
1006
1221
  return;
1007
1222
  }
1008
1223
  removeStoredRaw();
@@ -1039,6 +1254,22 @@ function createStorageItem(config) {
1039
1254
  }
1040
1255
  };
1041
1256
  };
1257
+ const subscribeSelector = (selector, listener, options = {}) => {
1258
+ const isEqual = options.isEqual ?? Object.is;
1259
+ let currentValue = selector(getInternal());
1260
+ if (options.fireImmediately === true) {
1261
+ listener(currentValue, currentValue);
1262
+ }
1263
+ return subscribe(() => {
1264
+ const nextValue = selector(getInternal());
1265
+ if (isEqual(currentValue, nextValue)) {
1266
+ return;
1267
+ }
1268
+ const previousValue = currentValue;
1269
+ currentValue = nextValue;
1270
+ listener(nextValue, previousValue);
1271
+ });
1272
+ };
1042
1273
  const storageItem = {
1043
1274
  get,
1044
1275
  getWithVersion,
@@ -1047,6 +1278,7 @@ function createStorageItem(config) {
1047
1278
  delete: deleteItem,
1048
1279
  has: hasItem,
1049
1280
  subscribe,
1281
+ subscribeSelector,
1050
1282
  serialize,
1051
1283
  deserialize,
1052
1284
  _triggerListeners: () => {
@@ -1148,6 +1380,10 @@ function setBatch(items, scope) {
1148
1380
  }) => item.set(value));
1149
1381
  return;
1150
1382
  }
1383
+ const changes = items.map(({
1384
+ item,
1385
+ value
1386
+ }) => createKeyChange(scope, item.key, getEventRawValue(scope, item.key), typeof value === "string" ? value : undefined, "setBatch", "memory"));
1151
1387
 
1152
1388
  // Atomic write: update all values in memoryStore, invalidate caches, then batch-notify
1153
1389
  items.forEach(({
@@ -1160,6 +1396,7 @@ function setBatch(items, scope) {
1160
1396
  items.forEach(({
1161
1397
  item
1162
1398
  }) => notifyKeyListeners(memoryListeners, item.key));
1399
+ emitBatchChange(scope, "setBatch", "memory", changes);
1163
1400
  return;
1164
1401
  }
1165
1402
  if (scope === _Storage.StorageScope.Secure) {
@@ -1183,6 +1420,10 @@ function setBatch(items, scope) {
1183
1420
  }
1184
1421
  flushSecureWrites();
1185
1422
  const storageModule = getStorageModule();
1423
+ const keys = secureEntries.map(({
1424
+ item
1425
+ }) => item.key);
1426
+ const oldValues = hasStorageChangeObservers(scope) ? storageModule.getBatch(keys, scope) ?? [] : [];
1186
1427
  const groupedByAccessControl = new Map();
1187
1428
  secureEntries.forEach(({
1188
1429
  item,
@@ -1206,6 +1447,10 @@ function setBatch(items, scope) {
1206
1447
  storageModule.setBatch(group.keys, group.values, scope);
1207
1448
  group.keys.forEach((key, index) => cacheRawValue(scope, key, group.values[index]));
1208
1449
  });
1450
+ emitBatchChange(scope, "setBatch", "native", secureEntries.map(({
1451
+ item,
1452
+ value
1453
+ }, index) => createKeyChange(scope, item.key, oldValues[index], item.serialize(value), "setBatch", "native")));
1209
1454
  return;
1210
1455
  }
1211
1456
  flushDiskWrites();
@@ -1221,15 +1466,19 @@ function setBatch(items, scope) {
1221
1466
  }
1222
1467
  const keys = items.map(entry => entry.item.key);
1223
1468
  const values = items.map(entry => entry.item.serialize(entry.value));
1469
+ const oldValues = hasStorageChangeObservers(scope) ? getStorageModule().getBatch(keys, scope) ?? [] : [];
1224
1470
  getStorageModule().setBatch(keys, values, scope);
1225
1471
  keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
1472
+ emitBatchChange(scope, "setBatch", "native", keys.map((key, index) => createKeyChange(scope, key, oldValues[index], values[index], "setBatch", "native")));
1226
1473
  }, items.length);
1227
1474
  }
1228
1475
  function removeBatch(items, scope) {
1229
1476
  measureOperation("batch:remove", scope, () => {
1230
1477
  (0, _internal.assertBatchScope)(items, scope);
1231
1478
  if (scope === _Storage.StorageScope.Memory) {
1479
+ const changes = items.map(item => createKeyChange(scope, item.key, getEventRawValue(scope, item.key), undefined, "removeBatch", "memory"));
1232
1480
  items.forEach(item => item.delete());
1481
+ emitBatchChange(scope, "removeBatch", "memory", changes);
1233
1482
  return;
1234
1483
  }
1235
1484
  const keys = items.map(item => item.key);
@@ -1239,8 +1488,10 @@ function removeBatch(items, scope) {
1239
1488
  if (scope === _Storage.StorageScope.Secure) {
1240
1489
  flushSecureWrites();
1241
1490
  }
1491
+ const oldValues = hasStorageChangeObservers(scope) ? getStorageModule().getBatch(keys, scope) ?? [] : [];
1242
1492
  getStorageModule().removeBatch(keys, scope);
1243
1493
  keys.forEach(key => cacheRawValue(scope, key, undefined));
1494
+ emitBatchChange(scope, "removeBatch", "native", keys.map((key, index) => createKeyChange(scope, key, oldValues[index], undefined, "removeBatch", "native")));
1244
1495
  }, items.length);
1245
1496
  }
1246
1497
  function registerMigration(version, migration) {