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