react-native-nitro-storage 0.3.2 → 0.4.1
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 +192 -30
- package/android/src/main/cpp/AndroidStorageAdapterCpp.cpp +22 -2
- package/android/src/main/cpp/AndroidStorageAdapterCpp.hpp +3 -0
- package/android/src/main/cpp/cpp-adapter.cpp +3 -1
- package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +54 -5
- package/cpp/bindings/HybridStorage.cpp +167 -22
- package/cpp/bindings/HybridStorage.hpp +12 -1
- package/cpp/core/NativeStorageAdapter.hpp +3 -0
- package/ios/IOSStorageAdapterCpp.hpp +16 -0
- package/ios/IOSStorageAdapterCpp.mm +135 -11
- package/lib/commonjs/index.js +522 -275
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +614 -270
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/indexeddb-backend.js +130 -0
- package/lib/commonjs/indexeddb-backend.js.map +1 -0
- package/lib/commonjs/internal.js +25 -0
- package/lib/commonjs/internal.js.map +1 -1
- package/lib/module/index.js +516 -277
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +608 -272
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/indexeddb-backend.js +126 -0
- package/lib/module/indexeddb-backend.js.map +1 -0
- package/lib/module/internal.js +24 -0
- package/lib/module/internal.js.map +1 -1
- 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 +40 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/index.web.d.ts +42 -1
- package/lib/typescript/index.web.d.ts.map +1 -1
- package/lib/typescript/indexeddb-backend.d.ts +29 -0
- package/lib/typescript/indexeddb-backend.d.ts.map +1 -0
- package/lib/typescript/internal.d.ts +1 -0
- package/lib/typescript/internal.d.ts.map +1 -1
- package/nitrogen/generated/android/NitroStorageOnLoad.cpp +22 -17
- package/nitrogen/generated/android/NitroStorageOnLoad.hpp +13 -4
- package/nitrogen/generated/shared/c++/HybridStorageSpec.cpp +2 -0
- package/nitrogen/generated/shared/c++/HybridStorageSpec.hpp +2 -0
- package/package.json +7 -3
- package/src/Storage.nitro.ts +2 -0
- package/src/index.ts +671 -296
- package/src/index.web.ts +776 -288
- package/src/indexeddb-backend.ts +143 -0
- package/src/internal.ts +28 -0
package/src/index.web.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
assertValidScope,
|
|
8
8
|
serializeWithPrimitiveFastPath,
|
|
9
9
|
deserializeWithPrimitiveFastPath,
|
|
10
|
+
toVersionToken,
|
|
10
11
|
prefixKey,
|
|
11
12
|
isNamespaced,
|
|
12
13
|
} from "./internal";
|
|
@@ -18,6 +19,31 @@ export type Validator<T> = (value: unknown) => value is T;
|
|
|
18
19
|
export type ExpirationConfig = {
|
|
19
20
|
ttlMs: number;
|
|
20
21
|
};
|
|
22
|
+
export type StorageVersion = string;
|
|
23
|
+
export type VersionedValue<T> = {
|
|
24
|
+
value: T;
|
|
25
|
+
version: StorageVersion;
|
|
26
|
+
};
|
|
27
|
+
export type StorageMetricsEvent = {
|
|
28
|
+
operation: string;
|
|
29
|
+
scope: StorageScope;
|
|
30
|
+
durationMs: number;
|
|
31
|
+
keysCount: number;
|
|
32
|
+
};
|
|
33
|
+
export type StorageMetricsObserver = (event: StorageMetricsEvent) => void;
|
|
34
|
+
export type StorageMetricSummary = {
|
|
35
|
+
count: number;
|
|
36
|
+
totalDurationMs: number;
|
|
37
|
+
avgDurationMs: number;
|
|
38
|
+
maxDurationMs: number;
|
|
39
|
+
};
|
|
40
|
+
export type WebSecureStorageBackend = {
|
|
41
|
+
getItem: (key: string) => string | null;
|
|
42
|
+
setItem: (key: string, value: string) => void;
|
|
43
|
+
removeItem: (key: string) => void;
|
|
44
|
+
clear: () => void;
|
|
45
|
+
getAllKeys: () => string[];
|
|
46
|
+
};
|
|
21
47
|
|
|
22
48
|
export type MigrationContext = {
|
|
23
49
|
scope: StorageScope;
|
|
@@ -65,7 +91,11 @@ function typedKeys<K extends string, V>(record: Record<K, V>): K[] {
|
|
|
65
91
|
return Object.keys(record) as K[];
|
|
66
92
|
}
|
|
67
93
|
type NonMemoryScope = StorageScope.Disk | StorageScope.Secure;
|
|
68
|
-
type PendingSecureWrite = {
|
|
94
|
+
type PendingSecureWrite = {
|
|
95
|
+
key: string;
|
|
96
|
+
value: string | undefined;
|
|
97
|
+
accessControl?: AccessControl;
|
|
98
|
+
};
|
|
69
99
|
type BrowserStorageLike = {
|
|
70
100
|
setItem: (key: string, value: string) => void;
|
|
71
101
|
getItem: (key: string) => string | null;
|
|
@@ -93,6 +123,7 @@ export interface Storage {
|
|
|
93
123
|
clear(scope: number): void;
|
|
94
124
|
has(key: string, scope: number): boolean;
|
|
95
125
|
getAllKeys(scope: number): string[];
|
|
126
|
+
getKeysByPrefix(prefix: string, scope: number): string[];
|
|
96
127
|
size(scope: number): number;
|
|
97
128
|
setBatch(keys: string[], values: string[], scope: number): void;
|
|
98
129
|
getBatch(keys: string[], scope: number): (string | undefined)[];
|
|
@@ -106,6 +137,7 @@ export interface Storage {
|
|
|
106
137
|
setSecureWritesAsync(enabled: boolean): void;
|
|
107
138
|
setKeychainAccessGroup(group: string): void;
|
|
108
139
|
setSecureBiometric(key: string, value: string): void;
|
|
140
|
+
setSecureBiometricWithLevel(key: string, value: string, level: number): void;
|
|
109
141
|
getSecureBiometric(key: string): string | undefined;
|
|
110
142
|
deleteSecureBiometric(key: string): void;
|
|
111
143
|
hasSecureBiometric(key: string): boolean;
|
|
@@ -134,13 +166,90 @@ let secureFlushScheduled = false;
|
|
|
134
166
|
const SECURE_WEB_PREFIX = "__secure_";
|
|
135
167
|
const BIOMETRIC_WEB_PREFIX = "__bio_";
|
|
136
168
|
let hasWarnedAboutWebBiometricFallback = false;
|
|
169
|
+
let hasWebStorageEventSubscription = false;
|
|
170
|
+
let metricsObserver: StorageMetricsObserver | undefined;
|
|
171
|
+
const metricsCounters = new Map<
|
|
172
|
+
string,
|
|
173
|
+
{ count: number; totalDurationMs: number; maxDurationMs: number }
|
|
174
|
+
>();
|
|
175
|
+
|
|
176
|
+
function recordMetric(
|
|
177
|
+
operation: string,
|
|
178
|
+
scope: StorageScope,
|
|
179
|
+
durationMs: number,
|
|
180
|
+
keysCount = 1,
|
|
181
|
+
): void {
|
|
182
|
+
const existing = metricsCounters.get(operation);
|
|
183
|
+
if (!existing) {
|
|
184
|
+
metricsCounters.set(operation, {
|
|
185
|
+
count: 1,
|
|
186
|
+
totalDurationMs: durationMs,
|
|
187
|
+
maxDurationMs: durationMs,
|
|
188
|
+
});
|
|
189
|
+
} else {
|
|
190
|
+
existing.count += 1;
|
|
191
|
+
existing.totalDurationMs += durationMs;
|
|
192
|
+
existing.maxDurationMs = Math.max(existing.maxDurationMs, durationMs);
|
|
193
|
+
}
|
|
194
|
+
metricsObserver?.({ operation, scope, durationMs, keysCount });
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function measureOperation<T>(
|
|
198
|
+
operation: string,
|
|
199
|
+
scope: StorageScope,
|
|
200
|
+
fn: () => T,
|
|
201
|
+
keysCount = 1,
|
|
202
|
+
): T {
|
|
203
|
+
const start = Date.now();
|
|
204
|
+
try {
|
|
205
|
+
return fn();
|
|
206
|
+
} finally {
|
|
207
|
+
recordMetric(operation, scope, Date.now() - start, keysCount);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function createLocalStorageWebSecureBackend(): WebSecureStorageBackend {
|
|
212
|
+
return {
|
|
213
|
+
getItem: (key) => globalThis.localStorage?.getItem(key) ?? null,
|
|
214
|
+
setItem: (key, value) => globalThis.localStorage?.setItem(key, value),
|
|
215
|
+
removeItem: (key) => globalThis.localStorage?.removeItem(key),
|
|
216
|
+
clear: () => globalThis.localStorage?.clear(),
|
|
217
|
+
getAllKeys: () => {
|
|
218
|
+
const storage = globalThis.localStorage;
|
|
219
|
+
if (!storage) return [];
|
|
220
|
+
const keys: string[] = [];
|
|
221
|
+
for (let index = 0; index < storage.length; index += 1) {
|
|
222
|
+
const key = storage.key(index);
|
|
223
|
+
if (key) {
|
|
224
|
+
keys.push(key);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return keys;
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
let webSecureStorageBackend: WebSecureStorageBackend | undefined =
|
|
233
|
+
createLocalStorageWebSecureBackend();
|
|
137
234
|
|
|
138
235
|
function getBrowserStorage(scope: number): BrowserStorageLike | undefined {
|
|
139
236
|
if (scope === StorageScope.Disk) {
|
|
140
237
|
return globalThis.localStorage;
|
|
141
238
|
}
|
|
142
239
|
if (scope === StorageScope.Secure) {
|
|
143
|
-
|
|
240
|
+
if (!webSecureStorageBackend) {
|
|
241
|
+
return undefined;
|
|
242
|
+
}
|
|
243
|
+
return {
|
|
244
|
+
setItem: (key, value) => webSecureStorageBackend?.setItem(key, value),
|
|
245
|
+
getItem: (key) => webSecureStorageBackend?.getItem(key) ?? null,
|
|
246
|
+
removeItem: (key) => webSecureStorageBackend?.removeItem(key),
|
|
247
|
+
clear: () => webSecureStorageBackend?.clear(),
|
|
248
|
+
key: (index) => webSecureStorageBackend?.getAllKeys()[index] ?? null,
|
|
249
|
+
get length() {
|
|
250
|
+
return webSecureStorageBackend?.getAllKeys().length ?? 0;
|
|
251
|
+
},
|
|
252
|
+
};
|
|
144
253
|
}
|
|
145
254
|
return undefined;
|
|
146
255
|
}
|
|
@@ -206,6 +315,74 @@ function ensureWebScopeKeyIndex(scope: NonMemoryScope): Set<string> {
|
|
|
206
315
|
return getWebScopeKeyIndex(scope);
|
|
207
316
|
}
|
|
208
317
|
|
|
318
|
+
function handleWebStorageEvent(event: StorageEvent): void {
|
|
319
|
+
const key = event.key;
|
|
320
|
+
if (key === null) {
|
|
321
|
+
clearScopeRawCache(StorageScope.Disk);
|
|
322
|
+
clearScopeRawCache(StorageScope.Secure);
|
|
323
|
+
ensureWebScopeKeyIndex(StorageScope.Disk).clear();
|
|
324
|
+
ensureWebScopeKeyIndex(StorageScope.Secure).clear();
|
|
325
|
+
notifyAllListeners(getScopedListeners(StorageScope.Disk));
|
|
326
|
+
notifyAllListeners(getScopedListeners(StorageScope.Secure));
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (key.startsWith(SECURE_WEB_PREFIX)) {
|
|
331
|
+
const plainKey = fromSecureStorageKey(key);
|
|
332
|
+
if (event.newValue === null) {
|
|
333
|
+
ensureWebScopeKeyIndex(StorageScope.Secure).delete(plainKey);
|
|
334
|
+
cacheRawValue(StorageScope.Secure, plainKey, undefined);
|
|
335
|
+
} else {
|
|
336
|
+
ensureWebScopeKeyIndex(StorageScope.Secure).add(plainKey);
|
|
337
|
+
cacheRawValue(StorageScope.Secure, plainKey, event.newValue);
|
|
338
|
+
}
|
|
339
|
+
notifyKeyListeners(getScopedListeners(StorageScope.Secure), plainKey);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (key.startsWith(BIOMETRIC_WEB_PREFIX)) {
|
|
344
|
+
const plainKey = fromBiometricStorageKey(key);
|
|
345
|
+
if (event.newValue === null) {
|
|
346
|
+
if (
|
|
347
|
+
getBrowserStorage(StorageScope.Secure)?.getItem(
|
|
348
|
+
toSecureStorageKey(plainKey),
|
|
349
|
+
) === null
|
|
350
|
+
) {
|
|
351
|
+
ensureWebScopeKeyIndex(StorageScope.Secure).delete(plainKey);
|
|
352
|
+
}
|
|
353
|
+
cacheRawValue(StorageScope.Secure, plainKey, undefined);
|
|
354
|
+
} else {
|
|
355
|
+
ensureWebScopeKeyIndex(StorageScope.Secure).add(plainKey);
|
|
356
|
+
cacheRawValue(StorageScope.Secure, plainKey, event.newValue);
|
|
357
|
+
}
|
|
358
|
+
notifyKeyListeners(getScopedListeners(StorageScope.Secure), plainKey);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (event.newValue === null) {
|
|
363
|
+
ensureWebScopeKeyIndex(StorageScope.Disk).delete(key);
|
|
364
|
+
cacheRawValue(StorageScope.Disk, key, undefined);
|
|
365
|
+
} else {
|
|
366
|
+
ensureWebScopeKeyIndex(StorageScope.Disk).add(key);
|
|
367
|
+
cacheRawValue(StorageScope.Disk, key, event.newValue);
|
|
368
|
+
}
|
|
369
|
+
notifyKeyListeners(getScopedListeners(StorageScope.Disk), key);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function ensureWebStorageEventSubscription(): void {
|
|
373
|
+
if (hasWebStorageEventSubscription) {
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
if (
|
|
377
|
+
typeof window === "undefined" ||
|
|
378
|
+
typeof window.addEventListener !== "function"
|
|
379
|
+
) {
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
window.addEventListener("storage", handleWebStorageEvent);
|
|
383
|
+
hasWebStorageEventSubscription = true;
|
|
384
|
+
}
|
|
385
|
+
|
|
209
386
|
function getScopedListeners(scope: NonMemoryScope): KeyListenerRegistry {
|
|
210
387
|
return webScopeListeners.get(scope)!;
|
|
211
388
|
}
|
|
@@ -295,29 +472,46 @@ function flushSecureWrites(): void {
|
|
|
295
472
|
const writes = Array.from(pendingSecureWrites.values());
|
|
296
473
|
pendingSecureWrites.clear();
|
|
297
474
|
|
|
298
|
-
const
|
|
299
|
-
|
|
475
|
+
const groupedSetWrites = new Map<
|
|
476
|
+
AccessControl,
|
|
477
|
+
{ keys: string[]; values: string[] }
|
|
478
|
+
>();
|
|
300
479
|
const keysToRemove: string[] = [];
|
|
301
480
|
|
|
302
|
-
writes.forEach(({ key, value }) => {
|
|
481
|
+
writes.forEach(({ key, value, accessControl }) => {
|
|
303
482
|
if (value === undefined) {
|
|
304
483
|
keysToRemove.push(key);
|
|
305
484
|
} else {
|
|
306
|
-
|
|
307
|
-
|
|
485
|
+
const resolvedAccessControl = accessControl ?? AccessControl.WhenUnlocked;
|
|
486
|
+
const existingGroup = groupedSetWrites.get(resolvedAccessControl);
|
|
487
|
+
const group = existingGroup ?? { keys: [], values: [] };
|
|
488
|
+
group.keys.push(key);
|
|
489
|
+
group.values.push(value);
|
|
490
|
+
if (!existingGroup) {
|
|
491
|
+
groupedSetWrites.set(resolvedAccessControl, group);
|
|
492
|
+
}
|
|
308
493
|
}
|
|
309
494
|
});
|
|
310
495
|
|
|
311
|
-
|
|
312
|
-
WebStorage.
|
|
313
|
-
|
|
496
|
+
groupedSetWrites.forEach((group, accessControl) => {
|
|
497
|
+
WebStorage.setSecureAccessControl(accessControl);
|
|
498
|
+
WebStorage.setBatch(group.keys, group.values, StorageScope.Secure);
|
|
499
|
+
});
|
|
314
500
|
if (keysToRemove.length > 0) {
|
|
315
501
|
WebStorage.removeBatch(keysToRemove, StorageScope.Secure);
|
|
316
502
|
}
|
|
317
503
|
}
|
|
318
504
|
|
|
319
|
-
function scheduleSecureWrite(
|
|
320
|
-
|
|
505
|
+
function scheduleSecureWrite(
|
|
506
|
+
key: string,
|
|
507
|
+
value: string | undefined,
|
|
508
|
+
accessControl?: AccessControl,
|
|
509
|
+
): void {
|
|
510
|
+
const pendingWrite: PendingSecureWrite = { key, value };
|
|
511
|
+
if (accessControl !== undefined) {
|
|
512
|
+
pendingWrite.accessControl = accessControl;
|
|
513
|
+
}
|
|
514
|
+
pendingSecureWrites.set(key, pendingWrite);
|
|
321
515
|
if (secureFlushScheduled) {
|
|
322
516
|
return;
|
|
323
517
|
}
|
|
@@ -491,6 +685,14 @@ const WebStorage: Storage = {
|
|
|
491
685
|
}
|
|
492
686
|
return Array.from(ensureWebScopeKeyIndex(scope));
|
|
493
687
|
},
|
|
688
|
+
getKeysByPrefix: (prefix: string, scope: number) => {
|
|
689
|
+
if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
|
|
690
|
+
return [];
|
|
691
|
+
}
|
|
692
|
+
return Array.from(ensureWebScopeKeyIndex(scope)).filter((key) =>
|
|
693
|
+
key.startsWith(prefix),
|
|
694
|
+
);
|
|
695
|
+
},
|
|
494
696
|
size: (scope: number) => {
|
|
495
697
|
if (scope === StorageScope.Disk || scope === StorageScope.Secure) {
|
|
496
698
|
return ensureWebScopeKeyIndex(scope).size;
|
|
@@ -501,6 +703,13 @@ const WebStorage: Storage = {
|
|
|
501
703
|
setSecureWritesAsync: (_enabled: boolean) => {},
|
|
502
704
|
setKeychainAccessGroup: () => {},
|
|
503
705
|
setSecureBiometric: (key: string, value: string) => {
|
|
706
|
+
WebStorage.setSecureBiometricWithLevel(
|
|
707
|
+
key,
|
|
708
|
+
value,
|
|
709
|
+
BiometricLevel.BiometryOnly,
|
|
710
|
+
);
|
|
711
|
+
},
|
|
712
|
+
setSecureBiometricWithLevel: (key: string, value: string, _level: number) => {
|
|
504
713
|
if (
|
|
505
714
|
typeof __DEV__ !== "undefined" &&
|
|
506
715
|
__DEV__ &&
|
|
@@ -511,17 +720,22 @@ const WebStorage: Storage = {
|
|
|
511
720
|
"[NitroStorage] Biometric storage is not supported on web. Using localStorage.",
|
|
512
721
|
);
|
|
513
722
|
}
|
|
514
|
-
|
|
723
|
+
getBrowserStorage(StorageScope.Secure)?.setItem(
|
|
724
|
+
toBiometricStorageKey(key),
|
|
725
|
+
value,
|
|
726
|
+
);
|
|
515
727
|
ensureWebScopeKeyIndex(StorageScope.Secure).add(key);
|
|
516
728
|
notifyKeyListeners(getScopedListeners(StorageScope.Secure), key);
|
|
517
729
|
},
|
|
518
730
|
getSecureBiometric: (key: string) => {
|
|
519
731
|
return (
|
|
520
|
-
|
|
732
|
+
getBrowserStorage(StorageScope.Secure)?.getItem(
|
|
733
|
+
toBiometricStorageKey(key),
|
|
734
|
+
) ?? undefined
|
|
521
735
|
);
|
|
522
736
|
},
|
|
523
737
|
deleteSecureBiometric: (key: string) => {
|
|
524
|
-
const storage =
|
|
738
|
+
const storage = getBrowserStorage(StorageScope.Secure);
|
|
525
739
|
storage?.removeItem(toBiometricStorageKey(key));
|
|
526
740
|
if (storage?.getItem(toSecureStorageKey(key)) === null) {
|
|
527
741
|
ensureWebScopeKeyIndex(StorageScope.Secure).delete(key);
|
|
@@ -530,11 +744,13 @@ const WebStorage: Storage = {
|
|
|
530
744
|
},
|
|
531
745
|
hasSecureBiometric: (key: string) => {
|
|
532
746
|
return (
|
|
533
|
-
|
|
747
|
+
getBrowserStorage(StorageScope.Secure)?.getItem(
|
|
748
|
+
toBiometricStorageKey(key),
|
|
749
|
+
) !== null
|
|
534
750
|
);
|
|
535
751
|
},
|
|
536
752
|
clearSecureBiometric: () => {
|
|
537
|
-
const storage =
|
|
753
|
+
const storage = getBrowserStorage(StorageScope.Secure);
|
|
538
754
|
if (!storage) return;
|
|
539
755
|
const keysToNotify: string[] = [];
|
|
540
756
|
const toRemove: string[] = [];
|
|
@@ -621,85 +837,213 @@ function writeMigrationVersion(scope: StorageScope, version: number): void {
|
|
|
621
837
|
|
|
622
838
|
export const storage = {
|
|
623
839
|
clear: (scope: StorageScope) => {
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
840
|
+
measureOperation("storage:clear", scope, () => {
|
|
841
|
+
if (scope === StorageScope.Memory) {
|
|
842
|
+
memoryStore.clear();
|
|
843
|
+
notifyAllListeners(memoryListeners);
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
629
846
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
847
|
+
if (scope === StorageScope.Secure) {
|
|
848
|
+
flushSecureWrites();
|
|
849
|
+
pendingSecureWrites.clear();
|
|
850
|
+
}
|
|
634
851
|
|
|
635
|
-
|
|
636
|
-
|
|
852
|
+
clearScopeRawCache(scope);
|
|
853
|
+
WebStorage.clear(scope);
|
|
854
|
+
});
|
|
637
855
|
},
|
|
638
856
|
clearAll: () => {
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
857
|
+
measureOperation(
|
|
858
|
+
"storage:clearAll",
|
|
859
|
+
StorageScope.Memory,
|
|
860
|
+
() => {
|
|
861
|
+
storage.clear(StorageScope.Memory);
|
|
862
|
+
storage.clear(StorageScope.Disk);
|
|
863
|
+
storage.clear(StorageScope.Secure);
|
|
864
|
+
},
|
|
865
|
+
3,
|
|
866
|
+
);
|
|
642
867
|
},
|
|
643
868
|
clearNamespace: (namespace: string, scope: StorageScope) => {
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
869
|
+
measureOperation("storage:clearNamespace", scope, () => {
|
|
870
|
+
assertValidScope(scope);
|
|
871
|
+
if (scope === StorageScope.Memory) {
|
|
872
|
+
for (const key of memoryStore.keys()) {
|
|
873
|
+
if (isNamespaced(key, namespace)) {
|
|
874
|
+
memoryStore.delete(key);
|
|
875
|
+
}
|
|
649
876
|
}
|
|
877
|
+
notifyAllListeners(memoryListeners);
|
|
878
|
+
return;
|
|
650
879
|
}
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
WebStorage.removeByPrefix(keyPrefix, scope);
|
|
880
|
+
|
|
881
|
+
const keyPrefix = prefixKey(namespace, "");
|
|
882
|
+
if (scope === StorageScope.Secure) {
|
|
883
|
+
flushSecureWrites();
|
|
884
|
+
}
|
|
885
|
+
clearScopeRawCache(scope);
|
|
886
|
+
WebStorage.removeByPrefix(keyPrefix, scope);
|
|
887
|
+
});
|
|
660
888
|
},
|
|
661
889
|
clearBiometric: () => {
|
|
662
|
-
|
|
890
|
+
measureOperation("storage:clearBiometric", StorageScope.Secure, () => {
|
|
891
|
+
WebStorage.clearSecureBiometric();
|
|
892
|
+
});
|
|
663
893
|
},
|
|
664
894
|
has: (key: string, scope: StorageScope): boolean => {
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
895
|
+
return measureOperation("storage:has", scope, () => {
|
|
896
|
+
assertValidScope(scope);
|
|
897
|
+
if (scope === StorageScope.Memory) return memoryStore.has(key);
|
|
898
|
+
return WebStorage.has(key, scope);
|
|
899
|
+
});
|
|
668
900
|
},
|
|
669
901
|
getAllKeys: (scope: StorageScope): string[] => {
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
902
|
+
return measureOperation("storage:getAllKeys", scope, () => {
|
|
903
|
+
assertValidScope(scope);
|
|
904
|
+
if (scope === StorageScope.Memory) return Array.from(memoryStore.keys());
|
|
905
|
+
return WebStorage.getAllKeys(scope);
|
|
906
|
+
});
|
|
907
|
+
},
|
|
908
|
+
getKeysByPrefix: (prefix: string, scope: StorageScope): string[] => {
|
|
909
|
+
return measureOperation("storage:getKeysByPrefix", scope, () => {
|
|
910
|
+
assertValidScope(scope);
|
|
911
|
+
if (scope === StorageScope.Memory) {
|
|
912
|
+
return Array.from(memoryStore.keys()).filter((key) =>
|
|
913
|
+
key.startsWith(prefix),
|
|
914
|
+
);
|
|
915
|
+
}
|
|
916
|
+
return WebStorage.getKeysByPrefix(prefix, scope);
|
|
917
|
+
});
|
|
918
|
+
},
|
|
919
|
+
getByPrefix: (
|
|
920
|
+
prefix: string,
|
|
921
|
+
scope: StorageScope,
|
|
922
|
+
): Record<string, string> => {
|
|
923
|
+
return measureOperation("storage:getByPrefix", scope, () => {
|
|
924
|
+
const result: Record<string, string> = {};
|
|
925
|
+
const keys = storage.getKeysByPrefix(prefix, scope);
|
|
926
|
+
if (keys.length === 0) {
|
|
927
|
+
return result;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
if (scope === StorageScope.Memory) {
|
|
931
|
+
keys.forEach((key) => {
|
|
932
|
+
const value = memoryStore.get(key);
|
|
933
|
+
if (typeof value === "string") {
|
|
934
|
+
result[key] = value;
|
|
935
|
+
}
|
|
936
|
+
});
|
|
937
|
+
return result;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
const values = WebStorage.getBatch(keys, scope);
|
|
941
|
+
keys.forEach((key, index) => {
|
|
942
|
+
const value = values[index];
|
|
943
|
+
if (value !== undefined) {
|
|
944
|
+
result[key] = value;
|
|
945
|
+
}
|
|
946
|
+
});
|
|
947
|
+
return result;
|
|
948
|
+
});
|
|
673
949
|
},
|
|
674
950
|
getAll: (scope: StorageScope): Record<string, string> => {
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
951
|
+
return measureOperation("storage:getAll", scope, () => {
|
|
952
|
+
assertValidScope(scope);
|
|
953
|
+
const result: Record<string, string> = {};
|
|
954
|
+
if (scope === StorageScope.Memory) {
|
|
955
|
+
memoryStore.forEach((value, key) => {
|
|
956
|
+
if (typeof value === "string") result[key] = value;
|
|
957
|
+
});
|
|
958
|
+
return result;
|
|
959
|
+
}
|
|
960
|
+
const keys = WebStorage.getAllKeys(scope);
|
|
961
|
+
keys.forEach((key) => {
|
|
962
|
+
const val = WebStorage.get(key, scope);
|
|
963
|
+
if (val !== undefined) result[key] = val;
|
|
680
964
|
});
|
|
681
965
|
return result;
|
|
682
|
-
}
|
|
683
|
-
const keys = WebStorage.getAllKeys(scope);
|
|
684
|
-
keys.forEach((key) => {
|
|
685
|
-
const val = WebStorage.get(key, scope);
|
|
686
|
-
if (val !== undefined) result[key] = val;
|
|
687
966
|
});
|
|
688
|
-
return result;
|
|
689
967
|
},
|
|
690
968
|
size: (scope: StorageScope): number => {
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
969
|
+
return measureOperation("storage:size", scope, () => {
|
|
970
|
+
assertValidScope(scope);
|
|
971
|
+
if (scope === StorageScope.Memory) return memoryStore.size;
|
|
972
|
+
return WebStorage.size(scope);
|
|
973
|
+
});
|
|
974
|
+
},
|
|
975
|
+
setAccessControl: (_level: AccessControl) => {
|
|
976
|
+
recordMetric("storage:setAccessControl", StorageScope.Secure, 0);
|
|
977
|
+
},
|
|
978
|
+
setSecureWritesAsync: (_enabled: boolean) => {
|
|
979
|
+
recordMetric("storage:setSecureWritesAsync", StorageScope.Secure, 0);
|
|
694
980
|
},
|
|
695
|
-
setAccessControl: (_level: AccessControl) => {},
|
|
696
|
-
setSecureWritesAsync: (_enabled: boolean) => {},
|
|
697
981
|
flushSecureWrites: () => {
|
|
698
|
-
flushSecureWrites()
|
|
982
|
+
measureOperation("storage:flushSecureWrites", StorageScope.Secure, () => {
|
|
983
|
+
flushSecureWrites();
|
|
984
|
+
});
|
|
985
|
+
},
|
|
986
|
+
setKeychainAccessGroup: (_group: string) => {
|
|
987
|
+
recordMetric("storage:setKeychainAccessGroup", StorageScope.Secure, 0);
|
|
988
|
+
},
|
|
989
|
+
setMetricsObserver: (observer?: StorageMetricsObserver) => {
|
|
990
|
+
metricsObserver = observer;
|
|
991
|
+
},
|
|
992
|
+
getMetricsSnapshot: (): Record<string, StorageMetricSummary> => {
|
|
993
|
+
const snapshot: Record<string, StorageMetricSummary> = {};
|
|
994
|
+
metricsCounters.forEach((value, key) => {
|
|
995
|
+
snapshot[key] = {
|
|
996
|
+
count: value.count,
|
|
997
|
+
totalDurationMs: value.totalDurationMs,
|
|
998
|
+
avgDurationMs:
|
|
999
|
+
value.count === 0 ? 0 : value.totalDurationMs / value.count,
|
|
1000
|
+
maxDurationMs: value.maxDurationMs,
|
|
1001
|
+
};
|
|
1002
|
+
});
|
|
1003
|
+
return snapshot;
|
|
1004
|
+
},
|
|
1005
|
+
resetMetrics: () => {
|
|
1006
|
+
metricsCounters.clear();
|
|
1007
|
+
},
|
|
1008
|
+
import: (data: Record<string, string>, scope: StorageScope): void => {
|
|
1009
|
+
measureOperation(
|
|
1010
|
+
"storage:import",
|
|
1011
|
+
scope,
|
|
1012
|
+
() => {
|
|
1013
|
+
assertValidScope(scope);
|
|
1014
|
+
const keys = Object.keys(data);
|
|
1015
|
+
if (keys.length === 0) return;
|
|
1016
|
+
const values = keys.map((k) => data[k]!);
|
|
1017
|
+
|
|
1018
|
+
if (scope === StorageScope.Memory) {
|
|
1019
|
+
keys.forEach((key, index) => {
|
|
1020
|
+
memoryStore.set(key, values[index]);
|
|
1021
|
+
});
|
|
1022
|
+
keys.forEach((key) => notifyKeyListeners(memoryListeners, key));
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
WebStorage.setBatch(keys, values, scope);
|
|
1027
|
+
},
|
|
1028
|
+
Object.keys(data).length,
|
|
1029
|
+
);
|
|
699
1030
|
},
|
|
700
|
-
setKeychainAccessGroup: (_group: string) => {},
|
|
701
1031
|
};
|
|
702
1032
|
|
|
1033
|
+
export function setWebSecureStorageBackend(
|
|
1034
|
+
backend?: WebSecureStorageBackend,
|
|
1035
|
+
): void {
|
|
1036
|
+
webSecureStorageBackend = backend ?? createLocalStorageWebSecureBackend();
|
|
1037
|
+
hydratedWebScopeKeyIndex.delete(StorageScope.Secure);
|
|
1038
|
+
clearScopeRawCache(StorageScope.Secure);
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
export function getWebSecureStorageBackend():
|
|
1042
|
+
| WebSecureStorageBackend
|
|
1043
|
+
| undefined {
|
|
1044
|
+
return webSecureStorageBackend;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
703
1047
|
export interface StorageItemConfig<T> {
|
|
704
1048
|
key: string;
|
|
705
1049
|
scope: StorageScope;
|
|
@@ -714,12 +1058,18 @@ export interface StorageItemConfig<T> {
|
|
|
714
1058
|
coalesceSecureWrites?: boolean;
|
|
715
1059
|
namespace?: string;
|
|
716
1060
|
biometric?: boolean;
|
|
1061
|
+
biometricLevel?: BiometricLevel;
|
|
717
1062
|
accessControl?: AccessControl;
|
|
718
1063
|
}
|
|
719
1064
|
|
|
720
1065
|
export interface StorageItem<T> {
|
|
721
1066
|
get: () => T;
|
|
1067
|
+
getWithVersion: () => VersionedValue<T>;
|
|
722
1068
|
set: (value: T | ((prev: T) => T)) => void;
|
|
1069
|
+
setIfVersion: (
|
|
1070
|
+
version: StorageVersion,
|
|
1071
|
+
value: T | ((prev: T) => T),
|
|
1072
|
+
) => boolean;
|
|
723
1073
|
delete: () => void;
|
|
724
1074
|
has: () => boolean;
|
|
725
1075
|
subscribe: (callback: () => void) => () => void;
|
|
@@ -731,10 +1081,12 @@ export interface StorageItem<T> {
|
|
|
731
1081
|
|
|
732
1082
|
type StorageItemInternal<T> = StorageItem<T> & {
|
|
733
1083
|
_triggerListeners: () => void;
|
|
1084
|
+
_invalidateParsedCacheOnly: () => void;
|
|
734
1085
|
_hasValidation: boolean;
|
|
735
1086
|
_hasExpiration: boolean;
|
|
736
1087
|
_readCacheEnabled: boolean;
|
|
737
1088
|
_isBiometric: boolean;
|
|
1089
|
+
_defaultValue: T;
|
|
738
1090
|
_secureAccessControl?: AccessControl;
|
|
739
1091
|
};
|
|
740
1092
|
|
|
@@ -770,8 +1122,14 @@ export function createStorageItem<T = undefined>(
|
|
|
770
1122
|
const serialize = config.serialize ?? defaultSerialize;
|
|
771
1123
|
const deserialize = config.deserialize ?? defaultDeserialize;
|
|
772
1124
|
const isMemory = config.scope === StorageScope.Memory;
|
|
773
|
-
const
|
|
774
|
-
config.
|
|
1125
|
+
const resolvedBiometricLevel =
|
|
1126
|
+
config.scope === StorageScope.Secure
|
|
1127
|
+
? (config.biometricLevel ??
|
|
1128
|
+
(config.biometric === true
|
|
1129
|
+
? BiometricLevel.BiometryOnly
|
|
1130
|
+
: BiometricLevel.None))
|
|
1131
|
+
: BiometricLevel.None;
|
|
1132
|
+
const isBiometric = resolvedBiometricLevel !== BiometricLevel.None;
|
|
775
1133
|
const secureAccessControl = config.accessControl;
|
|
776
1134
|
const validate = config.validate;
|
|
777
1135
|
const onValidationError = config.onValidationError;
|
|
@@ -784,8 +1142,7 @@ export function createStorageItem<T = undefined>(
|
|
|
784
1142
|
const coalesceSecureWrites =
|
|
785
1143
|
config.scope === StorageScope.Secure &&
|
|
786
1144
|
config.coalesceSecureWrites === true &&
|
|
787
|
-
!isBiometric
|
|
788
|
-
secureAccessControl === undefined;
|
|
1145
|
+
!isBiometric;
|
|
789
1146
|
const defaultValue = config.defaultValue as T;
|
|
790
1147
|
const nonMemoryScope: NonMemoryScope | null =
|
|
791
1148
|
config.scope === StorageScope.Disk
|
|
@@ -827,6 +1184,7 @@ export function createStorageItem<T = undefined>(
|
|
|
827
1184
|
return;
|
|
828
1185
|
}
|
|
829
1186
|
|
|
1187
|
+
ensureWebStorageEventSubscription();
|
|
830
1188
|
unsubscribe = addKeyListener(
|
|
831
1189
|
getScopedListeners(nonMemoryScope!),
|
|
832
1190
|
storageKey,
|
|
@@ -874,14 +1232,22 @@ export function createStorageItem<T = undefined>(
|
|
|
874
1232
|
|
|
875
1233
|
const writeStoredRaw = (rawValue: string): void => {
|
|
876
1234
|
if (isBiometric) {
|
|
877
|
-
WebStorage.
|
|
1235
|
+
WebStorage.setSecureBiometricWithLevel(
|
|
1236
|
+
storageKey,
|
|
1237
|
+
rawValue,
|
|
1238
|
+
resolvedBiometricLevel,
|
|
1239
|
+
);
|
|
878
1240
|
return;
|
|
879
1241
|
}
|
|
880
1242
|
|
|
881
1243
|
cacheRawValue(nonMemoryScope!, storageKey, rawValue);
|
|
882
1244
|
|
|
883
1245
|
if (coalesceSecureWrites) {
|
|
884
|
-
scheduleSecureWrite(
|
|
1246
|
+
scheduleSecureWrite(
|
|
1247
|
+
storageKey,
|
|
1248
|
+
rawValue,
|
|
1249
|
+
secureAccessControl ?? AccessControl.WhenUnlocked,
|
|
1250
|
+
);
|
|
885
1251
|
return;
|
|
886
1252
|
}
|
|
887
1253
|
|
|
@@ -901,7 +1267,11 @@ export function createStorageItem<T = undefined>(
|
|
|
901
1267
|
cacheRawValue(nonMemoryScope!, storageKey, undefined);
|
|
902
1268
|
|
|
903
1269
|
if (coalesceSecureWrites) {
|
|
904
|
-
scheduleSecureWrite(
|
|
1270
|
+
scheduleSecureWrite(
|
|
1271
|
+
storageKey,
|
|
1272
|
+
undefined,
|
|
1273
|
+
secureAccessControl ?? AccessControl.WhenUnlocked,
|
|
1274
|
+
);
|
|
905
1275
|
return;
|
|
906
1276
|
}
|
|
907
1277
|
|
|
@@ -962,7 +1332,7 @@ export function createStorageItem<T = undefined>(
|
|
|
962
1332
|
return resolved;
|
|
963
1333
|
};
|
|
964
1334
|
|
|
965
|
-
const
|
|
1335
|
+
const getInternal = (): T => {
|
|
966
1336
|
const raw = readStoredRaw();
|
|
967
1337
|
|
|
968
1338
|
if (!memoryExpiration && raw === lastRaw && hasLastValue) {
|
|
@@ -980,6 +1350,7 @@ export function createStorageItem<T = undefined>(
|
|
|
980
1350
|
onExpired?.(storageKey);
|
|
981
1351
|
lastValue = ensureValidatedValue(defaultValue, false);
|
|
982
1352
|
hasLastValue = true;
|
|
1353
|
+
listeners.forEach((cb) => cb());
|
|
983
1354
|
return lastValue;
|
|
984
1355
|
}
|
|
985
1356
|
}
|
|
@@ -1021,6 +1392,7 @@ export function createStorageItem<T = undefined>(
|
|
|
1021
1392
|
onExpired?.(storageKey);
|
|
1022
1393
|
lastValue = ensureValidatedValue(defaultValue, false);
|
|
1023
1394
|
hasLastValue = true;
|
|
1395
|
+
listeners.forEach((cb) => cb());
|
|
1024
1396
|
return lastValue;
|
|
1025
1397
|
}
|
|
1026
1398
|
|
|
@@ -1039,40 +1411,74 @@ export function createStorageItem<T = undefined>(
|
|
|
1039
1411
|
return lastValue;
|
|
1040
1412
|
};
|
|
1041
1413
|
|
|
1414
|
+
const getCurrentVersion = (): StorageVersion => {
|
|
1415
|
+
const raw = readStoredRaw();
|
|
1416
|
+
return toVersionToken(raw);
|
|
1417
|
+
};
|
|
1418
|
+
|
|
1419
|
+
const get = (): T =>
|
|
1420
|
+
measureOperation("item:get", config.scope, () => getInternal());
|
|
1421
|
+
|
|
1422
|
+
const getWithVersion = (): VersionedValue<T> =>
|
|
1423
|
+
measureOperation("item:getWithVersion", config.scope, () => ({
|
|
1424
|
+
value: getInternal(),
|
|
1425
|
+
version: getCurrentVersion(),
|
|
1426
|
+
}));
|
|
1427
|
+
|
|
1042
1428
|
const set = (valueOrFn: T | ((prev: T) => T)): void => {
|
|
1043
|
-
|
|
1429
|
+
measureOperation("item:set", config.scope, () => {
|
|
1430
|
+
const newValue = isUpdater(valueOrFn)
|
|
1431
|
+
? valueOrFn(getInternal())
|
|
1432
|
+
: valueOrFn;
|
|
1044
1433
|
|
|
1045
|
-
|
|
1434
|
+
invalidateParsedCache();
|
|
1046
1435
|
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1436
|
+
if (validate && !validate(newValue)) {
|
|
1437
|
+
throw new Error(
|
|
1438
|
+
`Validation failed for key "${storageKey}" in scope "${StorageScope[config.scope]}".`,
|
|
1439
|
+
);
|
|
1440
|
+
}
|
|
1052
1441
|
|
|
1053
|
-
|
|
1442
|
+
writeValueWithoutValidation(newValue);
|
|
1443
|
+
});
|
|
1054
1444
|
};
|
|
1055
1445
|
|
|
1446
|
+
const setIfVersion = (
|
|
1447
|
+
version: StorageVersion,
|
|
1448
|
+
valueOrFn: T | ((prev: T) => T),
|
|
1449
|
+
): boolean =>
|
|
1450
|
+
measureOperation("item:setIfVersion", config.scope, () => {
|
|
1451
|
+
const currentVersion = getCurrentVersion();
|
|
1452
|
+
if (currentVersion !== version) {
|
|
1453
|
+
return false;
|
|
1454
|
+
}
|
|
1455
|
+
set(valueOrFn);
|
|
1456
|
+
return true;
|
|
1457
|
+
});
|
|
1458
|
+
|
|
1056
1459
|
const deleteItem = (): void => {
|
|
1057
|
-
|
|
1460
|
+
measureOperation("item:delete", config.scope, () => {
|
|
1461
|
+
invalidateParsedCache();
|
|
1058
1462
|
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1463
|
+
if (isMemory) {
|
|
1464
|
+
if (memoryExpiration) {
|
|
1465
|
+
memoryExpiration.delete(storageKey);
|
|
1466
|
+
}
|
|
1467
|
+
memoryStore.delete(storageKey);
|
|
1468
|
+
notifyKeyListeners(memoryListeners, storageKey);
|
|
1469
|
+
return;
|
|
1062
1470
|
}
|
|
1063
|
-
memoryStore.delete(storageKey);
|
|
1064
|
-
notifyKeyListeners(memoryListeners, storageKey);
|
|
1065
|
-
return;
|
|
1066
|
-
}
|
|
1067
1471
|
|
|
1068
|
-
|
|
1472
|
+
removeStoredRaw();
|
|
1473
|
+
});
|
|
1069
1474
|
};
|
|
1070
1475
|
|
|
1071
|
-
const hasItem = (): boolean =>
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1476
|
+
const hasItem = (): boolean =>
|
|
1477
|
+
measureOperation("item:has", config.scope, () => {
|
|
1478
|
+
if (isMemory) return memoryStore.has(storageKey);
|
|
1479
|
+
if (isBiometric) return WebStorage.hasSecureBiometric(storageKey);
|
|
1480
|
+
return WebStorage.has(storageKey, config.scope);
|
|
1481
|
+
});
|
|
1076
1482
|
|
|
1077
1483
|
const subscribe = (callback: () => void): (() => void) => {
|
|
1078
1484
|
ensureSubscription();
|
|
@@ -1088,7 +1494,9 @@ export function createStorageItem<T = undefined>(
|
|
|
1088
1494
|
|
|
1089
1495
|
const storageItem: StorageItemInternal<T> = {
|
|
1090
1496
|
get,
|
|
1497
|
+
getWithVersion,
|
|
1091
1498
|
set,
|
|
1499
|
+
setIfVersion,
|
|
1092
1500
|
delete: deleteItem,
|
|
1093
1501
|
has: hasItem,
|
|
1094
1502
|
subscribe,
|
|
@@ -1098,10 +1506,14 @@ export function createStorageItem<T = undefined>(
|
|
|
1098
1506
|
invalidateParsedCache();
|
|
1099
1507
|
listeners.forEach((listener) => listener());
|
|
1100
1508
|
},
|
|
1509
|
+
_invalidateParsedCacheOnly: () => {
|
|
1510
|
+
invalidateParsedCache();
|
|
1511
|
+
},
|
|
1101
1512
|
_hasValidation: validate !== undefined,
|
|
1102
1513
|
_hasExpiration: expiration !== undefined,
|
|
1103
1514
|
_readCacheEnabled: readCache,
|
|
1104
1515
|
_isBiometric: isBiometric,
|
|
1516
|
+
_defaultValue: defaultValue,
|
|
1105
1517
|
...(secureAccessControl !== undefined
|
|
1106
1518
|
? { _secureAccessControl: secureAccessControl }
|
|
1107
1519
|
: {}),
|
|
@@ -1113,6 +1525,7 @@ export function createStorageItem<T = undefined>(
|
|
|
1113
1525
|
}
|
|
1114
1526
|
|
|
1115
1527
|
export { useStorage, useStorageSelector, useSetStorage } from "./storage-hooks";
|
|
1528
|
+
export { createIndexedDBBackend } from "./indexeddb-backend";
|
|
1116
1529
|
|
|
1117
1530
|
type BatchReadItem<T> = Pick<
|
|
1118
1531
|
StorageItem<T>,
|
|
@@ -1122,6 +1535,7 @@ type BatchReadItem<T> = Pick<
|
|
|
1122
1535
|
_hasExpiration?: boolean;
|
|
1123
1536
|
_readCacheEnabled?: boolean;
|
|
1124
1537
|
_isBiometric?: boolean;
|
|
1538
|
+
_defaultValue?: unknown;
|
|
1125
1539
|
_secureAccessControl?: AccessControl;
|
|
1126
1540
|
};
|
|
1127
1541
|
type BatchRemoveItem = Pick<StorageItem<unknown>, "key" | "scope" | "delete">;
|
|
@@ -1135,154 +1549,192 @@ export function getBatch(
|
|
|
1135
1549
|
items: readonly BatchReadItem<unknown>[],
|
|
1136
1550
|
scope: StorageScope,
|
|
1137
1551
|
): unknown[] {
|
|
1138
|
-
|
|
1552
|
+
return measureOperation(
|
|
1553
|
+
"batch:get",
|
|
1554
|
+
scope,
|
|
1555
|
+
() => {
|
|
1556
|
+
assertBatchScope(items, scope);
|
|
1139
1557
|
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1558
|
+
if (scope === StorageScope.Memory) {
|
|
1559
|
+
return items.map((item) => item.get());
|
|
1560
|
+
}
|
|
1143
1561
|
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
const useBatchCache = items.every((item) => item._readCacheEnabled === true);
|
|
1562
|
+
const useRawBatchPath = items.every((item) =>
|
|
1563
|
+
scope === StorageScope.Secure
|
|
1564
|
+
? canUseSecureRawBatchPath(item)
|
|
1565
|
+
: canUseRawBatchPath(item),
|
|
1566
|
+
);
|
|
1567
|
+
if (!useRawBatchPath) {
|
|
1568
|
+
return items.map((item) => item.get());
|
|
1569
|
+
}
|
|
1153
1570
|
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1571
|
+
const rawValues = new Array<string | undefined>(items.length);
|
|
1572
|
+
const keysToFetch: string[] = [];
|
|
1573
|
+
const keyIndexes: number[] = [];
|
|
1157
1574
|
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1575
|
+
items.forEach((item, index) => {
|
|
1576
|
+
if (scope === StorageScope.Secure) {
|
|
1577
|
+
if (hasPendingSecureWrite(item.key)) {
|
|
1578
|
+
rawValues[index] = readPendingSecureWrite(item.key);
|
|
1579
|
+
return;
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1165
1582
|
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1583
|
+
if (item._readCacheEnabled === true) {
|
|
1584
|
+
if (hasCachedRawValue(scope, item.key)) {
|
|
1585
|
+
rawValues[index] = readCachedRawValue(scope, item.key);
|
|
1586
|
+
return;
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1172
1589
|
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1590
|
+
keysToFetch.push(item.key);
|
|
1591
|
+
keyIndexes.push(index);
|
|
1592
|
+
});
|
|
1176
1593
|
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1594
|
+
if (keysToFetch.length > 0) {
|
|
1595
|
+
const fetchedValues = WebStorage.getBatch(keysToFetch, scope);
|
|
1596
|
+
fetchedValues.forEach((value, index) => {
|
|
1597
|
+
const key = keysToFetch[index];
|
|
1598
|
+
const targetIndex = keyIndexes[index];
|
|
1599
|
+
if (key === undefined || targetIndex === undefined) {
|
|
1600
|
+
return;
|
|
1601
|
+
}
|
|
1602
|
+
rawValues[targetIndex] = value;
|
|
1603
|
+
cacheRawValue(scope, key, value);
|
|
1604
|
+
});
|
|
1184
1605
|
}
|
|
1185
|
-
rawValues[targetIndex] = value;
|
|
1186
|
-
cacheRawValue(scope, key, value);
|
|
1187
|
-
});
|
|
1188
|
-
}
|
|
1189
1606
|
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1607
|
+
return items.map((item, index) => {
|
|
1608
|
+
const raw = rawValues[index];
|
|
1609
|
+
if (raw === undefined) {
|
|
1610
|
+
return asInternal(item as StorageItem<unknown>)._defaultValue;
|
|
1611
|
+
}
|
|
1612
|
+
return item.deserialize(raw);
|
|
1613
|
+
});
|
|
1614
|
+
},
|
|
1615
|
+
items.length,
|
|
1616
|
+
);
|
|
1197
1617
|
}
|
|
1198
1618
|
|
|
1199
1619
|
export function setBatch<T>(
|
|
1200
1620
|
items: readonly StorageBatchSetItem<T>[],
|
|
1201
1621
|
scope: StorageScope,
|
|
1202
1622
|
): void {
|
|
1203
|
-
|
|
1204
|
-
|
|
1623
|
+
measureOperation(
|
|
1624
|
+
"batch:set",
|
|
1205
1625
|
scope,
|
|
1206
|
-
|
|
1626
|
+
() => {
|
|
1627
|
+
assertBatchScope(
|
|
1628
|
+
items.map((batchEntry) => batchEntry.item),
|
|
1629
|
+
scope,
|
|
1630
|
+
);
|
|
1207
1631
|
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1632
|
+
if (scope === StorageScope.Memory) {
|
|
1633
|
+
// Determine if any item needs per-item handling (validation or TTL)
|
|
1634
|
+
const needsIndividualSets = items.some(({ item }) => {
|
|
1635
|
+
const internal = asInternal(item as StorageItem<unknown>);
|
|
1636
|
+
return internal._hasValidation || internal._hasExpiration;
|
|
1637
|
+
});
|
|
1212
1638
|
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
internal: asInternal(item),
|
|
1218
|
-
}));
|
|
1219
|
-
const canUseSecureBatchPath = secureEntries.every(({ internal }) =>
|
|
1220
|
-
canUseSecureRawBatchPath(internal),
|
|
1221
|
-
);
|
|
1222
|
-
if (!canUseSecureBatchPath) {
|
|
1223
|
-
items.forEach(({ item, value }) => item.set(value));
|
|
1224
|
-
return;
|
|
1225
|
-
}
|
|
1639
|
+
if (needsIndividualSets) {
|
|
1640
|
+
items.forEach(({ item, value }) => item.set(value));
|
|
1641
|
+
return;
|
|
1642
|
+
}
|
|
1226
1643
|
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1644
|
+
// Atomic write: update all values in memoryStore, invalidate caches, then batch-notify
|
|
1645
|
+
items.forEach(({ item, value }) => {
|
|
1646
|
+
memoryStore.set(item.key, value);
|
|
1647
|
+
asInternal(item as StorageItem<unknown>)._invalidateParsedCacheOnly();
|
|
1648
|
+
});
|
|
1649
|
+
items.forEach(({ item }) =>
|
|
1650
|
+
notifyKeyListeners(memoryListeners, item.key),
|
|
1651
|
+
);
|
|
1652
|
+
return;
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
if (scope === StorageScope.Secure) {
|
|
1656
|
+
const secureEntries = items.map(({ item, value }) => ({
|
|
1657
|
+
item,
|
|
1658
|
+
value,
|
|
1659
|
+
internal: asInternal(item),
|
|
1660
|
+
}));
|
|
1661
|
+
const canUseSecureBatchPath = secureEntries.every(({ internal }) =>
|
|
1662
|
+
canUseSecureRawBatchPath(internal),
|
|
1663
|
+
);
|
|
1664
|
+
if (!canUseSecureBatchPath) {
|
|
1665
|
+
items.forEach(({ item, value }) => item.set(value));
|
|
1666
|
+
return;
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
flushSecureWrites();
|
|
1670
|
+
const groupedByAccessControl = new Map<
|
|
1671
|
+
number,
|
|
1672
|
+
{ keys: string[]; values: string[] }
|
|
1673
|
+
>();
|
|
1674
|
+
|
|
1675
|
+
secureEntries.forEach(({ item, value, internal }) => {
|
|
1676
|
+
const accessControl =
|
|
1677
|
+
internal._secureAccessControl ?? AccessControl.WhenUnlocked;
|
|
1678
|
+
const existingGroup = groupedByAccessControl.get(accessControl);
|
|
1679
|
+
const group = existingGroup ?? { keys: [], values: [] };
|
|
1680
|
+
group.keys.push(item.key);
|
|
1681
|
+
group.values.push(item.serialize(value));
|
|
1682
|
+
if (!existingGroup) {
|
|
1683
|
+
groupedByAccessControl.set(accessControl, group);
|
|
1684
|
+
}
|
|
1685
|
+
});
|
|
1686
|
+
|
|
1687
|
+
groupedByAccessControl.forEach((group, accessControl) => {
|
|
1688
|
+
WebStorage.setSecureAccessControl(accessControl);
|
|
1689
|
+
WebStorage.setBatch(group.keys, group.values, scope);
|
|
1690
|
+
group.keys.forEach((key, index) =>
|
|
1691
|
+
cacheRawValue(scope, key, group.values[index]),
|
|
1692
|
+
);
|
|
1693
|
+
});
|
|
1694
|
+
return;
|
|
1242
1695
|
}
|
|
1243
|
-
});
|
|
1244
1696
|
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
WebStorage.setBatch(group.keys, group.values, scope);
|
|
1248
|
-
group.keys.forEach((key, index) =>
|
|
1249
|
-
cacheRawValue(scope, key, group.values[index]),
|
|
1697
|
+
const useRawBatchPath = items.every(({ item }) =>
|
|
1698
|
+
canUseRawBatchPath(asInternal(item)),
|
|
1250
1699
|
);
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1700
|
+
if (!useRawBatchPath) {
|
|
1701
|
+
items.forEach(({ item, value }) => item.set(value));
|
|
1702
|
+
return;
|
|
1703
|
+
}
|
|
1254
1704
|
|
|
1255
|
-
|
|
1256
|
-
|
|
1705
|
+
const keys = items.map((entry) => entry.item.key);
|
|
1706
|
+
const values = items.map((entry) => entry.item.serialize(entry.value));
|
|
1707
|
+
WebStorage.setBatch(keys, values, scope);
|
|
1708
|
+
keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
|
|
1709
|
+
},
|
|
1710
|
+
items.length,
|
|
1257
1711
|
);
|
|
1258
|
-
if (!useRawBatchPath) {
|
|
1259
|
-
items.forEach(({ item, value }) => item.set(value));
|
|
1260
|
-
return;
|
|
1261
|
-
}
|
|
1262
|
-
|
|
1263
|
-
const keys = items.map((entry) => entry.item.key);
|
|
1264
|
-
const values = items.map((entry) => entry.item.serialize(entry.value));
|
|
1265
|
-
WebStorage.setBatch(keys, values, scope);
|
|
1266
|
-
keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
|
|
1267
1712
|
}
|
|
1268
1713
|
|
|
1269
1714
|
export function removeBatch(
|
|
1270
1715
|
items: readonly BatchRemoveItem[],
|
|
1271
1716
|
scope: StorageScope,
|
|
1272
1717
|
): void {
|
|
1273
|
-
|
|
1718
|
+
measureOperation(
|
|
1719
|
+
"batch:remove",
|
|
1720
|
+
scope,
|
|
1721
|
+
() => {
|
|
1722
|
+
assertBatchScope(items, scope);
|
|
1274
1723
|
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1724
|
+
if (scope === StorageScope.Memory) {
|
|
1725
|
+
items.forEach((item) => item.delete());
|
|
1726
|
+
return;
|
|
1727
|
+
}
|
|
1279
1728
|
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1729
|
+
const keys = items.map((item) => item.key);
|
|
1730
|
+
if (scope === StorageScope.Secure) {
|
|
1731
|
+
flushSecureWrites();
|
|
1732
|
+
}
|
|
1733
|
+
WebStorage.removeBatch(keys, scope);
|
|
1734
|
+
keys.forEach((key) => cacheRawValue(scope, key, undefined));
|
|
1735
|
+
},
|
|
1736
|
+
items.length,
|
|
1737
|
+
);
|
|
1286
1738
|
}
|
|
1287
1739
|
|
|
1288
1740
|
export function registerMigration(version: number, migration: Migration): void {
|
|
@@ -1300,92 +1752,124 @@ export function registerMigration(version: number, migration: Migration): void {
|
|
|
1300
1752
|
export function migrateToLatest(
|
|
1301
1753
|
scope: StorageScope = StorageScope.Disk,
|
|
1302
1754
|
): number {
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
.
|
|
1307
|
-
|
|
1755
|
+
return measureOperation("migration:run", scope, () => {
|
|
1756
|
+
assertValidScope(scope);
|
|
1757
|
+
const currentVersion = readMigrationVersion(scope);
|
|
1758
|
+
const versions = Array.from(registeredMigrations.keys())
|
|
1759
|
+
.filter((version) => version > currentVersion)
|
|
1760
|
+
.sort((a, b) => a - b);
|
|
1761
|
+
|
|
1762
|
+
let appliedVersion = currentVersion;
|
|
1763
|
+
const context: MigrationContext = {
|
|
1764
|
+
scope,
|
|
1765
|
+
getRaw: (key) => getRawValue(key, scope),
|
|
1766
|
+
setRaw: (key, value) => setRawValue(key, value, scope),
|
|
1767
|
+
removeRaw: (key) => removeRawValue(key, scope),
|
|
1768
|
+
};
|
|
1308
1769
|
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1770
|
+
versions.forEach((version) => {
|
|
1771
|
+
const migration = registeredMigrations.get(version);
|
|
1772
|
+
if (!migration) {
|
|
1773
|
+
return;
|
|
1774
|
+
}
|
|
1775
|
+
migration(context);
|
|
1776
|
+
writeMigrationVersion(scope, version);
|
|
1777
|
+
appliedVersion = version;
|
|
1778
|
+
});
|
|
1316
1779
|
|
|
1317
|
-
|
|
1318
|
-
const migration = registeredMigrations.get(version);
|
|
1319
|
-
if (!migration) {
|
|
1320
|
-
return;
|
|
1321
|
-
}
|
|
1322
|
-
migration(context);
|
|
1323
|
-
writeMigrationVersion(scope, version);
|
|
1324
|
-
appliedVersion = version;
|
|
1780
|
+
return appliedVersion;
|
|
1325
1781
|
});
|
|
1326
|
-
|
|
1327
|
-
return appliedVersion;
|
|
1328
1782
|
}
|
|
1329
1783
|
|
|
1330
1784
|
export function runTransaction<T>(
|
|
1331
1785
|
scope: StorageScope,
|
|
1332
1786
|
transaction: (context: TransactionContext) => T,
|
|
1333
1787
|
): T {
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1788
|
+
return measureOperation("transaction:run", scope, () => {
|
|
1789
|
+
assertValidScope(scope);
|
|
1790
|
+
if (scope === StorageScope.Secure) {
|
|
1791
|
+
flushSecureWrites();
|
|
1792
|
+
}
|
|
1338
1793
|
|
|
1339
|
-
|
|
1794
|
+
const rollback = new Map<string, string | undefined>();
|
|
1340
1795
|
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1796
|
+
const rememberRollback = (key: string) => {
|
|
1797
|
+
if (rollback.has(key)) {
|
|
1798
|
+
return;
|
|
1799
|
+
}
|
|
1800
|
+
rollback.set(key, getRawValue(key, scope));
|
|
1801
|
+
};
|
|
1347
1802
|
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1803
|
+
const tx: TransactionContext = {
|
|
1804
|
+
scope,
|
|
1805
|
+
getRaw: (key) => getRawValue(key, scope),
|
|
1806
|
+
setRaw: (key, value) => {
|
|
1807
|
+
rememberRollback(key);
|
|
1808
|
+
setRawValue(key, value, scope);
|
|
1809
|
+
},
|
|
1810
|
+
removeRaw: (key) => {
|
|
1811
|
+
rememberRollback(key);
|
|
1812
|
+
removeRawValue(key, scope);
|
|
1813
|
+
},
|
|
1814
|
+
getItem: (item) => {
|
|
1815
|
+
assertBatchScope([item], scope);
|
|
1816
|
+
return item.get();
|
|
1817
|
+
},
|
|
1818
|
+
setItem: (item, value) => {
|
|
1819
|
+
assertBatchScope([item], scope);
|
|
1820
|
+
rememberRollback(item.key);
|
|
1821
|
+
item.set(value);
|
|
1822
|
+
},
|
|
1823
|
+
removeItem: (item) => {
|
|
1824
|
+
assertBatchScope([item], scope);
|
|
1825
|
+
rememberRollback(item.key);
|
|
1826
|
+
item.delete();
|
|
1827
|
+
},
|
|
1828
|
+
};
|
|
1374
1829
|
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
.
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1830
|
+
try {
|
|
1831
|
+
return transaction(tx);
|
|
1832
|
+
} catch (error) {
|
|
1833
|
+
const rollbackEntries = Array.from(rollback.entries()).reverse();
|
|
1834
|
+
if (scope === StorageScope.Memory) {
|
|
1835
|
+
rollbackEntries.forEach(([key, previousValue]) => {
|
|
1836
|
+
if (previousValue === undefined) {
|
|
1837
|
+
removeRawValue(key, scope);
|
|
1838
|
+
} else {
|
|
1839
|
+
setRawValue(key, previousValue, scope);
|
|
1840
|
+
}
|
|
1841
|
+
});
|
|
1842
|
+
} else {
|
|
1843
|
+
const keysToSet: string[] = [];
|
|
1844
|
+
const valuesToSet: string[] = [];
|
|
1845
|
+
const keysToRemove: string[] = [];
|
|
1846
|
+
|
|
1847
|
+
rollbackEntries.forEach(([key, previousValue]) => {
|
|
1848
|
+
if (previousValue === undefined) {
|
|
1849
|
+
keysToRemove.push(key);
|
|
1850
|
+
} else {
|
|
1851
|
+
keysToSet.push(key);
|
|
1852
|
+
valuesToSet.push(previousValue);
|
|
1853
|
+
}
|
|
1854
|
+
});
|
|
1855
|
+
|
|
1856
|
+
if (scope === StorageScope.Secure) {
|
|
1857
|
+
flushSecureWrites();
|
|
1385
1858
|
}
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1859
|
+
if (keysToSet.length > 0) {
|
|
1860
|
+
WebStorage.setBatch(keysToSet, valuesToSet, scope);
|
|
1861
|
+
keysToSet.forEach((key, index) =>
|
|
1862
|
+
cacheRawValue(scope, key, valuesToSet[index]),
|
|
1863
|
+
);
|
|
1864
|
+
}
|
|
1865
|
+
if (keysToRemove.length > 0) {
|
|
1866
|
+
WebStorage.removeBatch(keysToRemove, scope);
|
|
1867
|
+
keysToRemove.forEach((key) => cacheRawValue(scope, key, undefined));
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
throw error;
|
|
1871
|
+
}
|
|
1872
|
+
});
|
|
1389
1873
|
}
|
|
1390
1874
|
|
|
1391
1875
|
export type SecureAuthStorageConfig<K extends string = string> = Record<
|
|
@@ -1393,6 +1877,7 @@ export type SecureAuthStorageConfig<K extends string = string> = Record<
|
|
|
1393
1877
|
{
|
|
1394
1878
|
ttlMs?: number;
|
|
1395
1879
|
biometric?: boolean;
|
|
1880
|
+
biometricLevel?: BiometricLevel;
|
|
1396
1881
|
accessControl?: AccessControl;
|
|
1397
1882
|
}
|
|
1398
1883
|
>;
|
|
@@ -1416,6 +1901,9 @@ export function createSecureAuthStorage<K extends string>(
|
|
|
1416
1901
|
...(itemConfig.biometric !== undefined
|
|
1417
1902
|
? { biometric: itemConfig.biometric }
|
|
1418
1903
|
: {}),
|
|
1904
|
+
...(itemConfig.biometricLevel !== undefined
|
|
1905
|
+
? { biometricLevel: itemConfig.biometricLevel }
|
|
1906
|
+
: {}),
|
|
1419
1907
|
...(itemConfig.accessControl !== undefined
|
|
1420
1908
|
? { accessControl: itemConfig.accessControl }
|
|
1421
1909
|
: {}),
|