react-native-nitro-storage 0.5.9 → 0.7.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 +39 -0
- package/README.md +193 -16
- package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +225 -76
- package/app.plugin.js +2 -0
- package/cpp/bindings/HybridStorage.cpp +29 -21
- package/cpp/bindings/HybridStorage.hpp +5 -0
- package/docs/secure-storage.md +4 -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/indexeddb-backend.js.map +1 -1
- package/lib/commonjs/internal.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/indexeddb-backend.js.map +1 -1
- package/lib/module/internal.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 +28 -5
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/index.web.d.ts +28 -5
- package/lib/typescript/index.web.d.ts.map +1 -1
- package/lib/typescript/indexeddb-backend.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 +62 -3
- 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 +158 -0
- package/lib/typescript/testing.d.ts.map +1 -0
- package/package.json +11 -6
- package/src/index.ts +17 -4
- package/src/index.web.ts +17 -4
- package/src/indexeddb-backend.ts +1 -2
- package/src/internal.ts +1 -1
- package/src/shared.ts +1 -0
- package/src/storage-core.ts +463 -11
- package/src/storage-events.ts +3 -2
- package/src/storage-hooks.ts +42 -3
- package/src/testing.ts +270 -0
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,48 @@ export type StorageBatchSetItem<T> = {
|
|
|
149
158
|
value: T;
|
|
150
159
|
};
|
|
151
160
|
|
|
161
|
+
type StorageBatchSetCandidate = {
|
|
162
|
+
get: () => unknown;
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
type StorageBatchSetEntries<
|
|
166
|
+
TItems extends readonly StorageBatchSetCandidate[],
|
|
167
|
+
> = {
|
|
168
|
+
[Index in keyof TItems]: {
|
|
169
|
+
item: TItems[Index] & StorageItem<ReturnType<TItems[Index]["get"]>>;
|
|
170
|
+
value: ReturnType<TItems[Index]["get"]>;
|
|
171
|
+
};
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
export type StorageKeyRef = string | { readonly key: string };
|
|
175
|
+
|
|
176
|
+
export type StorageClearOptions = {
|
|
177
|
+
except?: readonly StorageKeyRef[];
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
export type SetItemConfig<TMember extends string = string> = Omit<
|
|
181
|
+
StorageItemConfig<Record<string, true>>,
|
|
182
|
+
"defaultValue" | "serialize" | "deserialize"
|
|
183
|
+
> & {
|
|
184
|
+
defaultValue?: readonly TMember[];
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
export type SetStorageItem<TMember extends string = string> = {
|
|
188
|
+
get: () => Record<string, true>;
|
|
189
|
+
has: (id: TMember) => boolean;
|
|
190
|
+
add: (id: TMember) => void;
|
|
191
|
+
delete: (id: TMember) => void;
|
|
192
|
+
toggle: (id: TMember) => boolean;
|
|
193
|
+
values: () => TMember[];
|
|
194
|
+
size: () => number;
|
|
195
|
+
clear: () => void;
|
|
196
|
+
reset: () => void;
|
|
197
|
+
subscribe: (callback: () => void) => () => void;
|
|
198
|
+
scope: StorageScope;
|
|
199
|
+
key: string;
|
|
200
|
+
item: StorageItem<Record<string, true>>;
|
|
201
|
+
};
|
|
202
|
+
|
|
152
203
|
export type StorageCoreBackend = {
|
|
153
204
|
get(key: string, scope: StorageScope): string | undefined;
|
|
154
205
|
set(key: string, value: string, scope: StorageScope): void;
|
|
@@ -234,6 +285,8 @@ export type StorageCoreInternals = {
|
|
|
234
285
|
setSecureDefaultAccessControl(level: AccessControl): void;
|
|
235
286
|
};
|
|
236
287
|
|
|
288
|
+
const EMPTY_KEYS: readonly string[] = Object.freeze([]);
|
|
289
|
+
|
|
237
290
|
function asInternal<T>(item: StorageItem<T>): StorageItemInternal<T> {
|
|
238
291
|
return item as StorageItemInternal<T>;
|
|
239
292
|
}
|
|
@@ -242,6 +295,8 @@ export function createStorageCore(
|
|
|
242
295
|
buildAdapter: (internals: StorageCoreInternals) => StorageCoreAdapter,
|
|
243
296
|
) {
|
|
244
297
|
const registeredMigrations = new Map<number, Migration>();
|
|
298
|
+
const itemGroups = new Map<string, Set<StorageItemInternal<unknown>>>();
|
|
299
|
+
const registeredKeyCounts = new Map<string, number>();
|
|
245
300
|
const memoryStore = new Map<string, unknown>();
|
|
246
301
|
const memoryListeners: KeyListenerRegistry = new Map();
|
|
247
302
|
const scopedListeners = new Map<NonMemoryScope, KeyListenerRegistry>([
|
|
@@ -723,6 +778,84 @@ export function createStorageCore(
|
|
|
723
778
|
|
|
724
779
|
const adapter = buildAdapter(internals);
|
|
725
780
|
|
|
781
|
+
function resolveKeyRef(ref: StorageKeyRef): string {
|
|
782
|
+
return typeof ref === "string" ? ref : ref.key;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function clearScopeExcept(
|
|
786
|
+
scope: StorageScope,
|
|
787
|
+
exceptRefs: readonly StorageKeyRef[],
|
|
788
|
+
): void {
|
|
789
|
+
measureOperation("storage:clearExcept", scope, () => {
|
|
790
|
+
assertValidScope(scope);
|
|
791
|
+
const keep = new Set(exceptRefs.map(resolveKeyRef));
|
|
792
|
+
|
|
793
|
+
if (scope === StorageScope.Memory) {
|
|
794
|
+
const removeKeys = Array.from(memoryStore.keys()).filter(
|
|
795
|
+
(key) => !keep.has(key),
|
|
796
|
+
);
|
|
797
|
+
if (removeKeys.length === 0) {
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
const previousValues = shouldReadPreviousEventValues(scope)
|
|
801
|
+
? removeKeys.map((key) => getEventRawValue(scope, key))
|
|
802
|
+
: [];
|
|
803
|
+
removeKeys.forEach((key) => memoryStore.delete(key));
|
|
804
|
+
removeKeys.forEach((key) => notifyKeyListeners(memoryListeners, key));
|
|
805
|
+
emitBatchChange(
|
|
806
|
+
scope,
|
|
807
|
+
"clear",
|
|
808
|
+
"memory",
|
|
809
|
+
removeKeys.map((key, index) =>
|
|
810
|
+
createKeyChange(
|
|
811
|
+
scope,
|
|
812
|
+
key,
|
|
813
|
+
previousValues[index],
|
|
814
|
+
undefined,
|
|
815
|
+
"clear",
|
|
816
|
+
"memory",
|
|
817
|
+
),
|
|
818
|
+
),
|
|
819
|
+
);
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
if (scope === StorageScope.Disk) {
|
|
824
|
+
flushDiskWrites();
|
|
825
|
+
}
|
|
826
|
+
if (scope === StorageScope.Secure) {
|
|
827
|
+
flushSecureWrites();
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
const removeKeys = adapter.backend
|
|
831
|
+
.getAllKeys(scope)
|
|
832
|
+
.filter((key) => !keep.has(key));
|
|
833
|
+
if (removeKeys.length === 0) {
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
const previousValues = shouldReadPreviousEventValues(scope)
|
|
837
|
+
? adapter.backend.getBatch(removeKeys, scope)
|
|
838
|
+
: [];
|
|
839
|
+
adapter.backend.removeBatch(removeKeys, scope);
|
|
840
|
+
removeKeys.forEach((key) => cacheRawValue(scope, key, undefined));
|
|
841
|
+
emitBatchChange(
|
|
842
|
+
scope,
|
|
843
|
+
"clear",
|
|
844
|
+
adapter.changeSource,
|
|
845
|
+
removeKeys.map((key, index) =>
|
|
846
|
+
createKeyChange(
|
|
847
|
+
scope,
|
|
848
|
+
key,
|
|
849
|
+
previousValues[index],
|
|
850
|
+
undefined,
|
|
851
|
+
"clear",
|
|
852
|
+
adapter.changeSource,
|
|
853
|
+
),
|
|
854
|
+
),
|
|
855
|
+
);
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
|
|
726
859
|
const storage = {
|
|
727
860
|
subscribe: (
|
|
728
861
|
scope: StorageScope,
|
|
@@ -796,7 +929,11 @@ export function createStorageCore(
|
|
|
796
929
|
adapter.maybeCleanupScopeSubscription(StorageScope.Disk);
|
|
797
930
|
adapter.maybeCleanupScopeSubscription(StorageScope.Secure);
|
|
798
931
|
},
|
|
799
|
-
clear: (scope: StorageScope) => {
|
|
932
|
+
clear: (scope: StorageScope, options?: StorageClearOptions) => {
|
|
933
|
+
if (options?.except && options.except.length > 0) {
|
|
934
|
+
clearScopeExcept(scope, options.except);
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
800
937
|
measureOperation("storage:clear", scope, () => {
|
|
801
938
|
const previousValues = shouldReadPreviousEventValues(scope)
|
|
802
939
|
? storage.getAll(scope)
|
|
@@ -863,6 +1000,87 @@ export function createStorageCore(
|
|
|
863
1000
|
3,
|
|
864
1001
|
);
|
|
865
1002
|
},
|
|
1003
|
+
clearGroup: (group: string) => {
|
|
1004
|
+
const items = itemGroups.get(group);
|
|
1005
|
+
if (!items || items.size === 0) {
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
measureOperation(
|
|
1009
|
+
"storage:clearGroup",
|
|
1010
|
+
StorageScope.Memory,
|
|
1011
|
+
() => {
|
|
1012
|
+
const byScope = new Map<
|
|
1013
|
+
StorageScope,
|
|
1014
|
+
StorageItemInternal<unknown>[]
|
|
1015
|
+
>();
|
|
1016
|
+
for (const item of items) {
|
|
1017
|
+
const scoped = byScope.get(item.scope);
|
|
1018
|
+
if (scoped) {
|
|
1019
|
+
scoped.push(item);
|
|
1020
|
+
} else {
|
|
1021
|
+
byScope.set(item.scope, [item]);
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
byScope.forEach((groupItems, scope) => {
|
|
1025
|
+
removeBatch(groupItems, scope);
|
|
1026
|
+
});
|
|
1027
|
+
},
|
|
1028
|
+
items.size,
|
|
1029
|
+
);
|
|
1030
|
+
},
|
|
1031
|
+
getGroupItems: (group: string): StorageItem<unknown>[] => {
|
|
1032
|
+
const items = itemGroups.get(group);
|
|
1033
|
+
return items ? Array.from(items) : [];
|
|
1034
|
+
},
|
|
1035
|
+
subscribeExpired: (
|
|
1036
|
+
scope: StorageScope,
|
|
1037
|
+
listener: (event: StorageKeyChangeEvent) => void,
|
|
1038
|
+
): (() => void) => {
|
|
1039
|
+
return storage.subscribe(scope, (event) => {
|
|
1040
|
+
if (event.type === "key") {
|
|
1041
|
+
if (event.operation === "expire") {
|
|
1042
|
+
listener(event);
|
|
1043
|
+
}
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
event.changes.forEach((change) => {
|
|
1047
|
+
if (change.operation === "expire") {
|
|
1048
|
+
listener(change);
|
|
1049
|
+
}
|
|
1050
|
+
});
|
|
1051
|
+
});
|
|
1052
|
+
},
|
|
1053
|
+
findDuplicateKeys: (): {
|
|
1054
|
+
key: string;
|
|
1055
|
+
scope: StorageScope;
|
|
1056
|
+
count: number;
|
|
1057
|
+
}[] => {
|
|
1058
|
+
const duplicates: { key: string; scope: StorageScope; count: number }[] =
|
|
1059
|
+
[];
|
|
1060
|
+
registeredKeyCounts.forEach((count, registryKey) => {
|
|
1061
|
+
if (count <= 1) {
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
const separatorIndex = registryKey.indexOf(":");
|
|
1065
|
+
duplicates.push({
|
|
1066
|
+
scope: Number(registryKey.slice(0, separatorIndex)),
|
|
1067
|
+
key: registryKey.slice(separatorIndex + 1),
|
|
1068
|
+
count,
|
|
1069
|
+
});
|
|
1070
|
+
});
|
|
1071
|
+
return duplicates;
|
|
1072
|
+
},
|
|
1073
|
+
getRegisteredKeys: (): { key: string; scope: StorageScope }[] => {
|
|
1074
|
+
const result: { key: string; scope: StorageScope }[] = [];
|
|
1075
|
+
registeredKeyCounts.forEach((_count, registryKey) => {
|
|
1076
|
+
const separatorIndex = registryKey.indexOf(":");
|
|
1077
|
+
result.push({
|
|
1078
|
+
scope: Number(registryKey.slice(0, separatorIndex)),
|
|
1079
|
+
key: registryKey.slice(separatorIndex + 1),
|
|
1080
|
+
});
|
|
1081
|
+
});
|
|
1082
|
+
return result;
|
|
1083
|
+
},
|
|
866
1084
|
clearNamespace: (namespace: string, scope: StorageScope) => {
|
|
867
1085
|
measureOperation("storage:clearNamespace", scope, () => {
|
|
868
1086
|
assertValidScope(scope);
|
|
@@ -1269,6 +1487,16 @@ export function createStorageCore(
|
|
|
1269
1487
|
: config.scope === StorageScope.Secure
|
|
1270
1488
|
? StorageScope.Secure
|
|
1271
1489
|
: null;
|
|
1490
|
+
const renameFromKeys: readonly string[] =
|
|
1491
|
+
config.renameFrom === undefined
|
|
1492
|
+
? EMPTY_KEYS
|
|
1493
|
+
: typeof config.renameFrom === "string"
|
|
1494
|
+
? [config.renameFrom]
|
|
1495
|
+
: config.renameFrom;
|
|
1496
|
+
const fallbackToCacheOnReadError =
|
|
1497
|
+
config.fallbackToCacheOnReadError === true;
|
|
1498
|
+
const onReadError = config.onReadError;
|
|
1499
|
+
let renamesMigrated = isMemory || renameFromKeys.length === 0;
|
|
1272
1500
|
|
|
1273
1501
|
if (expiration && expiration.ttlMs <= 0) {
|
|
1274
1502
|
throw new Error("expiration.ttlMs must be greater than 0.");
|
|
@@ -1322,9 +1550,18 @@ export function createStorageCore(
|
|
|
1322
1550
|
if (memoryExpiration) {
|
|
1323
1551
|
const expiresAt = memoryExpiration.get(storageKey);
|
|
1324
1552
|
if (expiresAt !== undefined && expiresAt <= Date.now()) {
|
|
1553
|
+
const expiredRaw = memoryStore.get(storageKey);
|
|
1325
1554
|
memoryExpiration.delete(storageKey);
|
|
1326
1555
|
memoryStore.delete(storageKey);
|
|
1327
1556
|
notifyKeyListeners(memoryListeners, storageKey);
|
|
1557
|
+
emitKeyChange(
|
|
1558
|
+
config.scope,
|
|
1559
|
+
storageKey,
|
|
1560
|
+
typeof expiredRaw === "string" ? expiredRaw : undefined,
|
|
1561
|
+
undefined,
|
|
1562
|
+
"expire",
|
|
1563
|
+
"memory",
|
|
1564
|
+
);
|
|
1328
1565
|
onExpired?.(storageKey);
|
|
1329
1566
|
return undefined;
|
|
1330
1567
|
}
|
|
@@ -1332,6 +1569,8 @@ export function createStorageCore(
|
|
|
1332
1569
|
return memoryStore.get(storageKey);
|
|
1333
1570
|
}
|
|
1334
1571
|
|
|
1572
|
+
migrateRenamesIfNeeded();
|
|
1573
|
+
|
|
1335
1574
|
if (nonMemoryScope === StorageScope.Disk) {
|
|
1336
1575
|
const pending = pendingDiskWrites.get(storageKey);
|
|
1337
1576
|
if (pending !== undefined) {
|
|
@@ -1355,14 +1594,36 @@ export function createStorageCore(
|
|
|
1355
1594
|
}
|
|
1356
1595
|
|
|
1357
1596
|
if (isBiometric) {
|
|
1358
|
-
return
|
|
1597
|
+
return readBackendRaw(() =>
|
|
1598
|
+
adapter.backend.getSecureBiometric(storageKey),
|
|
1599
|
+
);
|
|
1359
1600
|
}
|
|
1360
1601
|
|
|
1361
|
-
const raw =
|
|
1602
|
+
const raw = readBackendRaw(() =>
|
|
1603
|
+
adapter.backend.get(storageKey, config.scope),
|
|
1604
|
+
);
|
|
1362
1605
|
cacheRawValue(nonMemoryScope!, storageKey, raw);
|
|
1363
1606
|
return raw;
|
|
1364
1607
|
};
|
|
1365
1608
|
|
|
1609
|
+
const readBackendRaw = (
|
|
1610
|
+
read: () => string | undefined,
|
|
1611
|
+
): string | undefined => {
|
|
1612
|
+
try {
|
|
1613
|
+
return read();
|
|
1614
|
+
} catch (error) {
|
|
1615
|
+
onReadError?.(error);
|
|
1616
|
+
if (fallbackToCacheOnReadError) {
|
|
1617
|
+
const cached = getScopeRawCache(nonMemoryScope!).get(storageKey);
|
|
1618
|
+
if (cached !== undefined) {
|
|
1619
|
+
return cached;
|
|
1620
|
+
}
|
|
1621
|
+
return typeof lastRaw === "string" ? lastRaw : undefined;
|
|
1622
|
+
}
|
|
1623
|
+
throw error;
|
|
1624
|
+
}
|
|
1625
|
+
};
|
|
1626
|
+
|
|
1366
1627
|
const writeStoredRaw = (rawValue: string): void => {
|
|
1367
1628
|
const oldValue = undefined;
|
|
1368
1629
|
if (isBiometric) {
|
|
@@ -1438,7 +1699,60 @@ export function createStorageCore(
|
|
|
1438
1699
|
);
|
|
1439
1700
|
};
|
|
1440
1701
|
|
|
1441
|
-
const
|
|
1702
|
+
const migrateRenamesIfNeeded = (): void => {
|
|
1703
|
+
if (renamesMigrated) {
|
|
1704
|
+
return;
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
try {
|
|
1708
|
+
const hasCurrent = isBiometric
|
|
1709
|
+
? adapter.backend.hasSecureBiometric(storageKey)
|
|
1710
|
+
: adapter.backend.has(storageKey, config.scope);
|
|
1711
|
+
|
|
1712
|
+
if (hasCurrent) {
|
|
1713
|
+
for (const legacyKey of renameFromKeys) {
|
|
1714
|
+
if (isBiometric) {
|
|
1715
|
+
if (adapter.backend.hasSecureBiometric(legacyKey)) {
|
|
1716
|
+
adapter.backend.deleteSecureBiometric(legacyKey);
|
|
1717
|
+
}
|
|
1718
|
+
} else if (adapter.backend.has(legacyKey, config.scope)) {
|
|
1719
|
+
adapter.backend.remove(legacyKey, config.scope);
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
renamesMigrated = true;
|
|
1723
|
+
return;
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
for (const legacyKey of renameFromKeys) {
|
|
1727
|
+
const legacyRaw = isBiometric
|
|
1728
|
+
? adapter.backend.getSecureBiometric(legacyKey)
|
|
1729
|
+
: adapter.backend.get(legacyKey, config.scope);
|
|
1730
|
+
if (legacyRaw === undefined) {
|
|
1731
|
+
continue;
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
writeStoredRaw(legacyRaw);
|
|
1735
|
+
if (isBiometric) {
|
|
1736
|
+
adapter.backend.deleteSecureBiometric(legacyKey);
|
|
1737
|
+
} else {
|
|
1738
|
+
adapter.backend.remove(legacyKey, config.scope);
|
|
1739
|
+
}
|
|
1740
|
+
break;
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
renamesMigrated = true;
|
|
1744
|
+
} catch (error) {
|
|
1745
|
+
if (isKeychainLockedError(error)) {
|
|
1746
|
+
onReadError?.(error);
|
|
1747
|
+
return;
|
|
1748
|
+
}
|
|
1749
|
+
throw error;
|
|
1750
|
+
}
|
|
1751
|
+
};
|
|
1752
|
+
|
|
1753
|
+
const removeStoredRaw = (
|
|
1754
|
+
operation: StorageChangeOperation = "remove",
|
|
1755
|
+
): void => {
|
|
1442
1756
|
const oldValue = getEventRawValue(config.scope, storageKey);
|
|
1443
1757
|
if (isBiometric) {
|
|
1444
1758
|
adapter.backend.deleteSecureBiometric(storageKey);
|
|
@@ -1447,7 +1761,7 @@ export function createStorageCore(
|
|
|
1447
1761
|
storageKey,
|
|
1448
1762
|
oldValue,
|
|
1449
1763
|
undefined,
|
|
1450
|
-
|
|
1764
|
+
operation,
|
|
1451
1765
|
adapter.changeSource,
|
|
1452
1766
|
);
|
|
1453
1767
|
return;
|
|
@@ -1463,7 +1777,7 @@ export function createStorageCore(
|
|
|
1463
1777
|
storageKey,
|
|
1464
1778
|
oldValue,
|
|
1465
1779
|
undefined,
|
|
1466
|
-
|
|
1780
|
+
operation,
|
|
1467
1781
|
adapter.changeSource,
|
|
1468
1782
|
);
|
|
1469
1783
|
return;
|
|
@@ -1483,7 +1797,7 @@ export function createStorageCore(
|
|
|
1483
1797
|
storageKey,
|
|
1484
1798
|
oldValue,
|
|
1485
1799
|
undefined,
|
|
1486
|
-
|
|
1800
|
+
operation,
|
|
1487
1801
|
adapter.changeSource,
|
|
1488
1802
|
);
|
|
1489
1803
|
return;
|
|
@@ -1499,7 +1813,7 @@ export function createStorageCore(
|
|
|
1499
1813
|
storageKey,
|
|
1500
1814
|
oldValue,
|
|
1501
1815
|
undefined,
|
|
1502
|
-
|
|
1816
|
+
operation,
|
|
1503
1817
|
adapter.changeSource,
|
|
1504
1818
|
);
|
|
1505
1819
|
};
|
|
@@ -1576,7 +1890,7 @@ export function createStorageCore(
|
|
|
1576
1890
|
return lastValue as T;
|
|
1577
1891
|
}
|
|
1578
1892
|
|
|
1579
|
-
removeStoredRaw();
|
|
1893
|
+
removeStoredRaw("expire");
|
|
1580
1894
|
invalidateParsedCache();
|
|
1581
1895
|
onExpired?.(storageKey);
|
|
1582
1896
|
lastValue = ensureValidatedValue(defaultValue, false);
|
|
@@ -1618,7 +1932,7 @@ export function createStorageCore(
|
|
|
1618
1932
|
if (isStoredEnvelope(parsed)) {
|
|
1619
1933
|
envelopeExpiresAt = parsed.expiresAt;
|
|
1620
1934
|
if (parsed.expiresAt <= Date.now()) {
|
|
1621
|
-
removeStoredRaw();
|
|
1935
|
+
removeStoredRaw("expire");
|
|
1622
1936
|
invalidateParsedCache();
|
|
1623
1937
|
onExpired?.(storageKey);
|
|
1624
1938
|
lastValue = ensureValidatedValue(defaultValue, false);
|
|
@@ -1712,6 +2026,27 @@ export function createStorageCore(
|
|
|
1712
2026
|
});
|
|
1713
2027
|
};
|
|
1714
2028
|
|
|
2029
|
+
const merge = (partial: Partial<T>): void => {
|
|
2030
|
+
set((prev) => {
|
|
2031
|
+
if (prev === null || typeof prev !== "object") {
|
|
2032
|
+
return partial as T;
|
|
2033
|
+
}
|
|
2034
|
+
return { ...(prev as object), ...(partial as object) } as T;
|
|
2035
|
+
});
|
|
2036
|
+
};
|
|
2037
|
+
|
|
2038
|
+
const reset = (): void => {
|
|
2039
|
+
deleteItem();
|
|
2040
|
+
};
|
|
2041
|
+
|
|
2042
|
+
const setOrDelete = (value: T | null | undefined): void => {
|
|
2043
|
+
if (value === null || value === undefined) {
|
|
2044
|
+
deleteItem();
|
|
2045
|
+
return;
|
|
2046
|
+
}
|
|
2047
|
+
set(value);
|
|
2048
|
+
};
|
|
2049
|
+
|
|
1715
2050
|
const hasItem = (): boolean =>
|
|
1716
2051
|
measureOperation("item:has", config.scope, () => {
|
|
1717
2052
|
if (isMemory) return memoryStore.has(storageKey);
|
|
@@ -1775,6 +2110,9 @@ export function createStorageCore(
|
|
|
1775
2110
|
getWithVersion,
|
|
1776
2111
|
set,
|
|
1777
2112
|
setIfVersion,
|
|
2113
|
+
merge,
|
|
2114
|
+
reset,
|
|
2115
|
+
setOrDelete,
|
|
1778
2116
|
delete: deleteItem,
|
|
1779
2117
|
has: hasItem,
|
|
1780
2118
|
subscribe,
|
|
@@ -1797,10 +2135,26 @@ export function createStorageCore(
|
|
|
1797
2135
|
...(secureAccessControl !== undefined
|
|
1798
2136
|
? { _secureAccessControl: secureAccessControl }
|
|
1799
2137
|
: {}),
|
|
2138
|
+
...(config.group !== undefined ? { _group: config.group } : {}),
|
|
1800
2139
|
scope: config.scope,
|
|
1801
2140
|
key: storageKey,
|
|
1802
2141
|
};
|
|
1803
2142
|
|
|
2143
|
+
if (config.group !== undefined) {
|
|
2144
|
+
let groupSet = itemGroups.get(config.group);
|
|
2145
|
+
if (!groupSet) {
|
|
2146
|
+
groupSet = new Set();
|
|
2147
|
+
itemGroups.set(config.group, groupSet);
|
|
2148
|
+
}
|
|
2149
|
+
groupSet.add(storageItem as StorageItemInternal<unknown>);
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
const registryKey = `${config.scope}:${storageKey}`;
|
|
2153
|
+
registeredKeyCounts.set(
|
|
2154
|
+
registryKey,
|
|
2155
|
+
(registeredKeyCounts.get(registryKey) ?? 0) + 1,
|
|
2156
|
+
);
|
|
2157
|
+
|
|
1804
2158
|
return storageItem;
|
|
1805
2159
|
}
|
|
1806
2160
|
|
|
@@ -1886,6 +2240,10 @@ export function createStorageCore(
|
|
|
1886
2240
|
) as BatchValues<TItems>;
|
|
1887
2241
|
}
|
|
1888
2242
|
|
|
2243
|
+
function setBatch<const TItems extends readonly StorageBatchSetCandidate[]>(
|
|
2244
|
+
items: StorageBatchSetEntries<TItems>,
|
|
2245
|
+
scope: StorageScope,
|
|
2246
|
+
): void;
|
|
1889
2247
|
function setBatch<T>(
|
|
1890
2248
|
items: readonly StorageBatchSetItem<T>[],
|
|
1891
2249
|
scope: StorageScope,
|
|
@@ -2298,7 +2656,11 @@ export function createStorageCore(
|
|
|
2298
2656
|
|
|
2299
2657
|
function createSecureAuthStorage<K extends string>(
|
|
2300
2658
|
config: SecureAuthStorageConfig<K>,
|
|
2301
|
-
options?: {
|
|
2659
|
+
options?: {
|
|
2660
|
+
namespace?: string;
|
|
2661
|
+
group?: string;
|
|
2662
|
+
fallbackToCacheOnReadError?: boolean;
|
|
2663
|
+
},
|
|
2302
2664
|
): Record<K, StorageItem<string>> {
|
|
2303
2665
|
const ns = options?.namespace ?? "auth";
|
|
2304
2666
|
const result: Partial<Record<K, StorageItem<string>>> = {};
|
|
@@ -2326,15 +2688,105 @@ export function createStorageCore(
|
|
|
2326
2688
|
...(expirationConfig !== undefined
|
|
2327
2689
|
? { expiration: expirationConfig }
|
|
2328
2690
|
: {}),
|
|
2691
|
+
...(itemConfig.renameFrom !== undefined
|
|
2692
|
+
? { renameFrom: itemConfig.renameFrom }
|
|
2693
|
+
: {}),
|
|
2694
|
+
...(options?.group !== undefined ? { group: options.group } : {}),
|
|
2695
|
+
...(options?.fallbackToCacheOnReadError !== undefined
|
|
2696
|
+
? { fallbackToCacheOnReadError: options.fallbackToCacheOnReadError }
|
|
2697
|
+
: {}),
|
|
2329
2698
|
});
|
|
2330
2699
|
}
|
|
2331
2700
|
|
|
2332
2701
|
return result as Record<K, StorageItem<string>>;
|
|
2333
2702
|
}
|
|
2334
2703
|
|
|
2704
|
+
function memoryItem<T = undefined>(
|
|
2705
|
+
config: Omit<StorageItemConfig<T>, "scope">,
|
|
2706
|
+
): StorageItem<T> {
|
|
2707
|
+
return createStorageItem<T>({ ...config, scope: StorageScope.Memory });
|
|
2708
|
+
}
|
|
2709
|
+
|
|
2710
|
+
function diskItem<T = undefined>(
|
|
2711
|
+
config: Omit<StorageItemConfig<T>, "scope">,
|
|
2712
|
+
): StorageItem<T> {
|
|
2713
|
+
return createStorageItem<T>({ ...config, scope: StorageScope.Disk });
|
|
2714
|
+
}
|
|
2715
|
+
|
|
2716
|
+
function secureItem<T = undefined>(
|
|
2717
|
+
config: Omit<StorageItemConfig<T>, "scope">,
|
|
2718
|
+
): StorageItem<T> {
|
|
2719
|
+
return createStorageItem<T>({ ...config, scope: StorageScope.Secure });
|
|
2720
|
+
}
|
|
2721
|
+
|
|
2722
|
+
function createSetItem<TMember extends string = string>(
|
|
2723
|
+
config: SetItemConfig<TMember>,
|
|
2724
|
+
): SetStorageItem<TMember> {
|
|
2725
|
+
const { defaultValue, ...rest } = config;
|
|
2726
|
+
const initial: Record<string, true> = {};
|
|
2727
|
+
if (defaultValue) {
|
|
2728
|
+
for (const id of defaultValue) {
|
|
2729
|
+
initial[id] = true;
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
|
|
2733
|
+
const item = createStorageItem<Record<string, true>>({
|
|
2734
|
+
...rest,
|
|
2735
|
+
defaultValue: initial,
|
|
2736
|
+
});
|
|
2737
|
+
|
|
2738
|
+
const has = (id: TMember): boolean => item.get()[id] === true;
|
|
2739
|
+
|
|
2740
|
+
const add = (id: TMember): void => {
|
|
2741
|
+
if (item.get()[id] === true) {
|
|
2742
|
+
return;
|
|
2743
|
+
}
|
|
2744
|
+
item.merge({ [id]: true });
|
|
2745
|
+
};
|
|
2746
|
+
|
|
2747
|
+
const deleteId = (id: TMember): void => {
|
|
2748
|
+
const current = item.get();
|
|
2749
|
+
if (current[id] !== true) {
|
|
2750
|
+
return;
|
|
2751
|
+
}
|
|
2752
|
+
const next = { ...current };
|
|
2753
|
+
delete next[id];
|
|
2754
|
+
item.set(next);
|
|
2755
|
+
};
|
|
2756
|
+
|
|
2757
|
+
const toggle = (id: TMember): boolean => {
|
|
2758
|
+
if (has(id)) {
|
|
2759
|
+
deleteId(id);
|
|
2760
|
+
return false;
|
|
2761
|
+
}
|
|
2762
|
+
add(id);
|
|
2763
|
+
return true;
|
|
2764
|
+
};
|
|
2765
|
+
|
|
2766
|
+
return {
|
|
2767
|
+
get: item.get,
|
|
2768
|
+
has,
|
|
2769
|
+
add,
|
|
2770
|
+
delete: deleteId,
|
|
2771
|
+
toggle,
|
|
2772
|
+
values: () => Object.keys(item.get()) as TMember[],
|
|
2773
|
+
size: () => Object.keys(item.get()).length,
|
|
2774
|
+
clear: () => item.set({}),
|
|
2775
|
+
reset: item.reset,
|
|
2776
|
+
subscribe: item.subscribe,
|
|
2777
|
+
scope: item.scope,
|
|
2778
|
+
key: item.key,
|
|
2779
|
+
item,
|
|
2780
|
+
};
|
|
2781
|
+
}
|
|
2782
|
+
|
|
2335
2783
|
return {
|
|
2336
2784
|
storage,
|
|
2337
2785
|
createStorageItem,
|
|
2786
|
+
memoryItem,
|
|
2787
|
+
diskItem,
|
|
2788
|
+
secureItem,
|
|
2789
|
+
createSetItem,
|
|
2338
2790
|
getBatch,
|
|
2339
2791
|
setBatch,
|
|
2340
2792
|
removeBatch,
|
package/src/storage-events.ts
CHANGED
|
@@ -5,9 +5,11 @@ export type StorageChangeOperation =
|
|
|
5
5
|
| "remove"
|
|
6
6
|
| "clear"
|
|
7
7
|
| "clearNamespace"
|
|
8
|
+
| "clearGroup"
|
|
8
9
|
| "setBatch"
|
|
9
10
|
| "removeBatch"
|
|
10
11
|
| "import"
|
|
12
|
+
| "expire"
|
|
11
13
|
| "external";
|
|
12
14
|
|
|
13
15
|
export type StorageChangeSource = "memory" | "native" | "web" | "external";
|
|
@@ -31,8 +33,7 @@ export type StorageBatchChangeEvent = {
|
|
|
31
33
|
};
|
|
32
34
|
|
|
33
35
|
export type StorageChangeEvent =
|
|
34
|
-
|
|
|
35
|
-
| StorageBatchChangeEvent;
|
|
36
|
+
StorageKeyChangeEvent | StorageBatchChangeEvent;
|
|
36
37
|
|
|
37
38
|
export type StorageEventListener = (event: StorageChangeEvent) => void;
|
|
38
39
|
|