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