react-native-nitro-storage 0.7.0 → 0.8.0

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 (106) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +20 -9
  3. package/SECURITY.md +2 -2
  4. package/docs/api-reference.md +11 -1
  5. package/docs/batch-transactions-migrations.md +8 -2
  6. package/docs/benchmarks.md +19 -15
  7. package/docs/web-backends.md +18 -1
  8. package/ios/IOSStorageAdapterCpp.mm +162 -17
  9. package/lib/commonjs/capabilities.js +26 -0
  10. package/lib/commonjs/capabilities.js.map +1 -0
  11. package/lib/commonjs/core/durability.js +137 -0
  12. package/lib/commonjs/core/durability.js.map +1 -0
  13. package/lib/commonjs/core/metrics.js +91 -0
  14. package/lib/commonjs/core/metrics.js.map +1 -0
  15. package/lib/commonjs/index.js +47 -22
  16. package/lib/commonjs/index.js.map +1 -1
  17. package/lib/commonjs/index.web.js +118 -66
  18. package/lib/commonjs/index.web.js.map +1 -1
  19. package/lib/commonjs/indexeddb-backend.js +73 -23
  20. package/lib/commonjs/indexeddb-backend.js.map +1 -1
  21. package/lib/commonjs/internal.js +27 -4
  22. package/lib/commonjs/internal.js.map +1 -1
  23. package/lib/commonjs/shared.js +2 -2
  24. package/lib/commonjs/shared.js.map +1 -1
  25. package/lib/commonjs/storage-core.js +217 -233
  26. package/lib/commonjs/storage-core.js.map +1 -1
  27. package/lib/commonjs/storage-events.js.map +1 -1
  28. package/lib/commonjs/storage-platform.js +6 -0
  29. package/lib/commonjs/storage-platform.js.map +1 -0
  30. package/lib/commonjs/storage-runtime.js +3 -21
  31. package/lib/commonjs/storage-runtime.js.map +1 -1
  32. package/lib/commonjs/testing.js +19 -4
  33. package/lib/commonjs/testing.js.map +1 -1
  34. package/lib/commonjs/web-backend-contract.js +27 -0
  35. package/lib/commonjs/web-backend-contract.js.map +1 -0
  36. package/lib/module/capabilities.js +21 -0
  37. package/lib/module/capabilities.js.map +1 -0
  38. package/lib/module/core/durability.js +133 -0
  39. package/lib/module/core/durability.js.map +1 -0
  40. package/lib/module/core/metrics.js +87 -0
  41. package/lib/module/core/metrics.js.map +1 -0
  42. package/lib/module/index.js +48 -23
  43. package/lib/module/index.js.map +1 -1
  44. package/lib/module/index.web.js +103 -64
  45. package/lib/module/index.web.js.map +1 -1
  46. package/lib/module/indexeddb-backend.js +73 -23
  47. package/lib/module/indexeddb-backend.js.map +1 -1
  48. package/lib/module/internal.js +24 -3
  49. package/lib/module/internal.js.map +1 -1
  50. package/lib/module/shared.js +2 -2
  51. package/lib/module/shared.js.map +1 -1
  52. package/lib/module/storage-core.js +218 -234
  53. package/lib/module/storage-core.js.map +1 -1
  54. package/lib/module/storage-events.js.map +1 -1
  55. package/lib/module/storage-platform.js +4 -0
  56. package/lib/module/storage-platform.js.map +1 -0
  57. package/lib/module/storage-runtime.js +3 -21
  58. package/lib/module/storage-runtime.js.map +1 -1
  59. package/lib/module/testing.js +5 -3
  60. package/lib/module/testing.js.map +1 -1
  61. package/lib/module/web-backend-contract.js +22 -0
  62. package/lib/module/web-backend-contract.js.map +1 -0
  63. package/lib/typescript/capabilities.d.ts +11 -0
  64. package/lib/typescript/capabilities.d.ts.map +1 -0
  65. package/lib/typescript/core/durability.d.ts +27 -0
  66. package/lib/typescript/core/durability.d.ts.map +1 -0
  67. package/lib/typescript/core/metrics.d.ts +12 -0
  68. package/lib/typescript/core/metrics.d.ts.map +1 -0
  69. package/lib/typescript/index.d.ts +5 -2
  70. package/lib/typescript/index.d.ts.map +1 -1
  71. package/lib/typescript/index.web.d.ts +8 -4
  72. package/lib/typescript/index.web.d.ts.map +1 -1
  73. package/lib/typescript/indexeddb-backend.d.ts.map +1 -1
  74. package/lib/typescript/internal.d.ts +3 -0
  75. package/lib/typescript/internal.d.ts.map +1 -1
  76. package/lib/typescript/shared.d.ts +2 -1
  77. package/lib/typescript/shared.d.ts.map +1 -1
  78. package/lib/typescript/storage-core.d.ts +3 -3
  79. package/lib/typescript/storage-core.d.ts.map +1 -1
  80. package/lib/typescript/storage-events.d.ts +2 -2
  81. package/lib/typescript/storage-events.d.ts.map +1 -1
  82. package/lib/typescript/storage-platform.d.ts +12 -0
  83. package/lib/typescript/storage-platform.d.ts.map +1 -0
  84. package/lib/typescript/storage-runtime.d.ts.map +1 -1
  85. package/lib/typescript/testing.d.ts +5 -1
  86. package/lib/typescript/testing.d.ts.map +1 -1
  87. package/lib/typescript/web-backend-contract.d.ts +10 -0
  88. package/lib/typescript/web-backend-contract.d.ts.map +1 -0
  89. package/lib/typescript/web-storage-backend.d.ts +1 -1
  90. package/lib/typescript/web-storage-backend.d.ts.map +1 -1
  91. package/package.json +3 -2
  92. package/src/capabilities.ts +36 -0
  93. package/src/core/durability.ts +170 -0
  94. package/src/core/metrics.ts +133 -0
  95. package/src/index.ts +66 -40
  96. package/src/index.web.ts +158 -86
  97. package/src/indexeddb-backend.ts +91 -20
  98. package/src/internal.ts +50 -4
  99. package/src/shared.ts +7 -6
  100. package/src/storage-core.ts +317 -285
  101. package/src/storage-events.ts +2 -1
  102. package/src/storage-platform.ts +16 -0
  103. package/src/storage-runtime.ts +13 -47
  104. package/src/testing.ts +14 -7
  105. package/src/web-backend-contract.ts +34 -0
  106. package/src/web-storage-backend.ts +1 -1
