react-native-nitro-storage 0.4.3 → 0.4.5

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 (41) hide show
  1. package/README.md +108 -8
  2. package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +61 -10
  3. package/ios/IOSStorageAdapterCpp.mm +44 -14
  4. package/lib/commonjs/index.js +221 -5
  5. package/lib/commonjs/index.js.map +1 -1
  6. package/lib/commonjs/index.web.js +444 -202
  7. package/lib/commonjs/index.web.js.map +1 -1
  8. package/lib/commonjs/indexeddb-backend.js +129 -7
  9. package/lib/commonjs/indexeddb-backend.js.map +1 -1
  10. package/lib/commonjs/storage-runtime.js +41 -0
  11. package/lib/commonjs/storage-runtime.js.map +1 -0
  12. package/lib/commonjs/web-storage-backend.js +90 -0
  13. package/lib/commonjs/web-storage-backend.js.map +1 -0
  14. package/lib/module/index.js +213 -5
  15. package/lib/module/index.js.map +1 -1
  16. package/lib/module/index.web.js +436 -202
  17. package/lib/module/index.web.js.map +1 -1
  18. package/lib/module/indexeddb-backend.js +129 -7
  19. package/lib/module/indexeddb-backend.js.map +1 -1
  20. package/lib/module/storage-runtime.js +36 -0
  21. package/lib/module/storage-runtime.js.map +1 -0
  22. package/lib/module/web-storage-backend.js +86 -0
  23. package/lib/module/web-storage-backend.js.map +1 -0
  24. package/lib/typescript/index.d.ts +11 -7
  25. package/lib/typescript/index.d.ts.map +1 -1
  26. package/lib/typescript/index.web.d.ts +12 -8
  27. package/lib/typescript/index.web.d.ts.map +1 -1
  28. package/lib/typescript/indexeddb-backend.d.ts +6 -2
  29. package/lib/typescript/indexeddb-backend.d.ts.map +1 -1
  30. package/lib/typescript/storage-runtime.d.ts +16 -0
  31. package/lib/typescript/storage-runtime.d.ts.map +1 -0
  32. package/lib/typescript/web-storage-backend.d.ts +30 -0
  33. package/lib/typescript/web-storage-backend.d.ts.map +1 -0
  34. package/nitro.json +8 -2
  35. package/nitrogen/generated/ios/NitroStorage+autolinking.rb +2 -0
  36. package/package.json +2 -2
  37. package/src/index.ts +268 -21
  38. package/src/index.web.ts +601 -246
  39. package/src/indexeddb-backend.ts +147 -6
  40. package/src/storage-runtime.ts +94 -0
  41. package/src/web-storage-backend.ts +129 -0
package/src/index.ts CHANGED
@@ -14,10 +14,30 @@ import {
14
14
  prefixKey,
15
15
  isNamespaced,
16
16
  } from "./internal";
17
+ import type {
18
+ WebDiskStorageBackend,
19
+ WebSecureStorageBackend,
20
+ } from "./web-storage-backend";
21
+ import {
22
+ getStorageErrorCode,
23
+ isLockedStorageErrorCode,
24
+ type StorageCapabilities,
25
+ type StorageErrorCode,
26
+ } from "./storage-runtime";
17
27
 
