react-native-nitro-storage 0.1.3 → 0.3.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 (48) hide show
  1. package/README.md +320 -391
  2. package/android/src/main/cpp/AndroidStorageAdapterCpp.cpp +101 -0
  3. package/android/src/main/cpp/AndroidStorageAdapterCpp.hpp +6 -41
  4. package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +125 -37
  5. package/app.plugin.js +9 -7
  6. package/cpp/bindings/HybridStorage.cpp +214 -19
  7. package/cpp/bindings/HybridStorage.hpp +1 -0
  8. package/cpp/core/NativeStorageAdapter.hpp +7 -0
  9. package/ios/IOSStorageAdapterCpp.hpp +6 -0
  10. package/ios/IOSStorageAdapterCpp.mm +90 -7
  11. package/lib/commonjs/index.js +537 -66
  12. package/lib/commonjs/index.js.map +1 -1
  13. package/lib/commonjs/index.web.js +558 -130
  14. package/lib/commonjs/index.web.js.map +1 -1
  15. package/lib/commonjs/internal.js +102 -0
  16. package/lib/commonjs/internal.js.map +1 -0
  17. package/lib/module/index.js +528 -67
  18. package/lib/module/index.js.map +1 -1
  19. package/lib/module/index.web.js +536 -122
  20. package/lib/module/index.web.js.map +1 -1
  21. package/lib/module/internal.js +92 -0
  22. package/lib/module/internal.js.map +1 -0
  23. package/lib/typescript/index.d.ts +42 -6
  24. package/lib/typescript/index.d.ts.map +1 -1
  25. package/lib/typescript/index.web.d.ts +45 -12
  26. package/lib/typescript/index.web.d.ts.map +1 -1
  27. package/lib/typescript/internal.d.ts +19 -0
  28. package/lib/typescript/internal.d.ts.map +1 -0
  29. package/lib/typescript/migration.d.ts +2 -3
  30. package/lib/typescript/migration.d.ts.map +1 -1
  31. package/nitrogen/generated/android/NitroStorage+autolinking.cmake +1 -1
  32. package/nitrogen/generated/android/NitroStorage+autolinking.gradle +1 -1
  33. package/nitrogen/generated/android/NitroStorageOnLoad.cpp +1 -1
  34. package/nitrogen/generated/android/NitroStorageOnLoad.hpp +1 -1
  35. package/nitrogen/generated/android/kotlin/com/margelo/nitro/com/nitrostorage/NitroStorageOnLoad.kt +1 -1
  36. package/nitrogen/generated/ios/NitroStorage+autolinking.rb +1 -1
  37. package/nitrogen/generated/ios/NitroStorage-Swift-Cxx-Bridge.cpp +1 -1
  38. package/nitrogen/generated/ios/NitroStorage-Swift-Cxx-Bridge.hpp +1 -1
  39. package/nitrogen/generated/ios/NitroStorage-Swift-Cxx-Umbrella.hpp +1 -1
  40. package/nitrogen/generated/ios/NitroStorageAutolinking.mm +1 -1
  41. package/nitrogen/generated/ios/NitroStorageAutolinking.swift +5 -1
  42. package/nitrogen/generated/shared/c++/HybridStorageSpec.cpp +1 -1
  43. package/nitrogen/generated/shared/c++/HybridStorageSpec.hpp +1 -1
  44. package/package.json +19 -8
  45. package/src/index.ts +734 -74
  46. package/src/index.web.ts +732 -128
  47. package/src/internal.ts +134 -0
  48. package/src/migration.ts +2 -2
@@ -1,9 +1,15 @@
1
1
  "use strict";
2
2
 
3
- import { useSyncExternalStore } from "react";
3
+ import { useRef, useSyncExternalStore } from "react";
4
4
  import { NitroModules } from "react-native-nitro-modules";
5
5
  import { StorageScope } from "./Storage.types";
6
+ import { MIGRATION_VERSION_KEY, isStoredEnvelope, assertBatchScope, assertValidScope, decodeNativeBatchValue, serializeWithPrimitiveFastPath, deserializeWithPrimitiveFastPath } from "./internal";
6
7
  export { StorageScope } from "./Storage.types";
