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.
- package/README.md +199 -10
- package/android/CMakeLists.txt +2 -0
- package/android/src/main/cpp/AndroidStorageAdapterCpp.cpp +4 -0
- package/android/src/main/cpp/AndroidStorageAdapterCpp.hpp +1 -0
- package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +36 -13
- package/cpp/bindings/HybridStorage.cpp +55 -9
- package/cpp/bindings/HybridStorage.hpp +19 -2
- package/cpp/core/NativeStorageAdapter.hpp +1 -0
- package/ios/IOSStorageAdapterCpp.hpp +1 -0
- package/ios/IOSStorageAdapterCpp.mm +7 -1
- package/lib/commonjs/index.js +139 -63
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +236 -89
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/storage-hooks.js +36 -0
- package/lib/commonjs/storage-hooks.js.map +1 -0
- package/lib/module/index.js +121 -60
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +219 -87
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/storage-hooks.js +30 -0
- package/lib/module/storage-hooks.js.map +1 -0
- package/lib/typescript/Storage.nitro.d.ts +2 -0
- package/lib/typescript/Storage.nitro.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +3 -3
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/index.web.d.ts +5 -3
- package/lib/typescript/index.web.d.ts.map +1 -1
- package/lib/typescript/storage-hooks.d.ts +10 -0
- package/lib/typescript/storage-hooks.d.ts.map +1 -0
- package/nitrogen/generated/shared/c++/HybridStorageSpec.cpp +2 -0
- package/nitrogen/generated/shared/c++/HybridStorageSpec.hpp +2 -0
- package/package.json +5 -3
- package/src/Storage.nitro.ts +2 -0
- package/src/index.ts +143 -83
- package/src/index.web.ts +255 -112
- 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<
|
|
60
|
-
return item as
|
|
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
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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)
|
|
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
|
|
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
|
|
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
|
-
|
|
682
|
-
|
|
683
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
|
|
840
|
+
...(secureAccessControl !== undefined
|
|
841
|
+
? { _secureAccessControl: secureAccessControl }
|
|
842
|
+
: {}),
|
|
798
843
|
scope: config.scope,
|
|
799
844
|
key: storageKey,
|
|
800
845
|
};
|
|
801
846
|
|
|
802
|
-
return storageItem
|
|
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
|
|
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) =>
|
|
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
|
|
1145
|
+
const result: Partial<Record<K, StorageItem<string>>> = {};
|
|
1094
1146
|
|
|
1095
|
-
for (const key of
|
|
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
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
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
|
}
|