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