@@ -1,3 +1,17 @@
1
+ import { createDurabilityCoordinator } from "./core/durability";
2
+ import { createMetricsRegistry } from "./core/metrics";
3
+ import {
4
+ MIGRATION_VERSION_KEY,
5
+ type StoredEnvelope,
6
+ isStoredEnvelope,
7
+ assertBatchScope,
8
+ assertValidScope,
9
+ toVersionToken,
10
+ prefixKey,
11
+ isNamespaced,
12
+ escapeCollidingRawValue,
13
+ unescapeCollidingRawValue,
14
+ } from "./internal";
1
15
  import {
2
16
  assertAccessControlLevel,
3
17
  assertBiometricLevel,
@@ -10,17 +24,12 @@ import {
10
24
  isUpdater,
11
25
  notifyAllListeners,
12
26
  notifyKeyListeners,
13
- now,
14
27
  redactSecureKeyChange,
15
- runMicrotask,
16
28
  typedKeys,
17
29
  type ExpirationConfig,
18
30
  type KeyListenerRegistry,
19
31
  type Migration,
20
- type MigrationContext,
21
32
  type NonMemoryScope,
22
- type PendingDiskWrite,
23
- type PendingSecureWrite,
24
33
  type RollbackRecord,
25
34
  type SecureAuthStorageConfig,
26
35
  type StorageEventObserverOptions,
@@ -33,18 +42,6 @@ import {
33
42
  type Validator,
34
43
  type VersionedValue,
35
44
  } from "./shared";
36
- import { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
37
- import {
38
- MIGRATION_VERSION_KEY,
39
- type StoredEnvelope,
40
- isStoredEnvelope,
41
- assertBatchScope,
42
- assertValidScope,
43
- toVersionToken,
44
- prefixKey,
45
- isNamespaced,
46
- } from "./internal";
47
- import type { SecureStorageMetadata } from "./storage-runtime";
48
45
  import {
49
46
  StorageEventRegistry,
50
47
  type StorageBatchChangeEvent,
@@ -55,6 +52,8 @@ import {
55
52
  type StorageKeyChangeEvent,
56
53
  } from "./storage-events";
57
54
  import type { StorageSetter } from "./storage-hooks";
55
+ import type { SecureStorageMetadata } from "./storage-runtime";
56
+ import { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
58
57
 
59
58
  export type TransactionContext = {
60
59
  scope: StorageScope;
@@ -122,6 +121,7 @@ export type StorageItem<T> = {
122
121
  type StorageItemInternal<T> = StorageItem<T> & {
123
122
  _triggerListeners: () => void;
124
123
  _invalidateParsedCacheOnly: () => void;
124
+ _deleteMemoryEntry: () => void;
125
125
  _hasValidation: boolean;
126
126
  _hasExpiration: boolean;
127
127
  _readCacheEnabled: boolean;
@@ -229,7 +229,6 @@ export type StorageCoreAdapter = {
229
229
  backend: StorageCoreBackend;
230
230
  changeSource: StorageChangeSource;
231
231
  applyAccessControlOnSecureRawWrite: boolean;
232
- flushDiskWritesOnImport: boolean;
233
232
  ensureScopeSubscription(scope: NonMemoryScope): void;
234
233
  maybeCleanupScopeSubscription(scope: NonMemoryScope): void;
235
234
  onWillEmitChanges(
@@ -299,84 +298,45 @@ export function createStorageCore(
299
298
  const registeredKeyCounts = new Map<string, number>();
300
299
  const memoryStore = new Map<string, unknown>();
301
300
  const memoryListeners: KeyListenerRegistry = new Map();
302
- const scopedListeners = new Map<NonMemoryScope, KeyListenerRegistry>([
303
- [StorageScope.Disk, new Map()],
304
- [StorageScope.Secure, new Map()],
305
- ]);
306
- const scopedRawCache = new Map<
301
+ const scopedListeners: Record<NonMemoryScope, KeyListenerRegistry> = {
302
+ [StorageScope.Disk]: new Map(),
303
+ [StorageScope.Secure]: new Map(),
304
+ };
305
+ const scopedRawCache: Record<
307
306
  NonMemoryScope,
308
307
  Map<string, string | undefined>
309
- >([
310
- [StorageScope.Disk, new Map()],
311
- [StorageScope.Secure, new Map()],
312
- ]);
313
- const pendingDiskWrites = new Map<string, PendingDiskWrite>();
314
- let diskFlushScheduled = false;
315
- let diskWritesAsync = false;
316
- const pendingSecureWrites = new Map<string, PendingSecureWrite>();
317
- let secureFlushScheduled = false;
308
+ > = {
309
+ [StorageScope.Disk]: new Map(),
310
+ [StorageScope.Secure]: new Map(),
311
+ };
318
312
  let secureDefaultAccessControl: AccessControl = AccessControl.WhenUnlocked;
319
- let metricsObserver: StorageMetricsObserver | undefined;
320
313
  let eventObserver: StorageEventListener | undefined;
321
314
  let eventObserverRedactSecureValues = true;
322
- const metricsCounters = new Map<
323
- string,
324
- { count: number; totalDurationMs: number; maxDurationMs: number }
325
- >();
326
315
  const storageEvents = new StorageEventRegistry();
327
-
328
- function recordMetric(
329
- operation: string,
330
- scope: StorageScope,
331
- durationMs: number,
332
- keysCount = 1,
333
- ): void {
334
- const existing = metricsCounters.get(operation);
335
- if (!existing) {
336
- metricsCounters.set(operation, {
337
- count: 1,
338
- totalDurationMs: durationMs,
339
- maxDurationMs: durationMs,
340
- });
341
- } else {
342
- existing.count += 1;
343
- existing.totalDurationMs += durationMs;
344
- existing.maxDurationMs = Math.max(existing.maxDurationMs, durationMs);
345
- }
346
-
347
- metricsObserver?.({
348
- operation,
349
- scope,
350
- durationMs,
351
- keysCount,
352
- });
353
- }
354
-
355
- function measureOperation<T>(
356
- operation: string,
357
- scope: StorageScope,
358
- fn: () => T,
359
- keysCount = 1,
360
- ): T {
361
- if (!metricsObserver) {
362
- return fn();
363
- }
364
- const start = now();
365
- try {
366
- return fn();
367
- } finally {
368
- recordMetric(operation, scope, now() - start, keysCount);
369
- }
370
- }
316
+ const metrics = createMetricsRegistry();
317
+ const durability = createDurabilityCoordinator({
318
+ backend: {
319
+ setBatch: (keys, values, scope) => {
320
+ adapter.backend.setBatch(keys, values, scope);
321
+ },
322
+ removeBatch: (keys, scope) => {
323
+ adapter.backend.removeBatch(keys, scope);
324
+ },
325
+ setSecureAccessControl: (level) => {
326
+ adapter.backend.setSecureAccessControl(level);
327
+ },
328
+ },
329
+ resolveSecureDefaultAccessControl: () => secureDefaultAccessControl,
330
+ });
371
331
 
372
332
  function getScopedListeners(scope: NonMemoryScope): KeyListenerRegistry {
373
- return scopedListeners.get(scope)!;
333
+ return scopedListeners[scope];
374
334
  }
375
335
 
376
336
  function getScopeRawCache(
377
337
  scope: NonMemoryScope,
378
338
  ): Map<string, string | undefined> {
379
- return scopedRawCache.get(scope)!;
339
+ return scopedRawCache[scope];
380
340
  }
381
341
 
382
342
  function cacheRawValue(
@@ -428,7 +388,9 @@ export function createStorageCore(
428
388
  ): string | undefined {
429
389
  if (scope === StorageScope.Memory) {
430
390
  const value = memoryStore.get(key);
431
- return typeof value === "string" ? value : undefined;
391
+ return typeof value === "string"
392
+ ? unescapeCollidingRawValue(value)
393
+ : undefined;
432
394
  }
433
395
 
434
396
  return getRawValue(key, scope);
@@ -513,110 +475,58 @@ export function createStorageCore(
513
475
  eventObserver?.(eventForGlobalObserver(event));
514
476
  }
515
477
 
478
+ function measureOperation<T>(
479
+ operation: string,
480
+ scope: StorageScope,
481
+ fn: () => T,
482
+ keysCount = 1,
483
+ ): T {
484
+ return metrics.measure(operation, scope, fn, keysCount);
485
+ }
486
+
487
+ function recordMetric(
488
+ operation: string,
489
+ scope: StorageScope,
490
+ durationMs: number,
491
+ keysCount = 1,
492
+ ): void {
493
+ metrics.record(operation, scope, durationMs, keysCount);
494
+ }
495
+
516
496
  function readPendingSecureWrite(key: string): string | undefined {
517
- return pendingSecureWrites.get(key)?.value;
497
+ return durability.readPendingSecureWrite(key);
518
498
  }
519
499
 
520
500
  function readPendingDiskWrite(key: string): string | undefined {
521
- return pendingDiskWrites.get(key)?.value;
501
+ return durability.readPendingDiskWrite(key);
522
502
  }
523
503
 
524
504
  function hasPendingDiskWrite(key: string): boolean {
525
- return pendingDiskWrites.has(key);
505
+ return durability.hasPendingDiskWrite(key);
526
506
  }
527
507
 
528
508
  function hasPendingSecureWrite(key: string): boolean {
529
- return pendingSecureWrites.has(key);
509
+ return durability.hasPendingSecureWrite(key);
530
510
  }
531
511
 
532
512
  function clearPendingDiskWrite(key: string): void {
533
- pendingDiskWrites.delete(key);
513
+ durability.clearPendingDiskWrite(key);
534
514
  }
535
515
 
536
516
  function clearPendingSecureWrite(key: string): void {
537
- pendingSecureWrites.delete(key);
517
+ durability.clearPendingSecureWrite(key);
538
518
  }
539
519
 
540
520
  function flushDiskWrites(): void {
541
- diskFlushScheduled = false;
542
-
543
- if (pendingDiskWrites.size === 0) {
544
- return;
545
- }
546
-
547
- const writes = Array.from(pendingDiskWrites.values());
548
- pendingDiskWrites.clear();
549
-
550
- const keysToSet: string[] = [];
551
- const valuesToSet: string[] = [];
552
- const keysToRemove: string[] = [];
553
-
554
- writes.forEach(({ key, value }) => {
555
- if (value === undefined) {
556
- keysToRemove.push(key);
557
- return;
558
- }
559
-
560
- keysToSet.push(key);
561
- valuesToSet.push(value);
562
- });
563
-
564
- if (keysToSet.length > 0) {
565
- adapter.backend.setBatch(keysToSet, valuesToSet, StorageScope.Disk);
566
- }
567
- if (keysToRemove.length > 0) {
568
- adapter.backend.removeBatch(keysToRemove, StorageScope.Disk);
569
- }
521
+ durability.flushDiskWrites();
570
522
  }
571
523
 
572
524
  function flushSecureWrites(): void {
573
- secureFlushScheduled = false;
574
-
575
- if (pendingSecureWrites.size === 0) {
576
- return;
577
- }
578
-
579
- const writes = Array.from(pendingSecureWrites.values());
580
- pendingSecureWrites.clear();
581
-
582
- const groupedSetWrites = new Map<
583
- AccessControl,
584
- { keys: string[]; values: string[] }
585
- >();
586
- const keysToRemove: string[] = [];
587
-
588
- writes.forEach(({ key, value, accessControl }) => {
589
- if (value === undefined) {
590
- keysToRemove.push(key);
591
- } else {
592
- const resolvedAccessControl =
593
- accessControl ?? secureDefaultAccessControl;
594
- const existingGroup = groupedSetWrites.get(resolvedAccessControl);
595
- const group = existingGroup ?? { keys: [], values: [] };
596
- group.keys.push(key);
597
- group.values.push(value);
598
- if (!existingGroup) {
599
- groupedSetWrites.set(resolvedAccessControl, group);
600
- }
601
- }
602
- });
603
-
604
- groupedSetWrites.forEach((group, accessControl) => {
605
- adapter.backend.setSecureAccessControl(accessControl);
606
- adapter.backend.setBatch(group.keys, group.values, StorageScope.Secure);
607
- });
608
- if (keysToRemove.length > 0) {
609
- adapter.backend.removeBatch(keysToRemove, StorageScope.Secure);
610
- }
525
+ durability.flushSecureWrites();
611
526
  }
612
527
 
613
528
  function scheduleDiskWrite(key: string, value: string | undefined): void {
614
- pendingDiskWrites.set(key, { key, value });
615
- if (diskFlushScheduled) {
616
- return;
617
- }
618
- diskFlushScheduled = true;
619
- runMicrotask(flushDiskWrites);
529
+ durability.scheduleDiskWrite(key, value);
620
530
  }
621
531
 
622
532
  function scheduleSecureWrite(
@@ -624,20 +534,21 @@ export function createStorageCore(
624
534
  value: string | undefined,
625
535
  accessControl?: AccessControl,
626
536
  ): void {
627
- const pendingWrite: PendingSecureWrite = { key, value };
628
- if (accessControl !== undefined) {
629
- pendingWrite.accessControl = accessControl;
630
- }
631
- pendingSecureWrites.set(key, pendingWrite);
632
- if (secureFlushScheduled) {
633
- return;
634
- }
635
- secureFlushScheduled = true;
636
- runMicrotask(flushSecureWrites);
537
+ durability.scheduleSecureWrite(key, value, accessControl);
637
538
  }
638
539
 
639
- function getRawValue(key: string, scope: StorageScope): string | undefined {
640
- assertValidScope(scope);
540
+ function setDiskWritesAsyncMode(enabled: boolean): void {
541
+ durability.setDiskWritesAsync(enabled);
542
+ }
543
+
544
+ function isDiskWritesAsync(): boolean {
545
+ return durability.isDiskWritesAsync();
546
+ }
547
+
548
+ function getStoredRawValue(
549
+ key: string,
550
+ scope: StorageScope,
551
+ ): string | undefined {
641
552
  if (scope === StorageScope.Memory) {
642
553
  const value = memoryStore.get(key);
643
554
  return typeof value === "string" ? value : undefined;
@@ -654,21 +565,49 @@ export function createStorageCore(
654
565
  return adapter.backend.get(key, scope);
655
566
  }
656
567
 
568
+ function getRawValue(key: string, scope: StorageScope): string | undefined {
569
+ assertValidScope(scope);
570
+ if (scope === StorageScope.Memory) {
571
+ const value = memoryStore.get(key);
572
+ return typeof value === "string"
573
+ ? unescapeCollidingRawValue(value)
574
+ : undefined;
575
+ }
576
+
577
+ if (scope === StorageScope.Disk && hasPendingDiskWrite(key)) {
578
+ const pending = readPendingDiskWrite(key);
579
+ return pending === undefined
580
+ ? undefined
581
+ : unescapeCollidingRawValue(pending);
582
+ }
583
+
584
+ if (scope === StorageScope.Secure && hasPendingSecureWrite(key)) {
585
+ const pending = readPendingSecureWrite(key);
586
+ return pending === undefined
587
+ ? undefined
588
+ : unescapeCollidingRawValue(pending);
589
+ }
590
+
591
+ const raw = adapter.backend.get(key, scope);
592
+ return raw === undefined ? undefined : unescapeCollidingRawValue(raw);
593
+ }
594
+
657
595
  function setRawValue(key: string, value: string, scope: StorageScope): void {
658
596
  assertValidScope(scope);
597
+ const storedValue = escapeCollidingRawValue(value);
659
598
  const oldValue =
660
599
  scope === StorageScope.Memory ? getEventRawValue(scope, key) : undefined;
661
600
  if (scope === StorageScope.Memory) {
662
- memoryStore.set(key, value);
601
+ memoryStore.set(key, storedValue);
663
602
  notifyKeyListeners(memoryListeners, key);
664
603
  emitKeyChange(scope, key, oldValue, value, "set", "memory");
665
604
  return;
666
605
  }
667
606
 
668
607
  if (scope === StorageScope.Disk) {
669
- cacheRawValue(scope, key, value);
670
- if (diskWritesAsync) {
671
- scheduleDiskWrite(key, value);
608
+ cacheRawValue(scope, key, storedValue);
609
+ if (isDiskWritesAsync()) {
610
+ scheduleDiskWrite(key, storedValue);
672
611
  emitKeyChange(scope, key, oldValue, value, "set", adapter.changeSource);
673
612
  return;
674
613
  }
@@ -685,8 +624,8 @@ export function createStorageCore(
685
624
  }
686
625
  }
687
626
 
688
- adapter.backend.set(key, value, scope);
689
- cacheRawValue(scope, key, value);
627
+ adapter.backend.set(key, storedValue, scope);
628
+ cacheRawValue(scope, key, storedValue);
690
629
  emitKeyChange(scope, key, oldValue, value, "set", adapter.changeSource);
691
630
  }
692
631
 
@@ -702,7 +641,7 @@ export function createStorageCore(
702
641
 
703
642
  if (scope === StorageScope.Disk) {
704
643
  cacheRawValue(scope, key, undefined);
705
- if (diskWritesAsync) {
644
+ if (isDiskWritesAsync()) {
706
645
  scheduleDiskWrite(key, undefined);
707
646
  emitKeyChange(
708
647
  scope,
@@ -746,10 +685,6 @@ export function createStorageCore(
746
685
  return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
747
686
  }
748
687
 
749
- function writeMigrationVersion(scope: StorageScope, version: number): void {
750
- setRawValue(MIGRATION_VERSION_KEY, String(version), scope);
751
- }
752
-
753
688
  const internals: StorageCoreInternals = {
754
689
  getScopedListeners,
755
690
  cacheRawValue,
@@ -758,10 +693,10 @@ export function createStorageCore(
758
693
  clearPendingDiskWrite,
759
694
  clearPendingSecureWrite,
760
695
  clearAllPendingDiskWrites: () => {
761
- pendingDiskWrites.clear();
696
+ durability.clearAllPendingDiskWrites();
762
697
  },
763
698
  clearAllPendingSecureWrites: () => {
764
- pendingSecureWrites.clear();
699
+ durability.clearAllPendingSecureWrites();
765
700
  },
766
701
  flushDiskWrites,
767
702
  flushSecureWrites,
@@ -801,7 +736,9 @@ export function createStorageCore(
801
736
  ? removeKeys.map((key) => getEventRawValue(scope, key))
802
737
  : [];
803
738
  removeKeys.forEach((key) => memoryStore.delete(key));
804
- removeKeys.forEach((key) => notifyKeyListeners(memoryListeners, key));
739
+ removeKeys.forEach((key) => {
740
+ notifyKeyListeners(memoryListeners, key);
741
+ });
805
742
  emitBatchChange(
806
743
  scope,
807
744
  "clear",
@@ -834,10 +771,18 @@ export function createStorageCore(
834
771
  return;
835
772
  }
836
773
  const previousValues = shouldReadPreviousEventValues(scope)
837
- ? adapter.backend.getBatch(removeKeys, scope)
774
+ ? adapter.backend
775
+ .getBatch(removeKeys, scope)
776
+ .map((value) =>
777
+ value === undefined
778
+ ? undefined
779
+ : unescapeCollidingRawValue(value),
780
+ )
838
781
  : [];
839
782
  adapter.backend.removeBatch(removeKeys, scope);
840
- removeKeys.forEach((key) => cacheRawValue(scope, key, undefined));
783
+ removeKeys.forEach((key) => {
784
+ cacheRawValue(scope, key, undefined);
785
+ });
841
786
  emitBatchChange(
842
787
  scope,
843
788
  "clear",
@@ -961,12 +906,12 @@ export function createStorageCore(
961
906
 
962
907
  if (scope === StorageScope.Disk) {
963
908
  flushDiskWrites();
964
- pendingDiskWrites.clear();
909
+ durability.clearAllPendingDiskWrites();
965
910
  }
966
911
 
967
912
  if (scope === StorageScope.Secure) {
968
913
  flushSecureWrites();
969
- pendingSecureWrites.clear();
914
+ durability.clearAllPendingSecureWrites();
970
915
  }
971
916
 
972
917
  clearScopeRawCache(scope);
@@ -1100,9 +1045,9 @@ export function createStorageCore(
1100
1045
  affectedKeys.forEach((key) => {
1101
1046
  memoryStore.delete(key);
1102
1047
  });
1103
- affectedKeys.forEach((key) =>
1104
- notifyKeyListeners(memoryListeners, key),
1105
- );
1048
+ affectedKeys.forEach((key) => {
1049
+ notifyKeyListeners(memoryListeners, key);
1050
+ });
1106
1051
  emitBatchChange(
1107
1052
  scope,
1108
1053
  "clearNamespace",
@@ -1239,7 +1184,7 @@ export function createStorageCore(
1239
1184
  keys.forEach((key, idx) => {
1240
1185
  const value = values[idx];
1241
1186
  if (value !== undefined) {
1242
- result[key] = value;
1187
+ result[key] = unescapeCollidingRawValue(value);
1243
1188
  }
1244
1189
  });
1245
1190
  return result;
@@ -1252,7 +1197,8 @@ export function createStorageCore(
1252
1197
  if (scope === StorageScope.Memory) {
1253
1198
  for (const key of memoryStore.keys()) {
1254
1199
  const value = memoryStore.get(key);
1255
- if (typeof value === "string") result[key] = value;
1200
+ if (typeof value === "string")
1201
+ result[key] = unescapeCollidingRawValue(value);
1256
1202
  }
1257
1203
  return result;
1258
1204
  }
@@ -1267,7 +1213,7 @@ export function createStorageCore(
1267
1213
  const values = adapter.backend.getBatch(keys, scope);
1268
1214
  keys.forEach((key, idx) => {
1269
1215
  const val = values[idx];
1270
- if (val !== undefined) result[key] = val;
1216
+ if (val !== undefined) result[key] = unescapeCollidingRawValue(val);
1271
1217
  });
1272
1218
  return result;
1273
1219
  });
@@ -1312,7 +1258,7 @@ export function createStorageCore(
1312
1258
  },
1313
1259
  setDiskWritesAsync: (enabled: boolean) => {
1314
1260
  measureOperation("storage:setDiskWritesAsync", StorageScope.Disk, () => {
1315
- diskWritesAsync = enabled;
1261
+ setDiskWritesAsyncMode(enabled);
1316
1262
  if (!enabled) {
1317
1263
  flushDiskWrites();
1318
1264
  }
@@ -1329,23 +1275,16 @@ export function createStorageCore(
1329
1275
  });
1330
1276
  },
1331
1277
  setMetricsObserver: (observer?: StorageMetricsObserver) => {
1332
- metricsObserver = observer;
1278
+ metrics.setObserver(observer);
1333
1279
  },
1334
1280
  getMetricsSnapshot: (): Record<string, StorageMetricSummary> => {
1335
- const snapshot: Record<string, StorageMetricSummary> = {};
1336
- metricsCounters.forEach((value, key) => {
1337
- snapshot[key] = {
1338
- count: value.count,
1339
- totalDurationMs: value.totalDurationMs,
1340
- avgDurationMs:
1341
- value.count === 0 ? 0 : value.totalDurationMs / value.count,
1342
- maxDurationMs: value.maxDurationMs,
1343
- };
1344
- });
1345
- return snapshot;
1281
+ return metrics.getSnapshot();
1282
+ },
1283
+ getScopedMetricsSnapshot: (): Record<string, StorageMetricSummary> => {
1284
+ return metrics.getScopedSnapshot();
1346
1285
  },
1347
1286
  resetMetrics: () => {
1348
- metricsCounters.clear();
1287
+ metrics.reset();
1349
1288
  },
1350
1289
  getSecureMetadata: (key: string): SecureStorageMetadata => {
1351
1290
  return measureOperation(
@@ -1410,7 +1349,8 @@ export function createStorageCore(
1410
1349
  () => {
1411
1350
  assertValidScope(scope);
1412
1351
  if (keys.length === 0) return;
1413
- const values = keys.map((k) => data[k]!);
1352
+ const values = keys.map((k) => data[k] as string);
1353
+ const storedValues = values.map(escapeCollidingRawValue);
1414
1354
  const changes = keys.map((key, index) =>
1415
1355
  createKeyChange(
1416
1356
  scope,
@@ -1424,9 +1364,11 @@ export function createStorageCore(
1424
1364
 
1425
1365
  if (scope === StorageScope.Memory) {
1426
1366
  keys.forEach((key, index) => {
1427
- memoryStore.set(key, values[index]);
1367
+ memoryStore.set(key, storedValues[index]);
1368
+ });
1369
+ keys.forEach((key) => {
1370
+ notifyKeyListeners(memoryListeners, key);
1428
1371
  });
1429
- keys.forEach((key) => notifyKeyListeners(memoryListeners, key));
1430
1372
  emitBatchChange(scope, "import", "memory", changes);
1431
1373
  return;
1432
1374
  }
@@ -1435,14 +1377,14 @@ export function createStorageCore(
1435
1377
  flushSecureWrites();
1436
1378
  adapter.backend.setSecureAccessControl(secureDefaultAccessControl);
1437
1379
  }
1438
- if (scope === StorageScope.Disk && adapter.flushDiskWritesOnImport) {
1380
+ if (scope === StorageScope.Disk) {
1439
1381
  flushDiskWrites();
1440
1382
  }
1441
1383
 
1442
- adapter.backend.setBatch(keys, values, scope);
1443
- keys.forEach((key, index) =>
1444
- cacheRawValue(scope, key, values[index]),
1445
- );
1384
+ adapter.backend.setBatch(keys, storedValues, scope);
1385
+ keys.forEach((key, index) => {
1386
+ cacheRawValue(scope, key, storedValues[index]);
1387
+ });
1446
1388
  emitBatchChange(scope, "import", adapter.changeSource, changes);
1447
1389
  },
1448
1390
  keys.length,
@@ -1522,6 +1464,15 @@ export function createStorageCore(
1522
1464
  lastExpiresAt = undefined;
1523
1465
  };
1524
1466
 
1467
+ const resolveNonMemoryScope = (): NonMemoryScope => {
1468
+ if (nonMemoryScope === null) {
1469
+ throw new Error(
1470
+ "NitroStorage: this operation requires Disk or Secure scope.",
1471
+ );
1472
+ }
1473
+ return nonMemoryScope;
1474
+ };
1475
+
1525
1476
  const ensureSubscription = () => {
1526
1477
  if (unsubscribe) {
1527
1478
  return;
@@ -1529,7 +1480,9 @@ export function createStorageCore(
1529
1480
 
1530
1481
  const listener = () => {
1531
1482
  invalidateParsedCache();
1532
- listeners.forEach((callback) => callback());
1483
+ listeners.forEach((callback) => {
1484
+ callback();
1485
+ });
1533
1486
  };
1534
1487
 
1535
1488
  if (isMemory) {
@@ -1537,9 +1490,9 @@ export function createStorageCore(
1537
1490
  return;
1538
1491
  }
1539
1492
 
1540
- adapter.ensureScopeSubscription(nonMemoryScope!);
1493
+ adapter.ensureScopeSubscription(resolveNonMemoryScope());
1541
1494
  unsubscribe = addKeyListener(
1542
- getScopedListeners(nonMemoryScope!),
1495
+ getScopedListeners(resolveNonMemoryScope()),
1543
1496
  storageKey,
1544
1497
  listener,
1545
1498
  );
@@ -1557,7 +1510,9 @@ export function createStorageCore(
1557
1510
  emitKeyChange(
1558
1511
  config.scope,
1559
1512
  storageKey,
1560
- typeof expiredRaw === "string" ? expiredRaw : undefined,
1513
+ typeof expiredRaw === "string"
1514
+ ? unescapeCollidingRawValue(expiredRaw)
1515
+ : undefined,
1561
1516
  undefined,
1562
1517
  "expire",
1563
1518
  "memory",
@@ -1566,27 +1521,30 @@ export function createStorageCore(
1566
1521
  return undefined;
1567
1522
  }
1568
1523
  }
1569
- return memoryStore.get(storageKey);
1524
+ const memoryStored = memoryStore.get(storageKey);
1525
+ return typeof memoryStored === "string"
1526
+ ? unescapeCollidingRawValue(memoryStored)
1527
+ : memoryStored;
1570
1528
  }
1571
1529
 
1572
1530
  migrateRenamesIfNeeded();
1573
1531
 
1574
1532
  if (nonMemoryScope === StorageScope.Disk) {
1575
- const pending = pendingDiskWrites.get(storageKey);
1533
+ const pending = durability.readPendingDiskWrite(storageKey);
1576
1534
  if (pending !== undefined) {
1577
- return pending.value;
1535
+ return pending;
1578
1536
  }
1579
1537
  }
1580
1538
 
1581
1539
  if (nonMemoryScope === StorageScope.Secure && !isBiometric) {
1582
- const pending = pendingSecureWrites.get(storageKey);
1540
+ const pending = durability.readPendingSecureWrite(storageKey);
1583
1541
  if (pending !== undefined) {
1584
- return pending.value;
1542
+ return pending;
1585
1543
  }
1586
1544
  }
1587
1545
 
1588
1546
  if (readCache) {
1589
- const cache = getScopeRawCache(nonMemoryScope!);
1547
+ const cache = getScopeRawCache(resolveNonMemoryScope());
1590
1548
  const cached = cache.get(storageKey);
1591
1549
  if (cached !== undefined || cache.has(storageKey)) {
1592
1550
  return cached;
@@ -1602,7 +1560,7 @@ export function createStorageCore(
1602
1560
  const raw = readBackendRaw(() =>
1603
1561
  adapter.backend.get(storageKey, config.scope),
1604
1562
  );
1605
- cacheRawValue(nonMemoryScope!, storageKey, raw);
1563
+ cacheRawValue(resolveNonMemoryScope(), storageKey, raw);
1606
1564
  return raw;
1607
1565
  };
1608
1566
 
@@ -1614,7 +1572,9 @@ export function createStorageCore(
1614
1572
  } catch (error) {
1615
1573
  onReadError?.(error);
1616
1574
  if (fallbackToCacheOnReadError) {
1617
- const cached = getScopeRawCache(nonMemoryScope!).get(storageKey);
1575
+ const cached = getScopeRawCache(resolveNonMemoryScope()).get(
1576
+ storageKey,
1577
+ );
1618
1578
  if (cached !== undefined) {
1619
1579
  return cached;
1620
1580
  }
@@ -1643,10 +1603,10 @@ export function createStorageCore(
1643
1603
  return;
1644
1604
  }
1645
1605
 
1646
- cacheRawValue(nonMemoryScope!, storageKey, rawValue);
1606
+ cacheRawValue(resolveNonMemoryScope(), storageKey, rawValue);
1647
1607
 
1648
1608
  if (nonMemoryScope === StorageScope.Disk) {
1649
- if (coalesceDiskWrites || diskWritesAsync) {
1609
+ if (coalesceDiskWrites || isDiskWritesAsync()) {
1650
1610
  scheduleDiskWrite(storageKey, rawValue);
1651
1611
  emitKeyChange(
1652
1612
  config.scope,
@@ -1767,10 +1727,10 @@ export function createStorageCore(
1767
1727
  return;
1768
1728
  }
1769
1729
 
1770
- cacheRawValue(nonMemoryScope!, storageKey, undefined);
1730
+ cacheRawValue(resolveNonMemoryScope(), storageKey, undefined);
1771
1731
 
1772
1732
  if (nonMemoryScope === StorageScope.Disk) {
1773
- if (coalesceDiskWrites || diskWritesAsync) {
1733
+ if (coalesceDiskWrites || isDiskWritesAsync()) {
1774
1734
  scheduleDiskWrite(storageKey, undefined);
1775
1735
  emitKeyChange(
1776
1736
  config.scope,
@@ -1824,7 +1784,9 @@ export function createStorageCore(
1824
1784
  if (memoryExpiration) {
1825
1785
  memoryExpiration.set(storageKey, Date.now() + (expirationTtlMs ?? 0));
1826
1786
  }
1827
- memoryStore.set(storageKey, value);
1787
+ const storedValue =
1788
+ typeof value === "string" ? escapeCollidingRawValue(value) : value;
1789
+ memoryStore.set(storageKey, storedValue);
1828
1790
  notifyKeyListeners(memoryListeners, storageKey);
1829
1791
  emitKeyChange(
1830
1792
  config.scope,
@@ -1895,7 +1857,9 @@ export function createStorageCore(
1895
1857
  onExpired?.(storageKey);
1896
1858
  lastValue = ensureValidatedValue(defaultValue, false);
1897
1859
  hasLastValue = true;
1898
- listeners.forEach((cb) => cb());
1860
+ listeners.forEach((cb) => {
1861
+ cb();
1862
+ });
1899
1863
  return lastValue;
1900
1864
  }
1901
1865
  }
@@ -1937,7 +1901,9 @@ export function createStorageCore(
1937
1901
  onExpired?.(storageKey);
1938
1902
  lastValue = ensureValidatedValue(defaultValue, false);
1939
1903
  hasLastValue = true;
1940
- listeners.forEach((cb) => cb());
1904
+ listeners.forEach((cb) => {
1905
+ cb();
1906
+ });
1941
1907
  return lastValue;
1942
1908
  }
1943
1909
 
@@ -1958,7 +1924,12 @@ export function createStorageCore(
1958
1924
 
1959
1925
  const getCurrentVersion = (): StorageVersion => {
1960
1926
  const raw = readStoredRaw();
1961
- return toVersionToken(raw);
1927
+ if (raw === undefined) {
1928
+ return toVersionToken(undefined);
1929
+ }
1930
+ return toVersionToken(
1931
+ typeof raw === "string" ? unescapeCollidingRawValue(raw) : raw,
1932
+ );
1962
1933
  };
1963
1934
 
1964
1935
  const get = (): T =>
@@ -2052,15 +2023,13 @@ export function createStorageCore(
2052
2023
  if (isMemory) return memoryStore.has(storageKey);
2053
2024
  if (isBiometric) return adapter.backend.hasSecureBiometric(storageKey);
2054
2025
  if (nonMemoryScope === StorageScope.Disk) {
2055
- const pending = pendingDiskWrites.get(storageKey);
2056
- if (pending !== undefined) {
2057
- return pending.value !== undefined;
2026
+ if (durability.hasPendingDiskWrite(storageKey)) {
2027
+ return durability.readPendingDiskWrite(storageKey) !== undefined;
2058
2028
  }
2059
2029
  }
2060
2030
  if (nonMemoryScope === StorageScope.Secure) {
2061
- const pending = pendingSecureWrites.get(storageKey);
2062
- if (pending !== undefined) {
2063
- return pending.value !== undefined;
2031
+ if (durability.hasPendingSecureWrite(storageKey)) {
2032
+ return durability.readPendingSecureWrite(storageKey) !== undefined;
2064
2033
  }
2065
2034
  }
2066
2035
  return adapter.backend.has(storageKey, config.scope);
@@ -2074,7 +2043,7 @@ export function createStorageCore(
2074
2043
  if (listeners.size === 0 && unsubscribe) {
2075
2044
  unsubscribe();
2076
2045
  if (!isMemory) {
2077
- adapter.maybeCleanupScopeSubscription(nonMemoryScope!);
2046
+ adapter.maybeCleanupScopeSubscription(resolveNonMemoryScope());
2078
2047
  }
2079
2048
  unsubscribe = null;
2080
2049
  }
@@ -2121,11 +2090,20 @@ export function createStorageCore(
2121
2090
  deserialize,
2122
2091
  _triggerListeners: () => {
2123
2092
  invalidateParsedCache();
2124
- listeners.forEach((listener) => listener());
2093
+ listeners.forEach((listener) => {
2094
+ listener();
2095
+ });
2125
2096
  },
2126
2097
  _invalidateParsedCacheOnly: () => {
2127
2098
  invalidateParsedCache();
2128
2099
  },
2100
+ _deleteMemoryEntry: () => {
2101
+ if (memoryExpiration) {
2102
+ memoryExpiration.delete(storageKey);
2103
+ }
2104
+ memoryStore.delete(storageKey);
2105
+ invalidateParsedCache();
2106
+ },
2129
2107
  _hasValidation: validate !== undefined,
2130
2108
  _hasExpiration: expiration !== undefined,
2131
2109
  _readCacheEnabled: readCache,
@@ -2187,17 +2165,17 @@ export function createStorageCore(
2187
2165
 
2188
2166
  items.forEach((item, index) => {
2189
2167
  if (scope === StorageScope.Disk) {
2190
- const pending = pendingDiskWrites.get(item.key);
2168
+ const pending = durability.readPendingDiskWrite(item.key);
2191
2169
  if (pending !== undefined) {
2192
- rawValues[index] = pending.value;
2170
+ rawValues[index] = pending;
2193
2171
  return;
2194
2172
  }
2195
2173
  }
2196
2174
 
2197
2175
  if (scope === StorageScope.Secure) {
2198
- const pending = pendingSecureWrites.get(item.key);
2176
+ const pending = durability.readPendingSecureWrite(item.key);
2199
2177
  if (pending !== undefined) {
2200
- rawValues[index] = pending.value;
2178
+ rawValues[index] = pending;
2201
2179
  return;
2202
2180
  }
2203
2181
  }
@@ -2266,7 +2244,9 @@ export function createStorageCore(
2266
2244
 
2267
2245
  if (needsIndividualSets) {
2268
2246
  // Fall back to individual sets to preserve validation and TTL semantics
2269
- items.forEach(({ item, value }) => item.set(value));
2247
+ items.forEach(({ item, value }) => {
2248
+ item.set(value);
2249
+ });
2270
2250
  return;
2271
2251
  }
2272
2252
 
@@ -2288,9 +2268,9 @@ export function createStorageCore(
2288
2268
  item as StorageItem<unknown>,
2289
2269
  )._invalidateParsedCacheOnly();
2290
2270
  });
2291
- items.forEach(({ item }) =>
2292
- notifyKeyListeners(memoryListeners, item.key),
2293
- );
2271
+ items.forEach(({ item }) => {
2272
+ notifyKeyListeners(memoryListeners, item.key);
2273
+ });
2294
2274
  emitBatchChange(scope, "setBatch", "memory", changes);
2295
2275
  return;
2296
2276
  }
@@ -2305,14 +2285,22 @@ export function createStorageCore(
2305
2285
  canUseSecureRawBatchPath(internal),
2306
2286
  );
2307
2287
  if (!canUseSecureBatchPath) {
2308
- items.forEach(({ item, value }) => item.set(value));
2288
+ items.forEach(({ item, value }) => {
2289
+ item.set(value);
2290
+ });
2309
2291
  return;
2310
2292
  }
2311
2293
 
2312
2294
  flushSecureWrites();
2313
2295
  const keys = secureEntries.map(({ item }) => item.key);
2314
2296
  const oldValues = shouldReadPreviousEventValues(scope)
2315
- ? adapter.backend.getBatch(keys, scope)
2297
+ ? adapter.backend
2298
+ .getBatch(keys, scope)
2299
+ .map((value) =>
2300
+ value === undefined
2301
+ ? undefined
2302
+ : unescapeCollidingRawValue(value),
2303
+ )
2316
2304
  : [];
2317
2305
  const groupedByAccessControl = new Map<
2318
2306
  number,
@@ -2334,9 +2322,9 @@ export function createStorageCore(
2334
2322
  groupedByAccessControl.forEach((group, accessControl) => {
2335
2323
  adapter.backend.setSecureAccessControl(accessControl);
2336
2324
  adapter.backend.setBatch(group.keys, group.values, scope);
2337
- group.keys.forEach((key, index) =>
2338
- cacheRawValue(scope, key, group.values[index]),
2339
- );
2325
+ group.keys.forEach((key, index) => {
2326
+ cacheRawValue(scope, key, group.values[index]);
2327
+ });
2340
2328
  });
2341
2329
  emitBatchChange(
2342
2330
  scope,
@@ -2362,18 +2350,28 @@ export function createStorageCore(
2362
2350
  canUseRawBatchPath(asInternal(item)),
2363
2351
  );
2364
2352
  if (!useRawBatchPath) {
2365
- items.forEach(({ item, value }) => item.set(value));
2353
+ items.forEach(({ item, value }) => {
2354
+ item.set(value);
2355
+ });
2366
2356
  return;
2367
2357
  }
2368
2358
 
2369
2359
  const keys = items.map((entry) => entry.item.key);
2370
2360
  const values = items.map((entry) => entry.item.serialize(entry.value));
2371
2361
  const oldValues = shouldReadPreviousEventValues(scope)
2372
- ? adapter.backend.getBatch(keys, scope)
2362
+ ? adapter.backend
2363
+ .getBatch(keys, scope)
2364
+ .map((value) =>
2365
+ value === undefined
2366
+ ? undefined
2367
+ : unescapeCollidingRawValue(value),
2368
+ )
2373
2369
  : [];
2374
2370
 
2375
2371
  adapter.backend.setBatch(keys, values, scope);
2376
- keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
2372
+ keys.forEach((key, index) => {
2373
+ cacheRawValue(scope, key, values[index]);
2374
+ });
2377
2375
  emitBatchChange(
2378
2376
  scope,
2379
2377
  "setBatch",
@@ -2415,7 +2413,12 @@ export function createStorageCore(
2415
2413
  "memory",
2416
2414
  ),
2417
2415
  );
2418
- items.forEach((item) => item.delete());
2416
+ items.forEach((item) => {
2417
+ asInternal(item as StorageItem<unknown>)._deleteMemoryEntry();
2418
+ });
2419
+ items.forEach((item) => {
2420
+ notifyKeyListeners(memoryListeners, item.key);
2421
+ });
2419
2422
  emitBatchChange(scope, "removeBatch", "memory", changes);
2420
2423
  return;
2421
2424
  }
@@ -2428,10 +2431,18 @@ export function createStorageCore(
2428
2431
  flushSecureWrites();
2429
2432
  }
2430
2433
  const oldValues = shouldReadPreviousEventValues(scope)
2431
- ? adapter.backend.getBatch(keys, scope)
2434
+ ? adapter.backend
2435
+ .getBatch(keys, scope)
2436
+ .map((value) =>
2437
+ value === undefined
2438
+ ? undefined
2439
+ : unescapeCollidingRawValue(value),
2440
+ )
2432
2441
  : [];
2433
2442
  adapter.backend.removeBatch(keys, scope);
2434
- keys.forEach((key) => cacheRawValue(scope, key, undefined));
2443
+ keys.forEach((key) => {
2444
+ cacheRawValue(scope, key, undefined);
2445
+ });
2435
2446
  emitBatchChange(
2436
2447
  scope,
2437
2448
  "removeBatch",
@@ -2473,26 +2484,19 @@ export function createStorageCore(
2473
2484
  .sort((a, b) => a - b);
2474
2485
 
2475
2486
  let appliedVersion = currentVersion;
2476
- const context: MigrationContext = {
2477
- scope,
2478
- getRaw: (key) => getRawValue(key, scope),
2479
- setRaw: (key, value) => setRawValue(key, value, scope),
2480
- removeRaw: (key) => removeRawValue(key, scope),
2481
- };
2482
2487
 
2483
2488
  versions.forEach((version) => {
2484
2489
  const migration = registeredMigrations.get(version);
2485
2490
  if (!migration) {
2486
2491
  return;
2487
2492
  }
2488
- migration(context);
2493
+ runTransaction(scope, (tx) => {
2494
+ migration(tx);
2495
+ tx.setRaw(MIGRATION_VERSION_KEY, String(version));
2496
+ });
2489
2497
  appliedVersion = version;
2490
2498
  });
2491
2499
 
2492
- if (appliedVersion !== currentVersion) {
2493
- writeMigrationVersion(scope, appliedVersion);
2494
- }
2495
-
2496
2500
  return appliedVersion;
2497
2501
  });
2498
2502
  }
@@ -2542,7 +2546,7 @@ export function createStorageCore(
2542
2546
  }
2543
2547
  rollback.set(key, {
2544
2548
  kind: "raw",
2545
- value: getRawValue(key, scope),
2549
+ value: getStoredRawValue(key, scope),
2546
2550
  ...(scope === StorageScope.Secure &&
2547
2551
  internal?._secureAccessControl !== undefined
2548
2552
  ? { accessControl: internal._secureAccessControl }
@@ -2582,6 +2586,12 @@ export function createStorageCore(
2582
2586
  return transaction(tx);
2583
2587
  } catch (error) {
2584
2588
  const rollbackEntries = Array.from(rollback.entries()).reverse();
2589
+ const rollbackSource =
2590
+ scope === StorageScope.Memory ? "memory" : adapter.changeSource;
2591
+ const preRollbackValues =
2592
+ rollbackEntries.length > 0
2593
+ ? rollbackEntries.map(([key]) => getEventRawValue(scope, key))
2594
+ : [];
2585
2595
  if (scope === StorageScope.Memory) {
2586
2596
  rollbackEntries.forEach(([key, record]) => {
2587
2597
  if (record.value === NOT_SET) {
@@ -2640,15 +2650,35 @@ export function createStorageCore(
2640
2650
  adapter.backend.setSecureAccessControl(accessControl);
2641
2651
  }
2642
2652
  adapter.backend.setBatch(group.keys, group.values, scope);
2643
- group.keys.forEach((key, index) =>
2644
- cacheRawValue(scope, key, group.values[index]),
2645
- );
2653
+ group.keys.forEach((key, index) => {
2654
+ cacheRawValue(scope, key, group.values[index]);
2655
+ });
2646
2656
  });
2647
2657
  if (keysToRemove.length > 0) {
2648
2658
  adapter.backend.removeBatch(keysToRemove, scope);
2649
- keysToRemove.forEach((key) => cacheRawValue(scope, key, undefined));
2659
+ keysToRemove.forEach((key) => {
2660
+ cacheRawValue(scope, key, undefined);
2661
+ });
2650
2662
  }
2651
2663
  }
2664
+
2665
+ if (rollbackEntries.length > 0) {
2666
+ emitBatchChange(
2667
+ scope,
2668
+ "rollback",
2669
+ rollbackSource,
2670
+ rollbackEntries.map(([key], index) =>
2671
+ createKeyChange(
2672
+ scope,
2673
+ key,
2674
+ preRollbackValues[index],
2675
+ getEventRawValue(scope, key),
2676
+ "rollback",
2677
+ rollbackSource,
2678
+ ),
2679
+ ),
2680
+ );
2681
+ }
2652
2682
  throw error;
2653
2683
  }
2654
2684
  });
@@ -2771,7 +2801,9 @@ export function createStorageCore(
2771
2801
  toggle,
2772
2802
  values: () => Object.keys(item.get()) as TMember[],
2773
2803
  size: () => Object.keys(item.get()).length,
2774
- clear: () => item.set({}),
2804
+ clear: () => {
2805
+ item.set({});
2806
+ },
2775
2807
  reset: item.reset,
2776
2808
  subscribe: item.subscribe,
2777
2809
  scope: item.scope,