react-native-nitro-storage 0.3.0 → 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 +594 -247
- package/android/CMakeLists.txt +2 -0
- package/android/src/main/cpp/AndroidStorageAdapterCpp.cpp +102 -11
- package/android/src/main/cpp/AndroidStorageAdapterCpp.hpp +16 -0
- package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +154 -34
- package/android/src/main/java/com/nitrostorage/NitroStoragePackage.kt +2 -2
- package/cpp/bindings/HybridStorage.cpp +176 -21
- package/cpp/bindings/HybridStorage.hpp +29 -2
- package/cpp/core/NativeStorageAdapter.hpp +16 -0
- package/ios/IOSStorageAdapterCpp.hpp +20 -0
- package/ios/IOSStorageAdapterCpp.mm +239 -32
- package/lib/commonjs/Storage.types.js +23 -1
- package/lib/commonjs/Storage.types.js.map +1 -1
- package/lib/commonjs/index.js +292 -75
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +473 -86
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/internal.js +10 -0
- package/lib/commonjs/internal.js.map +1 -1
- package/lib/commonjs/storage-hooks.js +36 -0
- package/lib/commonjs/storage-hooks.js.map +1 -0
- package/lib/module/Storage.types.js +22 -0
- package/lib/module/Storage.types.js.map +1 -1
- package/lib/module/index.js +264 -75
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +445 -86
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/internal.js +8 -0
- package/lib/module/internal.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 +12 -0
- package/lib/typescript/Storage.nitro.d.ts.map +1 -1
- package/lib/typescript/Storage.types.d.ts +20 -0
- package/lib/typescript/Storage.types.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +33 -10
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/index.web.d.ts +45 -10
- package/lib/typescript/index.web.d.ts.map +1 -1
- package/lib/typescript/internal.d.ts +2 -0
- package/lib/typescript/internal.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 +12 -0
- package/nitrogen/generated/shared/c++/HybridStorageSpec.hpp +12 -0
- package/package.json +8 -3
- package/src/Storage.nitro.ts +13 -2
- package/src/Storage.types.ts +22 -0
- package/src/index.ts +382 -123
- package/src/index.web.ts +618 -134
- package/src/internal.ts +14 -4
- package/src/migration.ts +1 -1
- package/src/storage-hooks.ts +48 -0
package/src/index.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { useRef, useSyncExternalStore } from "react";
|
|
2
1
|
import { NitroModules } from "react-native-nitro-modules";
|
|
3
2
|
import type { Storage } from "./Storage.nitro";
|
|
4
|
-
import { StorageScope } from "./Storage.types";
|
|
3
|
+
import { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
|
|
5
4
|
import {
|
|
6
5
|
MIGRATION_VERSION_KEY,
|
|
7
6
|
type StoredEnvelope,
|
|
@@ -11,9 +10,11 @@ import {
|
|
|
11
10
|
decodeNativeBatchValue,
|
|
12
11
|
serializeWithPrimitiveFastPath,
|
|
13
12
|
deserializeWithPrimitiveFastPath,
|
|
13
|
+
prefixKey,
|
|
14
|
+
isNamespaced,
|
|
14
15
|
} from "./internal";
|
|
15
16
|
|
|
16
|
-
export { StorageScope } from "./Storage.types";
|
|
17
|
+
export { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
|
|
17
18
|
export type { Storage } from "./Storage.nitro";
|
|
18
19
|
export { migrateFromMMKV } from "./migration";
|
|
19
20
|
|
|
@@ -37,15 +38,36 @@ export type TransactionContext = {
|
|
|
37
38
|
setRaw: (key: string, value: string) => void;
|
|
38
39
|
removeRaw: (key: string) => void;
|
|
39
40
|
getItem: <T>(item: Pick<StorageItem<T>, "scope" | "key" | "get">) => T;
|
|
40
|
-
setItem: <T>(
|
|
41
|
-
|
|
41
|
+
setItem: <T>(
|
|
42
|
+
item: Pick<StorageItem<T>, "scope" | "key" | "set">,
|
|
43
|
+
value: T,
|
|
44
|
+
) => void;
|
|
45
|
+
removeItem: (
|
|
46
|
+
item: Pick<StorageItem<unknown>, "scope" | "key" | "delete">,
|
|
47
|
+
) => void;
|
|
42
48
|
};
|
|
43
49
|
|
|
44
50
|
type KeyListenerRegistry = Map<string, Set<() => void>>;
|
|
45
51
|
type RawBatchPathItem = {
|
|
46
52
|
_hasValidation?: boolean;
|
|
47
53
|
_hasExpiration?: boolean;
|
|
54
|
+
_isBiometric?: boolean;
|
|
55
|
+
_secureAccessControl?: AccessControl;
|
|
48
56
|
};
|
|
57
|
+
|
|
58
|
+
function asInternal<T>(item: StorageItem<T>): StorageItemInternal<T> {
|
|
59
|
+
return item as StorageItemInternal<T>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function isUpdater<T>(
|
|
63
|
+
valueOrFn: T | ((prev: T) => T),
|
|
64
|
+
): valueOrFn is (prev: T) => T {
|
|
65
|
+
return typeof valueOrFn === "function";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function typedKeys<K extends string, V>(record: Record<K, V>): K[] {
|
|
69
|
+
return Object.keys(record) as K[];
|
|
70
|
+
}
|
|
49
71
|
type NonMemoryScope = StorageScope.Disk | StorageScope.Secure;
|
|
50
72
|
type PendingSecureWrite = { key: string; value: string | undefined };
|
|
51
73
|
|
|
@@ -73,28 +95,37 @@ const scopedListeners = new Map<NonMemoryScope, KeyListenerRegistry>([
|
|
|
73
95
|
[StorageScope.Secure, new Map()],
|
|
74
96
|
]);
|
|
75
97
|
const scopedUnsubscribers = new Map<NonMemoryScope, () => void>();
|
|
76
|
-
const scopedRawCache = new Map<NonMemoryScope, Map<string, string | undefined>>(
|
|
77
|
-
[
|
|
78
|
-
|
|
79
|
-
]
|
|
98
|
+
const scopedRawCache = new Map<NonMemoryScope, Map<string, string | undefined>>(
|
|
99
|
+
[
|
|
100
|
+
[StorageScope.Disk, new Map()],
|
|
101
|
+
[StorageScope.Secure, new Map()],
|
|
102
|
+
],
|
|
103
|
+
);
|
|
80
104
|
const pendingSecureWrites = new Map<string, PendingSecureWrite>();
|
|
81
105
|
let secureFlushScheduled = false;
|
|
106
|
+
let secureDefaultAccessControl: AccessControl = AccessControl.WhenUnlocked;
|
|
82
107
|
|
|
83
108
|
function getScopedListeners(scope: NonMemoryScope): KeyListenerRegistry {
|
|
84
109
|
return scopedListeners.get(scope)!;
|
|
85
110
|
}
|
|
86
111
|
|
|
87
|
-
function getScopeRawCache(
|
|
112
|
+
function getScopeRawCache(
|
|
113
|
+
scope: NonMemoryScope,
|
|
114
|
+
): Map<string, string | undefined> {
|
|
88
115
|
return scopedRawCache.get(scope)!;
|
|
89
116
|
}
|
|
90
117
|
|
|
91
|
-
function cacheRawValue(
|
|
118
|
+
function cacheRawValue(
|
|
119
|
+
scope: NonMemoryScope,
|
|
120
|
+
key: string,
|
|
121
|
+
value: string | undefined,
|
|
122
|
+
): void {
|
|
92
123
|
getScopeRawCache(scope).set(key, value);
|
|
93
124
|
}
|
|
94
125
|
|
|
95
126
|
function readCachedRawValue(
|
|
96
127
|
scope: NonMemoryScope,
|
|
97
|
-
key: string
|
|
128
|
+
key: string,
|
|
98
129
|
): string | undefined {
|
|
99
130
|
return getScopeRawCache(scope).get(key);
|
|
100
131
|
}
|
|
@@ -120,7 +151,7 @@ function notifyAllListeners(registry: KeyListenerRegistry): void {
|
|
|
120
151
|
function addKeyListener(
|
|
121
152
|
registry: KeyListenerRegistry,
|
|
122
153
|
key: string,
|
|
123
|
-
listener: () => void
|
|
154
|
+
listener: () => void,
|
|
124
155
|
): () => void {
|
|
125
156
|
let listeners = registry.get(key);
|
|
126
157
|
if (!listeners) {
|
|
@@ -177,6 +208,7 @@ function flushSecureWrites(): void {
|
|
|
177
208
|
});
|
|
178
209
|
|
|
179
210
|
const storageModule = getStorageModule();
|
|
211
|
+
storageModule.setSecureAccessControl(secureDefaultAccessControl);
|
|
180
212
|
if (keysToSet.length > 0) {
|
|
181
213
|
storageModule.setBatch(keysToSet, valuesToSet, StorageScope.Secure);
|
|
182
214
|
}
|
|
@@ -260,6 +292,7 @@ function setRawValue(key: string, value: string, scope: StorageScope): void {
|
|
|
260
292
|
if (scope === StorageScope.Secure) {
|
|
261
293
|
flushSecureWrites();
|
|
262
294
|
clearPendingSecureWrite(key);
|
|
295
|
+
getStorageModule().setSecureAccessControl(secureDefaultAccessControl);
|
|
263
296
|
}
|
|
264
297
|
|
|
265
298
|
getStorageModule().set(key, value, scope);
|
|
@@ -318,6 +351,81 @@ export const storage = {
|
|
|
318
351
|
storage.clear(StorageScope.Disk);
|
|
319
352
|
storage.clear(StorageScope.Secure);
|
|
320
353
|
},
|
|
354
|
+
clearNamespace: (namespace: string, scope: StorageScope) => {
|
|
355
|
+
assertValidScope(scope);
|
|
356
|
+
if (scope === StorageScope.Memory) {
|
|
357
|
+
for (const key of memoryStore.keys()) {
|
|
358
|
+
if (isNamespaced(key, namespace)) {
|
|
359
|
+
memoryStore.delete(key);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
notifyAllListeners(memoryListeners);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const keyPrefix = prefixKey(namespace, "");
|
|
367
|
+
if (scope === StorageScope.Secure) {
|
|
368
|
+
flushSecureWrites();
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
clearScopeRawCache(scope);
|
|
372
|
+
getStorageModule().removeByPrefix(keyPrefix, scope);
|
|
373
|
+
},
|
|
374
|
+
clearBiometric: () => {
|
|
375
|
+
getStorageModule().clearSecureBiometric();
|
|
376
|
+
},
|
|
377
|
+
has: (key: string, scope: StorageScope): boolean => {
|
|
378
|
+
assertValidScope(scope);
|
|
379
|
+
if (scope === StorageScope.Memory) {
|
|
380
|
+
return memoryStore.has(key);
|
|
381
|
+
}
|
|
382
|
+
return getStorageModule().has(key, scope);
|
|
383
|
+
},
|
|
384
|
+
getAllKeys: (scope: StorageScope): string[] => {
|
|
385
|
+
assertValidScope(scope);
|
|
386
|
+
if (scope === StorageScope.Memory) {
|
|
387
|
+
return Array.from(memoryStore.keys());
|
|
388
|
+
}
|
|
389
|
+
return getStorageModule().getAllKeys(scope);
|
|
390
|
+
},
|
|
391
|
+
getAll: (scope: StorageScope): Record<string, string> => {
|
|
392
|
+
assertValidScope(scope);
|
|
393
|
+
const result: Record<string, string> = {};
|
|
394
|
+
if (scope === StorageScope.Memory) {
|
|
395
|
+
memoryStore.forEach((value, key) => {
|
|
396
|
+
if (typeof value === "string") result[key] = value;
|
|
397
|
+
});
|
|
398
|
+
return result;
|
|
399
|
+
}
|
|
400
|
+
const keys = getStorageModule().getAllKeys(scope);
|
|
401
|
+
if (keys.length === 0) return result;
|
|
402
|
+
const values = getStorageModule().getBatch(keys, scope);
|
|
403
|
+
keys.forEach((key, idx) => {
|
|
404
|
+
const val = decodeNativeBatchValue(values[idx]);
|
|
405
|
+
if (val !== undefined) result[key] = val;
|
|
406
|
+
});
|
|
407
|
+
return result;
|
|
408
|
+
},
|
|
409
|
+
size: (scope: StorageScope): number => {
|
|
410
|
+
assertValidScope(scope);
|
|
411
|
+
if (scope === StorageScope.Memory) {
|
|
412
|
+
return memoryStore.size;
|
|
413
|
+
}
|
|
414
|
+
return getStorageModule().size(scope);
|
|
415
|
+
},
|
|
416
|
+
setAccessControl: (level: AccessControl) => {
|
|
417
|
+
secureDefaultAccessControl = level;
|
|
418
|
+
getStorageModule().setSecureAccessControl(level);
|
|
419
|
+
},
|
|
420
|
+
setSecureWritesAsync: (enabled: boolean) => {
|
|
421
|
+
getStorageModule().setSecureWritesAsync(enabled);
|
|
422
|
+
},
|
|
423
|
+
flushSecureWrites: () => {
|
|
424
|
+
flushSecureWrites();
|
|
425
|
+
},
|
|
426
|
+
setKeychainAccessGroup: (group: string) => {
|
|
427
|
+
getStorageModule().setKeychainAccessGroup(group);
|
|
428
|
+
},
|
|
321
429
|
};
|
|
322
430
|
|
|
323
431
|
export interface StorageItemConfig<T> {
|
|
@@ -329,27 +437,50 @@ export interface StorageItemConfig<T> {
|
|
|
329
437
|
validate?: Validator<T>;
|
|
330
438
|
onValidationError?: (invalidValue: unknown) => T;
|
|
331
439
|
expiration?: ExpirationConfig;
|
|
440
|
+
onExpired?: (key: string) => void;
|
|
332
441
|
readCache?: boolean;
|
|
333
442
|
coalesceSecureWrites?: boolean;
|
|
443
|
+
namespace?: string;
|
|
444
|
+
biometric?: boolean;
|
|
445
|
+
accessControl?: AccessControl;
|
|
334
446
|
}
|
|
335
447
|
|
|
336
448
|
export interface StorageItem<T> {
|
|
337
449
|
get: () => T;
|
|
338
450
|
set: (value: T | ((prev: T) => T)) => void;
|
|
339
451
|
delete: () => void;
|
|
452
|
+
has: () => boolean;
|
|
340
453
|
subscribe: (callback: () => void) => () => void;
|
|
341
454
|
serialize: (value: T) => string;
|
|
342
455
|
deserialize: (value: string) => T;
|
|
343
|
-
_triggerListeners: () => void;
|
|
344
|
-
_hasValidation?: boolean;
|
|
345
|
-
_hasExpiration?: boolean;
|
|
346
|
-
_readCacheEnabled?: boolean;
|
|
347
456
|
scope: StorageScope;
|
|
348
457
|
key: string;
|
|
349
458
|
}
|
|
350
459
|
|
|
460
|
+
type StorageItemInternal<T> = StorageItem<T> & {
|
|
461
|
+
_triggerListeners: () => void;
|
|
462
|
+
_hasValidation: boolean;
|
|
463
|
+
_hasExpiration: boolean;
|
|
464
|
+
_readCacheEnabled: boolean;
|
|
465
|
+
_isBiometric: boolean;
|
|
466
|
+
_secureAccessControl?: AccessControl;
|
|
467
|
+
};
|
|
468
|
+
|
|
351
469
|
function canUseRawBatchPath(item: RawBatchPathItem): boolean {
|
|
352
|
-
return
|
|
470
|
+
return (
|
|
471
|
+
item._hasExpiration === false &&
|
|
472
|
+
item._hasValidation === false &&
|
|
473
|
+
item._isBiometric !== true &&
|
|
474
|
+
item._secureAccessControl === undefined
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function canUseSecureRawBatchPath(item: RawBatchPathItem): boolean {
|
|
479
|
+
return (
|
|
480
|
+
item._hasExpiration === false &&
|
|
481
|
+
item._hasValidation === false &&
|
|
482
|
+
item._isBiometric !== true
|
|
483
|
+
);
|
|
353
484
|
}
|
|
354
485
|
|
|
355
486
|
function defaultSerialize<T>(value: T): string {
|
|
@@ -361,19 +492,29 @@ function defaultDeserialize<T>(value: string): T {
|
|
|
361
492
|
}
|
|
362
493
|
|
|
363
494
|
export function createStorageItem<T = undefined>(
|
|
364
|
-
config: StorageItemConfig<T
|
|
495
|
+
config: StorageItemConfig<T>,
|
|
365
496
|
): StorageItem<T> {
|
|
497
|
+
const storageKey = prefixKey(config.namespace, config.key);
|
|
366
498
|
const serialize = config.serialize ?? defaultSerialize;
|
|
367
499
|
const deserialize = config.deserialize ?? defaultDeserialize;
|
|
368
500
|
const isMemory = config.scope === StorageScope.Memory;
|
|
501
|
+
const isBiometric =
|
|
502
|
+
config.biometric === true && config.scope === StorageScope.Secure;
|
|
503
|
+
const secureAccessControl = config.accessControl;
|
|
369
504
|
const validate = config.validate;
|
|
370
505
|
const onValidationError = config.onValidationError;
|
|
371
506
|
const expiration = config.expiration;
|
|
507
|
+
const onExpired = config.onExpired;
|
|
372
508
|
const expirationTtlMs = expiration?.ttlMs;
|
|
373
|
-
const memoryExpiration =
|
|
509
|
+
const memoryExpiration =
|
|
510
|
+
expiration && isMemory ? new Map<string, number>() : null;
|
|
374
511
|
const readCache = !isMemory && config.readCache === true;
|
|
375
512
|
const coalesceSecureWrites =
|
|
376
|
-
config.scope === StorageScope.Secure &&
|
|
513
|
+
config.scope === StorageScope.Secure &&
|
|
514
|
+
config.coalesceSecureWrites === true &&
|
|
515
|
+
!isBiometric &&
|
|
516
|
+
secureAccessControl === undefined;
|
|
517
|
+
const defaultValue = config.defaultValue as T;
|
|
377
518
|
const nonMemoryScope: NonMemoryScope | null =
|
|
378
519
|
config.scope === StorageScope.Disk
|
|
379
520
|
? StorageScope.Disk
|
|
@@ -390,11 +531,13 @@ export function createStorageItem<T = undefined>(
|
|
|
390
531
|
let lastRaw: unknown = undefined;
|
|
391
532
|
let lastValue: T | undefined;
|
|
392
533
|
let hasLastValue = false;
|
|
534
|
+
let lastExpiresAt: number | null | undefined = undefined;
|
|
393
535
|
|
|
394
536
|
const invalidateParsedCache = () => {
|
|
395
537
|
lastRaw = undefined;
|
|
396
538
|
lastValue = undefined;
|
|
397
539
|
hasLastValue = false;
|
|
540
|
+
lastExpiresAt = undefined;
|
|
398
541
|
};
|
|
399
542
|
|
|
400
543
|
const ensureSubscription = () => {
|
|
@@ -408,80 +551,106 @@ export function createStorageItem<T = undefined>(
|
|
|
408
551
|
};
|
|
409
552
|
|
|
410
553
|
if (isMemory) {
|
|
411
|
-
unsubscribe = addKeyListener(memoryListeners,
|
|
554
|
+
unsubscribe = addKeyListener(memoryListeners, storageKey, listener);
|
|
412
555
|
return;
|
|
413
556
|
}
|
|
414
557
|
|
|
415
558
|
ensureNativeScopeSubscription(nonMemoryScope!);
|
|
416
|
-
unsubscribe = addKeyListener(
|
|
559
|
+
unsubscribe = addKeyListener(
|
|
560
|
+
getScopedListeners(nonMemoryScope!),
|
|
561
|
+
storageKey,
|
|
562
|
+
listener,
|
|
563
|
+
);
|
|
417
564
|
};
|
|
418
565
|
|
|
419
566
|
const readStoredRaw = (): unknown => {
|
|
420
567
|
if (isMemory) {
|
|
421
568
|
if (memoryExpiration) {
|
|
422
|
-
const expiresAt = memoryExpiration.get(
|
|
569
|
+
const expiresAt = memoryExpiration.get(storageKey);
|
|
423
570
|
if (expiresAt !== undefined && expiresAt <= Date.now()) {
|
|
424
|
-
memoryExpiration.delete(
|
|
425
|
-
memoryStore.delete(
|
|
426
|
-
notifyKeyListeners(memoryListeners,
|
|
571
|
+
memoryExpiration.delete(storageKey);
|
|
572
|
+
memoryStore.delete(storageKey);
|
|
573
|
+
notifyKeyListeners(memoryListeners, storageKey);
|
|
574
|
+
onExpired?.(storageKey);
|
|
427
575
|
return undefined;
|
|
428
576
|
}
|
|
429
577
|
}
|
|
430
|
-
return memoryStore.get(
|
|
578
|
+
return memoryStore.get(storageKey);
|
|
431
579
|
}
|
|
432
580
|
|
|
433
|
-
if (
|
|
434
|
-
|
|
581
|
+
if (
|
|
582
|
+
nonMemoryScope === StorageScope.Secure &&
|
|
583
|
+
!isBiometric &&
|
|
584
|
+
hasPendingSecureWrite(storageKey)
|
|
585
|
+
) {
|
|
586
|
+
return readPendingSecureWrite(storageKey);
|
|
435
587
|
}
|
|
436
588
|
|
|
437
589
|
if (readCache) {
|
|
438
|
-
if (hasCachedRawValue(nonMemoryScope!,
|
|
439
|
-
return readCachedRawValue(nonMemoryScope!,
|
|
590
|
+
if (hasCachedRawValue(nonMemoryScope!, storageKey)) {
|
|
591
|
+
return readCachedRawValue(nonMemoryScope!, storageKey);
|
|
440
592
|
}
|
|
441
593
|
}
|
|
442
594
|
|
|
443
|
-
|
|
444
|
-
|
|
595
|
+
if (isBiometric) {
|
|
596
|
+
return getStorageModule().getSecureBiometric(storageKey);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
const raw = getStorageModule().get(storageKey, config.scope);
|
|
600
|
+
cacheRawValue(nonMemoryScope!, storageKey, raw);
|
|
445
601
|
return raw;
|
|
446
602
|
};
|
|
447
603
|
|
|
448
604
|
const writeStoredRaw = (rawValue: string): void => {
|
|
449
|
-
|
|
605
|
+
if (isBiometric) {
|
|
606
|
+
getStorageModule().setSecureBiometric(storageKey, rawValue);
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
cacheRawValue(nonMemoryScope!, storageKey, rawValue);
|
|
450
611
|
|
|
451
612
|
if (coalesceSecureWrites) {
|
|
452
|
-
scheduleSecureWrite(
|
|
613
|
+
scheduleSecureWrite(storageKey, rawValue);
|
|
453
614
|
return;
|
|
454
615
|
}
|
|
455
616
|
|
|
456
617
|
if (nonMemoryScope === StorageScope.Secure) {
|
|
457
|
-
clearPendingSecureWrite(
|
|
618
|
+
clearPendingSecureWrite(storageKey);
|
|
619
|
+
getStorageModule().setSecureAccessControl(
|
|
620
|
+
secureAccessControl ?? secureDefaultAccessControl,
|
|
621
|
+
);
|
|
458
622
|
}
|
|
459
623
|
|
|
460
|
-
getStorageModule().set(
|
|
624
|
+
getStorageModule().set(storageKey, rawValue, config.scope);
|
|
461
625
|
};
|
|
462
626
|
|
|
463
627
|
const removeStoredRaw = (): void => {
|
|
464
|
-
|
|
628
|
+
if (isBiometric) {
|
|
629
|
+
getStorageModule().deleteSecureBiometric(storageKey);
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
cacheRawValue(nonMemoryScope!, storageKey, undefined);
|
|
465
634
|
|
|
466
635
|
if (coalesceSecureWrites) {
|
|
467
|
-
scheduleSecureWrite(
|
|
636
|
+
scheduleSecureWrite(storageKey, undefined);
|
|
468
637
|
return;
|
|
469
638
|
}
|
|
470
639
|
|
|
471
640
|
if (nonMemoryScope === StorageScope.Secure) {
|
|
472
|
-
clearPendingSecureWrite(
|
|
641
|
+
clearPendingSecureWrite(storageKey);
|
|
473
642
|
}
|
|
474
643
|
|
|
475
|
-
getStorageModule().remove(
|
|
644
|
+
getStorageModule().remove(storageKey, config.scope);
|
|
476
645
|
};
|
|
477
646
|
|
|
478
647
|
const writeValueWithoutValidation = (value: T): void => {
|
|
479
648
|
if (isMemory) {
|
|
480
649
|
if (memoryExpiration) {
|
|
481
|
-
memoryExpiration.set(
|
|
650
|
+
memoryExpiration.set(storageKey, Date.now() + (expirationTtlMs ?? 0));
|
|
482
651
|
}
|
|
483
|
-
memoryStore.set(
|
|
484
|
-
notifyKeyListeners(memoryListeners,
|
|
652
|
+
memoryStore.set(storageKey, value);
|
|
653
|
+
notifyKeyListeners(memoryListeners, storageKey);
|
|
485
654
|
return;
|
|
486
655
|
}
|
|
487
656
|
|
|
@@ -504,12 +673,12 @@ export function createStorageItem<T = undefined>(
|
|
|
504
673
|
return onValidationError(invalidValue);
|
|
505
674
|
}
|
|
506
675
|
|
|
507
|
-
return
|
|
676
|
+
return defaultValue;
|
|
508
677
|
};
|
|
509
678
|
|
|
510
679
|
const ensureValidatedValue = (
|
|
511
680
|
candidate: unknown,
|
|
512
|
-
hadStoredValue: boolean
|
|
681
|
+
hadStoredValue: boolean,
|
|
513
682
|
): T => {
|
|
514
683
|
if (!validate || validate(candidate)) {
|
|
515
684
|
return candidate as T;
|
|
@@ -517,7 +686,7 @@ export function createStorageItem<T = undefined>(
|
|
|
517
686
|
|
|
518
687
|
const resolved = resolveInvalidValue(candidate);
|
|
519
688
|
if (validate && !validate(resolved)) {
|
|
520
|
-
return
|
|
689
|
+
return defaultValue;
|
|
521
690
|
}
|
|
522
691
|
if (hadStoredValue) {
|
|
523
692
|
writeValueWithoutValidation(resolved);
|
|
@@ -528,35 +697,61 @@ export function createStorageItem<T = undefined>(
|
|
|
528
697
|
const get = (): T => {
|
|
529
698
|
const raw = readStoredRaw();
|
|
530
699
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
700
|
+
if (!memoryExpiration && raw === lastRaw && hasLastValue) {
|
|
701
|
+
if (!expiration || lastExpiresAt === null) {
|
|
702
|
+
return lastValue as T;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
if (typeof lastExpiresAt === "number") {
|
|
706
|
+
if (lastExpiresAt > Date.now()) {
|
|
707
|
+
return lastValue as T;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
removeStoredRaw();
|
|
711
|
+
invalidateParsedCache();
|
|
712
|
+
onExpired?.(storageKey);
|
|
713
|
+
lastValue = ensureValidatedValue(defaultValue, false);
|
|
714
|
+
hasLastValue = true;
|
|
715
|
+
return lastValue;
|
|
716
|
+
}
|
|
534
717
|
}
|
|
535
718
|
|
|
536
719
|
lastRaw = raw;
|
|
537
720
|
|
|
538
721
|
if (raw === undefined) {
|
|
539
|
-
|
|
722
|
+
lastExpiresAt = undefined;
|
|
723
|
+
lastValue = ensureValidatedValue(defaultValue, false);
|
|
540
724
|
hasLastValue = true;
|
|
541
725
|
return lastValue;
|
|
542
726
|
}
|
|
543
727
|
|
|
544
728
|
if (isMemory) {
|
|
729
|
+
lastExpiresAt = undefined;
|
|
545
730
|
lastValue = ensureValidatedValue(raw, true);
|
|
546
731
|
hasLastValue = true;
|
|
547
732
|
return lastValue;
|
|
548
733
|
}
|
|
549
734
|
|
|
550
|
-
|
|
735
|
+
if (typeof raw !== "string") {
|
|
736
|
+
lastExpiresAt = undefined;
|
|
737
|
+
lastValue = ensureValidatedValue(defaultValue, false);
|
|
738
|
+
hasLastValue = true;
|
|
739
|
+
return lastValue;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
let deserializableRaw = raw;
|
|
551
743
|
|
|
552
744
|
if (expiration) {
|
|
745
|
+
let envelopeExpiresAt: number | null = null;
|
|
553
746
|
try {
|
|
554
|
-
const parsed = JSON.parse(raw
|
|
747
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
555
748
|
if (isStoredEnvelope(parsed)) {
|
|
749
|
+
envelopeExpiresAt = parsed.expiresAt;
|
|
556
750
|
if (parsed.expiresAt <= Date.now()) {
|
|
557
751
|
removeStoredRaw();
|
|
558
752
|
invalidateParsedCache();
|
|
559
|
-
|
|
753
|
+
onExpired?.(storageKey);
|
|
754
|
+
lastValue = ensureValidatedValue(defaultValue, false);
|
|
560
755
|
hasLastValue = true;
|
|
561
756
|
return lastValue;
|
|
562
757
|
}
|
|
@@ -566,6 +761,9 @@ export function createStorageItem<T = undefined>(
|
|
|
566
761
|
} catch {
|
|
567
762
|
// Keep backward compatibility with legacy raw values.
|
|
568
763
|
}
|
|
764
|
+
lastExpiresAt = envelopeExpiresAt;
|
|
765
|
+
} else {
|
|
766
|
+
lastExpiresAt = undefined;
|
|
569
767
|
}
|
|
570
768
|
|
|
571
769
|
lastValue = ensureValidatedValue(deserialize(deserializableRaw), true);
|
|
@@ -574,17 +772,13 @@ export function createStorageItem<T = undefined>(
|
|
|
574
772
|
};
|
|
575
773
|
|
|
576
774
|
const set = (valueOrFn: T | ((prev: T) => T)): void => {
|
|
577
|
-
const
|
|
578
|
-
const newValue =
|
|
579
|
-
typeof valueOrFn === "function"
|
|
580
|
-
? (valueOrFn as (prev: T) => T)(currentValue)
|
|
581
|
-
: valueOrFn;
|
|
775
|
+
const newValue = isUpdater(valueOrFn) ? valueOrFn(get()) : valueOrFn;
|
|
582
776
|
|
|
583
777
|
invalidateParsedCache();
|
|
584
778
|
|
|
585
779
|
if (validate && !validate(newValue)) {
|
|
586
780
|
throw new Error(
|
|
587
|
-
`Validation failed for key "${
|
|
781
|
+
`Validation failed for key "${storageKey}" in scope "${StorageScope[config.scope]}".`,
|
|
588
782
|
);
|
|
589
783
|
}
|
|
590
784
|
|
|
@@ -596,16 +790,22 @@ export function createStorageItem<T = undefined>(
|
|
|
596
790
|
|
|
597
791
|
if (isMemory) {
|
|
598
792
|
if (memoryExpiration) {
|
|
599
|
-
memoryExpiration.delete(
|
|
793
|
+
memoryExpiration.delete(storageKey);
|
|
600
794
|
}
|
|
601
|
-
memoryStore.delete(
|
|
602
|
-
notifyKeyListeners(memoryListeners,
|
|
795
|
+
memoryStore.delete(storageKey);
|
|
796
|
+
notifyKeyListeners(memoryListeners, storageKey);
|
|
603
797
|
return;
|
|
604
798
|
}
|
|
605
799
|
|
|
606
800
|
removeStoredRaw();
|
|
607
801
|
};
|
|
608
802
|
|
|
803
|
+
const hasItem = (): boolean => {
|
|
804
|
+
if (isMemory) return memoryStore.has(storageKey);
|
|
805
|
+
if (isBiometric) return getStorageModule().hasSecureBiometric(storageKey);
|
|
806
|
+
return getStorageModule().has(storageKey, config.scope);
|
|
807
|
+
};
|
|
808
|
+
|
|
609
809
|
const subscribe = (callback: () => void): (() => void) => {
|
|
610
810
|
ensureSubscription();
|
|
611
811
|
listeners.add(callback);
|
|
@@ -621,10 +821,11 @@ export function createStorageItem<T = undefined>(
|
|
|
621
821
|
};
|
|
622
822
|
};
|
|
623
823
|
|
|
624
|
-
const storageItem:
|
|
824
|
+
const storageItem: StorageItemInternal<T> = {
|
|
625
825
|
get,
|
|
626
826
|
set,
|
|
627
827
|
delete: deleteItem,
|
|
828
|
+
has: hasItem,
|
|
628
829
|
subscribe,
|
|
629
830
|
serialize,
|
|
630
831
|
deserialize,
|
|
@@ -635,62 +836,29 @@ export function createStorageItem<T = undefined>(
|
|
|
635
836
|
_hasValidation: validate !== undefined,
|
|
636
837
|
_hasExpiration: expiration !== undefined,
|
|
637
838
|
_readCacheEnabled: readCache,
|
|
839
|
+
_isBiometric: isBiometric,
|
|
840
|
+
...(secureAccessControl !== undefined
|
|
841
|
+
? { _secureAccessControl: secureAccessControl }
|
|
842
|
+
: {}),
|
|
638
843
|
scope: config.scope,
|
|
639
|
-
key:
|
|
844
|
+
key: storageKey,
|
|
640
845
|
};
|
|
641
846
|
|
|
642
847
|
return storageItem;
|
|
643
848
|
}
|
|
644
849
|
|
|
645
|
-
export
|
|
646
|
-
item: StorageItem<T>
|
|
647
|
-
): [T, (value: T | ((prev: T) => T)) => void] {
|
|
648
|
-
const value = useSyncExternalStore(item.subscribe, item.get, item.get);
|
|
649
|
-
return [value, item.set];
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
export function useStorageSelector<T, TSelected>(
|
|
653
|
-
item: StorageItem<T>,
|
|
654
|
-
selector: (value: T) => TSelected,
|
|
655
|
-
isEqual: (prev: TSelected, next: TSelected) => boolean = Object.is
|
|
656
|
-
): [TSelected, (value: T | ((prev: T) => T)) => void] {
|
|
657
|
-
const selectedRef = useRef<{ hasValue: false } | { hasValue: true; value: TSelected }>({
|
|
658
|
-
hasValue: false,
|
|
659
|
-
});
|
|
660
|
-
|
|
661
|
-
const getSelectedSnapshot = () => {
|
|
662
|
-
const nextSelected = selector(item.get());
|
|
663
|
-
const current = selectedRef.current;
|
|
664
|
-
if (current.hasValue && isEqual(current.value, nextSelected)) {
|
|
665
|
-
return current.value;
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
selectedRef.current = { hasValue: true, value: nextSelected };
|
|
669
|
-
return nextSelected;
|
|
670
|
-
};
|
|
671
|
-
|
|
672
|
-
const selectedValue = useSyncExternalStore(
|
|
673
|
-
item.subscribe,
|
|
674
|
-
getSelectedSnapshot,
|
|
675
|
-
getSelectedSnapshot
|
|
676
|
-
);
|
|
677
|
-
return [selectedValue, item.set];
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
export function useSetStorage<T>(item: StorageItem<T>) {
|
|
681
|
-
return item.set;
|
|
682
|
-
}
|
|
850
|
+
export { useStorage, useStorageSelector, useSetStorage } from "./storage-hooks";
|
|
683
851
|
|
|
684
852
|
type BatchReadItem<T> = Pick<
|
|
685
853
|
StorageItem<T>,
|
|
686
|
-
| "
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
854
|
+
"key" | "scope" | "get" | "deserialize"
|
|
855
|
+
> & {
|
|
856
|
+
_hasValidation?: boolean;
|
|
857
|
+
_hasExpiration?: boolean;
|
|
858
|
+
_readCacheEnabled?: boolean;
|
|
859
|
+
_isBiometric?: boolean;
|
|
860
|
+
_secureAccessControl?: AccessControl;
|
|
861
|
+
};
|
|
694
862
|
type BatchRemoveItem = Pick<StorageItem<unknown>, "key" | "scope" | "delete">;
|
|
695
863
|
|
|
696
864
|
export type StorageBatchSetItem<T> = {
|
|
@@ -700,7 +868,7 @@ export type StorageBatchSetItem<T> = {
|
|
|
700
868
|
|
|
701
869
|
export function getBatch(
|
|
702
870
|
items: readonly BatchReadItem<unknown>[],
|
|
703
|
-
scope: StorageScope
|
|
871
|
+
scope: StorageScope,
|
|
704
872
|
): unknown[] {
|
|
705
873
|
assertBatchScope(items, scope);
|
|
706
874
|
|
|
@@ -708,7 +876,11 @@ export function getBatch(
|
|
|
708
876
|
return items.map((item) => item.get());
|
|
709
877
|
}
|
|
710
878
|
|
|
711
|
-
const useRawBatchPath = items.every((item) =>
|
|
879
|
+
const useRawBatchPath = items.every((item) =>
|
|
880
|
+
scope === StorageScope.Secure
|
|
881
|
+
? canUseSecureRawBatchPath(item)
|
|
882
|
+
: canUseRawBatchPath(item),
|
|
883
|
+
);
|
|
712
884
|
if (!useRawBatchPath) {
|
|
713
885
|
return items.map((item) => item.get());
|
|
714
886
|
}
|
|
@@ -745,6 +917,9 @@ export function getBatch(
|
|
|
745
917
|
fetchedValues.forEach((value, index) => {
|
|
746
918
|
const key = keysToFetch[index];
|
|
747
919
|
const targetIndex = keyIndexes[index];
|
|
920
|
+
if (key === undefined || targetIndex === undefined) {
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
748
923
|
rawValues[targetIndex] = value;
|
|
749
924
|
cacheRawValue(scope, key, value);
|
|
750
925
|
});
|
|
@@ -761,11 +936,11 @@ export function getBatch(
|
|
|
761
936
|
|
|
762
937
|
export function setBatch<T>(
|
|
763
938
|
items: readonly StorageBatchSetItem<T>[],
|
|
764
|
-
scope: StorageScope
|
|
939
|
+
scope: StorageScope,
|
|
765
940
|
): void {
|
|
766
941
|
assertBatchScope(
|
|
767
942
|
items.map((batchEntry) => batchEntry.item),
|
|
768
|
-
scope
|
|
943
|
+
scope,
|
|
769
944
|
);
|
|
770
945
|
|
|
771
946
|
if (scope === StorageScope.Memory) {
|
|
@@ -773,7 +948,52 @@ export function setBatch<T>(
|
|
|
773
948
|
return;
|
|
774
949
|
}
|
|
775
950
|
|
|
776
|
-
|
|
951
|
+
if (scope === StorageScope.Secure) {
|
|
952
|
+
const secureEntries = items.map(({ item, value }) => ({
|
|
953
|
+
item,
|
|
954
|
+
value,
|
|
955
|
+
internal: asInternal(item),
|
|
956
|
+
}));
|
|
957
|
+
const canUseSecureBatchPath = secureEntries.every(({ internal }) =>
|
|
958
|
+
canUseSecureRawBatchPath(internal),
|
|
959
|
+
);
|
|
960
|
+
if (!canUseSecureBatchPath) {
|
|
961
|
+
items.forEach(({ item, value }) => item.set(value));
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
flushSecureWrites();
|
|
966
|
+
const storageModule = getStorageModule();
|
|
967
|
+
const groupedByAccessControl = new Map<
|
|
968
|
+
number,
|
|
969
|
+
{ keys: string[]; values: string[] }
|
|
970
|
+
>();
|
|
971
|
+
|
|
972
|
+
secureEntries.forEach(({ item, value, internal }) => {
|
|
973
|
+
const accessControl =
|
|
974
|
+
internal._secureAccessControl ?? secureDefaultAccessControl;
|
|
975
|
+
const existingGroup = groupedByAccessControl.get(accessControl);
|
|
976
|
+
const group = existingGroup ?? { keys: [], values: [] };
|
|
977
|
+
group.keys.push(item.key);
|
|
978
|
+
group.values.push(item.serialize(value));
|
|
979
|
+
if (!existingGroup) {
|
|
980
|
+
groupedByAccessControl.set(accessControl, group);
|
|
981
|
+
}
|
|
982
|
+
});
|
|
983
|
+
|
|
984
|
+
groupedByAccessControl.forEach((group, accessControl) => {
|
|
985
|
+
storageModule.setSecureAccessControl(accessControl);
|
|
986
|
+
storageModule.setBatch(group.keys, group.values, scope);
|
|
987
|
+
group.keys.forEach((key, index) =>
|
|
988
|
+
cacheRawValue(scope, key, group.values[index]),
|
|
989
|
+
);
|
|
990
|
+
});
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
const useRawBatchPath = items.every(({ item }) =>
|
|
995
|
+
canUseRawBatchPath(asInternal(item)),
|
|
996
|
+
);
|
|
777
997
|
if (!useRawBatchPath) {
|
|
778
998
|
items.forEach(({ item, value }) => item.set(value));
|
|
779
999
|
return;
|
|
@@ -782,16 +1002,13 @@ export function setBatch<T>(
|
|
|
782
1002
|
const keys = items.map((entry) => entry.item.key);
|
|
783
1003
|
const values = items.map((entry) => entry.item.serialize(entry.value));
|
|
784
1004
|
|
|
785
|
-
if (scope === StorageScope.Secure) {
|
|
786
|
-
flushSecureWrites();
|
|
787
|
-
}
|
|
788
1005
|
getStorageModule().setBatch(keys, values, scope);
|
|
789
1006
|
keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
|
|
790
1007
|
}
|
|
791
1008
|
|
|
792
1009
|
export function removeBatch(
|
|
793
1010
|
items: readonly BatchRemoveItem[],
|
|
794
|
-
scope: StorageScope
|
|
1011
|
+
scope: StorageScope,
|
|
795
1012
|
): void {
|
|
796
1013
|
assertBatchScope(items, scope);
|
|
797
1014
|
|
|
@@ -820,7 +1037,9 @@ export function registerMigration(version: number, migration: Migration): void {
|
|
|
820
1037
|
registeredMigrations.set(version, migration);
|
|
821
1038
|
}
|
|
822
1039
|
|
|
823
|
-
export function migrateToLatest(
|
|
1040
|
+
export function migrateToLatest(
|
|
1041
|
+
scope: StorageScope = StorageScope.Disk,
|
|
1042
|
+
): number {
|
|
824
1043
|
assertValidScope(scope);
|
|
825
1044
|
const currentVersion = readMigrationVersion(scope);
|
|
826
1045
|
const versions = Array.from(registeredMigrations.keys())
|
|
@@ -850,7 +1069,7 @@ export function migrateToLatest(scope: StorageScope = StorageScope.Disk): number
|
|
|
850
1069
|
|
|
851
1070
|
export function runTransaction<T>(
|
|
852
1071
|
scope: StorageScope,
|
|
853
|
-
transaction: (context: TransactionContext) => T
|
|
1072
|
+
transaction: (context: TransactionContext) => T,
|
|
854
1073
|
): T {
|
|
855
1074
|
assertValidScope(scope);
|
|
856
1075
|
if (scope === StorageScope.Secure) {
|
|
@@ -908,3 +1127,43 @@ export function runTransaction<T>(
|
|
|
908
1127
|
throw error;
|
|
909
1128
|
}
|
|
910
1129
|
}
|
|
1130
|
+
|
|
1131
|
+
export type SecureAuthStorageConfig<K extends string = string> = Record<
|
|
1132
|
+
K,
|
|
1133
|
+
{
|
|
1134
|
+
ttlMs?: number;
|
|
1135
|
+
biometric?: boolean;
|
|
1136
|
+
accessControl?: AccessControl;
|
|
1137
|
+
}
|
|
1138
|
+
>;
|
|
1139
|
+
|
|
1140
|
+
export function createSecureAuthStorage<K extends string>(
|
|
1141
|
+
config: SecureAuthStorageConfig<K>,
|
|
1142
|
+
options?: { namespace?: string },
|
|
1143
|
+
): Record<K, StorageItem<string>> {
|
|
1144
|
+
const ns = options?.namespace ?? "auth";
|
|
1145
|
+
const result: Partial<Record<K, StorageItem<string>>> = {};
|
|
1146
|
+
|
|
1147
|
+
for (const key of typedKeys(config)) {
|
|
1148
|
+
const itemConfig = config[key];
|
|
1149
|
+
const expirationConfig =
|
|
1150
|
+
itemConfig.ttlMs !== undefined ? { ttlMs: itemConfig.ttlMs } : undefined;
|
|
1151
|
+
result[key] = createStorageItem<string>({
|
|
1152
|
+
key,
|
|
1153
|
+
scope: StorageScope.Secure,
|
|
1154
|
+
defaultValue: "",
|
|
1155
|
+
namespace: ns,
|
|
1156
|
+
...(itemConfig.biometric !== undefined
|
|
1157
|
+
? { biometric: itemConfig.biometric }
|
|
1158
|
+
: {}),
|
|
1159
|
+
...(itemConfig.accessControl !== undefined
|
|
1160
|
+
? { accessControl: itemConfig.accessControl }
|
|
1161
|
+
: {}),
|
|
1162
|
+
...(expirationConfig !== undefined
|
|
1163
|
+
? { expiration: expirationConfig }
|
|
1164
|
+
: {}),
|
|
1165
|
+
});
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
return result as Record<K, StorageItem<string>>;
|
|
1169
|
+
}
|