8
+ export { migrateFromMMKV } from "./migration";
9
+ const registeredMigrations = new Map();
10
+ const runMicrotask = typeof queueMicrotask === "function" ? queueMicrotask : task => {
11
+ Promise.resolve().then(task);
12
+ };
7
13
  let _storageModule = null;
8
14
  function getStorageModule() {
9
15
  if (!_storageModule) {
@@ -12,18 +18,202 @@ function getStorageModule() {
12
18
  return _storageModule;
13
19
  }
14
20
  const memoryStore = new Map();
15
- const memoryListeners = new Set();
16
- function notifyMemoryListeners(key, value) {
17
- memoryListeners.forEach(listener => listener(key, value));
21
+ const memoryListeners = new Map();
22
+ const scopedListeners = new Map([[StorageScope.Disk, new Map()], [StorageScope.Secure, new Map()]]);
23
+ const scopedUnsubscribers = new Map();
24
+ const scopedRawCache = new Map([[StorageScope.Disk, new Map()], [StorageScope.Secure, new Map()]]);
25
+ const pendingSecureWrites = new Map();
26
+ let secureFlushScheduled = false;
27
+ function getScopedListeners(scope) {
28
+ return scopedListeners.get(scope);
29
+ }
30
+ function getScopeRawCache(scope) {
31
+ return scopedRawCache.get(scope);
32
+ }
33
+ function cacheRawValue(scope, key, value) {
34
+ getScopeRawCache(scope).set(key, value);
35
+ }
36
+ function readCachedRawValue(scope, key) {
37
+ return getScopeRawCache(scope).get(key);
38
+ }
39
+ function hasCachedRawValue(scope, key) {
40
+ return getScopeRawCache(scope).has(key);
41
+ }
42
+ function clearScopeRawCache(scope) {
43
+ getScopeRawCache(scope).clear();
44
+ }
45
+ function notifyKeyListeners(registry, key) {
46
+ registry.get(key)?.forEach(listener => listener());
47
+ }
48
+ function notifyAllListeners(registry) {
49
+ registry.forEach(listeners => {
50
+ listeners.forEach(listener => listener());
51
+ });
52
+ }
53
+ function addKeyListener(registry, key, listener) {
54
+ let listeners = registry.get(key);
55
+ if (!listeners) {
56
+ listeners = new Set();
57
+ registry.set(key, listeners);
58
+ }
59
+ listeners.add(listener);
60
+ return () => {
61
+ const scopedListeners = registry.get(key);
62
+ if (!scopedListeners) {
63
+ return;
64
+ }
65
+ scopedListeners.delete(listener);
66
+ if (scopedListeners.size === 0) {
67
+ registry.delete(key);
68
+ }
69
+ };
70
+ }
71
+ function readPendingSecureWrite(key) {
72
+ return pendingSecureWrites.get(key)?.value;
73
+ }
74
+ function hasPendingSecureWrite(key) {
75
+ return pendingSecureWrites.has(key);
76
+ }
77
+ function clearPendingSecureWrite(key) {
78
+ pendingSecureWrites.delete(key);
79
+ }
80
+ function flushSecureWrites() {
81
+ secureFlushScheduled = false;
82
+ if (pendingSecureWrites.size === 0) {
83
+ return;
84
+ }
85
+ const writes = Array.from(pendingSecureWrites.values());
86
+ pendingSecureWrites.clear();
87
+ const keysToSet = [];
88
+ const valuesToSet = [];
89
+ const keysToRemove = [];
90
+ writes.forEach(({
91
+ key,
92
+ value
93
+ }) => {
94
+ if (value === undefined) {
95
+ keysToRemove.push(key);
96
+ } else {
97
+ keysToSet.push(key);
98
+ valuesToSet.push(value);
99
+ }
100
+ });
101
+ const storageModule = getStorageModule();
102
+ if (keysToSet.length > 0) {
103
+ storageModule.setBatch(keysToSet, valuesToSet, StorageScope.Secure);
104
+ }
105
+ if (keysToRemove.length > 0) {
106
+ storageModule.removeBatch(keysToRemove, StorageScope.Secure);
107
+ }
108
+ }
109
+ function scheduleSecureWrite(key, value) {
110
+ pendingSecureWrites.set(key, {
111
+ key,
112
+ value
113
+ });
114
+ if (secureFlushScheduled) {
115
+ return;
116
+ }
117
+ secureFlushScheduled = true;
118
+ runMicrotask(flushSecureWrites);
119
+ }
120
+ function ensureNativeScopeSubscription(scope) {
121
+ if (scopedUnsubscribers.has(scope)) {
122
+ return;
123
+ }
124
+ const unsubscribe = getStorageModule().addOnChange(scope, (key, value) => {
125
+ if (scope === StorageScope.Secure) {
126
+ if (key === "") {
127
+ pendingSecureWrites.clear();
128
+ } else {
129
+ clearPendingSecureWrite(key);
130
+ }
131
+ }
132
+ if (key === "") {
133
+ clearScopeRawCache(scope);
134
+ notifyAllListeners(getScopedListeners(scope));
135
+ return;
136
+ }
137
+ cacheRawValue(scope, key, value);
138
+ notifyKeyListeners(getScopedListeners(scope), key);
139
+ });
140
+ scopedUnsubscribers.set(scope, unsubscribe);
141
+ }
142
+ function maybeCleanupNativeScopeSubscription(scope) {
143
+ const listeners = getScopedListeners(scope);
144
+ if (listeners.size > 0) {
145
+ return;
146
+ }
147
+ const unsubscribe = scopedUnsubscribers.get(scope);
148
+ if (!unsubscribe) {
149
+ return;
150
+ }
151
+ unsubscribe();
152
+ scopedUnsubscribers.delete(scope);
153
+ }
154
+ function getRawValue(key, scope) {
155
+ assertValidScope(scope);
156
+ if (scope === StorageScope.Memory) {
157
+ const value = memoryStore.get(key);
158
+ return typeof value === "string" ? value : undefined;
159
+ }
160
+ if (scope === StorageScope.Secure && hasPendingSecureWrite(key)) {
161
+ return readPendingSecureWrite(key);
162
+ }
163
+ return getStorageModule().get(key, scope);
164
+ }
165
+ function setRawValue(key, value, scope) {
166
+ assertValidScope(scope);
167
+ if (scope === StorageScope.Memory) {
168
+ memoryStore.set(key, value);
169
+ notifyKeyListeners(memoryListeners, key);
170
+ return;
171
+ }
172
+ if (scope === StorageScope.Secure) {
173
+ flushSecureWrites();
174
+ clearPendingSecureWrite(key);
175
+ }
176
+ getStorageModule().set(key, value, scope);
177
+ cacheRawValue(scope, key, value);
178
+ }
179
+ function removeRawValue(key, scope) {
180
+ assertValidScope(scope);
181
+ if (scope === StorageScope.Memory) {
182
+ memoryStore.delete(key);
183
+ notifyKeyListeners(memoryListeners, key);
184
+ return;
185
+ }
186
+ if (scope === StorageScope.Secure) {
187
+ flushSecureWrites();
188
+ clearPendingSecureWrite(key);
189
+ }
190
+ getStorageModule().remove(key, scope);
191
+ cacheRawValue(scope, key, undefined);
192
+ }
193
+ function readMigrationVersion(scope) {
194
+ const raw = getRawValue(MIGRATION_VERSION_KEY, scope);
195
+ if (raw === undefined) {
196
+ return 0;
197
+ }
198
+ const parsed = Number.parseInt(raw, 10);
199
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
200
+ }
201
+ function writeMigrationVersion(scope, version) {
202
+ setRawValue(MIGRATION_VERSION_KEY, String(version), scope);
18
203
  }
19
204
  export const storage = {
20
205
  clear: scope => {
21
206
  if (scope === StorageScope.Memory) {
22
207
  memoryStore.clear();
23
- notifyMemoryListeners("", undefined);
24
- } else {
25
- getStorageModule().clear(scope);
208
+ notifyAllListeners(memoryListeners);
209
+ return;
210
+ }
211
+ if (scope === StorageScope.Secure) {
212
+ flushSecureWrites();
213
+ pendingSecureWrites.clear();
26
214
  }
215
+ clearScopeRawCache(scope);
216
+ getStorageModule().clear(scope);
27
217
  },
28
218
  clearAll: () => {
29
219
  storage.clear(StorageScope.Memory);
@@ -31,83 +221,201 @@ export const storage = {
31
221
  storage.clear(StorageScope.Secure);
32
222
  }
33
223
  };
224
+ function canUseRawBatchPath(item) {
225
+ return item._hasExpiration === false && item._hasValidation === false;
226
+ }
34
227
  function defaultSerialize(value) {
35
- return JSON.stringify(value);
228
+ return serializeWithPrimitiveFastPath(value);
36
229
  }
37
230
  function defaultDeserialize(value) {
38
- return JSON.parse(value);
231
+ return deserializeWithPrimitiveFastPath(value);
39
232
  }
40
233
  export function createStorageItem(config) {
41
234
  const serialize = config.serialize ?? defaultSerialize;
42
235
  const deserialize = config.deserialize ?? defaultDeserialize;
43
236
  const isMemory = config.scope === StorageScope.Memory;
237
+ const validate = config.validate;
238
+ const onValidationError = config.onValidationError;
239
+ const expiration = config.expiration;
240
+ const expirationTtlMs = expiration?.ttlMs;
241
+ const memoryExpiration = expiration && isMemory ? new Map() : null;
242
+ const readCache = !isMemory && config.readCache === true;
243
+ const coalesceSecureWrites = config.scope === StorageScope.Secure && config.coalesceSecureWrites === true;
244
+ const nonMemoryScope = config.scope === StorageScope.Disk ? StorageScope.Disk : config.scope === StorageScope.Secure ? StorageScope.Secure : null;
245
+ if (expiration && expiration.ttlMs <= 0) {
246
+ throw new Error("expiration.ttlMs must be greater than 0.");
247
+ }
44
248
  const listeners = new Set();
45
249
  let unsubscribe = null;
250
+ let lastRaw = undefined;
251
+ let lastValue;
252
+ let hasLastValue = false;
253
+ const invalidateParsedCache = () => {
254
+ lastRaw = undefined;
255
+ lastValue = undefined;
256
+ hasLastValue = false;
257
+ };
46
258
  const ensureSubscription = () => {
47
- if (!unsubscribe) {
48
- if (isMemory) {
49
- const listener = key => {
50
- if (key === "" || key === config.key) {
51
- lastRaw = undefined;
52
- lastValue = undefined;
53
- listeners.forEach(l => l());
54
- }
55
- };
56
- memoryListeners.add(listener);
57
- unsubscribe = () => memoryListeners.delete(listener);
58
- } else {
59
- unsubscribe = getStorageModule().addOnChange(config.scope, key => {
60
- if (key === "" || key === config.key) {
61
- lastRaw = undefined;
62
- lastValue = undefined;
63
- listeners.forEach(listener => listener());
64
- }
65
- });
259
+ if (unsubscribe) {
260
+ return;
261
+ }
262
+ const listener = () => {
263
+ invalidateParsedCache();
264
+ listeners.forEach(callback => callback());
265
+ };
266
+ if (isMemory) {
267
+ unsubscribe = addKeyListener(memoryListeners, config.key, listener);
268
+ return;
269
+ }
270
+ ensureNativeScopeSubscription(nonMemoryScope);
271
+ unsubscribe = addKeyListener(getScopedListeners(nonMemoryScope), config.key, listener);
272
+ };
273
+ const readStoredRaw = () => {
274
+ if (isMemory) {
275
+ if (memoryExpiration) {
276
+ const expiresAt = memoryExpiration.get(config.key);
277
+ if (expiresAt !== undefined && expiresAt <= Date.now()) {
278
+ memoryExpiration.delete(config.key);
279
+ memoryStore.delete(config.key);
280
+ notifyKeyListeners(memoryListeners, config.key);
281
+ return undefined;
282
+ }
283
+ }
284
+ return memoryStore.get(config.key);
285
+ }
286
+ if (nonMemoryScope === StorageScope.Secure && hasPendingSecureWrite(config.key)) {
287
+ return readPendingSecureWrite(config.key);
288
+ }
289
+ if (readCache) {
290
+ if (hasCachedRawValue(nonMemoryScope, config.key)) {
291
+ return readCachedRawValue(nonMemoryScope, config.key);
66
292
  }
67
293
  }
294
+ const raw = getStorageModule().get(config.key, config.scope);
295
+ cacheRawValue(nonMemoryScope, config.key, raw);
296
+ return raw;
68
297
  };
69
- let lastRaw;
70
- let lastValue;
71
- const get = () => {
72
- let raw;
298
+ const writeStoredRaw = rawValue => {
299
+ cacheRawValue(nonMemoryScope, config.key, rawValue);
300
+ if (coalesceSecureWrites) {
301
+ scheduleSecureWrite(config.key, rawValue);
302
+ return;
303
+ }
304
+ if (nonMemoryScope === StorageScope.Secure) {
305
+ clearPendingSecureWrite(config.key);
306
+ }
307
+ getStorageModule().set(config.key, rawValue, config.scope);
308
+ };
309
+ const removeStoredRaw = () => {
310
+ cacheRawValue(nonMemoryScope, config.key, undefined);
311
+ if (coalesceSecureWrites) {
312
+ scheduleSecureWrite(config.key, undefined);
313
+ return;
314
+ }
315
+ if (nonMemoryScope === StorageScope.Secure) {
316
+ clearPendingSecureWrite(config.key);
317
+ }
318
+ getStorageModule().remove(config.key, config.scope);
319
+ };
320
+ const writeValueWithoutValidation = value => {
73
321
  if (isMemory) {
74
- raw = memoryStore.get(config.key);
75
- } else {
76
- raw = getStorageModule().get(config.key, config.scope);
322
+ if (memoryExpiration) {
323
+ memoryExpiration.set(config.key, Date.now() + (expirationTtlMs ?? 0));
324
+ }
325
+ memoryStore.set(config.key, value);
326
+ notifyKeyListeners(memoryListeners, config.key);
327
+ return;
77
328
  }
78
- if (raw === lastRaw && lastValue !== undefined) {
329
+ const serialized = serialize(value);
330
+ if (expiration) {
331
+ const envelope = {
332
+ __nitroStorageEnvelope: true,
333
+ expiresAt: Date.now() + expiration.ttlMs,
334
+ payload: serialized
335
+ };
336
+ writeStoredRaw(JSON.stringify(envelope));
337
+ return;
338
+ }
339
+ writeStoredRaw(serialized);
340
+ };
341
+ const resolveInvalidValue = invalidValue => {
342
+ if (onValidationError) {
343
+ return onValidationError(invalidValue);
344
+ }
345
+ return config.defaultValue;
346
+ };
347
+ const ensureValidatedValue = (candidate, hadStoredValue) => {
348
+ if (!validate || validate(candidate)) {
349
+ return candidate;
350
+ }
351
+ const resolved = resolveInvalidValue(candidate);
352
+ if (validate && !validate(resolved)) {
353
+ return config.defaultValue;
354
+ }
355
+ if (hadStoredValue) {
356
+ writeValueWithoutValidation(resolved);
357
+ }
358
+ return resolved;
359
+ };
360
+ const get = () => {
361
+ const raw = readStoredRaw();
362
+ const canUseCachedValue = !expiration && !memoryExpiration;
363
+ if (canUseCachedValue && raw === lastRaw && hasLastValue) {
79
364
  return lastValue;
80
365
  }
81
366
  lastRaw = raw;
82
367
  if (raw === undefined) {
83
- lastValue = config.defaultValue;
84
- } else {
85
- if (isMemory) {
86
- lastValue = raw;
87
- } else {
88
- lastValue = deserialize(raw);
368
+ lastValue = ensureValidatedValue(config.defaultValue, false);
369
+ hasLastValue = true;
370
+ return lastValue;
371
+ }
372
+ if (isMemory) {
373
+ lastValue = ensureValidatedValue(raw, true);
374
+ hasLastValue = true;
375
+ return lastValue;
376
+ }
377
+ let deserializableRaw = raw;
378
+ if (expiration) {
379
+ try {
380
+ const parsed = JSON.parse(raw);
381
+ if (isStoredEnvelope(parsed)) {
382
+ if (parsed.expiresAt <= Date.now()) {
383
+ removeStoredRaw();
384
+ invalidateParsedCache();
385
+ lastValue = ensureValidatedValue(config.defaultValue, false);
386
+ hasLastValue = true;
387
+ return lastValue;
388
+ }
389
+ deserializableRaw = parsed.payload;
390
+ }
391
+ } catch {
392
+ // Keep backward compatibility with legacy raw values.
89
393
  }
90
394
  }
395
+ lastValue = ensureValidatedValue(deserialize(deserializableRaw), true);
396
+ hasLastValue = true;
91
397
  return lastValue;
92
398
  };
93
399
  const set = valueOrFn => {
94
400
  const currentValue = get();
95
401
  const newValue = typeof valueOrFn === "function" ? valueOrFn(currentValue) : valueOrFn;
96
- if (isMemory) {
97
- memoryStore.set(config.key, newValue);
98
- notifyMemoryListeners(config.key, newValue);
99
- } else {
100
- const serialized = serialize(newValue);
101
- getStorageModule().set(config.key, serialized, config.scope);
402
+ invalidateParsedCache();
403
+ if (validate && !validate(newValue)) {
404
+ throw new Error(`Validation failed for key "${config.key}" in scope "${StorageScope[config.scope]}".`);
102
405
  }
406
+ writeValueWithoutValidation(newValue);
103
407
  };
104
408
  const deleteItem = () => {
409
+ invalidateParsedCache();
105
410
  if (isMemory) {
411
+ if (memoryExpiration) {
412
+ memoryExpiration.delete(config.key);
413
+ }
106
414
  memoryStore.delete(config.key);
107
- notifyMemoryListeners(config.key, undefined);
108
- } else {
109
- getStorageModule().remove(config.key, config.scope);
415
+ notifyKeyListeners(memoryListeners, config.key);
416
+ return;
110
417
  }
418
+ removeStoredRaw();
111
419
  };
112
420
  const subscribe = callback => {
113
421
  ensureSubscription();
@@ -116,11 +424,14 @@ export function createStorageItem(config) {
116
424
  listeners.delete(callback);
117
425
  if (listeners.size === 0 && unsubscribe) {
118
426
  unsubscribe();
427
+ if (!isMemory) {
428
+ maybeCleanupNativeScopeSubscription(nonMemoryScope);
429
+ }
119
430
  unsubscribe = null;
120
431
  }
121
432
  };
122
433
  };
123
- return {
434
+ const storageItem = {
124
435
  get,
125
436
  set,
126
437
  delete: deleteItem,
@@ -128,29 +439,83 @@ export function createStorageItem(config) {
128
439
  serialize,
129
440
  deserialize,
130
441
  _triggerListeners: () => {
131
- lastRaw = undefined;
132
- lastValue = undefined;
133
- listeners.forEach(l => l());
442
+ invalidateParsedCache();
443
+ listeners.forEach(listener => listener());
134
444
  },
445
+ _hasValidation: validate !== undefined,
446
+ _hasExpiration: expiration !== undefined,
447
+ _readCacheEnabled: readCache,
135
448
  scope: config.scope,
136
449
  key: config.key
137
450
  };
451
+ return storageItem;
138
452
  }
139
453
  export function useStorage(item) {
140
454
  const value = useSyncExternalStore(item.subscribe, item.get, item.get);
141
455
  return [value, item.set];
142
456
  }
457
+ export function useStorageSelector(item, selector, isEqual = Object.is) {
458
+ const selectedRef = useRef({
459
+ hasValue: false
460
+ });
461
+ const getSelectedSnapshot = () => {
462
+ const nextSelected = selector(item.get());
463
+ const current = selectedRef.current;
464
+ if (current.hasValue && isEqual(current.value, nextSelected)) {
465
+ return current.value;
466
+ }
467
+ selectedRef.current = {
468
+ hasValue: true,
469
+ value: nextSelected
470
+ };
471
+ return nextSelected;
472
+ };
473
+ const selectedValue = useSyncExternalStore(item.subscribe, getSelectedSnapshot, getSelectedSnapshot);
474
+ return [selectedValue, item.set];
475
+ }
143
476
  export function useSetStorage(item) {
144
477
  return item.set;
145
478
  }
146
479
  export function getBatch(items, scope) {
480
+ assertBatchScope(items, scope);
147
481
  if (scope === StorageScope.Memory) {
148
482
  return items.map(item => item.get());
149
483
  }
150
- const keys = items.map(item => item.key);
151
- const rawValues = getStorageModule().getBatch(keys, scope);
152
- return items.map((item, idx) => {
153
- const raw = rawValues[idx];
484
+ const useRawBatchPath = items.every(item => canUseRawBatchPath(item));
485
+ if (!useRawBatchPath) {
486
+ return items.map(item => item.get());
487
+ }
488
+ const useBatchCache = items.every(item => item._readCacheEnabled === true);
489
+ const rawValues = new Array(items.length);
490
+ const keysToFetch = [];
491
+ const keyIndexes = [];
492
+ items.forEach((item, index) => {
493
+ if (scope === StorageScope.Secure) {
494
+ if (hasPendingSecureWrite(item.key)) {
495
+ rawValues[index] = readPendingSecureWrite(item.key);
496
+ return;
497
+ }
498
+ }
499
+ if (useBatchCache) {
500
+ if (hasCachedRawValue(scope, item.key)) {
501
+ rawValues[index] = readCachedRawValue(scope, item.key);
502
+ return;
503
+ }
504
+ }
505
+ keysToFetch.push(item.key);
506
+ keyIndexes.push(index);
507
+ });
508
+ if (keysToFetch.length > 0) {
509
+ const fetchedValues = getStorageModule().getBatch(keysToFetch, scope).map(value => decodeNativeBatchValue(value));
510
+ fetchedValues.forEach((value, index) => {
511
+ const key = keysToFetch[index];
512
+ const targetIndex = keyIndexes[index];
513
+ rawValues[targetIndex] = value;
514
+ cacheRawValue(scope, key, value);
515
+ });
516
+ }
517
+ return items.map((item, index) => {
518
+ const raw = rawValues[index];
154
519
  if (raw === undefined) {
155
520
  return item.get();
156
521
  }
@@ -158,6 +523,7 @@ export function getBatch(items, scope) {
158
523
  });
159
524
  }
160
525
  export function setBatch(items, scope) {
526
+ assertBatchScope(items.map(batchEntry => batchEntry.item), scope);
161
527
  if (scope === StorageScope.Memory) {
162
528
  items.forEach(({
163
529
  item,
@@ -165,22 +531,117 @@ export function setBatch(items, scope) {
165
531
  }) => item.set(value));
166
532
  return;
167
533
  }
168
- const keys = items.map(i => i.item.key);
169
- const values = items.map(i => i.item.serialize(i.value));
170
- getStorageModule().setBatch(keys, values, scope);
171
- items.forEach(({
534
+ const useRawBatchPath = items.every(({
172
535
  item
173
- }) => {
174
- item._triggerListeners();
175
- });
536
+ }) => canUseRawBatchPath(item));
537
+ if (!useRawBatchPath) {
538
+ items.forEach(({
539
+ item,
540
+ value
541
+ }) => item.set(value));
542
+ return;
543
+ }
544
+ const keys = items.map(entry => entry.item.key);
545
+ const values = items.map(entry => entry.item.serialize(entry.value));
546
+ if (scope === StorageScope.Secure) {
547
+ flushSecureWrites();
548
+ }
549
+ getStorageModule().setBatch(keys, values, scope);
550
+ keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
176
551
  }
177
552
  export function removeBatch(items, scope) {
553
+ assertBatchScope(items, scope);
178
554
  if (scope === StorageScope.Memory) {
179
555
  items.forEach(item => item.delete());
180
556
  return;
181
557
  }
182
558
  const keys = items.map(item => item.key);
559
+ if (scope === StorageScope.Secure) {
560
+ flushSecureWrites();
561
+ }
183
562
  getStorageModule().removeBatch(keys, scope);
184
- items.forEach(item => item.delete());
563
+ keys.forEach(key => cacheRawValue(scope, key, undefined));
564
+ }
565
+ export function registerMigration(version, migration) {
566
+ if (!Number.isInteger(version) || version <= 0) {
567
+ throw new Error("Migration version must be a positive integer.");
568
+ }
569
+ if (registeredMigrations.has(version)) {
570
+ throw new Error(`Migration version ${version} is already registered.`);
571
+ }
572
+ registeredMigrations.set(version, migration);
573
+ }
574
+ export function migrateToLatest(scope = StorageScope.Disk) {
575
+ assertValidScope(scope);
576
+ const currentVersion = readMigrationVersion(scope);
577
+ const versions = Array.from(registeredMigrations.keys()).filter(version => version > currentVersion).sort((a, b) => a - b);
578
+ let appliedVersion = currentVersion;
579
+ const context = {
580
+ scope,
581
+ getRaw: key => getRawValue(key, scope),
582
+ setRaw: (key, value) => setRawValue(key, value, scope),
583
+ removeRaw: key => removeRawValue(key, scope)
584
+ };
585
+ versions.forEach(version => {
586
+ const migration = registeredMigrations.get(version);
587
+ if (!migration) {
588
+ return;
589
+ }
590
+ migration(context);
591
+ writeMigrationVersion(scope, version);
592
+ appliedVersion = version;
593
+ });
594
+ return appliedVersion;
595
+ }
596
+ export function runTransaction(scope, transaction) {
597
+ assertValidScope(scope);
598
+ if (scope === StorageScope.Secure) {
599
+ flushSecureWrites();
600
+ }
601
+ const rollback = new Map();
602
+ const rememberRollback = key => {
603
+ if (rollback.has(key)) {
604
+ return;
605
+ }
606
+ rollback.set(key, getRawValue(key, scope));
607
+ };
608
+ const tx = {
609
+ scope,
610
+ getRaw: key => getRawValue(key, scope),
611
+ setRaw: (key, value) => {
612
+ rememberRollback(key);
613
+ setRawValue(key, value, scope);
614
+ },
615
+ removeRaw: key => {
616
+ rememberRollback(key);
617
+ removeRawValue(key, scope);
618
+ },
619
+ getItem: item => {
620
+ assertBatchScope([item], scope);
621
+ return item.get();
622
+ },
623
+ setItem: (item, value) => {
624
+ assertBatchScope([item], scope);
625
+ rememberRollback(item.key);
626
+ item.set(value);
627
+ },
628
+ removeItem: item => {
629
+ assertBatchScope([item], scope);
630
+ rememberRollback(item.key);
631
+ item.delete();
632
+ }
633
+ };
634
+ try {
635
+ return transaction(tx);
636
+ } catch (error) {
637
+ Array.from(rollback.entries()).reverse().forEach(([key, previousValue]) => {
638
+ if (previousValue === undefined) {
639
+ removeRawValue(key, scope);
640
+ } else {
641
+ setRawValue(key, previousValue, scope);
642
+ }
643
+ });
644
+ throw error;
645
+ }
185
646
  }
186
647
  //# sourceMappingURL=index.js.map