18
28
  export { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
19
29
  export type { Storage } from "./Storage.nitro";
20
30
  export { migrateFromMMKV } from "./migration";
31
+ export {
32
+ getStorageErrorCode,
33
+ type StorageCapabilities,
34
+ type StorageErrorCode,
35
+ } from "./storage-runtime";
36
+ export type {
37
+ WebStorageBackend,
38
+ WebStorageChangeEvent,
39
+ WebStorageScope,
40
+ } from "./web-storage-backend";
21
41
 
22
42
  export type Validator<T> = (value: unknown) => value is T;
23
43
  export type ExpirationConfig = {
@@ -41,14 +61,6 @@ export type StorageMetricSummary = {
41
61
  avgDurationMs: number;
42
62
  maxDurationMs: number;
43
63
  };
44
- export type WebSecureStorageBackend = {
45
- getItem: (key: string) => string | null;
46
- setItem: (key: string, value: string) => void;
47
- removeItem: (key: string) => void;
48
- clear: () => void;
49
- getAllKeys: () => string[];
50
- };
51
-
52
64
  export type MigrationContext = {
53
65
  scope: StorageScope;
54
66
  getRaw: (key: string) => string | undefined;
@@ -95,6 +107,10 @@ function typedKeys<K extends string, V>(record: Record<K, V>): K[] {
95
107
  return Object.keys(record) as K[];
96
108
  }
97
109
  type NonMemoryScope = StorageScope.Disk | StorageScope.Secure;
110
+ type PendingDiskWrite = {
111
+ key: string;
112
+ value: string | undefined;
113
+ };
98
114
  type PendingSecureWrite = {
99
115
  key: string;
100
116
  value: string | undefined;
@@ -108,6 +124,10 @@ const runMicrotask =
108
124
  : (task: () => void) => {
109
125
  Promise.resolve().then(task);
110
126
  };
127
+ const now =
128
+ typeof performance !== "undefined" && typeof performance.now === "function"
129
+ ? () => performance.now()
130
+ : () => Date.now();
111
131
 
112
132
  let _storageModule: Storage | null = null;
113
133
 
@@ -131,6 +151,9 @@ const scopedRawCache = new Map<NonMemoryScope, Map<string, string | undefined>>(
131
151
  [StorageScope.Secure, new Map()],
132
152
  ],
133
153
  );
154
+ const pendingDiskWrites = new Map<string, PendingDiskWrite>();
155
+ let diskFlushScheduled = false;
156
+ let diskWritesAsync = false;
134
157
  const pendingSecureWrites = new Map<string, PendingSecureWrite>();
135
158
  let secureFlushScheduled = false;
136
159
  let secureDefaultAccessControl: AccessControl = AccessControl.WhenUnlocked;
@@ -176,11 +199,11 @@ function measureOperation<T>(
176
199
  if (!metricsObserver) {
177
200
  return fn();
178
201
  }
179
- const start = Date.now();
202
+ const start = now();
180
203
  try {
181
204
  return fn();
182
205
  } finally {
183
- recordMetric(operation, scope, Date.now() - start, keysCount);
206
+ recordMetric(operation, scope, now() - start, keysCount);
184
207
  }
185
208
  }
186
209
 
@@ -262,14 +285,59 @@ function readPendingSecureWrite(key: string): string | undefined {
262
285
  return pendingSecureWrites.get(key)?.value;
263
286
  }
264
287
 
288
+ function readPendingDiskWrite(key: string): string | undefined {
289
+ return pendingDiskWrites.get(key)?.value;
290
+ }
291
+
292
+ function hasPendingDiskWrite(key: string): boolean {
293
+ return pendingDiskWrites.has(key);
294
+ }
295
+
265
296
  function hasPendingSecureWrite(key: string): boolean {
266
297
  return pendingSecureWrites.has(key);
267
298
  }
268
299
 
300
+ function clearPendingDiskWrite(key: string): void {
301
+ pendingDiskWrites.delete(key);
302
+ }
303
+
269
304
  function clearPendingSecureWrite(key: string): void {
270
305
  pendingSecureWrites.delete(key);
271
306
  }
272
307
 
308
+ function flushDiskWrites(): void {
309
+ diskFlushScheduled = false;
310
+
311
+ if (pendingDiskWrites.size === 0) {
312
+ return;
313
+ }
314
+
315
+ const writes = Array.from(pendingDiskWrites.values());
316
+ pendingDiskWrites.clear();
317
+
318
+ const keysToSet: string[] = [];
319
+ const valuesToSet: string[] = [];
320
+ const keysToRemove: string[] = [];
321
+
322
+ writes.forEach(({ key, value }) => {
323
+ if (value === undefined) {
324
+ keysToRemove.push(key);
325
+ return;
326
+ }
327
+
328
+ keysToSet.push(key);
329
+ valuesToSet.push(value);
330
+ });
331
+
332
+ const storageModule = getStorageModule();
333
+ if (keysToSet.length > 0) {
334
+ storageModule.setBatch(keysToSet, valuesToSet, StorageScope.Disk);
335
+ }
336
+ if (keysToRemove.length > 0) {
337
+ storageModule.removeBatch(keysToRemove, StorageScope.Disk);
338
+ }
339
+ }
340
+
273
341
  function flushSecureWrites(): void {
274
342
  secureFlushScheduled = false;
275
343
 
@@ -311,6 +379,15 @@ function flushSecureWrites(): void {
311
379
  }
312
380
  }
313
381
 
382
+ function scheduleDiskWrite(key: string, value: string | undefined): void {
383
+ pendingDiskWrites.set(key, { key, value });
384
+ if (diskFlushScheduled) {
385
+ return;
386
+ }
387
+ diskFlushScheduled = true;
388
+ runMicrotask(flushDiskWrites);
389
+ }
390
+
314
391
  function scheduleSecureWrite(
315
392
  key: string,
316
393
  value: string | undefined,
@@ -334,6 +411,14 @@ function ensureNativeScopeSubscription(scope: NonMemoryScope): void {
334
411
  }
335
412
 
336
413
  const unsubscribe = getStorageModule().addOnChange(scope, (key, value) => {
414
+ if (scope === StorageScope.Disk) {
415
+ if (key === "") {
416
+ pendingDiskWrites.clear();
417
+ } else {
418
+ clearPendingDiskWrite(key);
419
+ }
420
+ }
421
+
337
422
  if (scope === StorageScope.Secure) {
338
423
  if (key === "") {
339
424
  pendingSecureWrites.clear();
@@ -376,6 +461,10 @@ function getRawValue(key: string, scope: StorageScope): string | undefined {
376
461
  return typeof value === "string" ? value : undefined;
377
462
  }
378
463
 
464
+ if (scope === StorageScope.Disk && hasPendingDiskWrite(key)) {
465
+ return readPendingDiskWrite(key);
466
+ }
467
+
379
468
  if (scope === StorageScope.Secure && hasPendingSecureWrite(key)) {
380
469
  return readPendingSecureWrite(key);
381
470
  }
@@ -391,6 +480,17 @@ function setRawValue(key: string, value: string, scope: StorageScope): void {
391
480
  return;
392
481
  }
393
482
 
483
+ if (scope === StorageScope.Disk) {
484
+ cacheRawValue(scope, key, value);
485
+ if (diskWritesAsync) {
486
+ scheduleDiskWrite(key, value);
487
+ return;
488
+ }
489
+
490
+ flushDiskWrites();
491
+ clearPendingDiskWrite(key);
492
+ }
493
+
394
494
  if (scope === StorageScope.Secure) {
395
495
  flushSecureWrites();
396
496
  clearPendingSecureWrite(key);
@@ -409,6 +509,17 @@ function removeRawValue(key: string, scope: StorageScope): void {
409
509
  return;
410
510
  }
411
511
 
512
+ if (scope === StorageScope.Disk) {
513
+ cacheRawValue(scope, key, undefined);
514
+ if (diskWritesAsync) {
515
+ scheduleDiskWrite(key, undefined);
516
+ return;
517
+ }
518
+
519
+ flushDiskWrites();
520
+ clearPendingDiskWrite(key);
521
+ }
522
+
412
523
  if (scope === StorageScope.Secure) {
413
524
  flushSecureWrites();
414
525
  clearPendingSecureWrite(key);
@@ -441,6 +552,11 @@ export const storage = {
441
552
  return;
442
553
  }
443
554
 
555
+ if (scope === StorageScope.Disk) {
556
+ flushDiskWrites();
557
+ pendingDiskWrites.clear();
558
+ }
559
+
444
560
  if (scope === StorageScope.Secure) {
445
561
  flushSecureWrites();
446
562
  pendingSecureWrites.clear();
@@ -476,6 +592,9 @@ export const storage = {
476
592
  }
477
593
 
478
594
  const keyPrefix = prefixKey(namespace, "");
595
+ if (scope === StorageScope.Disk) {
596
+ flushDiskWrites();
597
+ }
479
598
  if (scope === StorageScope.Secure) {
480
599
  flushSecureWrites();
481
600
  }
@@ -500,6 +619,12 @@ export const storage = {
500
619
  if (scope === StorageScope.Memory) {
501
620
  return memoryStore.has(key);
502
621
  }
622
+ if (scope === StorageScope.Disk) {
623
+ flushDiskWrites();
624
+ }
625
+ if (scope === StorageScope.Secure) {
626
+ flushSecureWrites();
627
+ }
503
628
  return getStorageModule().has(key, scope);
504
629
  });
505
630
  },
@@ -509,6 +634,12 @@ export const storage = {
509
634
  if (scope === StorageScope.Memory) {
510
635
  return Array.from(memoryStore.keys());
511
636
  }
637
+ if (scope === StorageScope.Disk) {
638
+ flushDiskWrites();
639
+ }
640
+ if (scope === StorageScope.Secure) {
641
+ flushSecureWrites();
642
+ }
512
643
  return getStorageModule().getAllKeys(scope);
513
644
  });
514
645
  },
@@ -520,6 +651,12 @@ export const storage = {
520
651
  key.startsWith(prefix),
521
652
  );
522
653
  }
654
+ if (scope === StorageScope.Disk) {
655
+ flushDiskWrites();
656
+ }
657
+ if (scope === StorageScope.Secure) {
658
+ flushSecureWrites();
659
+ }
523
660
  return getStorageModule().getKeysByPrefix(prefix, scope);
524
661
  });
525
662
  },
@@ -544,6 +681,12 @@ export const storage = {
544
681
  return result;
545
682
  }
546
683
 
684
+ if (scope === StorageScope.Disk) {
685
+ flushDiskWrites();
686
+ }
687
+ if (scope === StorageScope.Secure) {
688
+ flushSecureWrites();
689
+ }
547
690
  const values = getStorageModule().getBatch(keys, scope);
548
691
  keys.forEach((key, idx) => {
549
692
  const value = decodeNativeBatchValue(values[idx]);
@@ -564,6 +707,12 @@ export const storage = {
564
707
  });
565
708
  return result;
566
709
  }
710
+ if (scope === StorageScope.Disk) {
711
+ flushDiskWrites();
712
+ }
713
+ if (scope === StorageScope.Secure) {
714
+ flushSecureWrites();
715
+ }
567
716
  const keys = getStorageModule().getAllKeys(scope);
568
717
  if (keys.length === 0) return result;
569
718
  const values = getStorageModule().getBatch(keys, scope);
@@ -580,6 +729,12 @@ export const storage = {
580
729
  if (scope === StorageScope.Memory) {
581
730
  return memoryStore.size;
582
731
  }
732
+ if (scope === StorageScope.Disk) {
733
+ flushDiskWrites();
734
+ }
735
+ if (scope === StorageScope.Secure) {
736
+ flushSecureWrites();
737
+ }
583
738
  return getStorageModule().size(scope);
584
739
  });
585
740
  },
@@ -598,6 +753,19 @@ export const storage = {
598
753
  },
599
754
  );
