react-native-nitro-storage 0.7.0 → 0.9.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 (110) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/README.md +89 -22
  3. package/SECURITY.md +2 -2
  4. package/cpp/bindings/HybridStorage.cpp +5 -12
  5. package/cpp/bindings/HybridStorage.hpp +1 -1
  6. package/docs/api-reference.md +11 -1
  7. package/docs/batch-transactions-migrations.md +8 -2
  8. package/docs/benchmarks.md +19 -15
  9. package/docs/web-backends.md +18 -1
  10. package/ios/IOSStorageAdapterCpp.mm +162 -17
  11. package/lib/commonjs/capabilities.js +26 -0
  12. package/lib/commonjs/capabilities.js.map +1 -0
  13. package/lib/commonjs/core/durability.js +137 -0
  14. package/lib/commonjs/core/durability.js.map +1 -0
  15. package/lib/commonjs/core/metrics.js +91 -0
  16. package/lib/commonjs/core/metrics.js.map +1 -0
  17. package/lib/commonjs/index.js +47 -22
  18. package/lib/commonjs/index.js.map +1 -1
  19. package/lib/commonjs/index.web.js +118 -66
  20. package/lib/commonjs/index.web.js.map +1 -1
  21. package/lib/commonjs/indexeddb-backend.js +73 -23
  22. package/lib/commonjs/indexeddb-backend.js.map +1 -1
  23. package/lib/commonjs/internal.js +27 -4
  24. package/lib/commonjs/internal.js.map +1 -1
  25. package/lib/commonjs/shared.js +2 -2
  26. package/lib/commonjs/shared.js.map +1 -1
  27. package/lib/commonjs/storage-core.js +224 -237
  28. package/lib/commonjs/storage-core.js.map +1 -1
  29. package/lib/commonjs/storage-events.js.map +1 -1
  30. package/lib/commonjs/storage-platform.js +6 -0
  31. package/lib/commonjs/storage-platform.js.map +1 -0
  32. package/lib/commonjs/storage-runtime.js +3 -21
  33. package/lib/commonjs/storage-runtime.js.map +1 -1
  34. package/lib/commonjs/testing.js +19 -4
  35. package/lib/commonjs/testing.js.map +1 -1
  36. package/lib/commonjs/web-backend-contract.js +27 -0
  37. package/lib/commonjs/web-backend-contract.js.map +1 -0
  38. package/lib/module/capabilities.js +21 -0
  39. package/lib/module/capabilities.js.map +1 -0
  40. package/lib/module/core/durability.js +133 -0
  41. package/lib/module/core/durability.js.map +1 -0
  42. package/lib/module/core/metrics.js +87 -0
  43. package/lib/module/core/metrics.js.map +1 -0
  44. package/lib/module/index.js +48 -23
  45. package/lib/module/index.js.map +1 -1
  46. package/lib/module/index.web.js +103 -64
  47. package/lib/module/index.web.js.map +1 -1
  48. package/lib/module/indexeddb-backend.js +73 -23
  49. package/lib/module/indexeddb-backend.js.map +1 -1
  50. package/lib/module/internal.js +24 -3
  51. package/lib/module/internal.js.map +1 -1
  52. package/lib/module/shared.js +2 -2
  53. package/lib/module/shared.js.map +1 -1
  54. package/lib/module/storage-core.js +225 -238
  55. package/lib/module/storage-core.js.map +1 -1
  56. package/lib/module/storage-events.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 +5 -2
  72. package/lib/typescript/index.d.ts.map +1 -1
  73. package/lib/typescript/index.web.d.ts +8 -4
  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 +6 -6
  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 +5 -1
  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/nitrogen/generated/shared/c++/HybridStorageSpec.hpp +1 -1
  94. package/package.json +4 -3
  95. package/react-native-nitro-storage.podspec +3 -1
  96. package/src/capabilities.ts +36 -0
  97. package/src/core/durability.ts +170 -0
  98. package/src/core/metrics.ts +133 -0
  99. package/src/index.ts +66 -40
  100. package/src/index.web.ts +158 -86
  101. package/src/indexeddb-backend.ts +91 -20
  102. package/src/internal.ts +50 -4
  103. package/src/shared.ts +7 -6
  104. package/src/storage-core.ts +327 -292
  105. package/src/storage-events.ts +2 -1
  106. package/src/storage-platform.ts +16 -0
  107. package/src/storage-runtime.ts +13 -47
  108. package/src/testing.ts +14 -7
  109. package/src/web-backend-contract.ts +34 -0
  110. package/src/web-storage-backend.ts +1 -1
@@ -1,9 +1,11 @@
1
1
  "use strict";
2
2
 
