react-native-nitro-storage 0.5.9 → 0.6.0
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/CHANGELOG.md +28 -0
- package/README.md +171 -7
- package/cpp/bindings/HybridStorage.cpp +25 -9
- package/cpp/bindings/HybridStorage.hpp +5 -0
- package/lib/commonjs/index.js +19 -2
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +19 -2
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/shared.js.map +1 -1
- package/lib/commonjs/storage-core.js +306 -10
- package/lib/commonjs/storage-core.js.map +1 -1
- package/lib/commonjs/storage-events.js.map +1 -1
- package/lib/commonjs/storage-hooks.js +16 -1
- package/lib/commonjs/storage-hooks.js.map +1 -1
- package/lib/commonjs/testing.js +267 -0
- package/lib/commonjs/testing.js.map +1 -0
- package/lib/module/index.js +5 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +5 -1
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/shared.js.map +1 -1
- package/lib/module/storage-core.js +307 -11
- package/lib/module/storage-core.js.map +1 -1
- package/lib/module/storage-events.js.map +1 -1
- package/lib/module/storage-hooks.js +15 -2
- package/lib/module/storage-hooks.js.map +1 -1
- package/lib/module/testing.js +188 -0
- package/lib/module/testing.js.map +1 -0
- package/lib/typescript/index.d.ts +22 -4
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/index.web.d.ts +22 -4
- package/lib/typescript/index.web.d.ts.map +1 -1
- package/lib/typescript/shared.d.ts +1 -0
- package/lib/typescript/shared.d.ts.map +1 -1
- package/lib/typescript/storage-core.d.ts +51 -2
- package/lib/typescript/storage-core.d.ts.map +1 -1
- package/lib/typescript/storage-events.d.ts +1 -1
- package/lib/typescript/storage-events.d.ts.map +1 -1
- package/lib/typescript/storage-hooks.d.ts +18 -1
- package/lib/typescript/storage-hooks.d.ts.map +1 -1
- package/lib/typescript/testing.d.ts +148 -0
- package/lib/typescript/testing.d.ts.map +1 -0
- package/package.json +7 -1
- package/src/index.ts +16 -2
- package/src/index.web.ts +16 -2
- package/src/shared.ts +1 -0
- package/src/storage-core.ts +446 -11
- package/src/storage-events.ts +2 -0
- package/src/storage-hooks.ts +42 -3
- package/src/testing.ts +270 -0
package/src/index.web.ts
CHANGED
|
@@ -57,7 +57,7 @@ export type {
|
|
|
57
57
|
StorageEventListener,
|
|
58
58
|
StorageKeyChangeEvent,
|
|
59
59
|
} from "./storage-events";
|
|
60
|
-
export type { StorageSetter } from "./storage-hooks";
|
|
60
|
+
export type { StorageActions, StorageSetter } from "./storage-hooks";
|
|
61
61
|
export type {
|
|
62
62
|
WebDiskStorageBackend,
|
|
63
63
|
WebSecureStorageBackend,
|
|
@@ -66,9 +66,13 @@ export type {
|
|
|
66
66
|
WebStorageScope,
|
|
67
67
|
} from "./web-storage-backend";
|
|
68
68
|
export type {
|
|
69
|
+
SetItemConfig,
|
|
70
|
+
SetStorageItem,
|
|
69
71
|
StorageBatchSetItem,
|
|
72
|
+
StorageClearOptions,
|
|
70
73
|
StorageItem,
|
|
71
74
|
StorageItemConfig,
|
|
75
|
+
StorageKeyRef,
|
|
72
76
|
TransactionContext,
|
|
73
77
|
} from "./storage-core";
|
|
74
78
|
|
|
@@ -818,6 +822,10 @@ export const storage = {
|
|
|
818
822
|
};
|
|
819
823
|
|
|
820
824
|
export const createStorageItem = core.createStorageItem;
|
|
825
|
+
export const memoryItem = core.memoryItem;
|
|
826
|
+
export const diskItem = core.diskItem;
|
|
827
|
+
export const secureItem = core.secureItem;
|
|
828
|
+
export const createSetItem = core.createSetItem;
|
|
821
829
|
export const getBatch = core.getBatch;
|
|
822
830
|
export const setBatch = core.setBatch;
|
|
823
831
|
export const removeBatch = core.removeBatch;
|
|
@@ -886,5 +894,11 @@ export async function flushWebStorageBackends(): Promise<void> {
|
|
|
886
894
|
await Promise.all(flushes);
|
|
887
895
|
}
|
|
888
896
|
|
|
889
|
-
export {
|
|
897
|
+
export {
|
|
898
|
+
useSetStorage,
|
|
899
|
+
useStorage,
|
|
900
|
+
useStorageActions,
|
|
901
|
+
useStorageSelector,
|
|
902
|
+
useStorageValue,
|
|
903
|
+
} from "./storage-hooks";
|
|
890
904
|
export { createIndexedDBBackend } from "./indexeddb-backend";
|
package/src/shared.ts
CHANGED
package/src/storage-core.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
createKeyChange,
|
|
7
7
|
defaultDeserialize,
|
|
8
8
|
defaultSerialize,
|
|
9
|
+
isKeychainLockedError,
|
|
9
10
|
isUpdater,
|
|
10
11
|
notifyAllListeners,
|
|
11
12
|
notifyKeyListeners,
|
|
@@ -87,6 +88,10 @@ export type StorageItemConfig<T> = {
|
|
|
87
88
|
biometric?: boolean;
|
|
88
89
|
biometricLevel?: BiometricLevel;
|
|
89
90
|
accessControl?: AccessControl;
|
|
91
|
+
group?: string;
|
|
92
|
+
renameFrom?: string | readonly string[];
|
|
93
|
+
fallbackToCacheOnReadError?: boolean;
|
|
94
|
+
onReadError?: (error: unknown) => void;
|
|
90
95
|
};
|
|
91
96
|
|
|
92
97
|
export type StorageItem<T> = {
|
|
@@ -97,6 +102,9 @@ export type StorageItem<T> = {
|
|
|
97
102
|
version: StorageVersion,
|
|
98
103
|
value: T | ((prev: T) => T),
|
|
99
104
|
) => boolean;
|
|
105
|
+
merge: (partial: Partial<T>) => void;
|
|
106
|
+
reset: () => void;
|
|
107
|
+
setOrDelete: (value: T | null | undefined) => void;
|
|
100
108
|
delete: () => void;
|
|
101
109
|
has: () => boolean;
|
|
102
110
|
subscribe: (callback: () => void) => () => void;
|
|
@@ -121,6 +129,7 @@ type StorageItemInternal<T> = StorageItem<T> & {
|
|
|
121
129
|
_biometricLevel: BiometricLevel;
|
|
122
130
|
_defaultValue: T;
|
|
123
131
|
_secureAccessControl?: AccessControl;
|
|
132
|
+
_group?: string;
|
|
124
133
|
};
|
|
125
134
|
|
|
126
135
|
export type BatchReadItem<T> = Pick<
|
|
@@ -149,6 +158,35 @@ export type StorageBatchSetItem<T> = {
|
|
|
149
158
|
value: T;
|
|
150
159
|
};
|
|
151
160
|
|
|
161
|
+
export type StorageKeyRef = string | { readonly key: string };
|
|
162
|
+
|
|
163
|
+
export type StorageClearOptions = {
|
|
164
|
+
except?: readonly StorageKeyRef[];
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
export type SetItemConfig<TMember extends string = string> = Omit<
|
|
168
|
+
StorageItemConfig<Record<string, true>>,
|
|
169
|
+
"defaultValue" | "serialize" | "deserialize"
|
|
170
|
+
> & {
|
|
171
|
+
defaultValue?: readonly TMember[];
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
export type SetStorageItem<TMember extends string = string> = {
|
|
175
|
+
get: () => Record<string, true>;
|
|
176
|
+
has: (id: TMember) => boolean;
|
|
177
|
+
add: (id: TMember) => void;
|
|
178
|
+
delete: (id: TMember) => void;
|
|
179
|
+
toggle: (id: TMember) => boolean;
|
|
180
|
+
values: () => TMember[];
|
|
181
|
+
size: () => number;
|
|
182
|
+
clear: () => void;
|
|
183
|
+
reset: () => void;
|
|
184
|
+
subscribe: (callback: () => void) => () => void;
|
|
185
|
+
scope: StorageScope;
|
|
186
|
+
key: string;
|
|
187
|
+
item: StorageItem<Record<string, true>>;
|
|
188
|
+
};
|
|
189
|
+
|
|
152
190
|
export type StorageCoreBackend = {
|
|
153
191
|
get(key: string, scope: StorageScope): string | undefined;
|
|
154
192
|
set(key: string, value: string, scope: StorageScope): void;
|
|
@@ -234,6 +272,8 @@ export type StorageCoreInternals = {
|
|
|
234
272
|
setSecureDefaultAccessControl(level: AccessControl): void;
|
|
235
273
|
};
|
|
236
274
|
|
|
275
|
+
const EMPTY_KEYS: readonly string[] = Object.freeze([]);
|
|
276
|
+
|
|
237
277
|
function asInternal<T>(item: StorageItem<T>): StorageItemInternal<T> {
|
|
238
278
|
return item as StorageItemInternal<T>;
|
|
239
279
|
}
|
|
@@ -242,6 +282,8 @@ export function createStorageCore(
|
|
|
242
282
|
buildAdapter: (internals: StorageCoreInternals) => StorageCoreAdapter,
|
|
243
283
|
) {
|
|
244
284
|
const registeredMigrations = new Map<number, Migration>();
|
|
285
|
+
const itemGroups = new Map<string, Set<StorageItemInternal<unknown>>>();
|
|
286
|
+
const registeredKeyCounts = new Map<string, number>();
|
|
245
287
|
const memoryStore = new Map<string, unknown>();
|
|
246
288
|
const memoryListeners: KeyListenerRegistry = new Map();
|
|
247
289
|
const scopedListeners = new Map<NonMemoryScope, KeyListenerRegistry>([
|
|
@@ -723,6 +765,84 @@ export function createStorageCore(
|
|
|
723
765
|
|
|
724
766
|
const adapter = buildAdapter(internals);
|
|
725
767
|
|
|
768
|
+
function resolveKeyRef(ref: StorageKeyRef): string {
|
|
769
|
+
return typeof ref === "string" ? ref : ref.key;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
function clearScopeExcept(
|
|
773
|
+
scope: StorageScope,
|
|
774
|
+
exceptRefs: readonly StorageKeyRef[],
|
|
775
|
+
): void {
|
|
776
|
+
measureOperation("storage:clearExcept", scope, () => {
|
|
777
|
+
assertValidScope(scope);
|
|
778
|
+
const keep = new Set(exceptRefs.map(resolveKeyRef));
|
|
779
|
+
|
|
780
|
+
if (scope === StorageScope.Memory) {
|
|
781
|
+
const removeKeys = Array.from(memoryStore.keys()).filter(
|
|
782
|
+
(key) => !keep.has(key),
|
|
783
|
+
);
|
|
784
|
+
if (removeKeys.length === 0) {
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
const previousValues = shouldReadPreviousEventValues(scope)
|
|
788
|
+
? removeKeys.map((key) => getEventRawValue(scope, key))
|
|
789
|
+
: [];
|
|
790
|
+
removeKeys.forEach((key) => memoryStore.delete(key));
|
|
791
|
+
removeKeys.forEach((key) => notifyKeyListeners(memoryListeners, key));
|
|
792
|
+
emitBatchChange(
|
|
793
|
+
scope,
|
|
794
|
+
"clear",
|
|
795
|
+
"memory",
|
|
796
|
+
removeKeys.map((key, index) =>
|
|
797
|
+
createKeyChange(
|
|
798
|
+
scope,
|
|
799
|
+
key,
|
|
800
|
+
previousValues[index],
|
|
801
|
+
undefined,
|
|
802
|
+
"clear",
|
|
803
|
+
"memory",
|
|
804
|
+
),
|
|
805
|
+
),
|
|
806
|
+
);
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
if (scope === StorageScope.Disk) {
|
|
811
|
+
flushDiskWrites();
|
|
812
|
+
}
|
|
813
|
+
if (scope === StorageScope.Secure) {
|
|
814
|
+
flushSecureWrites();
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
const removeKeys = adapter.backend
|
|
818
|
+
.getAllKeys(scope)
|
|
819
|
+
.filter((key) => !keep.has(key));
|
|
820
|
+
if (removeKeys.length === 0) {
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
const previousValues = shouldReadPreviousEventValues(scope)
|
|
824
|
+
? adapter.backend.getBatch(removeKeys, scope)
|
|
825
|
+
: [];
|
|
826
|
+
adapter.backend.removeBatch(removeKeys, scope);
|
|
827
|
+
removeKeys.forEach((key) => cacheRawValue(scope, key, undefined));
|
|
828
|
+
emitBatchChange(
|
|
829
|
+
scope,
|
|
830
|
+
"clear",
|
|
831
|
+
adapter.changeSource,
|
|
832
|
+
removeKeys.map((key, index) =>
|
|
833
|
+
createKeyChange(
|
|
834
|
+
scope,
|
|
835
|
+
key,
|
|
836
|
+
previousValues[index],
|
|
837
|
+
undefined,
|
|
838
|
+
"clear",
|
|
839
|
+
adapter.changeSource,
|
|
840
|
+
),
|
|
841
|
+
),
|
|
842
|
+
);
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
|
|
726
846
|
const storage = {
|
|
727
847
|
subscribe: (
|
|
728
848
|
scope: StorageScope,
|
|
@@ -796,7 +916,11 @@ export function createStorageCore(
|
|
|
796
916
|
adapter.maybeCleanupScopeSubscription(StorageScope.Disk);
|
|
797
917
|
adapter.maybeCleanupScopeSubscription(StorageScope.Secure);
|
|
798
918
|
},
|
|
799
|
-
clear: (scope: StorageScope) => {
|
|
919
|
+
clear: (scope: StorageScope, options?: StorageClearOptions) => {
|
|
920
|
+
if (options?.except && options.except.length > 0) {
|
|
921
|
+
clearScopeExcept(scope, options.except);
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
800
924
|
measureOperation("storage:clear", scope, () => {
|
|
801
925
|
const previousValues = shouldReadPreviousEventValues(scope)
|
|
802
926
|
? storage.getAll(scope)
|
|
@@ -863,6 +987,87 @@ export function createStorageCore(
|
|
|
863
987
|
3,
|
|
864
988
|
);
|
|
865
989
|
},
|
|
990
|
+
clearGroup: (group: string) => {
|
|
991
|
+
const items = itemGroups.get(group);
|
|
992
|
+
if (!items || items.size === 0) {
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
measureOperation(
|
|
996
|
+
"storage:clearGroup",
|
|
997
|
+
StorageScope.Memory,
|
|
998
|
+
() => {
|
|
999
|
+
const byScope = new Map<
|
|
1000
|
+
StorageScope,
|
|
1001
|
+
StorageItemInternal<unknown>[]
|
|
1002
|
+
>();
|
|
1003
|
+
for (const item of items) {
|
|
1004
|
+
const scoped = byScope.get(item.scope);
|
|
1005
|
+
if (scoped) {
|
|
1006
|
+
scoped.push(item);
|
|
1007
|
+
} else {
|
|
1008
|
+
byScope.set(item.scope, [item]);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
byScope.forEach((groupItems, scope) => {
|
|
1012
|
+
removeBatch(groupItems, scope);
|
|
1013
|
+
});
|
|
1014
|
+
},
|
|
1015
|
+
items.size,
|
|
1016
|
+
);
|
|
1017
|
+
},
|
|
1018
|
+
getGroupItems: (group: string): StorageItem<unknown>[] => {
|
|
1019
|
+
const items = itemGroups.get(group);
|
|
1020
|
+
return items ? Array.from(items) : [];
|
|
1021
|
+
},
|
|
1022
|
+
subscribeExpired: (
|
|
1023
|
+
scope: StorageScope,
|
|
1024
|
+
listener: (event: StorageKeyChangeEvent) => void,
|
|
1025
|
+
): (() => void) => {
|
|
1026
|
+
return storage.subscribe(scope, (event) => {
|
|
1027
|
+
if (event.type === "key") {
|
|
1028
|
+
if (event.operation === "expire") {
|
|
1029
|
+
listener(event);
|
|
1030
|
+
}
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
event.changes.forEach((change) => {
|
|
1034
|
+
if (change.operation === "expire") {
|
|
1035
|
+
listener(change);
|
|
1036
|
+
}
|
|
1037
|
+
});
|
|
1038
|
+
});
|
|
1039
|
+
},
|
|
1040
|
+
findDuplicateKeys: (): {
|
|
1041
|
+
key: string;
|
|
1042
|
+
scope: StorageScope;
|
|
1043
|
+
count: number;
|
|
1044
|
+
}[] => {
|
|
1045
|
+
const duplicates: { key: string; scope: StorageScope; count: number }[] =
|
|
1046
|
+
[];
|
|
1047
|
+
registeredKeyCounts.forEach((count, registryKey) => {
|
|
1048
|
+
if (count <= 1) {
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
const separatorIndex = registryKey.indexOf(":");
|
|
1052
|
+
duplicates.push({
|
|
1053
|
+
scope: Number(registryKey.slice(0, separatorIndex)) as StorageScope,
|
|
1054
|
+
key: registryKey.slice(separatorIndex + 1),
|
|
1055
|
+
count,
|
|
1056
|
+
});
|
|
1057
|
+
});
|
|
1058
|
+
return duplicates;
|
|
1059
|
+
},
|
|
1060
|
+
getRegisteredKeys: (): { key: string; scope: StorageScope }[] => {
|
|
1061
|
+
const result: { key: string; scope: StorageScope }[] = [];
|
|
1062
|
+
registeredKeyCounts.forEach((_count, registryKey) => {
|
|
1063
|
+
const separatorIndex = registryKey.indexOf(":");
|
|
1064
|
+
result.push({
|
|
1065
|
+
scope: Number(registryKey.slice(0, separatorIndex)) as StorageScope,
|
|
1066
|
+
key: registryKey.slice(separatorIndex + 1),
|
|
1067
|
+
});
|
|
1068
|
+
});
|
|
1069
|
+
return result;
|
|
1070
|
+
},
|
|
866
1071
|
clearNamespace: (namespace: string, scope: StorageScope) => {
|
|
867
1072
|
measureOperation("storage:clearNamespace", scope, () => {
|
|
868
1073
|
assertValidScope(scope);
|
|
@@ -1269,6 +1474,16 @@ export function createStorageCore(
|
|
|
1269
1474
|
: config.scope === StorageScope.Secure
|
|
1270
1475
|
? StorageScope.Secure
|
|
1271
1476
|
: null;
|
|
1477
|
+
const renameFromKeys: readonly string[] =
|
|
1478
|
+
config.renameFrom === undefined
|
|
1479
|
+
? EMPTY_KEYS
|
|
1480
|
+
: typeof config.renameFrom === "string"
|
|
1481
|
+
? [config.renameFrom]
|
|
1482
|
+
: config.renameFrom;
|
|
1483
|
+
const fallbackToCacheOnReadError =
|
|
1484
|
+
config.fallbackToCacheOnReadError === true;
|
|
1485
|
+
const onReadError = config.onReadError;
|
|
1486
|
+
let renamesMigrated = isMemory || renameFromKeys.length === 0;
|
|
1272
1487
|
|
|
1273
1488
|
if (expiration && expiration.ttlMs <= 0) {
|
|
1274
1489
|
throw new Error("expiration.ttlMs must be greater than 0.");
|
|
@@ -1322,9 +1537,18 @@ export function createStorageCore(
|
|
|
1322
1537
|
if (memoryExpiration) {
|
|
1323
1538
|
const expiresAt = memoryExpiration.get(storageKey);
|
|
1324
1539
|
if (expiresAt !== undefined && expiresAt <= Date.now()) {
|
|
1540
|
+
const expiredRaw = memoryStore.get(storageKey);
|
|
1325
1541
|
memoryExpiration.delete(storageKey);
|
|
1326
1542
|
memoryStore.delete(storageKey);
|
|
1327
1543
|
notifyKeyListeners(memoryListeners, storageKey);
|
|
1544
|
+
emitKeyChange(
|
|
1545
|
+
config.scope,
|
|
1546
|
+
storageKey,
|
|
1547
|
+
typeof expiredRaw === "string" ? expiredRaw : undefined,
|
|
1548
|
+
undefined,
|
|
1549
|
+
"expire",
|
|
1550
|
+
"memory",
|
|
1551
|
+
);
|
|
1328
1552
|
onExpired?.(storageKey);
|
|
1329
1553
|
return undefined;
|
|
1330
1554
|
}
|
|
@@ -1332,6 +1556,8 @@ export function createStorageCore(
|
|
|
1332
1556
|
return memoryStore.get(storageKey);
|
|
1333
1557
|
}
|
|
1334
1558
|
|
|
1559
|
+
migrateRenamesIfNeeded();
|
|
1560
|
+
|
|
1335
1561
|
if (nonMemoryScope === StorageScope.Disk) {
|
|
1336
1562
|
const pending = pendingDiskWrites.get(storageKey);
|
|
1337
1563
|
if (pending !== undefined) {
|
|
@@ -1355,14 +1581,36 @@ export function createStorageCore(
|
|
|
1355
1581
|
}
|
|
1356
1582
|
|
|
1357
1583
|
if (isBiometric) {
|
|
1358
|
-
return
|
|
1584
|
+
return readBackendRaw(() =>
|
|
1585
|
+
adapter.backend.getSecureBiometric(storageKey),
|
|
1586
|
+
);
|
|
1359
1587
|
}
|
|
1360
1588
|
|
|
1361
|
-
const raw =
|
|
1589
|
+
const raw = readBackendRaw(() =>
|
|
1590
|
+
adapter.backend.get(storageKey, config.scope),
|
|
1591
|
+
);
|
|
1362
1592
|
cacheRawValue(nonMemoryScope!, storageKey, raw);
|
|
1363
1593
|
return raw;
|
|
1364
1594
|
};
|
|
1365
1595
|
|
|
1596
|
+
const readBackendRaw = (
|
|
1597
|
+
read: () => string | undefined,
|
|
1598
|
+
): string | undefined => {
|
|
1599
|
+
try {
|
|
1600
|
+
return read();
|
|
1601
|
+
} catch (error) {
|
|
1602
|
+
onReadError?.(error);
|
|
1603
|
+
if (fallbackToCacheOnReadError) {
|
|
1604
|
+
const cached = getScopeRawCache(nonMemoryScope!).get(storageKey);
|
|
1605
|
+
if (cached !== undefined) {
|
|
1606
|
+
return cached;
|
|
1607
|
+
}
|
|
1608
|
+
return typeof lastRaw === "string" ? lastRaw : undefined;
|
|
1609
|
+
}
|
|
1610
|
+
throw error;
|
|
1611
|
+
}
|
|
1612
|
+
};
|
|
1613
|
+
|
|
1366
1614
|
const writeStoredRaw = (rawValue: string): void => {
|
|
1367
1615
|
const oldValue = undefined;
|
|
1368
1616
|
if (isBiometric) {
|
|
@@ -1438,7 +1686,60 @@ export function createStorageCore(
|
|
|
1438
1686
|
);
|
|
1439
1687
|
};
|
|
1440
1688
|
|
|
1441
|
-
const
|
|
1689
|
+
const migrateRenamesIfNeeded = (): void => {
|
|
1690
|
+
if (renamesMigrated) {
|
|
1691
|
+
return;
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
try {
|
|
1695
|
+
const hasCurrent = isBiometric
|
|
1696
|
+
? adapter.backend.hasSecureBiometric(storageKey)
|
|
1697
|
+
: adapter.backend.has(storageKey, config.scope);
|
|
1698
|
+
|
|
1699
|
+
if (hasCurrent) {
|
|
1700
|
+
for (const legacyKey of renameFromKeys) {
|
|
1701
|
+
if (isBiometric) {
|
|
1702
|
+
if (adapter.backend.hasSecureBiometric(legacyKey)) {
|
|
1703
|
+
adapter.backend.deleteSecureBiometric(legacyKey);
|
|
1704
|
+
}
|
|
1705
|
+
} else if (adapter.backend.has(legacyKey, config.scope)) {
|
|
1706
|
+
adapter.backend.remove(legacyKey, config.scope);
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
renamesMigrated = true;
|
|
1710
|
+
return;
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
for (const legacyKey of renameFromKeys) {
|
|
1714
|
+
const legacyRaw = isBiometric
|
|
1715
|
+
? adapter.backend.getSecureBiometric(legacyKey)
|
|
1716
|
+
: adapter.backend.get(legacyKey, config.scope);
|
|
1717
|
+
if (legacyRaw === undefined) {
|
|
1718
|
+
continue;
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
writeStoredRaw(legacyRaw);
|
|
1722
|
+
if (isBiometric) {
|
|
1723
|
+
adapter.backend.deleteSecureBiometric(legacyKey);
|
|
1724
|
+
} else {
|
|
1725
|
+
adapter.backend.remove(legacyKey, config.scope);
|
|
1726
|
+
}
|
|
1727
|
+
break;
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
renamesMigrated = true;
|
|
1731
|
+
} catch (error) {
|
|
1732
|
+
if (isKeychainLockedError(error)) {
|
|
1733
|
+
onReadError?.(error);
|
|
1734
|
+
return;
|
|
1735
|
+
}
|
|
1736
|
+
throw error;
|
|
1737
|
+
}
|
|
1738
|
+
};
|
|
1739
|
+
|
|
1740
|
+
const removeStoredRaw = (
|
|
1741
|
+
operation: StorageChangeOperation = "remove",
|
|
1742
|
+
): void => {
|
|
1442
1743
|
const oldValue = getEventRawValue(config.scope, storageKey);
|
|
1443
1744
|
if (isBiometric) {
|
|
1444
1745
|
adapter.backend.deleteSecureBiometric(storageKey);
|
|
@@ -1447,7 +1748,7 @@ export function createStorageCore(
|
|
|
1447
1748
|
storageKey,
|
|
1448
1749
|
oldValue,
|
|
1449
1750
|
undefined,
|
|
1450
|
-
|
|
1751
|
+
operation,
|
|
1451
1752
|
adapter.changeSource,
|
|
1452
1753
|
);
|
|
1453
1754
|
return;
|
|
@@ -1463,7 +1764,7 @@ export function createStorageCore(
|
|
|
1463
1764
|
storageKey,
|
|
1464
1765
|
oldValue,
|
|
1465
1766
|
undefined,
|
|
1466
|
-
|
|
1767
|
+
operation,
|
|
1467
1768
|
adapter.changeSource,
|
|
1468
1769
|
);
|
|
1469
1770
|
return;
|
|
@@ -1483,7 +1784,7 @@ export function createStorageCore(
|
|
|
1483
1784
|
storageKey,
|
|
1484
1785
|
oldValue,
|
|
1485
1786
|
undefined,
|
|
1486
|
-
|
|
1787
|
+
operation,
|
|
1487
1788
|
adapter.changeSource,
|
|
1488
1789
|
);
|
|
1489
1790
|
return;
|
|
@@ -1499,7 +1800,7 @@ export function createStorageCore(
|
|
|
1499
1800
|
storageKey,
|
|
1500
1801
|
oldValue,
|
|
1501
1802
|
undefined,
|
|
1502
|
-
|
|
1803
|
+
operation,
|
|
1503
1804
|
adapter.changeSource,
|
|
1504
1805
|
);
|
|
1505
1806
|
};
|
|
@@ -1576,7 +1877,7 @@ export function createStorageCore(
|
|
|
1576
1877
|
return lastValue as T;
|
|
1577
1878
|
}
|
|
1578
1879
|
|
|
1579
|
-
removeStoredRaw();
|
|
1880
|
+
removeStoredRaw("expire");
|
|
1580
1881
|
invalidateParsedCache();
|
|
1581
1882
|
onExpired?.(storageKey);
|
|
1582
1883
|
lastValue = ensureValidatedValue(defaultValue, false);
|
|
@@ -1618,7 +1919,7 @@ export function createStorageCore(
|
|
|
1618
1919
|
if (isStoredEnvelope(parsed)) {
|
|
1619
1920
|
envelopeExpiresAt = parsed.expiresAt;
|
|
1620
1921
|
if (parsed.expiresAt <= Date.now()) {
|
|
1621
|
-
removeStoredRaw();
|
|
1922
|
+
removeStoredRaw("expire");
|
|
1622
1923
|
invalidateParsedCache();
|
|
1623
1924
|
onExpired?.(storageKey);
|
|
1624
1925
|
lastValue = ensureValidatedValue(defaultValue, false);
|
|
@@ -1712,6 +2013,27 @@ export function createStorageCore(
|
|
|
1712
2013
|
});
|
|
1713
2014
|
};
|
|
1714
2015
|
|
|
2016
|
+
const merge = (partial: Partial<T>): void => {
|
|
2017
|
+
set((prev) => {
|
|
2018
|
+
if (prev === null || typeof prev !== "object") {
|
|
2019
|
+
return partial as T;
|
|
2020
|
+
}
|
|
2021
|
+
return { ...(prev as object), ...(partial as object) } as T;
|
|
2022
|
+
});
|
|
2023
|
+
};
|
|
2024
|
+
|
|
2025
|
+
const reset = (): void => {
|
|
2026
|
+
deleteItem();
|
|
2027
|
+
};
|
|
2028
|
+
|
|
2029
|
+
const setOrDelete = (value: T | null | undefined): void => {
|
|
2030
|
+
if (value === null || value === undefined) {
|
|
2031
|
+
deleteItem();
|
|
2032
|
+
return;
|
|
2033
|
+
}
|
|
2034
|
+
set(value);
|
|
2035
|
+
};
|
|
2036
|
+
|
|
1715
2037
|
const hasItem = (): boolean =>
|
|
1716
2038
|
measureOperation("item:has", config.scope, () => {
|
|
1717
2039
|
if (isMemory) return memoryStore.has(storageKey);
|
|
@@ -1775,6 +2097,9 @@ export function createStorageCore(
|
|
|
1775
2097
|
getWithVersion,
|
|
1776
2098
|
set,
|
|
1777
2099
|
setIfVersion,
|
|
2100
|
+
merge,
|
|
2101
|
+
reset,
|
|
2102
|
+
setOrDelete,
|
|
1778
2103
|
delete: deleteItem,
|
|
1779
2104
|
has: hasItem,
|
|
1780
2105
|
subscribe,
|
|
@@ -1797,10 +2122,26 @@ export function createStorageCore(
|
|
|
1797
2122
|
...(secureAccessControl !== undefined
|
|
1798
2123
|
? { _secureAccessControl: secureAccessControl }
|
|
1799
2124
|
: {}),
|
|
2125
|
+
...(config.group !== undefined ? { _group: config.group } : {}),
|
|
1800
2126
|
scope: config.scope,
|
|
1801
2127
|
key: storageKey,
|
|
1802
2128
|
};
|
|
1803
2129
|
|
|
2130
|
+
if (config.group !== undefined) {
|
|
2131
|
+
let groupSet = itemGroups.get(config.group);
|
|
2132
|
+
if (!groupSet) {
|
|
2133
|
+
groupSet = new Set();
|
|
2134
|
+
itemGroups.set(config.group, groupSet);
|
|
2135
|
+
}
|
|
2136
|
+
groupSet.add(storageItem as StorageItemInternal<unknown>);
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
const registryKey = `${config.scope}:${storageKey}`;
|
|
2140
|
+
registeredKeyCounts.set(
|
|
2141
|
+
registryKey,
|
|
2142
|
+
(registeredKeyCounts.get(registryKey) ?? 0) + 1,
|
|
2143
|
+
);
|
|
2144
|
+
|
|
1804
2145
|
return storageItem;
|
|
1805
2146
|
}
|
|
1806
2147
|
|
|
@@ -2298,7 +2639,11 @@ export function createStorageCore(
|
|
|
2298
2639
|
|
|
2299
2640
|
function createSecureAuthStorage<K extends string>(
|
|
2300
2641
|
config: SecureAuthStorageConfig<K>,
|
|
2301
|
-
options?: {
|
|
2642
|
+
options?: {
|
|
2643
|
+
namespace?: string;
|
|
2644
|
+
group?: string;
|
|
2645
|
+
fallbackToCacheOnReadError?: boolean;
|
|
2646
|
+
},
|
|
2302
2647
|
): Record<K, StorageItem<string>> {
|
|
2303
2648
|
const ns = options?.namespace ?? "auth";
|
|
2304
2649
|
const result: Partial<Record<K, StorageItem<string>>> = {};
|
|
@@ -2326,15 +2671,105 @@ export function createStorageCore(
|
|
|
2326
2671
|
...(expirationConfig !== undefined
|
|
2327
2672
|
? { expiration: expirationConfig }
|
|
2328
2673
|
: {}),
|
|
2674
|
+
...(itemConfig.renameFrom !== undefined
|
|
2675
|
+
? { renameFrom: itemConfig.renameFrom }
|
|
2676
|
+
: {}),
|
|
2677
|
+
...(options?.group !== undefined ? { group: options.group } : {}),
|
|
2678
|
+
...(options?.fallbackToCacheOnReadError !== undefined
|
|
2679
|
+
? { fallbackToCacheOnReadError: options.fallbackToCacheOnReadError }
|
|
2680
|
+
: {}),
|
|
2329
2681
|
});
|
|
2330
2682
|
}
|
|
2331
2683
|
|
|
2332
2684
|
return result as Record<K, StorageItem<string>>;
|
|
2333
2685
|
}
|
|
2334
2686
|
|
|
2687
|
+
function memoryItem<T = undefined>(
|
|
2688
|
+
config: Omit<StorageItemConfig<T>, "scope">,
|
|
2689
|
+
): StorageItem<T> {
|
|
2690
|
+
return createStorageItem<T>({ ...config, scope: StorageScope.Memory });
|
|
2691
|
+
}
|
|
2692
|
+
|
|
2693
|
+
function diskItem<T = undefined>(
|
|
2694
|
+
config: Omit<StorageItemConfig<T>, "scope">,
|
|
2695
|
+
): StorageItem<T> {
|
|
2696
|
+
return createStorageItem<T>({ ...config, scope: StorageScope.Disk });
|
|
2697
|
+
}
|
|
2698
|
+
|
|
2699
|
+
function secureItem<T = undefined>(
|
|
2700
|
+
config: Omit<StorageItemConfig<T>, "scope">,
|
|
2701
|
+
): StorageItem<T> {
|
|
2702
|
+
return createStorageItem<T>({ ...config, scope: StorageScope.Secure });
|
|
2703
|
+
}
|
|
2704
|
+
|
|
2705
|
+
function createSetItem<TMember extends string = string>(
|
|
2706
|
+
config: SetItemConfig<TMember>,
|
|
2707
|
+
): SetStorageItem<TMember> {
|
|
2708
|
+
const { defaultValue, ...rest } = config;
|
|
2709
|
+
const initial: Record<string, true> = {};
|
|
2710
|
+
if (defaultValue) {
|
|
2711
|
+
for (const id of defaultValue) {
|
|
2712
|
+
initial[id] = true;
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2715
|
+
|
|
2716
|
+
const item = createStorageItem<Record<string, true>>({
|
|
2717
|
+
...rest,
|
|
2718
|
+
defaultValue: initial,
|
|
2719
|
+
});
|
|
2720
|
+
|
|
2721
|
+
const has = (id: TMember): boolean => item.get()[id] === true;
|
|
2722
|
+
|
|
2723
|
+
const add = (id: TMember): void => {
|
|
2724
|
+
if (item.get()[id] === true) {
|
|
2725
|
+
return;
|
|
2726
|
+
}
|
|
2727
|
+
item.merge({ [id]: true });
|
|
2728
|
+
};
|
|
2729
|
+
|
|
2730
|
+
const deleteId = (id: TMember): void => {
|
|
2731
|
+
const current = item.get();
|
|
2732
|
+
if (current[id] !== true) {
|
|
2733
|
+
return;
|
|
2734
|
+
}
|
|
2735
|
+
const next = { ...current };
|
|
2736
|
+
delete next[id];
|
|
2737
|
+
item.set(next);
|
|
2738
|
+
};
|
|
2739
|
+
|
|
2740
|
+
const toggle = (id: TMember): boolean => {
|
|
2741
|
+
if (has(id)) {
|
|
2742
|
+
deleteId(id);
|
|
2743
|
+
return false;
|
|
2744
|
+
}
|
|
2745
|
+
add(id);
|
|
2746
|
+
return true;
|
|
2747
|
+
};
|
|
2748
|
+
|
|
2749
|
+
return {
|
|
2750
|
+
get: item.get,
|
|
2751
|
+
has,
|
|
2752
|
+
add,
|
|
2753
|
+
delete: deleteId,
|
|
2754
|
+
toggle,
|
|
2755
|
+
values: () => Object.keys(item.get()) as TMember[],
|
|
2756
|
+
size: () => Object.keys(item.get()).length,
|
|
2757
|
+
clear: () => item.set({}),
|
|
2758
|
+
reset: item.reset,
|
|
2759
|
+
subscribe: item.subscribe,
|
|
2760
|
+
scope: item.scope,
|
|
2761
|
+
key: item.key,
|
|
2762
|
+
item,
|
|
2763
|
+
};
|
|
2764
|
+
}
|
|
2765
|
+
|
|
2335
2766
|
return {
|
|
2336
2767
|
storage,
|
|
2337
2768
|
createStorageItem,
|
|
2769
|
+
memoryItem,
|
|
2770
|
+
diskItem,
|
|
2771
|
+
secureItem,
|
|
2772
|
+
createSetItem,
|
|
2338
2773
|
getBatch,
|
|
2339
2774
|
setBatch,
|
|
2340
2775
|
removeBatch,
|