600
755
  },
756
+ setDiskWritesAsync: (enabled: boolean) => {
757
+ measureOperation("storage:setDiskWritesAsync", StorageScope.Disk, () => {
758
+ diskWritesAsync = enabled;
759
+ if (!enabled) {
760
+ flushDiskWrites();
761
+ }
762
+ });
763
+ },
764
+ flushDiskWrites: () => {
765
+ measureOperation("storage:flushDiskWrites", StorageScope.Disk, () => {
766
+ flushDiskWrites();
767
+ });
768
+ },
601
769
  flushSecureWrites: () => {
602
770
  measureOperation("storage:flushSecureWrites", StorageScope.Secure, () => {
603
771
  flushSecureWrites();
@@ -631,6 +799,18 @@ export const storage = {
631
799
  resetMetrics: () => {
632
800
  metricsCounters.clear();
633
801
  },
802
+ getCapabilities: (): StorageCapabilities => ({
803
+ platform: "native",
804
+ backend: {
805
+ disk: "platform-preferences",
806
+ secure: "platform-secure-storage",
807
+ },
808
+ writeBuffering: {
809
+ disk: true,
810
+ secure: true,
811
+ },
812
+ errorClassification: true,
813
+ }),
634
814
  getString: (key: string, scope: StorageScope): string | undefined => {
635
815
  return measureOperation("storage:getString", scope, () => {
636
816
  return getRawValue(key, scope);
@@ -689,6 +869,20 @@ export function getWebSecureStorageBackend():
689
869
  return undefined;
690
870
  }
691
871
 
872
+ export function setWebDiskStorageBackend(
873
+ _backend?: WebDiskStorageBackend,
874
+ ): void {
875
+ // Native platforms do not use web disk backends.
876
+ }
877
+
878
+ export function getWebDiskStorageBackend(): WebDiskStorageBackend | undefined {
879
+ return undefined;
880
+ }
881
+
882
+ export async function flushWebStorageBackends(): Promise<void> {
883
+ // Native platforms do not use web storage backends.
884
+ }
885
+
692
886
  export interface StorageItemConfig<T> {
693
887
  key: string;
694
888
  scope: StorageScope;
@@ -700,6 +894,7 @@ export interface StorageItemConfig<T> {
700
894
  expiration?: ExpirationConfig;
701
895
  onExpired?: (key: string) => void;
702
896
  readCache?: boolean;
897
+ coalesceDiskWrites?: boolean;
703
898
  coalesceSecureWrites?: boolean;
704
899
  namespace?: string;
705
900
  biometric?: boolean;
@@ -784,6 +979,8 @@ export function createStorageItem<T = undefined>(
784
979
  const memoryExpiration =
785
980
  expiration && isMemory ? new Map<string, number>() : null;
786
981
  const readCache = !isMemory && config.readCache === true;
982
+ const coalesceDiskWrites =
983
+ config.scope === StorageScope.Disk && config.coalesceDiskWrites === true;
787
984
  const coalesceSecureWrites =
788
985
  config.scope === StorageScope.Secure &&
789
986
  config.coalesceSecureWrites === true &&
@@ -852,6 +1049,13 @@ export function createStorageItem<T = undefined>(
852
1049
  return memoryStore.get(storageKey);
853
1050
  }
854
1051
 
1052
+ if (nonMemoryScope === StorageScope.Disk) {
1053
+ const pending = pendingDiskWrites.get(storageKey);
1054
+ if (pending !== undefined) {
1055
+ return pending.value;
1056
+ }
1057
+ }
1058
+
855
1059
  if (nonMemoryScope === StorageScope.Secure && !isBiometric) {
856
1060
  const pending = pendingSecureWrites.get(storageKey);
857
1061
  if (pending !== undefined) {
@@ -888,6 +1092,15 @@ export function createStorageItem<T = undefined>(
888
1092
 
889
1093
  cacheRawValue(nonMemoryScope!, storageKey, rawValue);
890
1094
 
1095
+ if (nonMemoryScope === StorageScope.Disk) {
1096
+ if (coalesceDiskWrites || diskWritesAsync) {
1097
+ scheduleDiskWrite(storageKey, rawValue);
1098
+ return;
1099
+ }
1100
+
1101
+ clearPendingDiskWrite(storageKey);
1102
+ }
1103
+
891
1104
  if (coalesceSecureWrites) {
892
1105
  scheduleSecureWrite(
893
1106
  storageKey,
@@ -915,6 +1128,15 @@ export function createStorageItem<T = undefined>(
915
1128
 
916
1129
  cacheRawValue(nonMemoryScope!, storageKey, undefined);
917
1130
 
1131
+ if (nonMemoryScope === StorageScope.Disk) {
1132
+ if (coalesceDiskWrites || diskWritesAsync) {
1133
+ scheduleDiskWrite(storageKey, undefined);
1134
+ return;
1135
+ }
1136
+
1137
+ clearPendingDiskWrite(storageKey);
1138
+ }
1139
+
918
1140
  if (coalesceSecureWrites) {
919
1141
  scheduleSecureWrite(
920
1142
  storageKey,
@@ -1125,6 +1347,18 @@ export function createStorageItem<T = undefined>(
1125
1347
  measureOperation("item:has", config.scope, () => {
1126
1348
  if (isMemory) return memoryStore.has(storageKey);
1127
1349
  if (isBiometric) return getStorageModule().hasSecureBiometric(storageKey);
1350
+ if (nonMemoryScope === StorageScope.Disk) {
1351
+ const pending = pendingDiskWrites.get(storageKey);
1352
+ if (pending !== undefined) {
1353
+ return pending.value !== undefined;
1354
+ }
1355
+ }
1356
+ if (nonMemoryScope === StorageScope.Secure) {
1357
+ const pending = pendingSecureWrites.get(storageKey);
1358
+ if (pending !== undefined) {
1359
+ return pending.value !== undefined;
1360
+ }
1361
+ }
1128
1362
  return getStorageModule().has(storageKey, config.scope);
1129
1363
  });
1130
1364
 
@@ -1224,6 +1458,14 @@ export function getBatch(
1224
1458
  const keyIndexes: number[] = [];
1225
1459
 
1226
1460
  items.forEach((item, index) => {
1461
+ if (scope === StorageScope.Disk) {
1462
+ const pending = pendingDiskWrites.get(item.key);
1463
+ if (pending !== undefined) {
1464
+ rawValues[index] = pending.value;
1465
+ return;
1466
+ }
1467
+ }
1468
+
1227
1469
  if (scope === StorageScope.Secure) {
1228
1470
  const pending = pendingSecureWrites.get(item.key);
1229
1471
  if (pending !== undefined) {
@@ -1353,6 +1595,8 @@ export function setBatch<T>(
1353
1595
  return;
1354
1596
  }
1355
1597
 
1598
+ flushDiskWrites();
1599
+
1356
1600
  const useRawBatchPath = items.every(({ item }) =>
1357
1601
  canUseRawBatchPath(asInternal(item)),
1358
1602
  );
@@ -1387,6 +1631,9 @@ export function removeBatch(
1387
1631
  }
1388
1632
 
1389
1633
  const keys = items.map((item) => item.key);
1634
+ if (scope === StorageScope.Disk) {
1635
+ flushDiskWrites();
1636
+ }
1390
1637
  if (scope === StorageScope.Secure) {
1391
1638
  flushSecureWrites();
1392
1639
  }
@@ -1450,6 +1697,9 @@ export function runTransaction<T>(
1450
1697
  ): T {
1451
1698
  return measureOperation("transaction:run", scope, () => {
1452
1699
  assertValidScope(scope);
1700
+ if (scope === StorageScope.Disk) {
1701
+ flushDiskWrites();
1702
+ }
1453
1703
  if (scope === StorageScope.Secure) {
1454
1704
  flushSecureWrites();
1455
1705
  }
@@ -1462,7 +1712,10 @@ export function runTransaction<T>(
1462
1712
  return;
1463
1713
  }
1464
1714
  if (scope === StorageScope.Memory) {
1465
- rollback.set(key, memoryStore.has(key) ? memoryStore.get(key) : NOT_SET);
1715
+ rollback.set(
1716
+ key,
1717
+ memoryStore.has(key) ? memoryStore.get(key) : NOT_SET,
1718
+ );
1466
1719
  } else {
1467
1720
  rollback.set(key, getRawValue(key, scope));
1468
1721
  }
@@ -1522,6 +1775,9 @@ export function runTransaction<T>(
1522
1775
  }
1523
1776
  });
1524
1777
 
1778
+ if (scope === StorageScope.Disk) {
1779
+ flushDiskWrites();
1780
+ }
1525
1781
  if (scope === StorageScope.Secure) {
1526
1782
  flushSecureWrites();
1527
1783
  }
@@ -1552,16 +1808,7 @@ export type SecureAuthStorageConfig<K extends string = string> = Record<
1552
1808
  >;
1553
1809
 
1554
1810
  export function isKeychainLockedError(err: unknown): boolean {
1555
- if (!(err instanceof Error)) return false;
1556
- const msg = err.message;
1557
- return (
1558
- msg.includes("errSecInteractionNotAllowed") ||
1559
- msg.includes("UserNotAuthenticatedException") ||
1560
- msg.includes("KeyStoreException") ||
1561
- msg.includes("KeyPermanentlyInvalidatedException") ||
1562
- msg.includes("InvalidKeyException") ||
1563
- msg.includes("android.security.keystore")
1564
- );
1811
+ return isLockedStorageErrorCode(getStorageErrorCode(err));
1565
1812
  }
1566
1813
 
1567
1814
  export function createSecureAuthStorage<K extends string>(