react-native-nitro-storage 0.1.4 → 0.3.1

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