react-native-nitro-storage 0.3.1 → 0.3.2

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 (37) hide show
  1. package/README.md +199 -10
  2. package/android/CMakeLists.txt +2 -0
  3. package/android/src/main/cpp/AndroidStorageAdapterCpp.cpp +4 -0
  4. package/android/src/main/cpp/AndroidStorageAdapterCpp.hpp +1 -0
  5. package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +36 -13
  6. package/cpp/bindings/HybridStorage.cpp +55 -9
  7. package/cpp/bindings/HybridStorage.hpp +19 -2
  8. package/cpp/core/NativeStorageAdapter.hpp +1 -0
  9. package/ios/IOSStorageAdapterCpp.hpp +1 -0
  10. package/ios/IOSStorageAdapterCpp.mm +7 -1
  11. package/lib/commonjs/index.js +139 -63
  12. package/lib/commonjs/index.js.map +1 -1
  13. package/lib/commonjs/index.web.js +236 -89
  14. package/lib/commonjs/index.web.js.map +1 -1
  15. package/lib/commonjs/storage-hooks.js +36 -0
  16. package/lib/commonjs/storage-hooks.js.map +1 -0
  17. package/lib/module/index.js +121 -60
  18. package/lib/module/index.js.map +1 -1
  19. package/lib/module/index.web.js +219 -87
  20. package/lib/module/index.web.js.map +1 -1
  21. package/lib/module/storage-hooks.js +30 -0
  22. package/lib/module/storage-hooks.js.map +1 -0
  23. package/lib/typescript/Storage.nitro.d.ts +2 -0
  24. package/lib/typescript/Storage.nitro.d.ts.map +1 -1
  25. package/lib/typescript/index.d.ts +3 -3
  26. package/lib/typescript/index.d.ts.map +1 -1
  27. package/lib/typescript/index.web.d.ts +5 -3
  28. package/lib/typescript/index.web.d.ts.map +1 -1
  29. package/lib/typescript/storage-hooks.d.ts +10 -0
  30. package/lib/typescript/storage-hooks.d.ts.map +1 -0
  31. package/nitrogen/generated/shared/c++/HybridStorageSpec.cpp +2 -0
  32. package/nitrogen/generated/shared/c++/HybridStorageSpec.hpp +2 -0
  33. package/package.json +5 -3
  34. package/src/Storage.nitro.ts +2 -0
  35. package/src/index.ts +143 -83
  36. package/src/index.web.ts +255 -112
  37. package/src/storage-hooks.ts +48 -0
package/src/index.ts CHANGED
@@ -1,4 +1,3 @@
1
- import { useRef, useSyncExternalStore } from "react";
2
1
  import { NitroModules } from "react-native-nitro-modules";
3
2
  import type { Storage } from "./Storage.nitro";
