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