react-native-nitro-storage 0.4.4 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +237 -862
- package/SECURITY.md +26 -0
- package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +61 -10
- package/docs/api-reference.md +217 -0
- package/docs/batch-transactions-migrations.md +186 -0
- package/docs/benchmarks.md +37 -0
- package/docs/mmkv-migration.md +80 -0
- package/docs/react-hooks.md +113 -0
- package/docs/recipes.md +281 -0
- package/docs/secure-storage.md +171 -0
- package/docs/web-backends.md +141 -0
- package/ios/IOSStorageAdapterCpp.mm +44 -14
- package/lib/commonjs/index.js +271 -5
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +498 -202
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/indexeddb-backend.js +129 -7
- package/lib/commonjs/indexeddb-backend.js.map +1 -1
- package/lib/commonjs/storage-runtime.js +41 -0
- package/lib/commonjs/storage-runtime.js.map +1 -0
- package/lib/commonjs/web-storage-backend.js +90 -0
- package/lib/commonjs/web-storage-backend.js.map +1 -0
- package/lib/module/index.js +263 -5
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +490 -202
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/indexeddb-backend.js +129 -7
- package/lib/module/indexeddb-backend.js.map +1 -1
- package/lib/module/storage-runtime.js +36 -0
- package/lib/module/storage-runtime.js.map +1 -0
- package/lib/module/web-storage-backend.js +86 -0
- package/lib/module/web-storage-backend.js.map +1 -0
- package/lib/typescript/index.d.ts +14 -7
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/index.web.d.ts +15 -8
- package/lib/typescript/index.web.d.ts.map +1 -1
- package/lib/typescript/indexeddb-backend.d.ts +6 -2
- package/lib/typescript/indexeddb-backend.d.ts.map +1 -1
- package/lib/typescript/storage-runtime.d.ts +48 -0
- package/lib/typescript/storage-runtime.d.ts.map +1 -0
- package/lib/typescript/web-storage-backend.d.ts +30 -0
- package/lib/typescript/web-storage-backend.d.ts.map +1 -0
- package/package.json +21 -8
- package/src/index.ts +330 -20
- package/src/index.web.ts +673 -245
- package/src/indexeddb-backend.ts +147 -6
- package/src/storage-runtime.ts +129 -0
- package/src/web-storage-backend.ts +129 -0
package/src/index.ts
CHANGED
|
@@ -14,10 +14,34 @@ import {
|
|
|
14
14
|
prefixKey,
|
|
15
15
|
isNamespaced,
|
|
16
16
|
} from "./internal";
|
|
17
|
+
import type {
|
|
18
|
+
WebDiskStorageBackend,
|
|
19
|
+
WebSecureStorageBackend,
|
|
20
|
+
} from "./web-storage-backend";
|
|
21
|
+
import {
|
|
22
|
+
getStorageErrorCode,
|
|
23
|
+
isLockedStorageErrorCode,
|
|
24
|
+
type SecureStorageMetadata,
|
|
25
|
+
type SecurityCapabilities,
|
|
26
|
+
type StorageCapabilities,
|
|
27
|
+
type StorageErrorCode,
|
|
28
|
+
} from "./storage-runtime";
|
|
17
29
|
|
|
18
30
|
export { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
|
|
19
31
|
export type { Storage } from "./Storage.nitro";
|
|
20
32
|
export { migrateFromMMKV } from "./migration";
|
|
33
|
+
export {
|
|
34
|
+
getStorageErrorCode,
|
|
35
|
+
type SecureStorageMetadata,
|
|
36
|
+
type SecurityCapabilities,
|
|
37
|
+
type StorageCapabilities,
|
|
38
|
+
type StorageErrorCode,
|
|
39
|
+
} from "./storage-runtime";
|
|
40
|
+
export type {
|
|
41
|
+
WebStorageBackend,
|
|
42
|
+
WebStorageChangeEvent,
|
|
43
|
+
WebStorageScope,
|
|
44
|
+
} from "./web-storage-backend";
|
|
21
45
|
|
|
22
46
|
export type Validator<T> = (value: unknown) => value is T;
|
|
23
47
|
export type ExpirationConfig = {
|
|
@@ -41,14 +65,6 @@ export type StorageMetricSummary = {
|
|
|
41
65
|
avgDurationMs: number;
|
|
42
66
|
maxDurationMs: number;
|
|
43
67
|
};
|
|
44
|
-
export type WebSecureStorageBackend = {
|
|
45
|
-
getItem: (key: string) => string | null;
|
|
46
|
-
setItem: (key: string, value: string) => void;
|
|
47
|
-
removeItem: (key: string) => void;
|
|
48
|
-
clear: () => void;
|
|
49
|
-
getAllKeys: () => string[];
|
|
50
|
-
};
|
|
51
|
-
|
|
52
68
|
export type MigrationContext = {
|
|
53
69
|
scope: StorageScope;
|
|
54
70
|
getRaw: (key: string) => string | undefined;
|
|
@@ -95,6 +111,10 @@ function typedKeys<K extends string, V>(record: Record<K, V>): K[] {
|
|
|
95
111
|
return Object.keys(record) as K[];
|
|
96
112
|
}
|
|
97
113
|
type NonMemoryScope = StorageScope.Disk | StorageScope.Secure;
|
|
114
|
+
type PendingDiskWrite = {
|
|
115
|
+
key: string;
|
|
116
|
+
value: string | undefined;
|
|
117
|
+
};
|
|
98
118
|
type PendingSecureWrite = {
|
|
99
119
|
key: string;
|
|
100
120
|
value: string | undefined;
|
|
@@ -108,6 +128,10 @@ const runMicrotask =
|
|
|
108
128
|
: (task: () => void) => {
|
|
109
129
|
Promise.resolve().then(task);
|
|
110
130
|
};
|
|
131
|
+
const now =
|
|
132
|
+
typeof performance !== "undefined" && typeof performance.now === "function"
|
|
133
|
+
? () => performance.now()
|
|
134
|
+
: () => Date.now();
|
|
111
135
|
|
|
112
136
|
let _storageModule: Storage | null = null;
|
|
113
137
|
|
|
@@ -131,6 +155,9 @@ const scopedRawCache = new Map<NonMemoryScope, Map<string, string | undefined>>(
|
|
|
131
155
|
[StorageScope.Secure, new Map()],
|
|
132
156
|
],
|
|
133
157
|
);
|
|
158
|
+
const pendingDiskWrites = new Map<string, PendingDiskWrite>();
|
|
159
|
+
let diskFlushScheduled = false;
|
|
160
|
+
let diskWritesAsync = false;
|
|
134
161
|
const pendingSecureWrites = new Map<string, PendingSecureWrite>();
|
|
135
162
|
let secureFlushScheduled = false;
|
|
136
163
|
let secureDefaultAccessControl: AccessControl = AccessControl.WhenUnlocked;
|
|
@@ -139,6 +166,7 @@ const metricsCounters = new Map<
|
|
|
139
166
|
string,
|
|
140
167
|
{ count: number; totalDurationMs: number; maxDurationMs: number }
|
|
141
168
|
>();
|
|
169
|
+
const nativeSecureBackend = "platform-secure-storage";
|
|
142
170
|
|
|
143
171
|
function recordMetric(
|
|
144
172
|
operation: string,
|
|
@@ -176,11 +204,11 @@ function measureOperation<T>(
|
|
|
176
204
|
if (!metricsObserver) {
|
|
177
205
|
return fn();
|
|
178
206
|
}
|
|
179
|
-
const start =
|
|
207
|
+
const start = now();
|
|
180
208
|
try {
|
|
181
209
|
return fn();
|
|
182
210
|
} finally {
|
|
183
|
-
recordMetric(operation, scope,
|
|
211
|
+
recordMetric(operation, scope, now() - start, keysCount);
|
|
184
212
|
}
|
|
185
213
|
}
|
|
186
214
|
|
|
@@ -262,14 +290,59 @@ function readPendingSecureWrite(key: string): string | undefined {
|
|
|
262
290
|
return pendingSecureWrites.get(key)?.value;
|
|
263
291
|
}
|
|
264
292
|
|
|
293
|
+
function readPendingDiskWrite(key: string): string | undefined {
|
|
294
|
+
return pendingDiskWrites.get(key)?.value;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function hasPendingDiskWrite(key: string): boolean {
|
|
298
|
+
return pendingDiskWrites.has(key);
|
|
299
|
+
}
|
|
300
|
+
|
|
265
301
|
function hasPendingSecureWrite(key: string): boolean {
|
|
266
302
|
return pendingSecureWrites.has(key);
|
|
267
303
|
}
|
|
268
304
|
|
|
305
|
+
function clearPendingDiskWrite(key: string): void {
|
|
306
|
+
pendingDiskWrites.delete(key);
|
|
307
|
+
}
|
|
308
|
+
|
|
269
309
|
function clearPendingSecureWrite(key: string): void {
|
|
270
310
|
pendingSecureWrites.delete(key);
|
|
271
311
|
}
|
|
272
312
|
|
|
313
|
+
function flushDiskWrites(): void {
|
|
314
|
+
diskFlushScheduled = false;
|
|
315
|
+
|
|
316
|
+
if (pendingDiskWrites.size === 0) {
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const writes = Array.from(pendingDiskWrites.values());
|
|
321
|
+
pendingDiskWrites.clear();
|
|
322
|
+
|
|
323
|
+
const keysToSet: string[] = [];
|
|
324
|
+
const valuesToSet: string[] = [];
|
|
325
|
+
const keysToRemove: string[] = [];
|
|
326
|
+
|
|
327
|
+
writes.forEach(({ key, value }) => {
|
|
328
|
+
if (value === undefined) {
|
|
329
|
+
keysToRemove.push(key);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
keysToSet.push(key);
|
|
334
|
+
valuesToSet.push(value);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
const storageModule = getStorageModule();
|
|
338
|
+
if (keysToSet.length > 0) {
|
|
339
|
+
storageModule.setBatch(keysToSet, valuesToSet, StorageScope.Disk);
|
|
340
|
+
}
|
|
341
|
+
if (keysToRemove.length > 0) {
|
|
342
|
+
storageModule.removeBatch(keysToRemove, StorageScope.Disk);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
273
346
|
function flushSecureWrites(): void {
|
|
274
347
|
secureFlushScheduled = false;
|
|
275
348
|
|
|
@@ -311,6 +384,15 @@ function flushSecureWrites(): void {
|
|
|
311
384
|
}
|
|
312
385
|
}
|
|
313
386
|
|
|
387
|
+
function scheduleDiskWrite(key: string, value: string | undefined): void {
|
|
388
|
+
pendingDiskWrites.set(key, { key, value });
|
|
389
|
+
if (diskFlushScheduled) {
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
diskFlushScheduled = true;
|
|
393
|
+
runMicrotask(flushDiskWrites);
|
|
394
|
+
}
|
|
395
|
+
|
|
314
396
|
function scheduleSecureWrite(
|
|
315
397
|
key: string,
|
|
316
398
|
value: string | undefined,
|
|
@@ -334,6 +416,14 @@ function ensureNativeScopeSubscription(scope: NonMemoryScope): void {
|
|
|
334
416
|
}
|
|
335
417
|
|
|
336
418
|
const unsubscribe = getStorageModule().addOnChange(scope, (key, value) => {
|
|
419
|
+
if (scope === StorageScope.Disk) {
|
|
420
|
+
if (key === "") {
|
|
421
|
+
pendingDiskWrites.clear();
|
|
422
|
+
} else {
|
|
423
|
+
clearPendingDiskWrite(key);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
337
427
|
if (scope === StorageScope.Secure) {
|
|
338
428
|
if (key === "") {
|
|
339
429
|
pendingSecureWrites.clear();
|
|
@@ -376,6 +466,10 @@ function getRawValue(key: string, scope: StorageScope): string | undefined {
|
|
|
376
466
|
return typeof value === "string" ? value : undefined;
|
|
377
467
|
}
|
|
378
468
|
|
|
469
|
+
if (scope === StorageScope.Disk && hasPendingDiskWrite(key)) {
|
|
470
|
+
return readPendingDiskWrite(key);
|
|
471
|
+
}
|
|
472
|
+
|
|
379
473
|
if (scope === StorageScope.Secure && hasPendingSecureWrite(key)) {
|
|
380
474
|
return readPendingSecureWrite(key);
|
|
381
475
|
}
|
|
@@ -391,6 +485,17 @@ function setRawValue(key: string, value: string, scope: StorageScope): void {
|
|
|
391
485
|
return;
|
|
392
486
|
}
|
|
393
487
|
|
|
488
|
+
if (scope === StorageScope.Disk) {
|
|
489
|
+
cacheRawValue(scope, key, value);
|
|
490
|
+
if (diskWritesAsync) {
|
|
491
|
+
scheduleDiskWrite(key, value);
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
flushDiskWrites();
|
|
496
|
+
clearPendingDiskWrite(key);
|
|
497
|
+
}
|
|
498
|
+
|
|
394
499
|
if (scope === StorageScope.Secure) {
|
|
395
500
|
flushSecureWrites();
|
|
396
501
|
clearPendingSecureWrite(key);
|
|
@@ -409,6 +514,17 @@ function removeRawValue(key: string, scope: StorageScope): void {
|
|
|
409
514
|
return;
|
|
410
515
|
}
|
|
411
516
|
|
|
517
|
+
if (scope === StorageScope.Disk) {
|
|
518
|
+
cacheRawValue(scope, key, undefined);
|
|
519
|
+
if (diskWritesAsync) {
|
|
520
|
+
scheduleDiskWrite(key, undefined);
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
flushDiskWrites();
|
|
525
|
+
clearPendingDiskWrite(key);
|
|
526
|
+
}
|
|
527
|
+
|
|
412
528
|
if (scope === StorageScope.Secure) {
|
|
413
529
|
flushSecureWrites();
|
|
414
530
|
clearPendingSecureWrite(key);
|
|
@@ -441,6 +557,11 @@ export const storage = {
|
|
|
441
557
|
return;
|
|
442
558
|
}
|
|
443
559
|
|
|
560
|
+
if (scope === StorageScope.Disk) {
|
|
561
|
+
flushDiskWrites();
|
|
562
|
+
pendingDiskWrites.clear();
|
|
563
|
+
}
|
|
564
|
+
|
|
444
565
|
if (scope === StorageScope.Secure) {
|
|
445
566
|
flushSecureWrites();
|
|
446
567
|
pendingSecureWrites.clear();
|
|
@@ -476,6 +597,9 @@ export const storage = {
|
|
|
476
597
|
}
|
|
477
598
|
|
|
478
599
|
const keyPrefix = prefixKey(namespace, "");
|
|
600
|
+
if (scope === StorageScope.Disk) {
|
|
601
|
+
flushDiskWrites();
|
|
602
|
+
}
|
|
479
603
|
if (scope === StorageScope.Secure) {
|
|
480
604
|
flushSecureWrites();
|
|
481
605
|
}
|
|
@@ -500,6 +624,12 @@ export const storage = {
|
|
|
500
624
|
if (scope === StorageScope.Memory) {
|
|
501
625
|
return memoryStore.has(key);
|
|
502
626
|
}
|
|
627
|
+
if (scope === StorageScope.Disk) {
|
|
628
|
+
flushDiskWrites();
|
|
629
|
+
}
|
|
630
|
+
if (scope === StorageScope.Secure) {
|
|
631
|
+
flushSecureWrites();
|
|
632
|
+
}
|
|
503
633
|
return getStorageModule().has(key, scope);
|
|
504
634
|
});
|
|
505
635
|
},
|
|
@@ -509,6 +639,12 @@ export const storage = {
|
|
|
509
639
|
if (scope === StorageScope.Memory) {
|
|
510
640
|
return Array.from(memoryStore.keys());
|
|
511
641
|
}
|
|
642
|
+
if (scope === StorageScope.Disk) {
|
|
643
|
+
flushDiskWrites();
|
|
644
|
+
}
|
|
645
|
+
if (scope === StorageScope.Secure) {
|
|
646
|
+
flushSecureWrites();
|
|
647
|
+
}
|
|
512
648
|
return getStorageModule().getAllKeys(scope);
|
|
513
649
|
});
|
|
514
650
|
},
|
|
@@ -520,6 +656,12 @@ export const storage = {
|
|
|
520
656
|
key.startsWith(prefix),
|
|
521
657
|
);
|
|
522
658
|
}
|
|
659
|
+
if (scope === StorageScope.Disk) {
|
|
660
|
+
flushDiskWrites();
|
|
661
|
+
}
|
|
662
|
+
if (scope === StorageScope.Secure) {
|
|
663
|
+
flushSecureWrites();
|
|
664
|
+
}
|
|
523
665
|
return getStorageModule().getKeysByPrefix(prefix, scope);
|
|
524
666
|
});
|
|
525
667
|
},
|
|
@@ -544,6 +686,12 @@ export const storage = {
|
|
|
544
686
|
return result;
|
|
545
687
|
}
|
|
546
688
|
|
|
689
|
+
if (scope === StorageScope.Disk) {
|
|
690
|
+
flushDiskWrites();
|
|
691
|
+
}
|
|
692
|
+
if (scope === StorageScope.Secure) {
|
|
693
|
+
flushSecureWrites();
|
|
694
|
+
}
|
|
547
695
|
const values = getStorageModule().getBatch(keys, scope);
|
|
548
696
|
keys.forEach((key, idx) => {
|
|
549
697
|
const value = decodeNativeBatchValue(values[idx]);
|
|
@@ -564,6 +712,12 @@ export const storage = {
|
|
|
564
712
|
});
|
|
565
713
|
return result;
|
|
566
714
|
}
|
|
715
|
+
if (scope === StorageScope.Disk) {
|
|
716
|
+
flushDiskWrites();
|
|
717
|
+
}
|
|
718
|
+
if (scope === StorageScope.Secure) {
|
|
719
|
+
flushSecureWrites();
|
|
720
|
+
}
|
|
567
721
|
const keys = getStorageModule().getAllKeys(scope);
|
|
568
722
|
if (keys.length === 0) return result;
|
|
569
723
|
const values = getStorageModule().getBatch(keys, scope);
|
|
@@ -580,6 +734,12 @@ export const storage = {
|
|
|
580
734
|
if (scope === StorageScope.Memory) {
|
|
581
735
|
return memoryStore.size;
|
|
582
736
|
}
|
|
737
|
+
if (scope === StorageScope.Disk) {
|
|
738
|
+
flushDiskWrites();
|
|
739
|
+
}
|
|
740
|
+
if (scope === StorageScope.Secure) {
|
|
741
|
+
flushSecureWrites();
|
|
742
|
+
}
|
|
583
743
|
return getStorageModule().size(scope);
|
|
584
744
|
});
|
|
585
745
|
},
|
|
@@ -598,6 +758,19 @@ export const storage = {
|
|
|
598
758
|
},
|
|
599
759
|
);
|
|
600
760
|
},
|
|
761
|
+
setDiskWritesAsync: (enabled: boolean) => {
|
|
762
|
+
measureOperation("storage:setDiskWritesAsync", StorageScope.Disk, () => {
|
|
763
|
+
diskWritesAsync = enabled;
|
|
764
|
+
if (!enabled) {
|
|
765
|
+
flushDiskWrites();
|
|
766
|
+
}
|
|
767
|
+
});
|
|
768
|
+
},
|
|
769
|
+
flushDiskWrites: () => {
|
|
770
|
+
measureOperation("storage:flushDiskWrites", StorageScope.Disk, () => {
|
|
771
|
+
flushDiskWrites();
|
|
772
|
+
});
|
|
773
|
+
},
|
|
601
774
|
flushSecureWrites: () => {
|
|
602
775
|
measureOperation("storage:flushSecureWrites", StorageScope.Secure, () => {
|
|
603
776
|
flushSecureWrites();
|
|
@@ -631,6 +804,79 @@ export const storage = {
|
|
|
631
804
|
resetMetrics: () => {
|
|
632
805
|
metricsCounters.clear();
|
|
633
806
|
},
|
|
807
|
+
getCapabilities: (): StorageCapabilities => ({
|
|
808
|
+
platform: "native",
|
|
809
|
+
backend: {
|
|
810
|
+
disk: "platform-preferences",
|
|
811
|
+
secure: nativeSecureBackend,
|
|
812
|
+
},
|
|
813
|
+
writeBuffering: {
|
|
814
|
+
disk: true,
|
|
815
|
+
secure: true,
|
|
816
|
+
},
|
|
817
|
+
errorClassification: true,
|
|
818
|
+
}),
|
|
819
|
+
getSecurityCapabilities: (): SecurityCapabilities => ({
|
|
820
|
+
platform: "native",
|
|
821
|
+
secureStorage: {
|
|
822
|
+
backend: nativeSecureBackend,
|
|
823
|
+
encrypted: "available",
|
|
824
|
+
accessControl: "unknown",
|
|
825
|
+
keychainAccessGroup: "unknown",
|
|
826
|
+
hardwareBacked: "unknown",
|
|
827
|
+
},
|
|
828
|
+
biometric: {
|
|
829
|
+
storage: "unknown",
|
|
830
|
+
prompt: "unknown",
|
|
831
|
+
biometryOnly: "unknown",
|
|
832
|
+
biometryOrPasscode: "unknown",
|
|
833
|
+
},
|
|
834
|
+
metadata: {
|
|
835
|
+
perKey: true,
|
|
836
|
+
listsWithoutValues: true,
|
|
837
|
+
persistsTimestamps: false,
|
|
838
|
+
},
|
|
839
|
+
}),
|
|
840
|
+
getSecureMetadata: (key: string): SecureStorageMetadata => {
|
|
841
|
+
return measureOperation(
|
|
842
|
+
"storage:getSecureMetadata",
|
|
843
|
+
StorageScope.Secure,
|
|
844
|
+
() => {
|
|
845
|
+
flushSecureWrites();
|
|
846
|
+
const storageModule = getStorageModule();
|
|
847
|
+
const biometricProtected = storageModule.hasSecureBiometric(key);
|
|
848
|
+
const exists =
|
|
849
|
+
biometricProtected || storageModule.has(key, StorageScope.Secure);
|
|
850
|
+
let kind: SecureStorageMetadata["kind"] = "missing";
|
|
851
|
+
if (exists) {
|
|
852
|
+
kind = biometricProtected ? "biometric" : "secure";
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
return {
|
|
856
|
+
key,
|
|
857
|
+
exists,
|
|
858
|
+
kind,
|
|
859
|
+
backend: nativeSecureBackend,
|
|
860
|
+
encrypted: "available",
|
|
861
|
+
hardwareBacked: "unknown",
|
|
862
|
+
biometricProtected,
|
|
863
|
+
valueExposed: false,
|
|
864
|
+
};
|
|
865
|
+
},
|
|
866
|
+
);
|
|
867
|
+
},
|
|
868
|
+
getAllSecureMetadata: (): SecureStorageMetadata[] => {
|
|
869
|
+
return measureOperation(
|
|
870
|
+
"storage:getAllSecureMetadata",
|
|
871
|
+
StorageScope.Secure,
|
|
872
|
+
() => {
|
|
873
|
+
flushSecureWrites();
|
|
874
|
+
return getStorageModule()
|
|
875
|
+
.getAllKeys(StorageScope.Secure)
|
|
876
|
+
.map((key) => storage.getSecureMetadata(key));
|
|
877
|
+
},
|
|
878
|
+
);
|
|
879
|
+
},
|
|
634
880
|
getString: (key: string, scope: StorageScope): string | undefined => {
|
|
635
881
|
return measureOperation("storage:getString", scope, () => {
|
|
636
882
|
return getRawValue(key, scope);
|
|
@@ -689,6 +935,20 @@ export function getWebSecureStorageBackend():
|
|
|
689
935
|
return undefined;
|
|
690
936
|
}
|
|
691
937
|
|
|
938
|
+
export function setWebDiskStorageBackend(
|
|
939
|
+
_backend?: WebDiskStorageBackend,
|
|
940
|
+
): void {
|
|
941
|
+
// Native platforms do not use web disk backends.
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
export function getWebDiskStorageBackend(): WebDiskStorageBackend | undefined {
|
|
945
|
+
return undefined;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
export async function flushWebStorageBackends(): Promise<void> {
|
|
949
|
+
// Native platforms do not use web storage backends.
|
|
950
|
+
}
|
|
951
|
+
|
|
692
952
|
export interface StorageItemConfig<T> {
|
|
693
953
|
key: string;
|
|
694
954
|
scope: StorageScope;
|
|
@@ -700,6 +960,7 @@ export interface StorageItemConfig<T> {
|
|
|
700
960
|
expiration?: ExpirationConfig;
|
|
701
961
|
onExpired?: (key: string) => void;
|
|
702
962
|
readCache?: boolean;
|
|
963
|
+
coalesceDiskWrites?: boolean;
|
|
703
964
|
coalesceSecureWrites?: boolean;
|
|
704
965
|
namespace?: string;
|
|
705
966
|
biometric?: boolean;
|
|
@@ -784,6 +1045,8 @@ export function createStorageItem<T = undefined>(
|
|
|
784
1045
|
const memoryExpiration =
|
|
785
1046
|
expiration && isMemory ? new Map<string, number>() : null;
|
|
786
1047
|
const readCache = !isMemory && config.readCache === true;
|
|
1048
|
+
const coalesceDiskWrites =
|
|
1049
|
+
config.scope === StorageScope.Disk && config.coalesceDiskWrites === true;
|
|
787
1050
|
const coalesceSecureWrites =
|
|
788
1051
|
config.scope === StorageScope.Secure &&
|
|
789
1052
|
config.coalesceSecureWrites === true &&
|
|
@@ -852,6 +1115,13 @@ export function createStorageItem<T = undefined>(
|
|
|
852
1115
|
return memoryStore.get(storageKey);
|
|
853
1116
|
}
|
|
854
1117
|
|
|
1118
|
+
if (nonMemoryScope === StorageScope.Disk) {
|
|
1119
|
+
const pending = pendingDiskWrites.get(storageKey);
|
|
1120
|
+
if (pending !== undefined) {
|
|
1121
|
+
return pending.value;
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
|
|
855
1125
|
if (nonMemoryScope === StorageScope.Secure && !isBiometric) {
|
|
856
1126
|
const pending = pendingSecureWrites.get(storageKey);
|
|
857
1127
|
if (pending !== undefined) {
|
|
@@ -888,6 +1158,15 @@ export function createStorageItem<T = undefined>(
|
|
|
888
1158
|
|
|
889
1159
|
cacheRawValue(nonMemoryScope!, storageKey, rawValue);
|
|
890
1160
|
|
|
1161
|
+
if (nonMemoryScope === StorageScope.Disk) {
|
|
1162
|
+
if (coalesceDiskWrites || diskWritesAsync) {
|
|
1163
|
+
scheduleDiskWrite(storageKey, rawValue);
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
clearPendingDiskWrite(storageKey);
|
|
1168
|
+
}
|
|
1169
|
+
|
|
891
1170
|
if (coalesceSecureWrites) {
|
|
892
1171
|
scheduleSecureWrite(
|
|
893
1172
|
storageKey,
|
|
@@ -915,6 +1194,15 @@ export function createStorageItem<T = undefined>(
|
|
|
915
1194
|
|
|
916
1195
|
cacheRawValue(nonMemoryScope!, storageKey, undefined);
|
|
917
1196
|
|
|
1197
|
+
if (nonMemoryScope === StorageScope.Disk) {
|
|
1198
|
+
if (coalesceDiskWrites || diskWritesAsync) {
|
|
1199
|
+
scheduleDiskWrite(storageKey, undefined);
|
|
1200
|
+
return;
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
clearPendingDiskWrite(storageKey);
|
|
1204
|
+
}
|
|
1205
|
+
|
|
918
1206
|
if (coalesceSecureWrites) {
|
|
919
1207
|
scheduleSecureWrite(
|
|
920
1208
|
storageKey,
|
|
@@ -1125,6 +1413,18 @@ export function createStorageItem<T = undefined>(
|
|
|
1125
1413
|
measureOperation("item:has", config.scope, () => {
|
|
1126
1414
|
if (isMemory) return memoryStore.has(storageKey);
|
|
1127
1415
|
if (isBiometric) return getStorageModule().hasSecureBiometric(storageKey);
|
|
1416
|
+
if (nonMemoryScope === StorageScope.Disk) {
|
|
1417
|
+
const pending = pendingDiskWrites.get(storageKey);
|
|
1418
|
+
if (pending !== undefined) {
|
|
1419
|
+
return pending.value !== undefined;
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
if (nonMemoryScope === StorageScope.Secure) {
|
|
1423
|
+
const pending = pendingSecureWrites.get(storageKey);
|
|
1424
|
+
if (pending !== undefined) {
|
|
1425
|
+
return pending.value !== undefined;
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1128
1428
|
return getStorageModule().has(storageKey, config.scope);
|
|
1129
1429
|
});
|
|
1130
1430
|
|
|
@@ -1224,6 +1524,14 @@ export function getBatch(
|
|
|
1224
1524
|
const keyIndexes: number[] = [];
|
|
1225
1525
|
|
|
1226
1526
|
items.forEach((item, index) => {
|
|
1527
|
+
if (scope === StorageScope.Disk) {
|
|
1528
|
+
const pending = pendingDiskWrites.get(item.key);
|
|
1529
|
+
if (pending !== undefined) {
|
|
1530
|
+
rawValues[index] = pending.value;
|
|
1531
|
+
return;
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1227
1535
|
if (scope === StorageScope.Secure) {
|
|
1228
1536
|
const pending = pendingSecureWrites.get(item.key);
|
|
1229
1537
|
if (pending !== undefined) {
|
|
@@ -1353,6 +1661,8 @@ export function setBatch<T>(
|
|
|
1353
1661
|
return;
|
|
1354
1662
|
}
|
|
1355
1663
|
|
|
1664
|
+
flushDiskWrites();
|
|
1665
|
+
|
|
1356
1666
|
const useRawBatchPath = items.every(({ item }) =>
|
|
1357
1667
|
canUseRawBatchPath(asInternal(item)),
|
|
1358
1668
|
);
|
|
@@ -1387,6 +1697,9 @@ export function removeBatch(
|
|
|
1387
1697
|
}
|
|
1388
1698
|
|
|
1389
1699
|
const keys = items.map((item) => item.key);
|
|
1700
|
+
if (scope === StorageScope.Disk) {
|
|
1701
|
+
flushDiskWrites();
|
|
1702
|
+
}
|
|
1390
1703
|
if (scope === StorageScope.Secure) {
|
|
1391
1704
|
flushSecureWrites();
|
|
1392
1705
|
}
|
|
@@ -1450,6 +1763,9 @@ export function runTransaction<T>(
|
|
|
1450
1763
|
): T {
|
|
1451
1764
|
return measureOperation("transaction:run", scope, () => {
|
|
1452
1765
|
assertValidScope(scope);
|
|
1766
|
+
if (scope === StorageScope.Disk) {
|
|
1767
|
+
flushDiskWrites();
|
|
1768
|
+
}
|
|
1453
1769
|
if (scope === StorageScope.Secure) {
|
|
1454
1770
|
flushSecureWrites();
|
|
1455
1771
|
}
|
|
@@ -1525,6 +1841,9 @@ export function runTransaction<T>(
|
|
|
1525
1841
|
}
|
|
1526
1842
|
});
|
|
1527
1843
|
|
|
1844
|
+
if (scope === StorageScope.Disk) {
|
|
1845
|
+
flushDiskWrites();
|
|
1846
|
+
}
|
|
1528
1847
|
if (scope === StorageScope.Secure) {
|
|
1529
1848
|
flushSecureWrites();
|
|
1530
1849
|
}
|
|
@@ -1555,16 +1874,7 @@ export type SecureAuthStorageConfig<K extends string = string> = Record<
|
|
|
1555
1874
|
>;
|
|
1556
1875
|
|
|
1557
1876
|
export function isKeychainLockedError(err: unknown): boolean {
|
|
1558
|
-
|
|
1559
|
-
const msg = err.message;
|
|
1560
|
-
return (
|
|
1561
|
-
msg.includes("errSecInteractionNotAllowed") ||
|
|
1562
|
-
msg.includes("UserNotAuthenticatedException") ||
|
|
1563
|
-
msg.includes("KeyStoreException") ||
|
|
1564
|
-
msg.includes("KeyPermanentlyInvalidatedException") ||
|
|
1565
|
-
msg.includes("InvalidKeyException") ||
|
|
1566
|
-
msg.includes("android.security.keystore")
|
|
1567
|
-
);
|
|
1877
|
+
return isLockedStorageErrorCode(getStorageErrorCode(err));
|
|
1568
1878
|
}
|
|
1569
1879
|
|
|
1570
1880
|
export function createSecureAuthStorage<K extends string>(
|