4
3
  import { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
@@ -56,8 +55,18 @@ type RawBatchPathItem = {
56
55
  _secureAccessControl?: AccessControl;
57
56
  };
58
57
 
59
- function asInternal(item: StorageItem<any>): StorageItemInternal<any> {
60
- return item as unknown as StorageItemInternal<any>;
58
+ function asInternal<T>(item: StorageItem<T>): StorageItemInternal<T> {
59
+ return item as StorageItemInternal<T>;
60
+ }
61
+
62
+ function isUpdater<T>(
63
+ valueOrFn: T | ((prev: T) => T),
64
+ ): valueOrFn is (prev: T) => T {
65
+ return typeof valueOrFn === "function";
66
+ }
67
+
68
+ function typedKeys<K extends string, V>(record: Record<K, V>): K[] {
69
+ return Object.keys(record) as K[];
61
70
  }
62
71
  type NonMemoryScope = StorageScope.Disk | StorageScope.Secure;
63
72
  type PendingSecureWrite = { key: string; value: string | undefined };
@@ -336,9 +345,6 @@ export const storage = {
336
345
 
337
346
  clearScopeRawCache(scope);
338
347
  getStorageModule().clear(scope);
339
- if (scope === StorageScope.Secure) {
340
- getStorageModule().clearSecureBiometric();
341
- }
342
348
  },
343
349
  clearAll: () => {
344
350
  storage.clear(StorageScope.Memory);
@@ -356,18 +362,14 @@ export const storage = {
356
362
  notifyAllListeners(memoryListeners);
357
363
  return;
358
364
  }
365
+
366
+ const keyPrefix = prefixKey(namespace, "");
359
367
  if (scope === StorageScope.Secure) {
360
368
  flushSecureWrites();
361
369
  }
362
- const keys = getStorageModule().getAllKeys(scope);
363
- const namespacedKeys = keys.filter((k) => isNamespaced(k, namespace));
364
- if (namespacedKeys.length > 0) {
365
- getStorageModule().removeBatch(namespacedKeys, scope);
366
- namespacedKeys.forEach((k) => cacheRawValue(scope, k, undefined));
367
- if (scope === StorageScope.Secure) {
368
- namespacedKeys.forEach((k) => clearPendingSecureWrite(k));
369
- }
370
- }
370
+
371
+ clearScopeRawCache(scope);
372
+ getStorageModule().removeByPrefix(keyPrefix, scope);
371
373
  },
372
374
  clearBiometric: () => {
373
375
  getStorageModule().clearSecureBiometric();
@@ -415,6 +417,12 @@ export const storage = {
415
417
  secureDefaultAccessControl = level;
416
418
  getStorageModule().setSecureAccessControl(level);
417
419
  },
420
+ setSecureWritesAsync: (enabled: boolean) => {
421
+ getStorageModule().setSecureWritesAsync(enabled);
422
+ },
423
+ flushSecureWrites: () => {
424
+ flushSecureWrites();
425
+ },
418
426
  setKeychainAccessGroup: (group: string) => {
419
427
  getStorageModule().setKeychainAccessGroup(group);
420
428
  },
@@ -467,6 +475,14 @@ function canUseRawBatchPath(item: RawBatchPathItem): boolean {
467
475
  );
468
476
  }
469
477
 
478
+ function canUseSecureRawBatchPath(item: RawBatchPathItem): boolean {
479
+ return (
480
+ item._hasExpiration === false &&
481
+ item._hasValidation === false &&
482
+ item._isBiometric !== true
483
+ );
484
+ }
485
+
470
486
  function defaultSerialize<T>(value: T): string {
471
487
  return serializeWithPrimitiveFastPath(value);
472
488
  }
@@ -498,6 +514,7 @@ export function createStorageItem<T = undefined>(
498
514
  config.coalesceSecureWrites === true &&
499
515
  !isBiometric &&
500
516
  secureAccessControl === undefined;
517
+ const defaultValue = config.defaultValue as T;
501
518
  const nonMemoryScope: NonMemoryScope | null =
502
519
  config.scope === StorageScope.Disk
503
520
  ? StorageScope.Disk
@@ -514,11 +531,13 @@ export function createStorageItem<T = undefined>(
514
531
  let lastRaw: unknown = undefined;
515
532
  let lastValue: T | undefined;
516
533
  let hasLastValue = false;
534
+ let lastExpiresAt: number | null | undefined = undefined;
517
535
 
518
536
  const invalidateParsedCache = () => {
519
537
  lastRaw = undefined;
520
538
  lastValue = undefined;
521
539
  hasLastValue = false;
540
+ lastExpiresAt = undefined;
522
541
  };
523
542
 
524
543
  const ensureSubscription = () => {
@@ -556,7 +575,7 @@ export function createStorageItem<T = undefined>(
556
575
  return undefined;
557
576
  }
558
577
  }
559
- return memoryStore.get(storageKey) as T | undefined;
578
+ return memoryStore.get(storageKey);
560
579
  }
561
580
 
562
581
  if (
@@ -654,7 +673,7 @@ export function createStorageItem<T = undefined>(
654
673
  return onValidationError(invalidValue);
655
674
  }
656
675
 
657
- return config.defaultValue as T;
676
+ return defaultValue;
658
677
  };
659
678
 
660
679
  const ensureValidatedValue = (
@@ -667,7 +686,7 @@ export function createStorageItem<T = undefined>(
667
686
 
668
687
  const resolved = resolveInvalidValue(candidate);
669
688
  if (validate && !validate(resolved)) {
670
- return config.defaultValue as T;
689
+ return defaultValue;
671
690
  }
672
691
  if (hadStoredValue) {
673
692
  writeValueWithoutValidation(resolved);
@@ -678,36 +697,61 @@ export function createStorageItem<T = undefined>(
678
697
  const get = (): T => {
679
698
  const raw = readStoredRaw();
680
699
 
681
- const canUseCachedValue = !expiration && !memoryExpiration;
682
- if (canUseCachedValue && raw === lastRaw && hasLastValue) {
683
- return lastValue as T;
700
+ if (!memoryExpiration && raw === lastRaw && hasLastValue) {
701
+ if (!expiration || lastExpiresAt === null) {
702
+ return lastValue as T;
703
+ }
704
+
705
+ if (typeof lastExpiresAt === "number") {
706
+ if (lastExpiresAt > Date.now()) {
707
+ return lastValue as T;
708
+ }
709
+
710
+ removeStoredRaw();
711
+ invalidateParsedCache();
712
+ onExpired?.(storageKey);
713
+ lastValue = ensureValidatedValue(defaultValue, false);
714
+ hasLastValue = true;
715
+ return lastValue;
716
+ }
684
717
  }
685
718
 
686
719
  lastRaw = raw;
687
720
 
688
721
  if (raw === undefined) {
689
- lastValue = ensureValidatedValue(config.defaultValue, false);
722
+ lastExpiresAt = undefined;
723
+ lastValue = ensureValidatedValue(defaultValue, false);
690
724
  hasLastValue = true;
691
725
  return lastValue;
692
726
  }
693
727
 
694
728
  if (isMemory) {
729
+ lastExpiresAt = undefined;
695
730
  lastValue = ensureValidatedValue(raw, true);
696
731
  hasLastValue = true;
697
732
  return lastValue;
698
733
  }
699
734
 
700
- let deserializableRaw = raw as string;
735
+ if (typeof raw !== "string") {
736
+ lastExpiresAt = undefined;
737
+ lastValue = ensureValidatedValue(defaultValue, false);
738
+ hasLastValue = true;
739
+ return lastValue;
740
+ }
741
+
742
+ let deserializableRaw = raw;
701
743
 
702
744
  if (expiration) {
745
+ let envelopeExpiresAt: number | null = null;
703
746
  try {
704
- const parsed = JSON.parse(raw as string) as unknown;
747
+ const parsed = JSON.parse(raw) as unknown;
705
748
  if (isStoredEnvelope(parsed)) {
749
+ envelopeExpiresAt = parsed.expiresAt;
706
750
  if (parsed.expiresAt <= Date.now()) {
707
751
  removeStoredRaw();
708
752
  invalidateParsedCache();
709
753
  onExpired?.(storageKey);
710
- lastValue = ensureValidatedValue(config.defaultValue, false);
754
+ lastValue = ensureValidatedValue(defaultValue, false);
711
755
  hasLastValue = true;
712
756
  return lastValue;
713
757
  }
@@ -717,6 +761,9 @@ export function createStorageItem<T = undefined>(
717
761
  } catch {
718
762
  // Keep backward compatibility with legacy raw values.
719
763
  }
764
+ lastExpiresAt = envelopeExpiresAt;
765
+ } else {
766
+ lastExpiresAt = undefined;
720
767
  }
721
768
 
722
769
  lastValue = ensureValidatedValue(deserialize(deserializableRaw), true);
@@ -725,11 +772,7 @@ export function createStorageItem<T = undefined>(
725
772
  };
726
773
 
727
774
  const set = (valueOrFn: T | ((prev: T) => T)): void => {
728
- const currentValue = get();
729
- const newValue =
730
- typeof valueOrFn === "function"
731
- ? (valueOrFn as (prev: T) => T)(currentValue)
732
- : valueOrFn;
775
+ const newValue = isUpdater(valueOrFn) ? valueOrFn(get()) : valueOrFn;
733
776
 
734
777
  invalidateParsedCache();
735
778
 
@@ -794,54 +837,17 @@ export function createStorageItem<T = undefined>(
794
837
  _hasExpiration: expiration !== undefined,
795
838
  _readCacheEnabled: readCache,
796
839
  _isBiometric: isBiometric,
797
- _secureAccessControl: secureAccessControl,
840
+ ...(secureAccessControl !== undefined
841
+ ? { _secureAccessControl: secureAccessControl }
842
+ : {}),
798
843
  scope: config.scope,
799
844
  key: storageKey,
800
845
  };
801
846
 
802
- return storageItem as StorageItem<T>;
803
- }
804
-
805
- export function useStorage<T>(
806
- item: StorageItem<T>,
807
- ): [T, (value: T | ((prev: T) => T)) => void] {
808
- const value = useSyncExternalStore(item.subscribe, item.get, item.get);
809
- return [value, item.set];
810
- }
811
-
812
- export function useStorageSelector<T, TSelected>(
813
- item: StorageItem<T>,
814
- selector: (value: T) => TSelected,
815
- isEqual: (prev: TSelected, next: TSelected) => boolean = Object.is,
816
- ): [TSelected, (value: T | ((prev: T) => T)) => void] {
817
- const selectedRef = useRef<
818
- { hasValue: false } | { hasValue: true; value: TSelected }
819
- >({
820
- hasValue: false,
821
- });
822
-
823
- const getSelectedSnapshot = () => {
824
- const nextSelected = selector(item.get());
825
- const current = selectedRef.current;
826
- if (current.hasValue && isEqual(current.value, nextSelected)) {
827
- return current.value;
828
- }
829
-
830
- selectedRef.current = { hasValue: true, value: nextSelected };
831
- return nextSelected;
832
- };
833
-
834
- const selectedValue = useSyncExternalStore(
835
- item.subscribe,
836
- getSelectedSnapshot,
837
- getSelectedSnapshot,
838
- );
839
- return [selectedValue, item.set];
847
+ return storageItem;
840
848
  }
841
849
 
842
- export function useSetStorage<T>(item: StorageItem<T>) {
843
- return item.set;
844
- }
850
+ export { useStorage, useStorageSelector, useSetStorage } from "./storage-hooks";
845
851
 
846
852
  type BatchReadItem<T> = Pick<
847
853
  StorageItem<T>,
@@ -870,7 +876,11 @@ export function getBatch(
870
876
  return items.map((item) => item.get());
871
877
  }
872
878
 
873
- const useRawBatchPath = items.every((item) => canUseRawBatchPath(item));
879
+ const useRawBatchPath = items.every((item) =>
880
+ scope === StorageScope.Secure
881
+ ? canUseSecureRawBatchPath(item)
882
+ : canUseRawBatchPath(item),
883
+ );
874
884
  if (!useRawBatchPath) {
875
885
  return items.map((item) => item.get());
876
886
  }
@@ -907,6 +917,9 @@ export function getBatch(
907
917
  fetchedValues.forEach((value, index) => {
908
918
  const key = keysToFetch[index];
909
919
  const targetIndex = keyIndexes[index];
920
+ if (key === undefined || targetIndex === undefined) {
921
+ return;
922
+ }
910
923
  rawValues[targetIndex] = value;
911
924
  cacheRawValue(scope, key, value);
912
925
  });
@@ -935,6 +948,49 @@ export function setBatch<T>(
935
948
  return;
936
949
  }
937
950
 
951
+ if (scope === StorageScope.Secure) {
952
+ const secureEntries = items.map(({ item, value }) => ({
953
+ item,
954
+ value,
955
+ internal: asInternal(item),
956
+ }));
957
+ const canUseSecureBatchPath = secureEntries.every(({ internal }) =>
958
+ canUseSecureRawBatchPath(internal),
959
+ );
960
+ if (!canUseSecureBatchPath) {
961
+ items.forEach(({ item, value }) => item.set(value));
962
+ return;
963
+ }
964
+
965
+ flushSecureWrites();
966
+ const storageModule = getStorageModule();
967
+ const groupedByAccessControl = new Map<
968
+ number,
969
+ { keys: string[]; values: string[] }
970
+ >();
971
+
972
+ secureEntries.forEach(({ item, value, internal }) => {
973
+ const accessControl =
974
+ internal._secureAccessControl ?? secureDefaultAccessControl;
975
+ const existingGroup = groupedByAccessControl.get(accessControl);
976
+ const group = existingGroup ?? { keys: [], values: [] };
977
+ group.keys.push(item.key);
978
+ group.values.push(item.serialize(value));
979
+ if (!existingGroup) {
980
+ groupedByAccessControl.set(accessControl, group);
981
+ }
982
+ });
983
+
984
+ groupedByAccessControl.forEach((group, accessControl) => {
985
+ storageModule.setSecureAccessControl(accessControl);
986
+ storageModule.setBatch(group.keys, group.values, scope);
987
+ group.keys.forEach((key, index) =>
988
+ cacheRawValue(scope, key, group.values[index]),
989
+ );
990
+ });
991
+ return;
992
+ }
993
+
938
994
  const useRawBatchPath = items.every(({ item }) =>
939
995
  canUseRawBatchPath(asInternal(item)),
940
996
  );
@@ -946,10 +1002,6 @@ export function setBatch<T>(
946
1002
  const keys = items.map((entry) => entry.item.key);
947
1003
  const values = items.map((entry) => entry.item.serialize(entry.value));
948
1004
 
949
- if (scope === StorageScope.Secure) {
950
- flushSecureWrites();
951
- getStorageModule().setSecureAccessControl(secureDefaultAccessControl);
952
- }
953
1005
  getStorageModule().setBatch(keys, values, scope);
954
1006
  keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
955
1007
  }
@@ -1090,20 +1142,28 @@ export function createSecureAuthStorage<K extends string>(
1090
1142
  options?: { namespace?: string },
1091
1143
  ): Record<K, StorageItem<string>> {
1092
1144
  const ns = options?.namespace ?? "auth";
1093
- const result = {} as Record<K, StorageItem<string>>;
1145
+ const result: Partial<Record<K, StorageItem<string>>> = {};
1094
1146
 
1095
- for (const key of Object.keys(config) as K[]) {
1147
+ for (const key of typedKeys(config)) {
1096
1148
  const itemConfig = config[key];
1149
+ const expirationConfig =
1150
+ itemConfig.ttlMs !== undefined ? { ttlMs: itemConfig.ttlMs } : undefined;
1097
1151
  result[key] = createStorageItem<string>({
1098
1152
  key,
1099
1153
  scope: StorageScope.Secure,
1100
1154
  defaultValue: "",
1101
1155
  namespace: ns,
1102
- biometric: itemConfig.biometric,
1103
- accessControl: itemConfig.accessControl,
1104
- expiration: itemConfig.ttlMs ? { ttlMs: itemConfig.ttlMs } : undefined,
1156
+ ...(itemConfig.biometric !== undefined
1157
+ ? { biometric: itemConfig.biometric }
1158
+ : {}),
1159
+ ...(itemConfig.accessControl !== undefined
1160
+ ? { accessControl: itemConfig.accessControl }
1161
+ : {}),
1162
+ ...(expirationConfig !== undefined
1163
+ ? { expiration: expirationConfig }
1164
+ : {}),
1105
1165
  });
1106
1166
  }
1107
1167
 
1108
- return result;
1168
+ return result as Record<K, StorageItem<string>>;
1109
1169
  }