react-native-nitro-storage 0.6.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 (108) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +36 -12
  3. package/SECURITY.md +2 -2
  4. package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +225 -76
  5. package/app.plugin.js +2 -0
  6. package/cpp/bindings/HybridStorage.cpp +4 -12
  7. package/docs/api-reference.md +11 -1
  8. package/docs/batch-transactions-migrations.md +8 -2
  9. package/docs/benchmarks.md +19 -15
  10. package/docs/secure-storage.md +4 -0
  11. package/docs/web-backends.md +18 -1
  12. package/ios/IOSStorageAdapterCpp.mm +162 -17
  13. package/lib/commonjs/capabilities.js +26 -0
  14. package/lib/commonjs/capabilities.js.map +1 -0
  15. package/lib/commonjs/core/durability.js +137 -0
  16. package/lib/commonjs/core/durability.js.map +1 -0
  17. package/lib/commonjs/core/metrics.js +91 -0
  18. package/lib/commonjs/core/metrics.js.map +1 -0
  19. package/lib/commonjs/index.js +47 -22
  20. package/lib/commonjs/index.js.map +1 -1
  21. package/lib/commonjs/index.web.js +118 -66
  22. package/lib/commonjs/index.web.js.map +1 -1
  23. package/lib/commonjs/indexeddb-backend.js +73 -23
  24. package/lib/commonjs/indexeddb-backend.js.map +1 -1
  25. package/lib/commonjs/internal.js +27 -4
  26. package/lib/commonjs/internal.js.map +1 -1
  27. package/lib/commonjs/shared.js +2 -2
  28. package/lib/commonjs/shared.js.map +1 -1
  29. package/lib/commonjs/storage-core.js +217 -233
  30. package/lib/commonjs/storage-core.js.map +1 -1
  31. package/lib/commonjs/storage-platform.js +6 -0
  32. package/lib/commonjs/storage-platform.js.map +1 -0
  33. package/lib/commonjs/storage-runtime.js +3 -21
  34. package/lib/commonjs/storage-runtime.js.map +1 -1
  35. package/lib/commonjs/testing.js +19 -4
  36. package/lib/commonjs/testing.js.map +1 -1
  37. package/lib/commonjs/web-backend-contract.js +27 -0
  38. package/lib/commonjs/web-backend-contract.js.map +1 -0
  39. package/lib/module/capabilities.js +21 -0
  40. package/lib/module/capabilities.js.map +1 -0
  41. package/lib/module/core/durability.js +133 -0
  42. package/lib/module/core/durability.js.map +1 -0
  43. package/lib/module/core/metrics.js +87 -0
  44. package/lib/module/core/metrics.js.map +1 -0
  45. package/lib/module/index.js +48 -23
  46. package/lib/module/index.js.map +1 -1
  47. package/lib/module/index.web.js +103 -64
  48. package/lib/module/index.web.js.map +1 -1
  49. package/lib/module/indexeddb-backend.js +73 -23
  50. package/lib/module/indexeddb-backend.js.map +1 -1
  51. package/lib/module/internal.js +24 -3
  52. package/lib/module/internal.js.map +1 -1
  53. package/lib/module/shared.js +2 -2
  54. package/lib/module/shared.js.map +1 -1
  55. package/lib/module/storage-core.js +218 -234
  56. package/lib/module/storage-core.js.map +1 -1
  57. package/lib/module/storage-platform.js +4 -0
  58. package/lib/module/storage-platform.js.map +1 -0
  59. package/lib/module/storage-runtime.js +3 -21
  60. package/lib/module/storage-runtime.js.map +1 -1
  61. package/lib/module/testing.js +5 -3
  62. package/lib/module/testing.js.map +1 -1
  63. package/lib/module/web-backend-contract.js +22 -0
  64. package/lib/module/web-backend-contract.js.map +1 -0
  65. package/lib/typescript/capabilities.d.ts +11 -0
  66. package/lib/typescript/capabilities.d.ts.map +1 -0
  67. package/lib/typescript/core/durability.d.ts +27 -0
  68. package/lib/typescript/core/durability.d.ts.map +1 -0
  69. package/lib/typescript/core/metrics.d.ts +12 -0
  70. package/lib/typescript/core/metrics.d.ts.map +1 -0
  71. package/lib/typescript/index.d.ts +11 -3
  72. package/lib/typescript/index.d.ts.map +1 -1
  73. package/lib/typescript/index.web.d.ts +14 -5
  74. package/lib/typescript/index.web.d.ts.map +1 -1
  75. package/lib/typescript/indexeddb-backend.d.ts.map +1 -1
  76. package/lib/typescript/internal.d.ts +3 -0
  77. package/lib/typescript/internal.d.ts.map +1 -1
  78. package/lib/typescript/shared.d.ts +2 -1
  79. package/lib/typescript/shared.d.ts.map +1 -1
  80. package/lib/typescript/storage-core.d.ts +14 -4
  81. package/lib/typescript/storage-core.d.ts.map +1 -1
  82. package/lib/typescript/storage-events.d.ts +2 -2
  83. package/lib/typescript/storage-events.d.ts.map +1 -1
  84. package/lib/typescript/storage-platform.d.ts +12 -0
  85. package/lib/typescript/storage-platform.d.ts.map +1 -0
  86. package/lib/typescript/storage-runtime.d.ts.map +1 -1
  87. package/lib/typescript/testing.d.ts +17 -3
  88. package/lib/typescript/testing.d.ts.map +1 -1
  89. package/lib/typescript/web-backend-contract.d.ts +10 -0
  90. package/lib/typescript/web-backend-contract.d.ts.map +1 -0
  91. package/lib/typescript/web-storage-backend.d.ts +1 -1
  92. package/lib/typescript/web-storage-backend.d.ts.map +1 -1
  93. package/package.json +6 -6
  94. package/src/capabilities.ts +36 -0
  95. package/src/core/durability.ts +170 -0
  96. package/src/core/metrics.ts +133 -0
  97. package/src/index.ts +67 -42
  98. package/src/index.web.ts +159 -88
  99. package/src/indexeddb-backend.ts +92 -22
  100. package/src/internal.ts +51 -5
  101. package/src/shared.ts +7 -6
  102. package/src/storage-core.ts +336 -287
  103. package/src/storage-events.ts +3 -3
  104. package/src/storage-platform.ts +16 -0
  105. package/src/storage-runtime.ts +13 -47
  106. package/src/testing.ts +14 -7
  107. package/src/web-backend-contract.ts +34 -0
  108. 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;
