react-native-nitro-storage 0.3.1 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +199 -10
- package/android/CMakeLists.txt +2 -0
- package/android/src/main/cpp/AndroidStorageAdapterCpp.cpp +4 -0
- package/android/src/main/cpp/AndroidStorageAdapterCpp.hpp +1 -0
- package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +36 -13
- package/cpp/bindings/HybridStorage.cpp +55 -9
- package/cpp/bindings/HybridStorage.hpp +19 -2
- package/cpp/core/NativeStorageAdapter.hpp +1 -0
- package/ios/IOSStorageAdapterCpp.hpp +1 -0
- package/ios/IOSStorageAdapterCpp.mm +7 -1
- package/lib/commonjs/index.js +139 -63
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +236 -89
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/storage-hooks.js +36 -0
- package/lib/commonjs/storage-hooks.js.map +1 -0
- package/lib/module/index.js +121 -60
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +219 -87
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/storage-hooks.js +30 -0
- package/lib/module/storage-hooks.js.map +1 -0
- package/lib/typescript/Storage.nitro.d.ts +2 -0
- package/lib/typescript/Storage.nitro.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +3 -3
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/index.web.d.ts +5 -3
- package/lib/typescript/index.web.d.ts.map +1 -1
- package/lib/typescript/storage-hooks.d.ts +10 -0
- package/lib/typescript/storage-hooks.d.ts.map +1 -0
- package/nitrogen/generated/shared/c++/HybridStorageSpec.cpp +2 -0
- package/nitrogen/generated/shared/c++/HybridStorageSpec.hpp +2 -0
- package/package.json +5 -3
- package/src/Storage.nitro.ts +2 -0
- package/src/index.ts +143 -83
- package/src/index.web.ts +255 -112
- package/src/storage-hooks.ts +48 -0
package/src/index.web.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { useRef, useSyncExternalStore } from "react";
|
|
2
1
|
import { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
|
|
3
2
|
import {
|
|
4
3
|
MIGRATION_VERSION_KEY,
|
|
@@ -52,8 +51,18 @@ type RawBatchPathItem = {
|
|
|
52
51
|
_secureAccessControl?: AccessControl;
|
|
53
52
|
};
|
|
54
53
|
|
|
55
|
-
function asInternal(item: StorageItem<
|
|
56
|
-
return item as
|
|
54
|
+
function asInternal<T>(item: StorageItem<T>): StorageItemInternal<T> {
|
|
55
|
+
return item as StorageItemInternal<T>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isUpdater<T>(
|
|
59
|
+
valueOrFn: T | ((prev: T) => T),
|
|
60
|
+
): valueOrFn is (prev: T) => T {
|
|
61
|
+
return typeof valueOrFn === "function";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function typedKeys<K extends string, V>(record: Record<K, V>): K[] {
|
|
65
|
+
return Object.keys(record) as K[];
|
|
57
66
|
}
|
|
58
67
|
type NonMemoryScope = StorageScope.Disk | StorageScope.Secure;
|
|
59
68
|
type PendingSecureWrite = { key: string; value: string | undefined };
|
|
@@ -88,11 +97,13 @@ export interface Storage {
|
|
|
88
97
|
setBatch(keys: string[], values: string[], scope: number): void;
|
|
89
98
|
getBatch(keys: string[], scope: number): (string | undefined)[];
|
|
90
99
|
removeBatch(keys: string[], scope: number): void;
|
|
100
|
+
removeByPrefix(prefix: string, scope: number): void;
|
|
91
101
|
addOnChange(
|
|
92
102
|
scope: number,
|
|
93
103
|
callback: (key: string, value: string | undefined) => void,
|
|
94
104
|
): () => void;
|
|
95
105
|
setSecureAccessControl(level: number): void;
|
|
106
|
+
setSecureWritesAsync(enabled: boolean): void;
|
|
96
107
|
setKeychainAccessGroup(group: string): void;
|
|
97
108
|
setSecureBiometric(key: string, value: string): void;
|
|
98
109
|
getSecureBiometric(key: string): string | undefined;
|
|
@@ -113,6 +124,11 @@ const scopedRawCache = new Map<NonMemoryScope, Map<string, string | undefined>>(
|
|
|
113
124
|
[StorageScope.Secure, new Map()],
|
|
114
125
|
],
|
|
115
126
|
);
|
|
127
|
+
const webScopeKeyIndex = new Map<NonMemoryScope, Set<string>>([
|
|
128
|
+
[StorageScope.Disk, new Set()],
|
|
129
|
+
[StorageScope.Secure, new Set()],
|
|
130
|
+
]);
|
|
131
|
+
const hydratedWebScopeKeyIndex = new Set<NonMemoryScope>();
|
|
116
132
|
const pendingSecureWrites = new Map<string, PendingSecureWrite>();
|
|
117
133
|
let secureFlushScheduled = false;
|
|
118
134
|
const SECURE_WEB_PREFIX = "__secure_";
|
|
@@ -145,6 +161,51 @@ function fromBiometricStorageKey(key: string): string {
|
|
|
145
161
|
return key.slice(BIOMETRIC_WEB_PREFIX.length);
|
|
146
162
|
}
|
|
147
163
|
|
|
164
|
+
function getWebScopeKeyIndex(scope: NonMemoryScope): Set<string> {
|
|
165
|
+
return webScopeKeyIndex.get(scope)!;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function hydrateWebScopeKeyIndex(scope: NonMemoryScope): void {
|
|
169
|
+
if (hydratedWebScopeKeyIndex.has(scope)) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const storage = getBrowserStorage(scope);
|
|
174
|
+
const keyIndex = getWebScopeKeyIndex(scope);
|
|
175
|
+
keyIndex.clear();
|
|
176
|
+
if (storage) {
|
|
177
|
+
for (let index = 0; index < storage.length; index += 1) {
|
|
178
|
+
const key = storage.key(index);
|
|
179
|
+
if (!key) {
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (scope === StorageScope.Disk) {
|
|
183
|
+
if (
|
|
184
|
+
!key.startsWith(SECURE_WEB_PREFIX) &&
|
|
185
|
+
!key.startsWith(BIOMETRIC_WEB_PREFIX)
|
|
186
|
+
) {
|
|
187
|
+
keyIndex.add(key);
|
|
188
|
+
}
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (key.startsWith(SECURE_WEB_PREFIX)) {
|
|
193
|
+
keyIndex.add(fromSecureStorageKey(key));
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (key.startsWith(BIOMETRIC_WEB_PREFIX)) {
|
|
197
|
+
keyIndex.add(fromBiometricStorageKey(key));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
hydratedWebScopeKeyIndex.add(scope);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function ensureWebScopeKeyIndex(scope: NonMemoryScope): Set<string> {
|
|
205
|
+
hydrateWebScopeKeyIndex(scope);
|
|
206
|
+
return getWebScopeKeyIndex(scope);
|
|
207
|
+
}
|
|
208
|
+
|
|
148
209
|
function getScopedListeners(scope: NonMemoryScope): KeyListenerRegistry {
|
|
149
210
|
return webScopeListeners.get(scope)!;
|
|
150
211
|
}
|
|
@@ -277,6 +338,7 @@ const WebStorage: Storage = {
|
|
|
277
338
|
scope === StorageScope.Secure ? toSecureStorageKey(key) : key;
|
|
278
339
|
storage.setItem(storageKey, value);
|
|
279
340
|
if (scope === StorageScope.Disk || scope === StorageScope.Secure) {
|
|
341
|
+
ensureWebScopeKeyIndex(scope).add(key);
|
|
280
342
|
notifyKeyListeners(getScopedListeners(scope), key);
|
|
281
343
|
}
|
|
282
344
|
},
|
|
@@ -298,6 +360,7 @@ const WebStorage: Storage = {
|
|
|
298
360
|
storage.removeItem(key);
|
|
299
361
|
}
|
|
300
362
|
if (scope === StorageScope.Disk || scope === StorageScope.Secure) {
|
|
363
|
+
ensureWebScopeKeyIndex(scope).delete(key);
|
|
301
364
|
notifyKeyListeners(getScopedListeners(scope), key);
|
|
302
365
|
}
|
|
303
366
|
},
|
|
@@ -335,6 +398,7 @@ const WebStorage: Storage = {
|
|
|
335
398
|
storage.clear();
|
|
336
399
|
}
|
|
337
400
|
if (scope === StorageScope.Disk || scope === StorageScope.Secure) {
|
|
401
|
+
ensureWebScopeKeyIndex(scope).clear();
|
|
338
402
|
notifyAllListeners(getScopedListeners(scope));
|
|
339
403
|
}
|
|
340
404
|
},
|
|
@@ -345,11 +409,17 @@ const WebStorage: Storage = {
|
|
|
345
409
|
}
|
|
346
410
|
|
|
347
411
|
keys.forEach((key, index) => {
|
|
412
|
+
const value = values[index];
|
|
413
|
+
if (value === undefined) {
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
348
416
|
const storageKey =
|
|
349
417
|
scope === StorageScope.Secure ? toSecureStorageKey(key) : key;
|
|
350
|
-
storage.setItem(storageKey,
|
|
418
|
+
storage.setItem(storageKey, value);
|
|
351
419
|
});
|
|
352
420
|
if (scope === StorageScope.Disk || scope === StorageScope.Secure) {
|
|
421
|
+
const keyIndex = ensureWebScopeKeyIndex(scope);
|
|
422
|
+
keys.forEach((key) => keyIndex.add(key));
|
|
353
423
|
const listeners = getScopedListeners(scope);
|
|
354
424
|
keys.forEach((key) => notifyKeyListeners(listeners, key));
|
|
355
425
|
}
|
|
@@ -363,9 +433,41 @@ const WebStorage: Storage = {
|
|
|
363
433
|
});
|
|
364
434
|
},
|
|
365
435
|
removeBatch: (keys: string[], scope: number) => {
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
436
|
+
const storage = getBrowserStorage(scope);
|
|
437
|
+
if (!storage) {
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (scope === StorageScope.Secure) {
|
|
442
|
+
keys.forEach((key) => {
|
|
443
|
+
storage.removeItem(toSecureStorageKey(key));
|
|
444
|
+
storage.removeItem(toBiometricStorageKey(key));
|
|
445
|
+
});
|
|
446
|
+
} else {
|
|
447
|
+
keys.forEach((key) => {
|
|
448
|
+
storage.removeItem(key);
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (scope === StorageScope.Disk || scope === StorageScope.Secure) {
|
|
453
|
+
const keyIndex = ensureWebScopeKeyIndex(scope);
|
|
454
|
+
keys.forEach((key) => keyIndex.delete(key));
|
|
455
|
+
const listeners = getScopedListeners(scope);
|
|
456
|
+
keys.forEach((key) => notifyKeyListeners(listeners, key));
|
|
457
|
+
}
|
|
458
|
+
},
|
|
459
|
+
removeByPrefix: (prefix: string, scope: number) => {
|
|
460
|
+
if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const keyIndex = ensureWebScopeKeyIndex(scope);
|
|
465
|
+
const keys = Array.from(keyIndex).filter((key) => key.startsWith(prefix));
|
|
466
|
+
if (keys.length === 0) {
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
WebStorage.removeBatch(keys, scope);
|
|
369
471
|
},
|
|
370
472
|
addOnChange: (
|
|
371
473
|
_scope: number,
|
|
@@ -384,36 +486,19 @@ const WebStorage: Storage = {
|
|
|
384
486
|
return storage?.getItem(key) !== null;
|
|
385
487
|
},
|
|
386
488
|
getAllKeys: (scope: number) => {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
const keys = new Set<string>();
|
|
390
|
-
for (let i = 0; i < storage.length; i++) {
|
|
391
|
-
const k = storage.key(i);
|
|
392
|
-
if (!k) {
|
|
393
|
-
continue;
|
|
394
|
-
}
|
|
395
|
-
if (scope === StorageScope.Secure) {
|
|
396
|
-
if (k.startsWith(SECURE_WEB_PREFIX)) {
|
|
397
|
-
keys.add(fromSecureStorageKey(k));
|
|
398
|
-
} else if (k.startsWith(BIOMETRIC_WEB_PREFIX)) {
|
|
399
|
-
keys.add(fromBiometricStorageKey(k));
|
|
400
|
-
}
|
|
401
|
-
continue;
|
|
402
|
-
}
|
|
403
|
-
if (
|
|
404
|
-
k.startsWith(SECURE_WEB_PREFIX) ||
|
|
405
|
-
k.startsWith(BIOMETRIC_WEB_PREFIX)
|
|
406
|
-
) {
|
|
407
|
-
continue;
|
|
408
|
-
}
|
|
409
|
-
keys.add(k);
|
|
489
|
+
if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
|
|
490
|
+
return [];
|
|
410
491
|
}
|
|
411
|
-
return Array.from(
|
|
492
|
+
return Array.from(ensureWebScopeKeyIndex(scope));
|
|
412
493
|
},
|
|
413
494
|
size: (scope: number) => {
|
|
414
|
-
|
|
495
|
+
if (scope === StorageScope.Disk || scope === StorageScope.Secure) {
|
|
496
|
+
return ensureWebScopeKeyIndex(scope).size;
|
|
497
|
+
}
|
|
498
|
+
return 0;
|
|
415
499
|
},
|
|
416
500
|
setSecureAccessControl: () => {},
|
|
501
|
+
setSecureWritesAsync: (_enabled: boolean) => {},
|
|
417
502
|
setKeychainAccessGroup: () => {},
|
|
418
503
|
setSecureBiometric: (key: string, value: string) => {
|
|
419
504
|
if (
|
|
@@ -427,6 +512,7 @@ const WebStorage: Storage = {
|
|
|
427
512
|
);
|
|
428
513
|
}
|
|
429
514
|
globalThis.localStorage?.setItem(toBiometricStorageKey(key), value);
|
|
515
|
+
ensureWebScopeKeyIndex(StorageScope.Secure).add(key);
|
|
430
516
|
notifyKeyListeners(getScopedListeners(StorageScope.Secure), key);
|
|
431
517
|
},
|
|
432
518
|
getSecureBiometric: (key: string) => {
|
|
@@ -435,7 +521,11 @@ const WebStorage: Storage = {
|
|
|
435
521
|
);
|
|
436
522
|
},
|
|
437
523
|
deleteSecureBiometric: (key: string) => {
|
|
438
|
-
globalThis.localStorage
|
|
524
|
+
const storage = globalThis.localStorage;
|
|
525
|
+
storage?.removeItem(toBiometricStorageKey(key));
|
|
526
|
+
if (storage?.getItem(toSecureStorageKey(key)) === null) {
|
|
527
|
+
ensureWebScopeKeyIndex(StorageScope.Secure).delete(key);
|
|
528
|
+
}
|
|
439
529
|
notifyKeyListeners(getScopedListeners(StorageScope.Secure), key);
|
|
440
530
|
},
|
|
441
531
|
hasSecureBiometric: (key: string) => {
|
|
@@ -456,6 +546,12 @@ const WebStorage: Storage = {
|
|
|
456
546
|
}
|
|
457
547
|
}
|
|
458
548
|
toRemove.forEach((k) => storage.removeItem(k));
|
|
549
|
+
const keyIndex = ensureWebScopeKeyIndex(StorageScope.Secure);
|
|
550
|
+
keysToNotify.forEach((key) => {
|
|
551
|
+
if (storage.getItem(toSecureStorageKey(key)) === null) {
|
|
552
|
+
keyIndex.delete(key);
|
|
553
|
+
}
|
|
554
|
+
});
|
|
459
555
|
const listeners = getScopedListeners(StorageScope.Secure);
|
|
460
556
|
keysToNotify.forEach((key) => notifyKeyListeners(listeners, key));
|
|
461
557
|
},
|
|
@@ -538,9 +634,6 @@ export const storage = {
|
|
|
538
634
|
|
|
539
635
|
clearScopeRawCache(scope);
|
|
540
636
|
WebStorage.clear(scope);
|
|
541
|
-
if (scope === StorageScope.Secure) {
|
|
542
|
-
WebStorage.clearSecureBiometric();
|
|
543
|
-
}
|
|
544
637
|
},
|
|
545
638
|
clearAll: () => {
|
|
546
639
|
storage.clear(StorageScope.Memory);
|
|
@@ -558,18 +651,12 @@ export const storage = {
|
|
|
558
651
|
notifyAllListeners(memoryListeners);
|
|
559
652
|
return;
|
|
560
653
|
}
|
|
654
|
+
const keyPrefix = prefixKey(namespace, "");
|
|
561
655
|
if (scope === StorageScope.Secure) {
|
|
562
656
|
flushSecureWrites();
|
|
563
657
|
}
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
if (namespacedKeys.length > 0) {
|
|
567
|
-
WebStorage.removeBatch(namespacedKeys, scope);
|
|
568
|
-
namespacedKeys.forEach((k) => cacheRawValue(scope, k, undefined));
|
|
569
|
-
if (scope === StorageScope.Secure) {
|
|
570
|
-
namespacedKeys.forEach((k) => clearPendingSecureWrite(k));
|
|
571
|
-
}
|
|
572
|
-
}
|
|
658
|
+
clearScopeRawCache(scope);
|
|
659
|
+
WebStorage.removeByPrefix(keyPrefix, scope);
|
|
573
660
|
},
|
|
574
661
|
clearBiometric: () => {
|
|
575
662
|
WebStorage.clearSecureBiometric();
|
|
@@ -606,6 +693,10 @@ export const storage = {
|
|
|
606
693
|
return WebStorage.size(scope);
|
|
607
694
|
},
|
|
608
695
|
setAccessControl: (_level: AccessControl) => {},
|
|
696
|
+
setSecureWritesAsync: (_enabled: boolean) => {},
|
|
697
|
+
flushSecureWrites: () => {
|
|
698
|
+
flushSecureWrites();
|
|
699
|
+
},
|
|
609
700
|
setKeychainAccessGroup: (_group: string) => {},
|
|
610
701
|
};
|
|
611
702
|
|
|
@@ -656,6 +747,14 @@ function canUseRawBatchPath(item: RawBatchPathItem): boolean {
|
|
|
656
747
|
);
|
|
657
748
|
}
|
|
658
749
|
|
|
750
|
+
function canUseSecureRawBatchPath(item: RawBatchPathItem): boolean {
|
|
751
|
+
return (
|
|
752
|
+
item._hasExpiration === false &&
|
|
753
|
+
item._hasValidation === false &&
|
|
754
|
+
item._isBiometric !== true
|
|
755
|
+
);
|
|
756
|
+
}
|
|
757
|
+
|
|
659
758
|
function defaultSerialize<T>(value: T): string {
|
|
660
759
|
return serializeWithPrimitiveFastPath(value);
|
|
661
760
|
}
|
|
@@ -687,6 +786,7 @@ export function createStorageItem<T = undefined>(
|
|
|
687
786
|
config.coalesceSecureWrites === true &&
|
|
688
787
|
!isBiometric &&
|
|
689
788
|
secureAccessControl === undefined;
|
|
789
|
+
const defaultValue = config.defaultValue as T;
|
|
690
790
|
const nonMemoryScope: NonMemoryScope | null =
|
|
691
791
|
config.scope === StorageScope.Disk
|
|
692
792
|
? StorageScope.Disk
|
|
@@ -703,11 +803,13 @@ export function createStorageItem<T = undefined>(
|
|
|
703
803
|
let lastRaw: unknown = undefined;
|
|
704
804
|
let lastValue: T | undefined;
|
|
705
805
|
let hasLastValue = false;
|
|
806
|
+
let lastExpiresAt: number | null | undefined = undefined;
|
|
706
807
|
|
|
707
808
|
const invalidateParsedCache = () => {
|
|
708
809
|
lastRaw = undefined;
|
|
709
810
|
lastValue = undefined;
|
|
710
811
|
hasLastValue = false;
|
|
812
|
+
lastExpiresAt = undefined;
|
|
711
813
|
};
|
|
712
814
|
|
|
713
815
|
const ensureSubscription = () => {
|
|
@@ -744,7 +846,7 @@ export function createStorageItem<T = undefined>(
|
|
|
744
846
|
return undefined;
|
|
745
847
|
}
|
|
746
848
|
}
|
|
747
|
-
return memoryStore.get(storageKey)
|
|
849
|
+
return memoryStore.get(storageKey);
|
|
748
850
|
}
|
|
749
851
|
|
|
750
852
|
if (
|
|
@@ -839,7 +941,7 @@ export function createStorageItem<T = undefined>(
|
|
|
839
941
|
return onValidationError(invalidValue);
|
|
840
942
|
}
|
|
841
943
|
|
|
842
|
-
return
|
|
944
|
+
return defaultValue;
|
|
843
945
|
};
|
|
844
946
|
|
|
845
947
|
const ensureValidatedValue = (
|
|
@@ -852,7 +954,7 @@ export function createStorageItem<T = undefined>(
|
|
|
852
954
|
|
|
853
955
|
const resolved = resolveInvalidValue(candidate);
|
|
854
956
|
if (validate && !validate(resolved)) {
|
|
855
|
-
return
|
|
957
|
+
return defaultValue;
|
|
856
958
|
}
|
|
857
959
|
if (hadStoredValue) {
|
|
858
960
|
writeValueWithoutValidation(resolved);
|
|
@@ -863,36 +965,61 @@ export function createStorageItem<T = undefined>(
|
|
|
863
965
|
const get = (): T => {
|
|
864
966
|
const raw = readStoredRaw();
|
|
865
967
|
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
968
|
+
if (!memoryExpiration && raw === lastRaw && hasLastValue) {
|
|
969
|
+
if (!expiration || lastExpiresAt === null) {
|
|
970
|
+
return lastValue as T;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
if (typeof lastExpiresAt === "number") {
|
|
974
|
+
if (lastExpiresAt > Date.now()) {
|
|
975
|
+
return lastValue as T;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
removeStoredRaw();
|
|
979
|
+
invalidateParsedCache();
|
|
980
|
+
onExpired?.(storageKey);
|
|
981
|
+
lastValue = ensureValidatedValue(defaultValue, false);
|
|
982
|
+
hasLastValue = true;
|
|
983
|
+
return lastValue;
|
|
984
|
+
}
|
|
869
985
|
}
|
|
870
986
|
|
|
871
987
|
lastRaw = raw;
|
|
872
988
|
|
|
873
989
|
if (raw === undefined) {
|
|
874
|
-
|
|
990
|
+
lastExpiresAt = undefined;
|
|
991
|
+
lastValue = ensureValidatedValue(defaultValue, false);
|
|
875
992
|
hasLastValue = true;
|
|
876
993
|
return lastValue;
|
|
877
994
|
}
|
|
878
995
|
|
|
879
996
|
if (isMemory) {
|
|
997
|
+
lastExpiresAt = undefined;
|
|
880
998
|
lastValue = ensureValidatedValue(raw, true);
|
|
881
999
|
hasLastValue = true;
|
|
882
1000
|
return lastValue;
|
|
883
1001
|
}
|
|
884
1002
|
|
|
885
|
-
|
|
1003
|
+
if (typeof raw !== "string") {
|
|
1004
|
+
lastExpiresAt = undefined;
|
|
1005
|
+
lastValue = ensureValidatedValue(defaultValue, false);
|
|
1006
|
+
hasLastValue = true;
|
|
1007
|
+
return lastValue;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
let deserializableRaw = raw;
|
|
886
1011
|
|
|
887
1012
|
if (expiration) {
|
|
1013
|
+
let envelopeExpiresAt: number | null = null;
|
|
888
1014
|
try {
|
|
889
|
-
const parsed = JSON.parse(raw
|
|
1015
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
890
1016
|
if (isStoredEnvelope(parsed)) {
|
|
1017
|
+
envelopeExpiresAt = parsed.expiresAt;
|
|
891
1018
|
if (parsed.expiresAt <= Date.now()) {
|
|
892
1019
|
removeStoredRaw();
|
|
893
1020
|
invalidateParsedCache();
|
|
894
1021
|
onExpired?.(storageKey);
|
|
895
|
-
lastValue = ensureValidatedValue(
|
|
1022
|
+
lastValue = ensureValidatedValue(defaultValue, false);
|
|
896
1023
|
hasLastValue = true;
|
|
897
1024
|
return lastValue;
|
|
898
1025
|
}
|
|
@@ -902,6 +1029,9 @@ export function createStorageItem<T = undefined>(
|
|
|
902
1029
|
} catch {
|
|
903
1030
|
// Keep backward compatibility with legacy raw values.
|
|
904
1031
|
}
|
|
1032
|
+
lastExpiresAt = envelopeExpiresAt;
|
|
1033
|
+
} else {
|
|
1034
|
+
lastExpiresAt = undefined;
|
|
905
1035
|
}
|
|
906
1036
|
|
|
907
1037
|
lastValue = ensureValidatedValue(deserialize(deserializableRaw), true);
|
|
@@ -910,11 +1040,7 @@ export function createStorageItem<T = undefined>(
|
|
|
910
1040
|
};
|
|
911
1041
|
|
|
912
1042
|
const set = (valueOrFn: T | ((prev: T) => T)): void => {
|
|
913
|
-
const
|
|
914
|
-
const newValue =
|
|
915
|
-
typeof valueOrFn === "function"
|
|
916
|
-
? (valueOrFn as (prev: T) => T)(currentValue)
|
|
917
|
-
: valueOrFn;
|
|
1043
|
+
const newValue = isUpdater(valueOrFn) ? valueOrFn(get()) : valueOrFn;
|
|
918
1044
|
|
|
919
1045
|
invalidateParsedCache();
|
|
920
1046
|
|
|
@@ -976,54 +1102,17 @@ export function createStorageItem<T = undefined>(
|
|
|
976
1102
|
_hasExpiration: expiration !== undefined,
|
|
977
1103
|
_readCacheEnabled: readCache,
|
|
978
1104
|
_isBiometric: isBiometric,
|
|
979
|
-
|
|
1105
|
+
...(secureAccessControl !== undefined
|
|
1106
|
+
? { _secureAccessControl: secureAccessControl }
|
|
1107
|
+
: {}),
|
|
980
1108
|
scope: config.scope,
|
|
981
1109
|
key: storageKey,
|
|
982
1110
|
};
|
|
983
1111
|
|
|
984
|
-
return storageItem
|
|
1112
|
+
return storageItem;
|
|
985
1113
|
}
|
|
986
1114
|
|
|
987
|
-
export
|
|
988
|
-
item: StorageItem<T>,
|
|
989
|
-
): [T, (value: T | ((prev: T) => T)) => void] {
|
|
990
|
-
const value = useSyncExternalStore(item.subscribe, item.get, item.get);
|
|
991
|
-
return [value, item.set];
|
|
992
|
-
}
|
|
993
|
-
|
|
994
|
-
export function useStorageSelector<T, TSelected>(
|
|
995
|
-
item: StorageItem<T>,
|
|
996
|
-
selector: (value: T) => TSelected,
|
|
997
|
-
isEqual: (prev: TSelected, next: TSelected) => boolean = Object.is,
|
|
998
|
-
): [TSelected, (value: T | ((prev: T) => T)) => void] {
|
|
999
|
-
const selectedRef = useRef<
|
|
1000
|
-
{ hasValue: false } | { hasValue: true; value: TSelected }
|
|
1001
|
-
>({
|
|
1002
|
-
hasValue: false,
|
|
1003
|
-
});
|
|
1004
|
-
|
|
1005
|
-
const getSelectedSnapshot = () => {
|
|
1006
|
-
const nextSelected = selector(item.get());
|
|
1007
|
-
const current = selectedRef.current;
|
|
1008
|
-
if (current.hasValue && isEqual(current.value, nextSelected)) {
|
|
1009
|
-
return current.value;
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
|
-
selectedRef.current = { hasValue: true, value: nextSelected };
|
|
1013
|
-
return nextSelected;
|
|
1014
|
-
};
|
|
1015
|
-
|
|
1016
|
-
const selectedValue = useSyncExternalStore(
|
|
1017
|
-
item.subscribe,
|
|
1018
|
-
getSelectedSnapshot,
|
|
1019
|
-
getSelectedSnapshot,
|
|
1020
|
-
);
|
|
1021
|
-
return [selectedValue, item.set];
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
export function useSetStorage<T>(item: StorageItem<T>) {
|
|
1025
|
-
return item.set;
|
|
1026
|
-
}
|
|
1115
|
+
export { useStorage, useStorageSelector, useSetStorage } from "./storage-hooks";
|
|
1027
1116
|
|
|
1028
1117
|
type BatchReadItem<T> = Pick<
|
|
1029
1118
|
StorageItem<T>,
|
|
@@ -1052,7 +1141,11 @@ export function getBatch(
|
|
|
1052
1141
|
return items.map((item) => item.get());
|
|
1053
1142
|
}
|
|
1054
1143
|
|
|
1055
|
-
const useRawBatchPath = items.every((item) =>
|
|
1144
|
+
const useRawBatchPath = items.every((item) =>
|
|
1145
|
+
scope === StorageScope.Secure
|
|
1146
|
+
? canUseSecureRawBatchPath(item)
|
|
1147
|
+
: canUseRawBatchPath(item),
|
|
1148
|
+
);
|
|
1056
1149
|
if (!useRawBatchPath) {
|
|
1057
1150
|
return items.map((item) => item.get());
|
|
1058
1151
|
}
|
|
@@ -1086,6 +1179,9 @@ export function getBatch(
|
|
|
1086
1179
|
fetchedValues.forEach((value, index) => {
|
|
1087
1180
|
const key = keysToFetch[index];
|
|
1088
1181
|
const targetIndex = keyIndexes[index];
|
|
1182
|
+
if (key === undefined || targetIndex === undefined) {
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1089
1185
|
rawValues[targetIndex] = value;
|
|
1090
1186
|
cacheRawValue(scope, key, value);
|
|
1091
1187
|
});
|
|
@@ -1114,6 +1210,48 @@ export function setBatch<T>(
|
|
|
1114
1210
|
return;
|
|
1115
1211
|
}
|
|
1116
1212
|
|
|
1213
|
+
if (scope === StorageScope.Secure) {
|
|
1214
|
+
const secureEntries = items.map(({ item, value }) => ({
|
|
1215
|
+
item,
|
|
1216
|
+
value,
|
|
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
|
+
}
|
|
1226
|
+
|
|
1227
|
+
flushSecureWrites();
|
|
1228
|
+
const groupedByAccessControl = new Map<
|
|
1229
|
+
number,
|
|
1230
|
+
{ keys: string[]; values: string[] }
|
|
1231
|
+
>();
|
|
1232
|
+
|
|
1233
|
+
secureEntries.forEach(({ item, value, internal }) => {
|
|
1234
|
+
const accessControl =
|
|
1235
|
+
internal._secureAccessControl ?? AccessControl.WhenUnlocked;
|
|
1236
|
+
const existingGroup = groupedByAccessControl.get(accessControl);
|
|
1237
|
+
const group = existingGroup ?? { keys: [], values: [] };
|
|
1238
|
+
group.keys.push(item.key);
|
|
1239
|
+
group.values.push(item.serialize(value));
|
|
1240
|
+
if (!existingGroup) {
|
|
1241
|
+
groupedByAccessControl.set(accessControl, group);
|
|
1242
|
+
}
|
|
1243
|
+
});
|
|
1244
|
+
|
|
1245
|
+
groupedByAccessControl.forEach((group, accessControl) => {
|
|
1246
|
+
WebStorage.setSecureAccessControl(accessControl);
|
|
1247
|
+
WebStorage.setBatch(group.keys, group.values, scope);
|
|
1248
|
+
group.keys.forEach((key, index) =>
|
|
1249
|
+
cacheRawValue(scope, key, group.values[index]),
|
|
1250
|
+
);
|
|
1251
|
+
});
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1117
1255
|
const useRawBatchPath = items.every(({ item }) =>
|
|
1118
1256
|
canUseRawBatchPath(asInternal(item)),
|
|
1119
1257
|
);
|
|
@@ -1124,9 +1262,6 @@ export function setBatch<T>(
|
|
|
1124
1262
|
|
|
1125
1263
|
const keys = items.map((entry) => entry.item.key);
|
|
1126
1264
|
const values = items.map((entry) => entry.item.serialize(entry.value));
|
|
1127
|
-
if (scope === StorageScope.Secure) {
|
|
1128
|
-
flushSecureWrites();
|
|
1129
|
-
}
|
|
1130
1265
|
WebStorage.setBatch(keys, values, scope);
|
|
1131
1266
|
keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
|
|
1132
1267
|
}
|
|
@@ -1267,20 +1402,28 @@ export function createSecureAuthStorage<K extends string>(
|
|
|
1267
1402
|
options?: { namespace?: string },
|
|
1268
1403
|
): Record<K, StorageItem<string>> {
|
|
1269
1404
|
const ns = options?.namespace ?? "auth";
|
|
1270
|
-
const result
|
|
1405
|
+
const result: Partial<Record<K, StorageItem<string>>> = {};
|
|
1271
1406
|
|
|
1272
|
-
for (const key of
|
|
1407
|
+
for (const key of typedKeys(config)) {
|
|
1273
1408
|
const itemConfig = config[key];
|
|
1409
|
+
const expirationConfig =
|
|
1410
|
+
itemConfig.ttlMs !== undefined ? { ttlMs: itemConfig.ttlMs } : undefined;
|
|
1274
1411
|
result[key] = createStorageItem<string>({
|
|
1275
1412
|
key,
|
|
1276
1413
|
scope: StorageScope.Secure,
|
|
1277
1414
|
defaultValue: "",
|
|
1278
1415
|
namespace: ns,
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1416
|
+
...(itemConfig.biometric !== undefined
|
|
1417
|
+
? { biometric: itemConfig.biometric }
|
|
1418
|
+
: {}),
|
|
1419
|
+
...(itemConfig.accessControl !== undefined
|
|
1420
|
+
? { accessControl: itemConfig.accessControl }
|
|
1421
|
+
: {}),
|
|
1422
|
+
...(expirationConfig !== undefined
|
|
1423
|
+
? { expiration: expirationConfig }
|
|
1424
|
+
: {}),
|
|
1282
1425
|
});
|
|
1283
1426
|
}
|
|
1284
1427
|
|
|
1285
|
-
return result
|
|
1428
|
+
return result as Record<K, StorageItem<string>>;
|
|
1286
1429
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { useRef, useSyncExternalStore } from "react";
|
|
2
|
+
|
|
3
|
+
type HookStorageItem<T> = {
|
|
4
|
+
get: () => T;
|
|
5
|
+
set: (value: T | ((prev: T) => T)) => void;
|
|
6
|
+
subscribe: (callback: () => void) => () => void;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function useStorage<T>(
|
|
10
|
+
item: HookStorageItem<T>,
|
|
11
|
+
): [T, (value: T | ((prev: T) => T)) => void] {
|
|
12
|
+
const value = useSyncExternalStore(item.subscribe, item.get, item.get);
|
|
13
|
+
return [value, item.set];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function useStorageSelector<T, TSelected>(
|
|
17
|
+
item: HookStorageItem<T>,
|
|
18
|
+
selector: (value: T) => TSelected,
|
|
19
|
+
isEqual: (prev: TSelected, next: TSelected) => boolean = Object.is,
|
|
20
|
+
): [TSelected, (value: T | ((prev: T) => T)) => void] {
|
|
21
|
+
const selectedRef = useRef<
|
|
22
|
+
{ hasValue: false } | { hasValue: true; value: TSelected }
|
|
23
|
+
>({
|
|
24
|
+
hasValue: false,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const getSelectedSnapshot = () => {
|
|
28
|
+
const nextSelected = selector(item.get());
|
|
29
|
+
const current = selectedRef.current;
|
|
30
|
+
if (current.hasValue && isEqual(current.value, nextSelected)) {
|
|
31
|
+
return current.value;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
selectedRef.current = { hasValue: true, value: nextSelected };
|
|
35
|
+
return nextSelected;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const selectedValue = useSyncExternalStore(
|
|
39
|
+
item.subscribe,
|
|
40
|
+
getSelectedSnapshot,
|
|
41
|
+
getSelectedSnapshot,
|
|
42
|
+
);
|
|
43
|
+
return [selectedValue, item.set];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function useSetStorage<T>(item: HookStorageItem<T>) {
|
|
47
|
+
return item.set;
|
|
48
|
+
}
|