3
- import { assertAccessControlLevel, assertBiometricLevel, canUseRawBatchPath, canUseSecureRawBatchPath, createKeyChange, defaultDeserialize, defaultSerialize, isKeychainLockedError, isUpdater, notifyAllListeners, notifyKeyListeners, now, redactSecureKeyChange, runMicrotask, typedKeys } from "./shared";
4
- import { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
5
- import { MIGRATION_VERSION_KEY, isStoredEnvelope, assertBatchScope, assertValidScope, toVersionToken, prefixKey, isNamespaced } from "./internal";
3
+ import { createDurabilityCoordinator } from "./core/durability";
4
+ import { createMetricsRegistry } from "./core/metrics";
5
+ import { MIGRATION_VERSION_KEY, isStoredEnvelope, assertBatchScope, assertValidScope, toVersionToken, prefixKey, isNamespaced, escapeCollidingRawValue, unescapeCollidingRawValue } from "./internal";
6
+ import { assertAccessControlLevel, assertBiometricLevel, canUseRawBatchPath, canUseSecureRawBatchPath, createKeyChange, defaultDeserialize, defaultSerialize, isKeychainLockedError, isUpdater, notifyAllListeners, notifyKeyListeners, redactSecureKeyChange, typedKeys } from "./shared";
6
7
  import { StorageEventRegistry } from "./storage-events";
8
+ import { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
7
9
  const EMPTY_KEYS = Object.freeze([]);
8
10
  function asInternal(item) {
9
11
  return item;
@@ -14,55 +16,38 @@ export function createStorageCore(buildAdapter) {
14
16
  const registeredKeyCounts = new Map();
15
17
  const memoryStore = new Map();
16
18
  const memoryListeners = new Map();
17
- const scopedListeners = new Map([[StorageScope.Disk, new Map()], [StorageScope.Secure, new Map()]]);
18
- const scopedRawCache = new Map([[StorageScope.Disk, new Map()], [StorageScope.Secure, new Map()]]);
19
- const pendingDiskWrites = new Map();
20
- let diskFlushScheduled = false;
21
- let diskWritesAsync = false;
22
- const pendingSecureWrites = new Map();
23
- let secureFlushScheduled = false;
19
+ const scopedListeners = {
20
+ [StorageScope.Disk]: new Map(),
21
+ [StorageScope.Secure]: new Map()
22
+ };
23
+ const scopedRawCache = {
24
+ [StorageScope.Disk]: new Map(),
25
+ [StorageScope.Secure]: new Map()
26
+ };
24
27
  let secureDefaultAccessControl = AccessControl.WhenUnlocked;
25
- let metricsObserver;
26
28
  let eventObserver;
27
29
  let eventObserverRedactSecureValues = true;
28
- const metricsCounters = new Map();
29
30
  const storageEvents = new StorageEventRegistry();
30
- function recordMetric(operation, scope, durationMs, keysCount = 1) {
31
- const existing = metricsCounters.get(operation);
32
- if (!existing) {
33
- metricsCounters.set(operation, {
34
- count: 1,
35
- totalDurationMs: durationMs,
36
- maxDurationMs: durationMs
37
- });
38
- } else {
39
- existing.count += 1;
40
- existing.totalDurationMs += durationMs;
41
- existing.maxDurationMs = Math.max(existing.maxDurationMs, durationMs);
42
- }
43
- metricsObserver?.({
44
- operation,
45
- scope,
46
- durationMs,
47
- keysCount
48
- });
49
- }
50
- function measureOperation(operation, scope, fn, keysCount = 1) {
51
- if (!metricsObserver) {
52
- return fn();
53
- }
54
- const start = now();
55
- try {
56
- return fn();
57
- } finally {
58
- recordMetric(operation, scope, now() - start, keysCount);
59
- }
60
- }
31
+ const metrics = createMetricsRegistry();
32
+ const durability = createDurabilityCoordinator({
33
+ backend: {
34
+ setBatch: (keys, values, scope) => {
35
+ adapter.backend.setBatch(keys, values, scope);
36
+ },
37
+ removeBatch: (keys, scope) => {
38
+ adapter.backend.removeBatch(keys, scope);
39
+ },
40
+ setSecureAccessControl: level => {
41
+ adapter.backend.setSecureAccessControl(level);
42
+ }
43
+ },
44
+ resolveSecureDefaultAccessControl: () => secureDefaultAccessControl
45
+ });
61
46
  function getScopedListeners(scope) {
62
- return scopedListeners.get(scope);
47
+ return scopedListeners[scope];
63
48
  }
64
49
  function getScopeRawCache(scope) {
65
- return scopedRawCache.get(scope);
50
+ return scopedRawCache[scope];
66
51
  }
67
52
  function cacheRawValue(scope, key, value) {
68
53
  getScopeRawCache(scope).set(key, value);
@@ -94,7 +79,7 @@ export function createStorageCore(buildAdapter) {
94
79
  function getEventRawValue(scope, key) {
95
80
  if (scope === StorageScope.Memory) {
96
81
  const value = memoryStore.get(key);
97
- return typeof value === "string" ? value : undefined;
82
+ return typeof value === "string" ? unescapeCollidingRawValue(value) : undefined;
98
83
  }
99
84
  return getRawValue(key, scope);
100
85
  }
@@ -140,118 +125,49 @@ export function createStorageCore(buildAdapter) {
140
125
  storageEvents.emitBatch(event);
141
126
  eventObserver?.(eventForGlobalObserver(event));
142
127
  }
128
+ function measureOperation(operation, scope, fn, keysCount = 1) {
129
+ return metrics.measure(operation, scope, fn, keysCount);
130
+ }
131
+ function recordMetric(operation, scope, durationMs, keysCount = 1) {
132
+ metrics.record(operation, scope, durationMs, keysCount);
133
+ }
143
134
  function readPendingSecureWrite(key) {
144
- return pendingSecureWrites.get(key)?.value;
135
+ return durability.readPendingSecureWrite(key);
145
136
  }
146
137
  function readPendingDiskWrite(key) {
147
- return pendingDiskWrites.get(key)?.value;
138
+ return durability.readPendingDiskWrite(key);
148
139
  }
149
140
  function hasPendingDiskWrite(key) {
150
- return pendingDiskWrites.has(key);
141
+ return durability.hasPendingDiskWrite(key);
151
142
  }
152
143
  function hasPendingSecureWrite(key) {
153
- return pendingSecureWrites.has(key);
144
+ return durability.hasPendingSecureWrite(key);
154
145
  }
155
146
  function clearPendingDiskWrite(key) {
156
- pendingDiskWrites.delete(key);
147
+ durability.clearPendingDiskWrite(key);
157
148
  }
158
149
  function clearPendingSecureWrite(key) {
159
- pendingSecureWrites.delete(key);
150
+ durability.clearPendingSecureWrite(key);
160
151
  }
161
152
  function flushDiskWrites() {
162
- diskFlushScheduled = false;
163
- if (pendingDiskWrites.size === 0) {
164
- return;
165
- }
166
- const writes = Array.from(pendingDiskWrites.values());
167
- pendingDiskWrites.clear();
168
- const keysToSet = [];
169
- const valuesToSet = [];
170
- const keysToRemove = [];
171
- writes.forEach(({
172
- key,
173
- value
174
- }) => {
175
- if (value === undefined) {
176
- keysToRemove.push(key);
177
- return;
178
- }
179
- keysToSet.push(key);
180
- valuesToSet.push(value);
181
- });
182
- if (keysToSet.length > 0) {
183
- adapter.backend.setBatch(keysToSet, valuesToSet, StorageScope.Disk);
184
- }
185
- if (keysToRemove.length > 0) {
186
- adapter.backend.removeBatch(keysToRemove, StorageScope.Disk);
187
- }
153
+ durability.flushDiskWrites();
188
154
  }
189
155
  function flushSecureWrites() {
190
- secureFlushScheduled = false;
191
- if (pendingSecureWrites.size === 0) {
192
- return;
193
- }
194
- const writes = Array.from(pendingSecureWrites.values());
195
- pendingSecureWrites.clear();
196
- const groupedSetWrites = new Map();
197
- const keysToRemove = [];
198
- writes.forEach(({
199
- key,
200
- value,
201
- accessControl
202
- }) => {
203
- if (value === undefined) {
204
- keysToRemove.push(key);
205
- } else {
206
- const resolvedAccessControl = accessControl ?? secureDefaultAccessControl;
207
- const existingGroup = groupedSetWrites.get(resolvedAccessControl);
208
- const group = existingGroup ?? {
209
- keys: [],
210
- values: []
211
- };
212
- group.keys.push(key);
213
- group.values.push(value);
214
- if (!existingGroup) {
215
- groupedSetWrites.set(resolvedAccessControl, group);
216
- }
217
- }
218
- });
219
- groupedSetWrites.forEach((group, accessControl) => {
220
- adapter.backend.setSecureAccessControl(accessControl);
221
- adapter.backend.setBatch(group.keys, group.values, StorageScope.Secure);
222
- });
223
- if (keysToRemove.length > 0) {
224
- adapter.backend.removeBatch(keysToRemove, StorageScope.Secure);
225
- }
156
+ durability.flushSecureWrites();
226
157
  }
227
158
  function scheduleDiskWrite(key, value) {
228
- pendingDiskWrites.set(key, {
229
- key,
230
- value
231
- });
232
- if (diskFlushScheduled) {
233
- return;
234
- }
235
- diskFlushScheduled = true;
236
- runMicrotask(flushDiskWrites);
159
+ durability.scheduleDiskWrite(key, value);
237
160
  }
238
161
  function scheduleSecureWrite(key, value, accessControl) {
239
- const pendingWrite = {
240
- key,
241
- value
242
- };
243
- if (accessControl !== undefined) {
244
- pendingWrite.accessControl = accessControl;
245
- }
246
- pendingSecureWrites.set(key, pendingWrite);
247
- if (secureFlushScheduled) {
248
- return;
249
- }
250
- secureFlushScheduled = true;
251
- runMicrotask(flushSecureWrites);
162
+ durability.scheduleSecureWrite(key, value, accessControl);
252
163
  }
253
- function getRawValue(key, scope) {
254
- assertValidScope(scope);
164
+ function setDiskWritesAsyncMode(enabled) {
165
+ durability.setDiskWritesAsync(enabled);
166
+ }
167
+ function isDiskWritesAsync() {
168
+ return durability.isDiskWritesAsync();
169
+ }
170
+ function getStoredRawValue(key, scope) {
255
171
  if (scope === StorageScope.Memory) {
256
172
  const value = memoryStore.get(key);
257
173
  return typeof value === "string" ? value : undefined;
@@ -264,19 +180,37 @@ export function createStorageCore(buildAdapter) {
264
180
  }
265
181
  return adapter.backend.get(key, scope);
266
182
  }
183
+ function getRawValue(key, scope) {
184
+ assertValidScope(scope);
185
+ if (scope === StorageScope.Memory) {
186
+ const value = memoryStore.get(key);
187
+ return typeof value === "string" ? unescapeCollidingRawValue(value) : undefined;
188
+ }
189
+ if (scope === StorageScope.Disk && hasPendingDiskWrite(key)) {
190
+ const pending = readPendingDiskWrite(key);
191
+ return pending === undefined ? undefined : unescapeCollidingRawValue(pending);
192
+ }
193
+ if (scope === StorageScope.Secure && hasPendingSecureWrite(key)) {
194
+ const pending = readPendingSecureWrite(key);
195
+ return pending === undefined ? undefined : unescapeCollidingRawValue(pending);
196
+ }
197
+ const raw = adapter.backend.get(key, scope);
198
+ return raw === undefined ? undefined : unescapeCollidingRawValue(raw);
199
+ }
267
200
  function setRawValue(key, value, scope) {
268
201
  assertValidScope(scope);
202
+ const storedValue = escapeCollidingRawValue(value);
269
203
  const oldValue = scope === StorageScope.Memory ? getEventRawValue(scope, key) : undefined;
270
204
  if (scope === StorageScope.Memory) {
271
- memoryStore.set(key, value);
205
+ memoryStore.set(key, storedValue);
272
206
  notifyKeyListeners(memoryListeners, key);
273
207
  emitKeyChange(scope, key, oldValue, value, "set", "memory");
274
208
  return;
275
209
  }
276
210
  if (scope === StorageScope.Disk) {
277
- cacheRawValue(scope, key, value);
278
- if (diskWritesAsync) {
279
- scheduleDiskWrite(key, value);
211
+ cacheRawValue(scope, key, storedValue);
212
+ if (isDiskWritesAsync()) {
213
+ scheduleDiskWrite(key, storedValue);
280
214
  emitKeyChange(scope, key, oldValue, value, "set", adapter.changeSource);
281
215
  return;
282
216
  }
@@ -290,8 +224,8 @@ export function createStorageCore(buildAdapter) {
290
224
  adapter.backend.setSecureAccessControl(secureDefaultAccessControl);
291
225
  }
292
226
  }
293
- adapter.backend.set(key, value, scope);
294
- cacheRawValue(scope, key, value);
227
+ adapter.backend.set(key, storedValue, scope);
228
+ cacheRawValue(scope, key, storedValue);
295
229
  emitKeyChange(scope, key, oldValue, value, "set", adapter.changeSource);
296
230
  }
297
231
  function removeRawValue(key, scope) {
@@ -305,7 +239,7 @@ export function createStorageCore(buildAdapter) {
305
239
  }
306
240
  if (scope === StorageScope.Disk) {
307
241
  cacheRawValue(scope, key, undefined);
308
- if (diskWritesAsync) {
242
+ if (isDiskWritesAsync()) {
309
243
  scheduleDiskWrite(key, undefined);
310
244
  emitKeyChange(scope, key, oldValue, undefined, "remove", adapter.changeSource);
311
245
  return;
@@ -329,9 +263,6 @@ export function createStorageCore(buildAdapter) {
329
263
  const parsed = Number.parseInt(raw, 10);
330
264
  return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
331
265
  }
332
- function writeMigrationVersion(scope, version) {
333
- setRawValue(MIGRATION_VERSION_KEY, String(version), scope);
334
- }
335
266
  const internals = {
336
267
  getScopedListeners,
337
268
  cacheRawValue,
@@ -340,10 +271,10 @@ export function createStorageCore(buildAdapter) {
340
271
  clearPendingDiskWrite,
341
272
  clearPendingSecureWrite,
342
273
  clearAllPendingDiskWrites: () => {
343
- pendingDiskWrites.clear();
274
+ durability.clearAllPendingDiskWrites();
344
275
  },
345
276
  clearAllPendingSecureWrites: () => {
346
- pendingSecureWrites.clear();
277
+ durability.clearAllPendingSecureWrites();
347
278
  },
348
279
  flushDiskWrites,
349
280
  flushSecureWrites,
@@ -372,7 +303,9 @@ export function createStorageCore(buildAdapter) {
372
303
  }
373
304
  const previousValues = shouldReadPreviousEventValues(scope) ? removeKeys.map(key => getEventRawValue(scope, key)) : [];
374
305
  removeKeys.forEach(key => memoryStore.delete(key));
375
- removeKeys.forEach(key => notifyKeyListeners(memoryListeners, key));
306
+ removeKeys.forEach(key => {
307
+ notifyKeyListeners(memoryListeners, key);
308
+ });
376
309
  emitBatchChange(scope, "clear", "memory", removeKeys.map((key, index) => createKeyChange(scope, key, previousValues[index], undefined, "clear", "memory")));
377
310
  return;
378
311
  }
@@ -386,9 +319,11 @@ export function createStorageCore(buildAdapter) {
386
319
  if (removeKeys.length === 0) {
387
320
  return;
388
321
  }
389
- const previousValues = shouldReadPreviousEventValues(scope) ? adapter.backend.getBatch(removeKeys, scope) : [];
322
+ const previousValues = shouldReadPreviousEventValues(scope) ? adapter.backend.getBatch(removeKeys, scope).map(value => value === undefined ? undefined : unescapeCollidingRawValue(value)) : [];
390
323
  adapter.backend.removeBatch(removeKeys, scope);
391
- removeKeys.forEach(key => cacheRawValue(scope, key, undefined));
324
+ removeKeys.forEach(key => {
325
+ cacheRawValue(scope, key, undefined);
326
+ });
392
327
  emitBatchChange(scope, "clear", adapter.changeSource, removeKeys.map((key, index) => createKeyChange(scope, key, previousValues[index], undefined, "clear", adapter.changeSource)));
393
328
  });
394
329
  }
@@ -458,11 +393,11 @@ export function createStorageCore(buildAdapter) {
458
393
  }
459
394
  if (scope === StorageScope.Disk) {
460
395
  flushDiskWrites();
461
- pendingDiskWrites.clear();
396
+ durability.clearAllPendingDiskWrites();
462
397
  }
463
398
  if (scope === StorageScope.Secure) {
464
399
  flushSecureWrites();
465
- pendingSecureWrites.clear();
400
+ durability.clearAllPendingSecureWrites();
466
401
  }
467
402
  clearScopeRawCache(scope);
468
403
  adapter.backend.clear(scope);
@@ -556,7 +491,9 @@ export function createStorageCore(buildAdapter) {
556
491
  affectedKeys.forEach(key => {
557
492
  memoryStore.delete(key);
558
493
  });
559
- affectedKeys.forEach(key => notifyKeyListeners(memoryListeners, key));
494
+ affectedKeys.forEach(key => {
495
+ notifyKeyListeners(memoryListeners, key);
496
+ });
560
497
  emitBatchChange(scope, "clearNamespace", "memory", previousValues.map(({
561
498
  key,
562
499
  value
@@ -657,7 +594,7 @@ export function createStorageCore(buildAdapter) {
657
594
  keys.forEach((key, idx) => {
658
595
  const value = values[idx];
659
596
  if (value !== undefined) {
660
- result[key] = value;
597
+ result[key] = unescapeCollidingRawValue(value);
661
598
  }
662
599
  });
663
600
  return result;
@@ -670,7 +607,7 @@ export function createStorageCore(buildAdapter) {
670
607
  if (scope === StorageScope.Memory) {
671
608
  for (const key of memoryStore.keys()) {
672
609
  const value = memoryStore.get(key);
673
- if (typeof value === "string") result[key] = value;
610
+ if (typeof value === "string") result[key] = unescapeCollidingRawValue(value);
674
611
  }
675
612
  return result;
676
613
  }
@@ -685,7 +622,7 @@ export function createStorageCore(buildAdapter) {
685
622
  const values = adapter.backend.getBatch(keys, scope);
686
623
  keys.forEach((key, idx) => {
687
624
  const val = values[idx];
688
- if (val !== undefined) result[key] = val;
625
+ if (val !== undefined) result[key] = unescapeCollidingRawValue(val);
689
626
  });
690
627
  return result;
691
628
  });
@@ -716,7 +653,7 @@ export function createStorageCore(buildAdapter) {
716
653
  },
717
654
  setDiskWritesAsync: enabled => {
718
655
  measureOperation("storage:setDiskWritesAsync", StorageScope.Disk, () => {
719
- diskWritesAsync = enabled;
656
+ setDiskWritesAsyncMode(enabled);
720
657
  if (!enabled) {
721
658
  flushDiskWrites();
722
659
  }
@@ -733,22 +670,16 @@ export function createStorageCore(buildAdapter) {
733
670
  });
734
671
  },
735
672
  setMetricsObserver: observer => {
736
- metricsObserver = observer;
673
+ metrics.setObserver(observer);
737
674
  },
738
675
  getMetricsSnapshot: () => {
739
- const snapshot = {};
740
- metricsCounters.forEach((value, key) => {
741
- snapshot[key] = {
742
- count: value.count,
743
- totalDurationMs: value.totalDurationMs,
744
- avgDurationMs: value.count === 0 ? 0 : value.totalDurationMs / value.count,
745
- maxDurationMs: value.maxDurationMs
746
- };
747
- });
748
- return snapshot;
676
+ return metrics.getSnapshot();
677
+ },
678
+ getScopedMetricsSnapshot: () => {
679
+ return metrics.getScopedSnapshot();
749
680
  },
750
681
  resetMetrics: () => {
751
- metricsCounters.clear();
682
+ metrics.reset();
752
683
  },
753
684
  getSecureMetadata: key => {
754
685
  return measureOperation("storage:getSecureMetadata", StorageScope.Secure, () => {
@@ -799,12 +730,15 @@ export function createStorageCore(buildAdapter) {
799
730
  assertValidScope(scope);
800
731
  if (keys.length === 0) return;
801
732
  const values = keys.map(k => data[k]);
733
+ const storedValues = values.map(escapeCollidingRawValue);
802
734
  const changes = keys.map((key, index) => createKeyChange(scope, key, getEventRawValue(scope, key), values[index], "import", scope === StorageScope.Memory ? "memory" : adapter.changeSource));
803
735
  if (scope === StorageScope.Memory) {
804
736
  keys.forEach((key, index) => {
805
- memoryStore.set(key, values[index]);
737
+ memoryStore.set(key, storedValues[index]);
738
+ });
739
+ keys.forEach(key => {
740
+ notifyKeyListeners(memoryListeners, key);
806
741
  });
807
- keys.forEach(key => notifyKeyListeners(memoryListeners, key));
808
742
  emitBatchChange(scope, "import", "memory", changes);
809
743
  return;
810
744
  }
@@ -812,11 +746,13 @@ export function createStorageCore(buildAdapter) {
812
746
  flushSecureWrites();
813
747
  adapter.backend.setSecureAccessControl(secureDefaultAccessControl);
814
748
  }
815
- if (scope === StorageScope.Disk && adapter.flushDiskWritesOnImport) {
749
+ if (scope === StorageScope.Disk) {
816
750
  flushDiskWrites();
817
751
  }
818
- adapter.backend.setBatch(keys, values, scope);
819
- keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
752
+ adapter.backend.setBatch(keys, storedValues, scope);
753
+ keys.forEach((key, index) => {
754
+ cacheRawValue(scope, key, storedValues[index]);
755
+ });
820
756
  emitBatchChange(scope, "import", adapter.changeSource, changes);
821
757
  }, keys.length);
822
758
  }
@@ -865,20 +801,28 @@ export function createStorageCore(buildAdapter) {
865
801
  hasLastValue = false;
866
802
  lastExpiresAt = undefined;
867
803
  };
804
+ const resolveNonMemoryScope = () => {
805
+ if (nonMemoryScope === null) {
806
+ throw new Error("NitroStorage: this operation requires Disk or Secure scope.");
807
+ }
808
+ return nonMemoryScope;
809
+ };
868
810
  const ensureSubscription = () => {
869
811
  if (unsubscribe) {
870
812
  return;
871
813
  }
872
814
  const listener = () => {
873
815
  invalidateParsedCache();
874
- listeners.forEach(callback => callback());
816
+ listeners.forEach(callback => {
817
+ callback();
818
+ });
875
819
  };
876
820
  if (isMemory) {
877
821
  unsubscribe = addKeyListener(memoryListeners, storageKey, listener);
878
822
  return;
879
823
  }
880
- adapter.ensureScopeSubscription(nonMemoryScope);
881
- unsubscribe = addKeyListener(getScopedListeners(nonMemoryScope), storageKey, listener);
824
+ adapter.ensureScopeSubscription(resolveNonMemoryScope());
825
+ unsubscribe = addKeyListener(getScopedListeners(resolveNonMemoryScope()), storageKey, listener);
882
826
  };
883
827
  const readStoredRaw = () => {
884
828
  if (isMemory) {
@@ -889,28 +833,29 @@ export function createStorageCore(buildAdapter) {
889
833
  memoryExpiration.delete(storageKey);
890
834
  memoryStore.delete(storageKey);
891
835
  notifyKeyListeners(memoryListeners, storageKey);
892
- emitKeyChange(config.scope, storageKey, typeof expiredRaw === "string" ? expiredRaw : undefined, undefined, "expire", "memory");
836
+ emitKeyChange(config.scope, storageKey, typeof expiredRaw === "string" ? unescapeCollidingRawValue(expiredRaw) : undefined, undefined, "expire", "memory");
893
837
  onExpired?.(storageKey);
894
838
  return undefined;
895
839
  }
896
840
  }
897
- return memoryStore.get(storageKey);
841
+ const memoryStored = memoryStore.get(storageKey);
842
+ return typeof memoryStored === "string" ? unescapeCollidingRawValue(memoryStored) : memoryStored;
898
843
  }
899
844
  migrateRenamesIfNeeded();
900
845
  if (nonMemoryScope === StorageScope.Disk) {
901
- const pending = pendingDiskWrites.get(storageKey);
846
+ const pending = durability.readPendingDiskWrite(storageKey);
902
847
  if (pending !== undefined) {
903
- return pending.value;
848
+ return pending;
904
849
  }
905
850
  }
906
851
  if (nonMemoryScope === StorageScope.Secure && !isBiometric) {
907
- const pending = pendingSecureWrites.get(storageKey);
852
+ const pending = durability.readPendingSecureWrite(storageKey);
908
853
  if (pending !== undefined) {
909
- return pending.value;
854
+ return pending;
910
855
  }
911
856
  }
912
857
  if (readCache) {
913
- const cache = getScopeRawCache(nonMemoryScope);
858
+ const cache = getScopeRawCache(resolveNonMemoryScope());
914
859
  const cached = cache.get(storageKey);
915
860
  if (cached !== undefined || cache.has(storageKey)) {
916
861
  return cached;
@@ -920,7 +865,7 @@ export function createStorageCore(buildAdapter) {
920
865
  return readBackendRaw(() => adapter.backend.getSecureBiometric(storageKey));
921
866
  }
922
867
  const raw = readBackendRaw(() => adapter.backend.get(storageKey, config.scope));
923
- cacheRawValue(nonMemoryScope, storageKey, raw);
868
+ cacheRawValue(resolveNonMemoryScope(), storageKey, raw);
924
869
  return raw;
925
870
  };
926
871
  const readBackendRaw = read => {
@@ -929,7 +874,7 @@ export function createStorageCore(buildAdapter) {
929
874
  } catch (error) {
930
875
  onReadError?.(error);
931
876
  if (fallbackToCacheOnReadError) {
932
- const cached = getScopeRawCache(nonMemoryScope).get(storageKey);
877
+ const cached = getScopeRawCache(resolveNonMemoryScope()).get(storageKey);
933
878
  if (cached !== undefined) {
934
879
  return cached;
935
880
  }
@@ -945,9 +890,9 @@ export function createStorageCore(buildAdapter) {
945
890
  emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", adapter.changeSource);
946
891
  return;
947
892
  }
948
- cacheRawValue(nonMemoryScope, storageKey, rawValue);
893
+ cacheRawValue(resolveNonMemoryScope(), storageKey, rawValue);
949
894
  if (nonMemoryScope === StorageScope.Disk) {
950
- if (coalesceDiskWrites || diskWritesAsync) {
895
+ if (coalesceDiskWrites || isDiskWritesAsync()) {
951
896
  scheduleDiskWrite(storageKey, rawValue);
952
897
  emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", adapter.changeSource);
953
898
  return;
@@ -1016,9 +961,9 @@ export function createStorageCore(buildAdapter) {
1016
961
  emitKeyChange(config.scope, storageKey, oldValue, undefined, operation, adapter.changeSource);
1017
962
  return;
1018
963
  }
1019
- cacheRawValue(nonMemoryScope, storageKey, undefined);
964
+ cacheRawValue(resolveNonMemoryScope(), storageKey, undefined);
1020
965
  if (nonMemoryScope === StorageScope.Disk) {
1021
- if (coalesceDiskWrites || diskWritesAsync) {
966
+ if (coalesceDiskWrites || isDiskWritesAsync()) {
1022
967
  scheduleDiskWrite(storageKey, undefined);
1023
968
  emitKeyChange(config.scope, storageKey, oldValue, undefined, operation, adapter.changeSource);
1024
969
  return;
@@ -1042,7 +987,8 @@ export function createStorageCore(buildAdapter) {
1042
987
  if (memoryExpiration) {
1043
988
  memoryExpiration.set(storageKey, Date.now() + (expirationTtlMs ?? 0));
1044
989
  }
1045
- memoryStore.set(storageKey, value);
990
+ const storedValue = typeof value === "string" ? escapeCollidingRawValue(value) : value;
991
+ memoryStore.set(storageKey, storedValue);
1046
992
  notifyKeyListeners(memoryListeners, storageKey);
1047
993
  emitKeyChange(config.scope, storageKey, oldValue, typeof value === "string" ? value : undefined, "set", "memory");
1048
994
  return;
@@ -1093,7 +1039,9 @@ export function createStorageCore(buildAdapter) {
1093
1039
  onExpired?.(storageKey);
1094
1040
  lastValue = ensureValidatedValue(defaultValue, false);
1095
1041
  hasLastValue = true;
1096
- listeners.forEach(cb => cb());
1042
+ listeners.forEach(cb => {
1043
+ cb();
1044
+ });
1097
1045
  return lastValue;
1098
1046
  }
1099
1047
  }
@@ -1129,7 +1077,9 @@ export function createStorageCore(buildAdapter) {
1129
1077
  onExpired?.(storageKey);
1130
1078
  lastValue = ensureValidatedValue(defaultValue, false);
1131
1079
  hasLastValue = true;
1132
- listeners.forEach(cb => cb());
1080
+ listeners.forEach(cb => {
1081
+ cb();
1082
+ });
1133
1083
  return lastValue;
1134
1084
  }
1135
1085
  deserializableRaw = parsed.payload;
@@ -1147,7 +1097,10 @@ export function createStorageCore(buildAdapter) {
1147
1097
  };
1148
1098
  const getCurrentVersion = () => {
1149
1099
  const raw = readStoredRaw();
1150
- return toVersionToken(raw);
1100
+ if (raw === undefined) {
1101
+ return toVersionToken(undefined);
1102
+ }
1103
+ return toVersionToken(typeof raw === "string" ? unescapeCollidingRawValue(raw) : raw);
1151
1104
  };
1152
1105
  const get = () => measureOperation("item:get", config.scope, () => getInternal());
1153
1106
  const getWithVersion = () => measureOperation("item:getWithVersion", config.scope, () => ({
@@ -1213,15 +1166,13 @@ export function createStorageCore(buildAdapter) {
1213
1166
  if (isMemory) return memoryStore.has(storageKey);
1214
1167
  if (isBiometric) return adapter.backend.hasSecureBiometric(storageKey);
1215
1168
  if (nonMemoryScope === StorageScope.Disk) {
1216
- const pending = pendingDiskWrites.get(storageKey);
1217
- if (pending !== undefined) {
1218
- return pending.value !== undefined;
1169
+ if (durability.hasPendingDiskWrite(storageKey)) {
1170
+ return durability.readPendingDiskWrite(storageKey) !== undefined;
1219
1171
  }
1220
1172
  }
1221
1173
  if (nonMemoryScope === StorageScope.Secure) {
1222
- const pending = pendingSecureWrites.get(storageKey);
1223
- if (pending !== undefined) {
1224
- return pending.value !== undefined;
1174
+ if (durability.hasPendingSecureWrite(storageKey)) {
1175
+ return durability.readPendingSecureWrite(storageKey) !== undefined;
1225
1176
  }
1226
1177
  }
1227
1178
  return adapter.backend.has(storageKey, config.scope);
@@ -1234,7 +1185,7 @@ export function createStorageCore(buildAdapter) {
1234
1185
  if (listeners.size === 0 && unsubscribe) {
1235
1186
  unsubscribe();
1236
1187
  if (!isMemory) {
1237
- adapter.maybeCleanupScopeSubscription(nonMemoryScope);
1188
+ adapter.maybeCleanupScopeSubscription(resolveNonMemoryScope());
1238
1189
  }
1239
1190
  unsubscribe = null;
1240
1191
  }
@@ -1272,11 +1223,20 @@ export function createStorageCore(buildAdapter) {
1272
1223
  deserialize,
1273
1224
  _triggerListeners: () => {
1274
1225
  invalidateParsedCache();
1275
- listeners.forEach(listener => listener());
1226
+ listeners.forEach(listener => {
1227
+ listener();
1228
+ });
1276
1229
  },
1277
1230
  _invalidateParsedCacheOnly: () => {
1278
1231
  invalidateParsedCache();
1279
1232
  },
1233
+ _deleteMemoryEntry: () => {
1234
+ if (memoryExpiration) {
1235
+ memoryExpiration.delete(storageKey);
1236
+ }
1237
+ memoryStore.delete(storageKey);
1238
+ invalidateParsedCache();
1239
+ },
1280
1240
  _hasValidation: validate !== undefined,
1281
1241
  _hasExpiration: expiration !== undefined,
1282
1242
  _readCacheEnabled: readCache,
@@ -1319,16 +1279,16 @@ export function createStorageCore(buildAdapter) {
1319
1279
  const keyIndexes = [];
1320
1280
  items.forEach((item, index) => {
1321
1281
  if (scope === StorageScope.Disk) {
1322
- const pending = pendingDiskWrites.get(item.key);
1282
+ const pending = durability.readPendingDiskWrite(item.key);
1323
1283
  if (pending !== undefined) {
1324
- rawValues[index] = pending.value;
1284
+ rawValues[index] = pending;
1325
1285
  return;
1326
1286
  }
1327
1287
  }
1328
1288
  if (scope === StorageScope.Secure) {
1329
- const pending = pendingSecureWrites.get(item.key);
1289
+ const pending = durability.readPendingSecureWrite(item.key);
1330
1290
  if (pending !== undefined) {
1331
- rawValues[index] = pending.value;
1291
+ rawValues[index] = pending;
1332
1292
  return;
1333
1293
  }
1334
1294
  }
@@ -1380,7 +1340,9 @@ export function createStorageCore(buildAdapter) {
1380
1340
  items.forEach(({
1381
1341
  item,
1382
1342
  value
1383
- }) => item.set(value));
1343
+ }) => {
1344
+ item.set(value);
1345
+ });
1384
1346
  return;
1385
1347
  }
1386
1348
  const changes = items.map(({
@@ -1398,7 +1360,9 @@ export function createStorageCore(buildAdapter) {
1398
1360
  });
1399
1361
  items.forEach(({
1400
1362
  item
1401
- }) => notifyKeyListeners(memoryListeners, item.key));
1363
+ }) => {
1364
+ notifyKeyListeners(memoryListeners, item.key);
1365
+ });
1402
1366
  emitBatchChange(scope, "setBatch", "memory", changes);
1403
1367
  return;
1404
1368
  }
@@ -1418,14 +1382,16 @@ export function createStorageCore(buildAdapter) {
1418
1382
  items.forEach(({
1419
1383
  item,
1420
1384
  value
1421
- }) => item.set(value));
1385
+ }) => {
1386
+ item.set(value);
1387
+ });
1422
1388
  return;
1423
1389
  }
1424
1390
  flushSecureWrites();
1425
1391
  const keys = secureEntries.map(({
1426
1392
  item
1427
1393
  }) => item.key);
1428
- const oldValues = shouldReadPreviousEventValues(scope) ? adapter.backend.getBatch(keys, scope) : [];
1394
+ const oldValues = shouldReadPreviousEventValues(scope) ? adapter.backend.getBatch(keys, scope).map(value => value === undefined ? undefined : unescapeCollidingRawValue(value)) : [];
1429
1395
  const groupedByAccessControl = new Map();
1430
1396
  secureEntries.forEach(({
1431
1397
  item,
@@ -1447,7 +1413,9 @@ export function createStorageCore(buildAdapter) {
1447
1413
  groupedByAccessControl.forEach((group, accessControl) => {
1448
1414
  adapter.backend.setSecureAccessControl(accessControl);
1449
1415
  adapter.backend.setBatch(group.keys, group.values, scope);
1450
- group.keys.forEach((key, index) => cacheRawValue(scope, key, group.values[index]));
1416
+ group.keys.forEach((key, index) => {
1417
+ cacheRawValue(scope, key, group.values[index]);
1418
+ });
1451
1419
  });
1452
1420
  emitBatchChange(scope, "setBatch", adapter.changeSource, secureEntries.map(({
1453
1421
  item,
@@ -1463,14 +1431,18 @@ export function createStorageCore(buildAdapter) {
1463
1431
  items.forEach(({
1464
1432
  item,
1465
1433
  value
1466
- }) => item.set(value));
1434
+ }) => {
1435
+ item.set(value);
1436
+ });
1467
1437
  return;
1468
1438
  }
1469
1439
  const keys = items.map(entry => entry.item.key);
1470
1440
  const values = items.map(entry => entry.item.serialize(entry.value));
1471
- const oldValues = shouldReadPreviousEventValues(scope) ? adapter.backend.getBatch(keys, scope) : [];
1441
+ const oldValues = shouldReadPreviousEventValues(scope) ? adapter.backend.getBatch(keys, scope).map(value => value === undefined ? undefined : unescapeCollidingRawValue(value)) : [];
1472
1442
  adapter.backend.setBatch(keys, values, scope);
1473
- keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
1443
+ keys.forEach((key, index) => {
1444
+ cacheRawValue(scope, key, values[index]);
1445
+ });
1474
1446
  emitBatchChange(scope, "setBatch", adapter.changeSource, keys.map((key, index) => createKeyChange(scope, key, oldValues[index], values[index], "setBatch", adapter.changeSource)));
1475
1447
  }, items.length);
1476
1448
  }
@@ -1479,7 +1451,12 @@ export function createStorageCore(buildAdapter) {
1479
1451
  assertBatchScope(items, scope);
1480
1452
  if (scope === StorageScope.Memory) {
1481
1453
  const changes = items.map(item => createKeyChange(scope, item.key, getEventRawValue(scope, item.key), undefined, "removeBatch", "memory"));
1482
- items.forEach(item => item.delete());
1454
+ items.forEach(item => {
1455
+ asInternal(item)._deleteMemoryEntry();
1456
+ });
1457
+ items.forEach(item => {
1458
+ notifyKeyListeners(memoryListeners, item.key);
1459
+ });
1483
1460
  emitBatchChange(scope, "removeBatch", "memory", changes);
1484
1461
  return;
1485
1462
  }
@@ -1490,9 +1467,11 @@ export function createStorageCore(buildAdapter) {
1490
1467
  if (scope === StorageScope.Secure) {
1491
1468
  flushSecureWrites();
1492
1469
  }
1493
- const oldValues = shouldReadPreviousEventValues(scope) ? adapter.backend.getBatch(keys, scope) : [];
1470
+ const oldValues = shouldReadPreviousEventValues(scope) ? adapter.backend.getBatch(keys, scope).map(value => value === undefined ? undefined : unescapeCollidingRawValue(value)) : [];
1494
1471
  adapter.backend.removeBatch(keys, scope);
1495
- keys.forEach(key => cacheRawValue(scope, key, undefined));
1472
+ keys.forEach(key => {
1473
+ cacheRawValue(scope, key, undefined);
1474
+ });
1496
1475
  emitBatchChange(scope, "removeBatch", adapter.changeSource, keys.map((key, index) => createKeyChange(scope, key, oldValues[index], undefined, "removeBatch", adapter.changeSource)));
1497
1476
  }, items.length);
1498
1477
  }
@@ -1511,23 +1490,17 @@ export function createStorageCore(buildAdapter) {
1511
1490
  const currentVersion = readMigrationVersion(scope);
1512
1491
  const versions = Array.from(registeredMigrations.keys()).filter(version => version > currentVersion).sort((a, b) => a - b);
1513
1492
  let appliedVersion = currentVersion;
1514
- const context = {
1515
- scope,
1516
- getRaw: key => getRawValue(key, scope),
1517
- setRaw: (key, value) => setRawValue(key, value, scope),
1518
- removeRaw: key => removeRawValue(key, scope)
1519
- };
1520
1493
  versions.forEach(version => {
1521
1494
  const migration = registeredMigrations.get(version);
1522
1495
  if (!migration) {
1523
1496
  return;
1524
1497
  }
1525
- migration(context);
1498
+ runTransaction(scope, tx => {
1499
+ migration(tx);
1500
+ tx.setRaw(MIGRATION_VERSION_KEY, String(version));
1501
+ });
1526
1502
  appliedVersion = version;
1527
1503
  });
1528
- if (appliedVersion !== currentVersion) {
1529
- writeMigrationVersion(scope, appliedVersion);
1530
- }
1531
1504
  return appliedVersion;
1532
1505
  });
1533
1506
  }
@@ -1563,7 +1536,7 @@ export function createStorageCore(buildAdapter) {
1563
1536
  }
1564
1537
  rollback.set(key, {
1565
1538
  kind: "raw",
1566
- value: getRawValue(key, scope),
1539
+ value: getStoredRawValue(key, scope),
1567
1540
  ...(scope === StorageScope.Secure && internal?._secureAccessControl !== undefined ? {
1568
1541
  accessControl: internal._secureAccessControl
1569
1542
  } : {})
@@ -1600,6 +1573,8 @@ export function createStorageCore(buildAdapter) {
1600
1573
  return transaction(tx);
1601
1574
  } catch (error) {
1602
1575
  const rollbackEntries = Array.from(rollback.entries()).reverse();
1576
+ const rollbackSource = scope === StorageScope.Memory ? "memory" : adapter.changeSource;
1577
+ const preRollbackValues = rollbackEntries.length > 0 ? rollbackEntries.map(([key]) => getEventRawValue(scope, key)) : [];
1603
1578
  if (scope === StorageScope.Memory) {
1604
1579
  rollbackEntries.forEach(([key, record]) => {
1605
1580
  if (record.value === NOT_SET) {
@@ -1651,13 +1626,20 @@ export function createStorageCore(buildAdapter) {
1651
1626
  adapter.backend.setSecureAccessControl(accessControl);
1652
1627
  }
1653
1628
  adapter.backend.setBatch(group.keys, group.values, scope);
1654
- group.keys.forEach((key, index) => cacheRawValue(scope, key, group.values[index]));
1629
+ group.keys.forEach((key, index) => {
1630
+ cacheRawValue(scope, key, group.values[index]);
1631
+ });
1655
1632
  });
1656
1633
  if (keysToRemove.length > 0) {
1657
1634
  adapter.backend.removeBatch(keysToRemove, scope);
1658
- keysToRemove.forEach(key => cacheRawValue(scope, key, undefined));
1635
+ keysToRemove.forEach(key => {
1636
+ cacheRawValue(scope, key, undefined);
1637
+ });
1659
1638
  }
1660
1639
  }
1640
+ if (rollbackEntries.length > 0) {
1641
+ emitBatchChange(scope, "rollback", rollbackSource, rollbackEntries.map(([key], index) => createKeyChange(scope, key, preRollbackValues[index], getEventRawValue(scope, key), "rollback", rollbackSource)));
1642
+ }
1661
1643
  throw error;
1662
1644
  }
1663
1645
  });
@@ -1735,12 +1717,15 @@ export function createStorageCore(buildAdapter) {
1735
1717
  });
1736
1718
  const has = id => item.get()[id] === true;
1737
1719
  const add = id => {
1738
- if (item.get()[id] === true) {
1720
+ const current = item.get();
1721
+ if (current[id] === true) {
1739
1722
  return;
1740
1723
  }
1741
- item.merge({
1742
- [id]: true
1743
- });
1724
+ const next = {
1725
+ ...current
1726
+ };
1727
+ next[id] = true;
1728
+ item.set(next);
1744
1729
  };
1745
1730
  const deleteId = id => {
1746
1731
  const current = item.get();
@@ -1769,7 +1754,9 @@ export function createStorageCore(buildAdapter) {
1769
1754
  toggle,
1770
1755
  values: () => Object.keys(item.get()),
1771
1756
  size: () => Object.keys(item.get()).length,
1772
- clear: () => item.set({}),
1757
+ clear: () => {
1758
+ item.set({});
1759
+ },
1773
1760
  reset: item.reset,
1774
1761
  subscribe: item.subscribe,
1775
1762
  scope: item.scope,