@@ -158,6 +158,19 @@ export type StorageBatchSetItem<T> = {
158
158
  value: T;
159
159
  };
160
160
 
161
+ type StorageBatchSetCandidate = {
162
+ get: () => unknown;
163
+ };
164
+
165
+ type StorageBatchSetEntries<
166
+ TItems extends readonly StorageBatchSetCandidate[],
167
+ > = {
168
+ [Index in keyof TItems]: {
169
+ item: TItems[Index] & StorageItem<ReturnType<TItems[Index]["get"]>>;
170
+ value: ReturnType<TItems[Index]["get"]>;
171
+ };
172
+ };
173
+
161
174
  export type StorageKeyRef = string | { readonly key: string };
162
175
 
163
176
  export type StorageClearOptions = {
@@ -216,7 +229,6 @@ export type StorageCoreAdapter = {
216
229
  backend: StorageCoreBackend;
217
230
  changeSource: StorageChangeSource;
218
231
  applyAccessControlOnSecureRawWrite: boolean;
219
- flushDiskWritesOnImport: boolean;
220
232
  ensureScopeSubscription(scope: NonMemoryScope): void;
221
233
  maybeCleanupScopeSubscription(scope: NonMemoryScope): void;
222
234
  onWillEmitChanges(
@@ -286,84 +298,45 @@ export function createStorageCore(
286
298
  const registeredKeyCounts = new Map<string, number>();
287
299
  const memoryStore = new Map<string, unknown>();
288
300
  const memoryListeners: KeyListenerRegistry = new Map();
289
- const scopedListeners = new Map<NonMemoryScope, KeyListenerRegistry>([
290
- [StorageScope.Disk, new Map()],
291
- [StorageScope.Secure, new Map()],
292
- ]);
293
- 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<
294
306
  NonMemoryScope,
295
307
  Map<string, string | undefined>
296
- >([
297
- [StorageScope.Disk, new Map()],
298
- [StorageScope.Secure, new Map()],
299
- ]);
300
- const pendingDiskWrites = new Map<string, PendingDiskWrite>();
301
- let diskFlushScheduled = false;
302
- let diskWritesAsync = false;
303
- const pendingSecureWrites = new Map<string, PendingSecureWrite>();
304
- let secureFlushScheduled = false;
308
+ > = {
309
+ [StorageScope.Disk]: new Map(),
310
+ [StorageScope.Secure]: new Map(),
311
+ };
305
312
  let secureDefaultAccessControl: AccessControl = AccessControl.WhenUnlocked;
306
- let metricsObserver: StorageMetricsObserver | undefined;
307
313
  let eventObserver: StorageEventListener | undefined;
308
314
  let eventObserverRedactSecureValues = true;
309
- const metricsCounters = new Map<
310
- string,
311
- { count: number; totalDurationMs: number; maxDurationMs: number }
312
- >();
313
315
  const storageEvents = new StorageEventRegistry();
314
-
315
- function recordMetric(
316
- operation: string,
317
- scope: StorageScope,
318
- durationMs: number,
319
- keysCount = 1,
320
- ): void {
321
- const existing = metricsCounters.get(operation);
322
- if (!existing) {
323
- metricsCounters.set(operation, {
324
- count: 1,
325
- totalDurationMs: durationMs,
326
- maxDurationMs: durationMs,
327
- });
328
- } else {
329
- existing.count += 1;
330
- existing.totalDurationMs += durationMs;
331
- existing.maxDurationMs = Math.max(existing.maxDurationMs, durationMs);
332
- }
333
-
334
- metricsObserver?.({
335
- operation,
336
- scope,
337
- durationMs,
338
- keysCount,
339
- });
340
- }
341
-
342
- function measureOperation<T>(
343
- operation: string,
344
- scope: StorageScope,
345
- fn: () => T,
346
- keysCount = 1,
347
- ): T {
348
- if (!metricsObserver) {
349
- return fn();
350
- }
351
- const start = now();
352
- try {
353
- return fn();
354
- } finally {
355
- recordMetric(operation, scope, now() - start, keysCount);
356
- }
357
- }
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
+ });
358
331
 
359
332
  function getScopedListeners(scope: NonMemoryScope): KeyListenerRegistry {
360
- return scopedListeners.get(scope)!;
333
+ return scopedListeners[scope];
361
334
  }
362
335
 
363
336
  function getScopeRawCache(
364
337
  scope: NonMemoryScope,
365
338
  ): Map<string, string | undefined> {
366
- return scopedRawCache.get(scope)!;
339
+ return scopedRawCache[scope];
367
340
  }
368
341
 
369
342
  function cacheRawValue(
@@ -415,7 +388,9 @@ export function createStorageCore(
415
388
  ): string | undefined {
416
389
  if (scope === StorageScope.Memory) {
417
390
  const value = memoryStore.get(key);
418
- return typeof value === "string" ? value : undefined;
391
+ return typeof value === "string"
392
+ ? unescapeCollidingRawValue(value)
393
+ : undefined;
419
394
  }
420
395
 
421
396
  return getRawValue(key, scope);
@@ -500,110 +475,58 @@ export function createStorageCore(
500
475
  eventObserver?.(eventForGlobalObserver(event));
501
476
  }
502
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
+
503
496
  function readPendingSecureWrite(key: string): string | undefined {
504
- return pendingSecureWrites.get(key)?.value;
497
+ return durability.readPendingSecureWrite(key);
505
498
  }
506
499
 
507
500
  function readPendingDiskWrite(key: string): string | undefined {
508
- return pendingDiskWrites.get(key)?.value;
501
+ return durability.readPendingDiskWrite(key);
509
502
  }
510
503
 
511
504
  function hasPendingDiskWrite(key: string): boolean {
512
- return pendingDiskWrites.has(key);
505
+ return durability.hasPendingDiskWrite(key);
513
506
  }
514
507
 
515
508
  function hasPendingSecureWrite(key: string): boolean {
516
- return pendingSecureWrites.has(key);
509
+ return durability.hasPendingSecureWrite(key);
517
510
  }
518
511
 
519
512
  function clearPendingDiskWrite(key: string): void {
520
- pendingDiskWrites.delete(key);
513
+ durability.clearPendingDiskWrite(key);
521
514
  }
522
515
 
523
516
  function clearPendingSecureWrite(key: string): void {
524
- pendingSecureWrites.delete(key);
517
+ durability.clearPendingSecureWrite(key);
525
518
  }
526
519
 
527
520
  function flushDiskWrites(): void {
528
- diskFlushScheduled = false;
529
-
530
- if (pendingDiskWrites.size === 0) {
531
- return;
532
- }
533
-
534
- const writes = Array.from(pendingDiskWrites.values());
535
- pendingDiskWrites.clear();
536
-
537
- const keysToSet: string[] = [];
538
- const valuesToSet: string[] = [];
539
- const keysToRemove: string[] = [];
540
-
541
- writes.forEach(({ key, value }) => {
542
- if (value === undefined) {
543
- keysToRemove.push(key);
544
- return;
545
- }
546
-
547
- keysToSet.push(key);
548
- valuesToSet.push(value);
549
- });
550
-
551
- if (keysToSet.length > 0) {
552
- adapter.backend.setBatch(keysToSet, valuesToSet, StorageScope.Disk);
553
- }
554
- if (keysToRemove.length > 0) {
555
- adapter.backend.removeBatch(keysToRemove, StorageScope.Disk);
556
- }
521
+ durability.flushDiskWrites();
557
522
  }
558
523
 
559
524
  function flushSecureWrites(): void {
560
- secureFlushScheduled = false;
561
-
562
- if (pendingSecureWrites.size === 0) {
563
- return;
564
- }
565
-
566
- const writes = Array.from(pendingSecureWrites.values());
567
- pendingSecureWrites.clear();
568
-
569
- const groupedSetWrites = new Map<
570
- AccessControl,
571
- { keys: string[]; values: string[] }
572
- >();
573
- const keysToRemove: string[] = [];
574
-
575
- writes.forEach(({ key, value, accessControl }) => {
576
- if (value === undefined) {
577
- keysToRemove.push(key);
578
- } else {
579
- const resolvedAccessControl =
580
- accessControl ?? secureDefaultAccessControl;
581
- const existingGroup = groupedSetWrites.get(resolvedAccessControl);
582
- const group = existingGroup ?? { keys: [], values: [] };
583
- group.keys.push(key);
584
- group.values.push(value);
585
- if (!existingGroup) {
586
- groupedSetWrites.set(resolvedAccessControl, group);
587
- }
588
- }
589
- });
590
-
591
- groupedSetWrites.forEach((group, accessControl) => {
592
- adapter.backend.setSecureAccessControl(accessControl);
593
- adapter.backend.setBatch(group.keys, group.values, StorageScope.Secure);
594
- });
595
- if (keysToRemove.length > 0) {
596
- adapter.backend.removeBatch(keysToRemove, StorageScope.Secure);
597
- }
525
+ durability.flushSecureWrites();
598
526
  }
599
527
 
600
528
  function scheduleDiskWrite(key: string, value: string | undefined): void {
601
- pendingDiskWrites.set(key, { key, value });
602
- if (diskFlushScheduled) {
603
- return;
604
- }
605
- diskFlushScheduled = true;
606
- runMicrotask(flushDiskWrites);
529
+ durability.scheduleDiskWrite(key, value);
607
530
  }
608
531
 
609
532
  function scheduleSecureWrite(
@@ -611,20 +534,21 @@ export function createStorageCore(
611
534
  value: string | undefined,
612
535
  accessControl?: AccessControl,
613
536
  ): void {
614
- const pendingWrite: PendingSecureWrite = { key, value };
615
- if (accessControl !== undefined) {
616
- pendingWrite.accessControl = accessControl;
617
- }
618
- pendingSecureWrites.set(key, pendingWrite);
619
- if (secureFlushScheduled) {
620
- return;
621
- }
622
- secureFlushScheduled = true;
623
- runMicrotask(flushSecureWrites);
537
+ durability.scheduleSecureWrite(key, value, accessControl);
624
538
  }
625
539
 
626
- function getRawValue(key: string, scope: StorageScope): string | undefined {
627
- 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 {
628
552
  if (scope === StorageScope.Memory) {
629
553
  const value = memoryStore.get(key);
630
554
  return typeof value === "string" ? value : undefined;
@@ -641,21 +565,49 @@ export function createStorageCore(
641
565
  return adapter.backend.get(key, scope);
642
566
  }
643
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
+
644
595
  function setRawValue(key: string, value: string, scope: StorageScope): void {
645
596
  assertValidScope(scope);
597
+ const storedValue = escapeCollidingRawValue(value);
646
598
  const oldValue =
647
599
  scope === StorageScope.Memory ? getEventRawValue(scope, key) : undefined;
648
600
  if (scope === StorageScope.Memory) {
649
- memoryStore.set(key, value);
601
+ memoryStore.set(key, storedValue);
650
602
  notifyKeyListeners(memoryListeners, key);
651
603
  emitKeyChange(scope, key, oldValue, value, "set", "memory");
652
604
  return;
653
605
  }
654
606
 
655
607
  if (scope === StorageScope.Disk) {
656
- cacheRawValue(scope, key, value);
657
- if (diskWritesAsync) {
658
- scheduleDiskWrite(key, value);
608
+ cacheRawValue(scope, key, storedValue);
609
+ if (isDiskWritesAsync()) {
610
+ scheduleDiskWrite(key, storedValue);
659
611
  emitKeyChange(scope, key, oldValue, value, "set", adapter.changeSource);
660
612
  return;
661
613
  }
@@ -672,8 +624,8 @@ export function createStorageCore(
672
624
  }
673
625
  }
674
626
 
675
- adapter.backend.set(key, value, scope);
676
- cacheRawValue(scope, key, value);
627
+ adapter.backend.set(key, storedValue, scope);
628
+ cacheRawValue(scope, key, storedValue);
677
629
  emitKeyChange(scope, key, oldValue, value, "set", adapter.changeSource);
678
630
  }
679
631
 
@@ -689,7 +641,7 @@ export function createStorageCore(
689
641
 
690
642
  if (scope === StorageScope.Disk) {
691
643
  cacheRawValue(scope, key, undefined);
692
- if (diskWritesAsync) {
644
+ if (isDiskWritesAsync()) {
693
645
  scheduleDiskWrite(key, undefined);
694
646
  emitKeyChange(
695
647
  scope,
@@ -733,10 +685,6 @@ export function createStorageCore(
733
685
  return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
734
686
  }
735
687
 
736
- function writeMigrationVersion(scope: StorageScope, version: number): void {
737
- setRawValue(MIGRATION_VERSION_KEY, String(version), scope);
738
- }
739
-
740
688
  const internals: StorageCoreInternals = {
741
689
  getScopedListeners,
742
690
  cacheRawValue,
@@ -745,10 +693,10 @@ export function createStorageCore(
745
693
  clearPendingDiskWrite,
746
694
  clearPendingSecureWrite,
747
695
  clearAllPendingDiskWrites: () => {
748
- pendingDiskWrites.clear();
696
+ durability.clearAllPendingDiskWrites();
749
697
  },
750
698
  clearAllPendingSecureWrites: () => {
751
- pendingSecureWrites.clear();
699
+ durability.clearAllPendingSecureWrites();
752
700
  },
753
701
  flushDiskWrites,
754
702
  flushSecureWrites,
@@ -788,7 +736,9 @@ export function createStorageCore(
788
736
  ? removeKeys.map((key) => getEventRawValue(scope, key))
789
737
  : [];
790
738
  removeKeys.forEach((key) => memoryStore.delete(key));
791
- removeKeys.forEach((key) => notifyKeyListeners(memoryListeners, key));
739
+ removeKeys.forEach((key) => {
740
+ notifyKeyListeners(memoryListeners, key);
741
+ });
792
742
  emitBatchChange(
793
743
  scope,
794
744
  "clear",
@@ -821,10 +771,18 @@ export function createStorageCore(
821
771
  return;
822
772
  }
823
773
  const previousValues = shouldReadPreviousEventValues(scope)
824
- ? adapter.backend.getBatch(removeKeys, scope)
774
+ ? adapter.backend
775
+ .getBatch(removeKeys, scope)
776
+ .map((value) =>
777
+ value === undefined
778
+ ? undefined
779
+ : unescapeCollidingRawValue(value),
780
+ )
825
781
  : [];
826
782
  adapter.backend.removeBatch(removeKeys, scope);
827
- removeKeys.forEach((key) => cacheRawValue(scope, key, undefined));
783
+ removeKeys.forEach((key) => {
784
+ cacheRawValue(scope, key, undefined);
785
+ });
828
786
  emitBatchChange(
829
787
  scope,
830
788
  "clear",
@@ -948,12 +906,12 @@ export function createStorageCore(
948
906
 
949
907
  if (scope === StorageScope.Disk) {
950
908
  flushDiskWrites();
951
- pendingDiskWrites.clear();
909
+ durability.clearAllPendingDiskWrites();
952
910
  }
953
911
 
954
912
  if (scope === StorageScope.Secure) {
955
913
  flushSecureWrites();
956
- pendingSecureWrites.clear();
914
+ durability.clearAllPendingSecureWrites();
957
915
  }
958
916
 
959
917
  clearScopeRawCache(scope);
@@ -1050,7 +1008,7 @@ export function createStorageCore(
1050
1008
  }
1051
1009
  const separatorIndex = registryKey.indexOf(":");
1052
1010
  duplicates.push({
1053
- scope: Number(registryKey.slice(0, separatorIndex)) as StorageScope,
1011
+ scope: Number(registryKey.slice(0, separatorIndex)),
1054
1012
  key: registryKey.slice(separatorIndex + 1),
1055
1013
  count,
1056
1014
  });
@@ -1062,7 +1020,7 @@ export function createStorageCore(
1062
1020
  registeredKeyCounts.forEach((_count, registryKey) => {
1063
1021
  const separatorIndex = registryKey.indexOf(":");
1064
1022
  result.push({
1065
- scope: Number(registryKey.slice(0, separatorIndex)) as StorageScope,
1023
+ scope: Number(registryKey.slice(0, separatorIndex)),
1066
1024
  key: registryKey.slice(separatorIndex + 1),
1067
1025
  });
1068
1026
  });
@@ -1087,9 +1045,9 @@ export function createStorageCore(
1087
1045
  affectedKeys.forEach((key) => {
1088
1046
  memoryStore.delete(key);
1089
1047
  });
1090
- affectedKeys.forEach((key) =>
1091
- notifyKeyListeners(memoryListeners, key),
1092
- );
1048
+ affectedKeys.forEach((key) => {
1049
+ notifyKeyListeners(memoryListeners, key);
1050
+ });
1093
1051
  emitBatchChange(
1094
1052
  scope,
1095
1053
  "clearNamespace",
@@ -1226,7 +1184,7 @@ export function createStorageCore(
1226
1184
  keys.forEach((key, idx) => {
1227
1185
  const value = values[idx];
1228
1186
  if (value !== undefined) {
1229
- result[key] = value;
1187
+ result[key] = unescapeCollidingRawValue(value);
1230
1188
  }
1231
1189
  });
1232
1190
  return result;
@@ -1239,7 +1197,8 @@ export function createStorageCore(
1239
1197
  if (scope === StorageScope.Memory) {
1240
1198
  for (const key of memoryStore.keys()) {
1241
1199
  const value = memoryStore.get(key);
1242
- if (typeof value === "string") result[key] = value;
1200
+ if (typeof value === "string")
1201
+ result[key] = unescapeCollidingRawValue(value);
1243
1202
  }
1244
1203
  return result;
1245
1204
  }
@@ -1254,7 +1213,7 @@ export function createStorageCore(
1254
1213
  const values = adapter.backend.getBatch(keys, scope);
1255
1214
  keys.forEach((key, idx) => {
1256
1215
  const val = values[idx];
1257
- if (val !== undefined) result[key] = val;
1216
+ if (val !== undefined) result[key] = unescapeCollidingRawValue(val);
1258
1217
  });
1259
1218
  return result;
1260
1219
  });
@@ -1299,7 +1258,7 @@ export function createStorageCore(
1299
1258
  },
1300
1259
  setDiskWritesAsync: (enabled: boolean) => {
1301
1260
  measureOperation("storage:setDiskWritesAsync", StorageScope.Disk, () => {
1302
- diskWritesAsync = enabled;
1261
+ setDiskWritesAsyncMode(enabled);
1303
1262
  if (!enabled) {
1304
1263
  flushDiskWrites();
1305
1264
  }
@@ -1316,23 +1275,16 @@ export function createStorageCore(
1316
1275
  });
1317
1276
  },
1318
1277
  setMetricsObserver: (observer?: StorageMetricsObserver) => {
1319
- metricsObserver = observer;
1278
+ metrics.setObserver(observer);
1320
1279
  },
1321
1280
  getMetricsSnapshot: (): Record<string, StorageMetricSummary> => {
1322
- const snapshot: Record<string, StorageMetricSummary> = {};
1323
- metricsCounters.forEach((value, key) => {
1324
- snapshot[key] = {
1325
- count: value.count,
1326
- totalDurationMs: value.totalDurationMs,
1327
- avgDurationMs:
1328
- value.count === 0 ? 0 : value.totalDurationMs / value.count,
1329
- maxDurationMs: value.maxDurationMs,
1330
- };
1331
- });
1332
- return snapshot;
1281
+ return metrics.getSnapshot();
1282
+ },
1283
+ getScopedMetricsSnapshot: (): Record<string, StorageMetricSummary> => {
1284
+ return metrics.getScopedSnapshot();
1333
1285
  },
1334
1286
  resetMetrics: () => {
1335
- metricsCounters.clear();
1287
+ metrics.reset();
1336
1288
  },
1337
1289
  getSecureMetadata: (key: string): SecureStorageMetadata => {
1338
1290
  return measureOperation(
@@ -1397,7 +1349,8 @@ export function createStorageCore(
1397
1349
  () => {
1398
1350
  assertValidScope(scope);
1399
1351
  if (keys.length === 0) return;
1400
- const values = keys.map((k) => data[k]!);
1352
+ const values = keys.map((k) => data[k] as string);
1353
+ const storedValues = values.map(escapeCollidingRawValue);
1401
1354
  const changes = keys.map((key, index) =>
1402
1355
  createKeyChange(
1403
1356
  scope,
@@ -1411,9 +1364,11 @@ export function createStorageCore(
1411
1364
 
1412
1365
  if (scope === StorageScope.Memory) {
1413
1366
  keys.forEach((key, index) => {
1414
- memoryStore.set(key, values[index]);
1367
+ memoryStore.set(key, storedValues[index]);
1368
+ });
1369
+ keys.forEach((key) => {
1370
+ notifyKeyListeners(memoryListeners, key);
1415
1371
  });
1416
- keys.forEach((key) => notifyKeyListeners(memoryListeners, key));
1417
1372
  emitBatchChange(scope, "import", "memory", changes);
1418
1373
  return;
1419
1374
  }
@@ -1422,14 +1377,14 @@ export function createStorageCore(
1422
1377
  flushSecureWrites();
1423
1378
  adapter.backend.setSecureAccessControl(secureDefaultAccessControl);
1424
1379
  }
1425
- if (scope === StorageScope.Disk && adapter.flushDiskWritesOnImport) {
1380
+ if (scope === StorageScope.Disk) {
1426
1381
  flushDiskWrites();
1427
1382
  }
1428
1383
 
1429
- adapter.backend.setBatch(keys, values, scope);
1430
- keys.forEach((key, index) =>
1431
- cacheRawValue(scope, key, values[index]),
1432
- );
1384
+ adapter.backend.setBatch(keys, storedValues, scope);
1385
+ keys.forEach((key, index) => {
1386
+ cacheRawValue(scope, key, storedValues[index]);
1387
+ });
1433
1388
  emitBatchChange(scope, "import", adapter.changeSource, changes);
1434
1389
  },
1435
1390
  keys.length,
@@ -1509,6 +1464,15 @@ export function createStorageCore(
1509
1464
  lastExpiresAt = undefined;
1510
1465
  };
1511
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
+
1512
1476
  const ensureSubscription = () => {
1513
1477
  if (unsubscribe) {
1514
1478
  return;
@@ -1516,7 +1480,9 @@ export function createStorageCore(
1516
1480
 
1517
1481
  const listener = () => {
1518
1482
  invalidateParsedCache();
1519
- listeners.forEach((callback) => callback());
1483
+ listeners.forEach((callback) => {
1484
+ callback();
1485
+ });
1520
1486
  };
1521
1487
 
1522
1488
  if (isMemory) {
@@ -1524,9 +1490,9 @@ export function createStorageCore(
1524
1490
  return;
1525
1491
  }
1526
1492
 
1527
- adapter.ensureScopeSubscription(nonMemoryScope!);
1493
+ adapter.ensureScopeSubscription(resolveNonMemoryScope());
1528
1494
  unsubscribe = addKeyListener(
1529
- getScopedListeners(nonMemoryScope!),
1495
+ getScopedListeners(resolveNonMemoryScope()),
1530
1496
  storageKey,
1531
1497
  listener,
1532
1498
  );
@@ -1544,7 +1510,9 @@ export function createStorageCore(
1544
1510
  emitKeyChange(
1545
1511
  config.scope,
1546
1512
  storageKey,
1547
- typeof expiredRaw === "string" ? expiredRaw : undefined,
1513
+ typeof expiredRaw === "string"
1514
+ ? unescapeCollidingRawValue(expiredRaw)
1515
+ : undefined,
1548
1516
  undefined,
1549
1517
  "expire",
1550
1518
  "memory",
@@ -1553,27 +1521,30 @@ export function createStorageCore(
1553
1521
  return undefined;
1554
1522
  }
1555
1523
  }
1556
- return memoryStore.get(storageKey);
1524
+ const memoryStored = memoryStore.get(storageKey);
1525
+ return typeof memoryStored === "string"
1526
+ ? unescapeCollidingRawValue(memoryStored)
1527
+ : memoryStored;
1557
1528
  }
1558
1529
 
1559
1530
  migrateRenamesIfNeeded();
1560
1531
 
1561
1532
  if (nonMemoryScope === StorageScope.Disk) {
1562
- const pending = pendingDiskWrites.get(storageKey);
1533
+ const pending = durability.readPendingDiskWrite(storageKey);
1563
1534
  if (pending !== undefined) {
1564
- return pending.value;
1535
+ return pending;
1565
1536
  }
1566
1537
  }
1567
1538
 
1568
1539
  if (nonMemoryScope === StorageScope.Secure && !isBiometric) {
1569
- const pending = pendingSecureWrites.get(storageKey);
1540
+ const pending = durability.readPendingSecureWrite(storageKey);
1570
1541
  if (pending !== undefined) {
1571
- return pending.value;
1542
+ return pending;
1572
1543
  }
1573
1544
  }
1574
1545
 
1575
1546
  if (readCache) {
1576
- const cache = getScopeRawCache(nonMemoryScope!);
1547
+ const cache = getScopeRawCache(resolveNonMemoryScope());
1577
1548
  const cached = cache.get(storageKey);
1578
1549
  if (cached !== undefined || cache.has(storageKey)) {
1579
1550
  return cached;
@@ -1589,7 +1560,7 @@ export function createStorageCore(
1589
1560
  const raw = readBackendRaw(() =>
1590
1561
  adapter.backend.get(storageKey, config.scope),
1591
1562
  );
1592
- cacheRawValue(nonMemoryScope!, storageKey, raw);
1563
+ cacheRawValue(resolveNonMemoryScope(), storageKey, raw);
1593
1564
  return raw;
1594
1565
  };
1595
1566
 
@@ -1601,7 +1572,9 @@ export function createStorageCore(
1601
1572
  } catch (error) {
1602
1573
  onReadError?.(error);
1603
1574
  if (fallbackToCacheOnReadError) {
1604
- const cached = getScopeRawCache(nonMemoryScope!).get(storageKey);
1575
+ const cached = getScopeRawCache(resolveNonMemoryScope()).get(
1576
+ storageKey,
1577
+ );
1605
1578
  if (cached !== undefined) {
1606
1579
  return cached;
1607
1580
  }
@@ -1630,10 +1603,10 @@ export function createStorageCore(
1630
1603
  return;
1631
1604
  }
1632
1605
 
1633
- cacheRawValue(nonMemoryScope!, storageKey, rawValue);
1606
+ cacheRawValue(resolveNonMemoryScope(), storageKey, rawValue);
1634
1607
 
1635
1608
  if (nonMemoryScope === StorageScope.Disk) {
1636
- if (coalesceDiskWrites || diskWritesAsync) {
1609
+ if (coalesceDiskWrites || isDiskWritesAsync()) {
1637
1610
  scheduleDiskWrite(storageKey, rawValue);
1638
1611
  emitKeyChange(
1639
1612
  config.scope,
@@ -1754,10 +1727,10 @@ export function createStorageCore(
1754
1727
  return;
1755
1728
  }
1756
1729
 
1757
- cacheRawValue(nonMemoryScope!, storageKey, undefined);
1730
+ cacheRawValue(resolveNonMemoryScope(), storageKey, undefined);
1758
1731
 
1759
1732
  if (nonMemoryScope === StorageScope.Disk) {
1760
- if (coalesceDiskWrites || diskWritesAsync) {
1733
+ if (coalesceDiskWrites || isDiskWritesAsync()) {
1761
1734
  scheduleDiskWrite(storageKey, undefined);
1762
1735
  emitKeyChange(
1763
1736
  config.scope,
@@ -1811,7 +1784,9 @@ export function createStorageCore(
1811
1784
  if (memoryExpiration) {
1812
1785
  memoryExpiration.set(storageKey, Date.now() + (expirationTtlMs ?? 0));
1813
1786
  }
1814
- memoryStore.set(storageKey, value);
1787
+ const storedValue =
1788
+ typeof value === "string" ? escapeCollidingRawValue(value) : value;
1789
+ memoryStore.set(storageKey, storedValue);
1815
1790
  notifyKeyListeners(memoryListeners, storageKey);
1816
1791
  emitKeyChange(
1817
1792
  config.scope,
@@ -1882,7 +1857,9 @@ export function createStorageCore(
1882
1857
  onExpired?.(storageKey);
1883
1858
  lastValue = ensureValidatedValue(defaultValue, false);
1884
1859
  hasLastValue = true;
1885
- listeners.forEach((cb) => cb());
1860
+ listeners.forEach((cb) => {
1861
+ cb();
1862
+ });
1886
1863
  return lastValue;
1887
1864
  }
1888
1865
  }
@@ -1924,7 +1901,9 @@ export function createStorageCore(
1924
1901
  onExpired?.(storageKey);
1925
1902
  lastValue = ensureValidatedValue(defaultValue, false);
1926
1903
  hasLastValue = true;
1927
- listeners.forEach((cb) => cb());
1904
+ listeners.forEach((cb) => {
1905
+ cb();
1906
+ });
1928
1907
  return lastValue;
1929
1908
  }
1930
1909
 
@@ -1945,7 +1924,12 @@ export function createStorageCore(
1945
1924
 
1946
1925
  const getCurrentVersion = (): StorageVersion => {
1947
1926
  const raw = readStoredRaw();
1948
- return toVersionToken(raw);
1927
+ if (raw === undefined) {
1928
+ return toVersionToken(undefined);
1929
+ }
1930
+ return toVersionToken(
1931
+ typeof raw === "string" ? unescapeCollidingRawValue(raw) : raw,
1932
+ );
1949
1933
  };
1950
1934
 
1951
1935
  const get = (): T =>
@@ -2039,15 +2023,13 @@ export function createStorageCore(
2039
2023
  if (isMemory) return memoryStore.has(storageKey);
2040
2024
  if (isBiometric) return adapter.backend.hasSecureBiometric(storageKey);
2041
2025
  if (nonMemoryScope === StorageScope.Disk) {
2042
- const pending = pendingDiskWrites.get(storageKey);
2043
- if (pending !== undefined) {
2044
- return pending.value !== undefined;
2026
+ if (durability.hasPendingDiskWrite(storageKey)) {
2027
+ return durability.readPendingDiskWrite(storageKey) !== undefined;
2045
2028
  }
2046
2029
  }
2047
2030
  if (nonMemoryScope === StorageScope.Secure) {
2048
- const pending = pendingSecureWrites.get(storageKey);
2049
- if (pending !== undefined) {
2050
- return pending.value !== undefined;
2031
+ if (durability.hasPendingSecureWrite(storageKey)) {
2032
+ return durability.readPendingSecureWrite(storageKey) !== undefined;
2051
2033
  }
2052
2034
  }
2053
2035
  return adapter.backend.has(storageKey, config.scope);
@@ -2061,7 +2043,7 @@ export function createStorageCore(
2061
2043
  if (listeners.size === 0 && unsubscribe) {
2062
2044
  unsubscribe();
2063
2045
  if (!isMemory) {
2064
- adapter.maybeCleanupScopeSubscription(nonMemoryScope!);
2046
+ adapter.maybeCleanupScopeSubscription(resolveNonMemoryScope());
2065
2047
  }
2066
2048
  unsubscribe = null;
2067
2049
  }
@@ -2108,11 +2090,20 @@ export function createStorageCore(
2108
2090
  deserialize,
2109
2091
  _triggerListeners: () => {
2110
2092
  invalidateParsedCache();
2111
- listeners.forEach((listener) => listener());
2093
+ listeners.forEach((listener) => {
2094
+ listener();
2095
+ });
2112
2096
  },
2113
2097
  _invalidateParsedCacheOnly: () => {
2114
2098
  invalidateParsedCache();
2115
2099
  },
2100
+ _deleteMemoryEntry: () => {
2101
+ if (memoryExpiration) {
2102
+ memoryExpiration.delete(storageKey);
2103
+ }
2104
+ memoryStore.delete(storageKey);
2105
+ invalidateParsedCache();
2106
+ },
2116
2107
  _hasValidation: validate !== undefined,
2117
2108
  _hasExpiration: expiration !== undefined,
2118
2109
  _readCacheEnabled: readCache,
@@ -2174,17 +2165,17 @@ export function createStorageCore(
2174
2165
 
2175
2166
  items.forEach((item, index) => {
2176
2167
  if (scope === StorageScope.Disk) {
2177
- const pending = pendingDiskWrites.get(item.key);
2168
+ const pending = durability.readPendingDiskWrite(item.key);
2178
2169
  if (pending !== undefined) {
2179
- rawValues[index] = pending.value;
2170
+ rawValues[index] = pending;
2180
2171
  return;
2181
2172
  }
2182
2173
  }
2183
2174
 
2184
2175
  if (scope === StorageScope.Secure) {
2185
- const pending = pendingSecureWrites.get(item.key);
2176
+ const pending = durability.readPendingSecureWrite(item.key);
2186
2177
  if (pending !== undefined) {
2187
- rawValues[index] = pending.value;
2178
+ rawValues[index] = pending;
2188
2179
  return;
2189
2180
  }
2190
2181
  }
@@ -2227,6 +2218,10 @@ export function createStorageCore(
2227
2218
  ) as BatchValues<TItems>;
2228
2219
  }
2229
2220
 
2221
+ function setBatch<const TItems extends readonly StorageBatchSetCandidate[]>(
2222
+ items: StorageBatchSetEntries<TItems>,
2223
+ scope: StorageScope,
2224
+ ): void;
2230
2225
  function setBatch<T>(
2231
2226
  items: readonly StorageBatchSetItem<T>[],
2232
2227
  scope: StorageScope,
@@ -2249,7 +2244,9 @@ export function createStorageCore(
2249
2244
 
2250
2245
  if (needsIndividualSets) {
2251
2246
  // Fall back to individual sets to preserve validation and TTL semantics
2252
- items.forEach(({ item, value }) => item.set(value));
2247
+ items.forEach(({ item, value }) => {
2248
+ item.set(value);
2249
+ });
2253
2250
  return;
2254
2251
  }
2255
2252
 
@@ -2271,9 +2268,9 @@ export function createStorageCore(
2271
2268
  item as StorageItem<unknown>,
2272
2269
  )._invalidateParsedCacheOnly();
2273
2270
  });
2274
- items.forEach(({ item }) =>
2275
- notifyKeyListeners(memoryListeners, item.key),
2276
- );
2271
+ items.forEach(({ item }) => {
2272
+ notifyKeyListeners(memoryListeners, item.key);
2273
+ });
2277
2274
  emitBatchChange(scope, "setBatch", "memory", changes);
2278
2275
  return;
2279
2276
  }
@@ -2288,14 +2285,22 @@ export function createStorageCore(
2288
2285
  canUseSecureRawBatchPath(internal),
2289
2286
  );
2290
2287
  if (!canUseSecureBatchPath) {
2291
- items.forEach(({ item, value }) => item.set(value));
2288
+ items.forEach(({ item, value }) => {
2289
+ item.set(value);
2290
+ });
2292
2291
  return;
2293
2292
  }
2294
2293
 
2295
2294
  flushSecureWrites();
2296
2295
  const keys = secureEntries.map(({ item }) => item.key);
2297
2296
  const oldValues = shouldReadPreviousEventValues(scope)
2298
- ? adapter.backend.getBatch(keys, scope)
2297
+ ? adapter.backend
2298
+ .getBatch(keys, scope)
2299
+ .map((value) =>
2300
+ value === undefined
2301
+ ? undefined
2302
+ : unescapeCollidingRawValue(value),
2303
+ )
2299
2304
  : [];
2300
2305
  const groupedByAccessControl = new Map<
2301
2306
  number,
@@ -2317,9 +2322,9 @@ export function createStorageCore(
2317
2322
  groupedByAccessControl.forEach((group, accessControl) => {
2318
2323
  adapter.backend.setSecureAccessControl(accessControl);
2319
2324
  adapter.backend.setBatch(group.keys, group.values, scope);
2320
- group.keys.forEach((key, index) =>
2321
- cacheRawValue(scope, key, group.values[index]),
2322
- );
2325
+ group.keys.forEach((key, index) => {
2326
+ cacheRawValue(scope, key, group.values[index]);
2327
+ });
2323
2328
  });
2324
2329
  emitBatchChange(
2325
2330
  scope,
@@ -2345,18 +2350,28 @@ export function createStorageCore(
2345
2350
  canUseRawBatchPath(asInternal(item)),
2346
2351
  );
2347
2352
  if (!useRawBatchPath) {
2348
- items.forEach(({ item, value }) => item.set(value));
2353
+ items.forEach(({ item, value }) => {
2354
+ item.set(value);
2355
+ });
2349
2356
  return;
2350
2357
  }
2351
2358
 
2352
2359
  const keys = items.map((entry) => entry.item.key);
2353
2360
  const values = items.map((entry) => entry.item.serialize(entry.value));
2354
2361
  const oldValues = shouldReadPreviousEventValues(scope)
2355
- ? adapter.backend.getBatch(keys, scope)
2362
+ ? adapter.backend
2363
+ .getBatch(keys, scope)
2364
+ .map((value) =>
2365
+ value === undefined
2366
+ ? undefined
2367
+ : unescapeCollidingRawValue(value),
2368
+ )
2356
2369
  : [];
2357
2370
 
2358
2371
  adapter.backend.setBatch(keys, values, scope);
2359
- keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
2372
+ keys.forEach((key, index) => {
2373
+ cacheRawValue(scope, key, values[index]);
2374
+ });
2360
2375
  emitBatchChange(
2361
2376
  scope,
2362
2377
  "setBatch",
@@ -2398,7 +2413,12 @@ export function createStorageCore(
2398
2413
  "memory",
2399
2414
  ),
2400
2415
  );
2401
- 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
+ });
2402
2422
  emitBatchChange(scope, "removeBatch", "memory", changes);
2403
2423
  return;
2404
2424
  }
@@ -2411,10 +2431,18 @@ export function createStorageCore(
2411
2431
  flushSecureWrites();
2412
2432
  }
2413
2433
  const oldValues = shouldReadPreviousEventValues(scope)
2414
- ? adapter.backend.getBatch(keys, scope)
2434
+ ? adapter.backend
2435
+ .getBatch(keys, scope)
2436
+ .map((value) =>
2437
+ value === undefined
2438
+ ? undefined
2439
+ : unescapeCollidingRawValue(value),
2440
+ )
2415
2441
  : [];
2416
2442
  adapter.backend.removeBatch(keys, scope);
2417
- keys.forEach((key) => cacheRawValue(scope, key, undefined));
2443
+ keys.forEach((key) => {
2444
+ cacheRawValue(scope, key, undefined);
2445
+ });
2418
2446
  emitBatchChange(
2419
2447
  scope,
2420
2448
  "removeBatch",
@@ -2456,26 +2484,19 @@ export function createStorageCore(
2456
2484
  .sort((a, b) => a - b);
2457
2485
 
2458
2486
  let appliedVersion = currentVersion;
2459
- const context: MigrationContext = {
2460
- scope,
2461
- getRaw: (key) => getRawValue(key, scope),
2462
- setRaw: (key, value) => setRawValue(key, value, scope),
2463
- removeRaw: (key) => removeRawValue(key, scope),
2464
- };
2465
2487
 
2466
2488
  versions.forEach((version) => {
2467
2489
  const migration = registeredMigrations.get(version);
2468
2490
  if (!migration) {
2469
2491
  return;
2470
2492
  }
2471
- migration(context);
2493
+ runTransaction(scope, (tx) => {
2494
+ migration(tx);
2495
+ tx.setRaw(MIGRATION_VERSION_KEY, String(version));
2496
+ });
2472
2497
  appliedVersion = version;
2473
2498
  });
2474
2499
 
2475
- if (appliedVersion !== currentVersion) {
2476
- writeMigrationVersion(scope, appliedVersion);
2477
- }
2478
-
2479
2500
  return appliedVersion;
2480
2501
  });
2481
2502
  }
@@ -2525,7 +2546,7 @@ export function createStorageCore(
2525
2546
  }
2526
2547
  rollback.set(key, {
2527
2548
  kind: "raw",
2528
- value: getRawValue(key, scope),
2549
+ value: getStoredRawValue(key, scope),
2529
2550
  ...(scope === StorageScope.Secure &&
2530
2551
  internal?._secureAccessControl !== undefined
2531
2552
  ? { accessControl: internal._secureAccessControl }
@@ -2565,6 +2586,12 @@ export function createStorageCore(
2565
2586
  return transaction(tx);
2566
2587
  } catch (error) {
2567
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
+ : [];
2568
2595
  if (scope === StorageScope.Memory) {
2569
2596
  rollbackEntries.forEach(([key, record]) => {
2570
2597
  if (record.value === NOT_SET) {
@@ -2623,15 +2650,35 @@ export function createStorageCore(
2623
2650
  adapter.backend.setSecureAccessControl(accessControl);
2624
2651
  }
2625
2652
  adapter.backend.setBatch(group.keys, group.values, scope);
2626
- group.keys.forEach((key, index) =>
2627
- cacheRawValue(scope, key, group.values[index]),
2628
- );
2653
+ group.keys.forEach((key, index) => {
2654
+ cacheRawValue(scope, key, group.values[index]);
2655
+ });
2629
2656
  });
2630
2657
  if (keysToRemove.length > 0) {
2631
2658
  adapter.backend.removeBatch(keysToRemove, scope);
2632
- keysToRemove.forEach((key) => cacheRawValue(scope, key, undefined));
2659
+ keysToRemove.forEach((key) => {
2660
+ cacheRawValue(scope, key, undefined);
2661
+ });
2633
2662
  }
2634
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
+ }
2635
2682
  throw error;
2636
2683
  }
2637
2684
  });
@@ -2754,7 +2801,9 @@ export function createStorageCore(
2754
2801
  toggle,
2755
2802
  values: () => Object.keys(item.get()) as TMember[],
2756
2803
  size: () => Object.keys(item.get()).length,
2757
- clear: () => item.set({}),
2804
+ clear: () => {
2805
+ item.set({});
2806
+ },
2758
2807
  reset: item.reset,
2759
2808
  subscribe: item.subscribe,
2760
2809
  scope: item.scope,