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.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
decodeNativeBatchValue,
|
|
11
11
|
serializeWithPrimitiveFastPath,
|
|
12
12
|
deserializeWithPrimitiveFastPath,
|
|
13
|
+
toVersionToken,
|
|
13
14
|
prefixKey,
|
|
14
15
|
isNamespaced,
|
|
15
16
|
} from "./internal";
|
|
@@ -22,6 +23,31 @@ export type Validator<T> = (value: unknown) => value is T;
|
|
|
22
23
|
export type ExpirationConfig = {
|
|
23
24
|
ttlMs: number;
|
|
24
25
|
};
|
|
26
|
+
export type StorageVersion = string;
|
|
27
|
+
export type VersionedValue<T> = {
|
|
28
|
+
value: T;
|
|
29
|
+
version: StorageVersion;
|
|
30
|
+
};
|
|
31
|
+
export type StorageMetricsEvent = {
|
|
32
|
+
operation: string;
|
|
33
|
+
scope: StorageScope;
|
|
34
|
+
durationMs: number;
|
|
35
|
+
keysCount: number;
|
|
36
|
+
};
|
|
37
|
+
export type StorageMetricsObserver = (event: StorageMetricsEvent) => void;
|
|
38
|
+
export type StorageMetricSummary = {
|
|
39
|
+
count: number;
|
|
40
|
+
totalDurationMs: number;
|
|
41
|
+
avgDurationMs: number;
|
|
42
|
+
maxDurationMs: number;
|
|
43
|
+
};
|
|
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
|
+
};
|
|
25
51
|
|
|
26
52
|
export type MigrationContext = {
|
|
27
53
|
scope: StorageScope;
|
|
@@ -69,7 +95,11 @@ function typedKeys<K extends string, V>(record: Record<K, V>): K[] {
|
|
|
69
95
|
return Object.keys(record) as K[];
|
|
70
96
|
}
|
|
71
97
|
type NonMemoryScope = StorageScope.Disk | StorageScope.Secure;
|
|
72
|
-
type PendingSecureWrite = {
|
|
98
|
+
type PendingSecureWrite = {
|
|
99
|
+
key: string;
|
|
100
|
+
value: string | undefined;
|
|
101
|
+
accessControl?: AccessControl;
|
|
102
|
+
};
|
|
73
103
|
|
|
74
104
|
const registeredMigrations = new Map<number, Migration>();
|
|
75
105
|
const runMicrotask =
|
|
@@ -104,6 +134,52 @@ const scopedRawCache = new Map<NonMemoryScope, Map<string, string | undefined>>(
|
|
|
104
134
|
const pendingSecureWrites = new Map<string, PendingSecureWrite>();
|
|
105
135
|
let secureFlushScheduled = false;
|
|
106
136
|
let secureDefaultAccessControl: AccessControl = AccessControl.WhenUnlocked;
|
|
137
|
+
let metricsObserver: StorageMetricsObserver | undefined;
|
|
138
|
+
const metricsCounters = new Map<
|
|
139
|
+
string,
|
|
140
|
+
{ count: number; totalDurationMs: number; maxDurationMs: number }
|
|
141
|
+
>();
|
|
142
|
+
|
|
143
|
+
function recordMetric(
|
|
144
|
+
operation: string,
|
|
145
|
+
scope: StorageScope,
|
|
146
|
+
durationMs: number,
|
|
147
|
+
keysCount = 1,
|
|
148
|
+
): void {
|
|
149
|
+
const existing = metricsCounters.get(operation);
|
|
150
|
+
if (!existing) {
|
|
151
|
+
metricsCounters.set(operation, {
|
|
152
|
+
count: 1,
|
|
153
|
+
totalDurationMs: durationMs,
|
|
154
|
+
maxDurationMs: durationMs,
|
|
155
|
+
});
|
|
156
|
+
} else {
|
|
157
|
+
existing.count += 1;
|
|
158
|
+
existing.totalDurationMs += durationMs;
|
|
159
|
+
existing.maxDurationMs = Math.max(existing.maxDurationMs, durationMs);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
metricsObserver?.({
|
|
163
|
+
operation,
|
|
164
|
+
scope,
|
|
165
|
+
durationMs,
|
|
166
|
+
keysCount,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function measureOperation<T>(
|
|
171
|
+
operation: string,
|
|
172
|
+
scope: StorageScope,
|
|
173
|
+
fn: () => T,
|
|
174
|
+
keysCount = 1,
|
|
175
|
+
): T {
|
|
176
|
+
const start = Date.now();
|
|
177
|
+
try {
|
|
178
|
+
return fn();
|
|
179
|
+
} finally {
|
|
180
|
+
recordMetric(operation, scope, Date.now() - start, keysCount);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
107
183
|
|
|
108
184
|
function getScopedListeners(scope: NonMemoryScope): KeyListenerRegistry {
|
|
109
185
|
return scopedListeners.get(scope)!;
|
|
@@ -194,31 +270,47 @@ function flushSecureWrites(): void {
|
|
|
194
270
|
const writes = Array.from(pendingSecureWrites.values());
|
|
195
271
|
pendingSecureWrites.clear();
|
|
196
272
|
|
|
197
|
-
const
|
|
198
|
-
|
|
273
|
+
const groupedSetWrites = new Map<
|
|
274
|
+
AccessControl,
|
|
275
|
+
{ keys: string[]; values: string[] }
|
|
276
|
+
>();
|
|
199
277
|
const keysToRemove: string[] = [];
|
|
200
278
|
|
|
201
|
-
writes.forEach(({ key, value }) => {
|
|
279
|
+
writes.forEach(({ key, value, accessControl }) => {
|
|
202
280
|
if (value === undefined) {
|
|
203
281
|
keysToRemove.push(key);
|
|
204
282
|
} else {
|
|
205
|
-
|
|
206
|
-
|
|
283
|
+
const resolvedAccessControl = accessControl ?? secureDefaultAccessControl;
|
|
284
|
+
const existingGroup = groupedSetWrites.get(resolvedAccessControl);
|
|
285
|
+
const group = existingGroup ?? { keys: [], values: [] };
|
|
286
|
+
group.keys.push(key);
|
|
287
|
+
group.values.push(value);
|
|
288
|
+
if (!existingGroup) {
|
|
289
|
+
groupedSetWrites.set(resolvedAccessControl, group);
|
|
290
|
+
}
|
|
207
291
|
}
|
|
208
292
|
});
|
|
209
293
|
|
|
210
294
|
const storageModule = getStorageModule();
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
storageModule.setBatch(
|
|
214
|
-
}
|
|
295
|
+
groupedSetWrites.forEach((group, accessControl) => {
|
|
296
|
+
storageModule.setSecureAccessControl(accessControl);
|
|
297
|
+
storageModule.setBatch(group.keys, group.values, StorageScope.Secure);
|
|
298
|
+
});
|
|
215
299
|
if (keysToRemove.length > 0) {
|
|
216
300
|
storageModule.removeBatch(keysToRemove, StorageScope.Secure);
|
|
217
301
|
}
|
|
218
302
|
}
|
|
219
303
|
|
|
220
|
-
function scheduleSecureWrite(
|
|
221
|
-
|
|
304
|
+
function scheduleSecureWrite(
|
|
305
|
+
key: string,
|
|
306
|
+
value: string | undefined,
|
|
307
|
+
accessControl?: AccessControl,
|
|
308
|
+
): void {
|
|
309
|
+
const pendingWrite: PendingSecureWrite = { key, value };
|
|
310
|
+
if (accessControl !== undefined) {
|
|
311
|
+
pendingWrite.accessControl = accessControl;
|
|
312
|
+
}
|
|
313
|
+
pendingSecureWrites.set(key, pendingWrite);
|
|
222
314
|
if (secureFlushScheduled) {
|
|
223
315
|
return;
|
|
224
316
|
}
|
|
@@ -332,102 +424,241 @@ function writeMigrationVersion(scope: StorageScope, version: number): void {
|
|
|
332
424
|
|
|
333
425
|
export const storage = {
|
|
334
426
|
clear: (scope: StorageScope) => {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
427
|
+
measureOperation("storage:clear", scope, () => {
|
|
428
|
+
if (scope === StorageScope.Memory) {
|
|
429
|
+
memoryStore.clear();
|
|
430
|
+
notifyAllListeners(memoryListeners);
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
340
433
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
434
|
+
if (scope === StorageScope.Secure) {
|
|
435
|
+
flushSecureWrites();
|
|
436
|
+
pendingSecureWrites.clear();
|
|
437
|
+
}
|
|
345
438
|
|
|
346
|
-
|
|
347
|
-
|
|
439
|
+
clearScopeRawCache(scope);
|
|
440
|
+
getStorageModule().clear(scope);
|
|
441
|
+
});
|
|
348
442
|
},
|
|
349
443
|
clearAll: () => {
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
444
|
+
measureOperation(
|
|
445
|
+
"storage:clearAll",
|
|
446
|
+
StorageScope.Memory,
|
|
447
|
+
() => {
|
|
448
|
+
storage.clear(StorageScope.Memory);
|
|
449
|
+
storage.clear(StorageScope.Disk);
|
|
450
|
+
storage.clear(StorageScope.Secure);
|
|
451
|
+
},
|
|
452
|
+
3,
|
|
453
|
+
);
|
|
353
454
|
},
|
|
354
455
|
clearNamespace: (namespace: string, scope: StorageScope) => {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
456
|
+
measureOperation("storage:clearNamespace", scope, () => {
|
|
457
|
+
assertValidScope(scope);
|
|
458
|
+
if (scope === StorageScope.Memory) {
|
|
459
|
+
for (const key of memoryStore.keys()) {
|
|
460
|
+
if (isNamespaced(key, namespace)) {
|
|
461
|
+
memoryStore.delete(key);
|
|
462
|
+
}
|
|
360
463
|
}
|
|
464
|
+
notifyAllListeners(memoryListeners);
|
|
465
|
+
return;
|
|
361
466
|
}
|
|
362
|
-
notifyAllListeners(memoryListeners);
|
|
363
|
-
return;
|
|
364
|
-
}
|
|
365
467
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
468
|
+
const keyPrefix = prefixKey(namespace, "");
|
|
469
|
+
if (scope === StorageScope.Secure) {
|
|
470
|
+
flushSecureWrites();
|
|
471
|
+
}
|
|
370
472
|
|
|
371
|
-
|
|
372
|
-
|
|
473
|
+
clearScopeRawCache(scope);
|
|
474
|
+
getStorageModule().removeByPrefix(keyPrefix, scope);
|
|
475
|
+
});
|
|
373
476
|
},
|
|
374
477
|
clearBiometric: () => {
|
|
375
|
-
|
|
478
|
+
measureOperation("storage:clearBiometric", StorageScope.Secure, () => {
|
|
479
|
+
getStorageModule().clearSecureBiometric();
|
|
480
|
+
});
|
|
376
481
|
},
|
|
377
482
|
has: (key: string, scope: StorageScope): boolean => {
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
483
|
+
return measureOperation("storage:has", scope, () => {
|
|
484
|
+
assertValidScope(scope);
|
|
485
|
+
if (scope === StorageScope.Memory) {
|
|
486
|
+
return memoryStore.has(key);
|
|
487
|
+
}
|
|
488
|
+
return getStorageModule().has(key, scope);
|
|
489
|
+
});
|
|
383
490
|
},
|
|
384
491
|
getAllKeys: (scope: StorageScope): string[] => {
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
492
|
+
return measureOperation("storage:getAllKeys", scope, () => {
|
|
493
|
+
assertValidScope(scope);
|
|
494
|
+
if (scope === StorageScope.Memory) {
|
|
495
|
+
return Array.from(memoryStore.keys());
|
|
496
|
+
}
|
|
497
|
+
return getStorageModule().getAllKeys(scope);
|
|
498
|
+
});
|
|
499
|
+
},
|
|
500
|
+
getKeysByPrefix: (prefix: string, scope: StorageScope): string[] => {
|
|
501
|
+
return measureOperation("storage:getKeysByPrefix", scope, () => {
|
|
502
|
+
assertValidScope(scope);
|
|
503
|
+
if (scope === StorageScope.Memory) {
|
|
504
|
+
return Array.from(memoryStore.keys()).filter((key) =>
|
|
505
|
+
key.startsWith(prefix),
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
return getStorageModule().getKeysByPrefix(prefix, scope);
|
|
509
|
+
});
|
|
510
|
+
},
|
|
511
|
+
getByPrefix: (
|
|
512
|
+
prefix: string,
|
|
513
|
+
scope: StorageScope,
|
|
514
|
+
): Record<string, string> => {
|
|
515
|
+
return measureOperation("storage:getByPrefix", scope, () => {
|
|
516
|
+
const result: Record<string, string> = {};
|
|
517
|
+
const keys = storage.getKeysByPrefix(prefix, scope);
|
|
518
|
+
if (keys.length === 0) {
|
|
519
|
+
return result;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (scope === StorageScope.Memory) {
|
|
523
|
+
keys.forEach((key) => {
|
|
524
|
+
const value = memoryStore.get(key);
|
|
525
|
+
if (typeof value === "string") {
|
|
526
|
+
result[key] = value;
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
return result;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const values = getStorageModule().getBatch(keys, scope);
|
|
533
|
+
keys.forEach((key, idx) => {
|
|
534
|
+
const value = decodeNativeBatchValue(values[idx]);
|
|
535
|
+
if (value !== undefined) {
|
|
536
|
+
result[key] = value;
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
return result;
|
|
540
|
+
});
|
|
390
541
|
},
|
|
391
542
|
getAll: (scope: StorageScope): Record<string, string> => {
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
543
|
+
return measureOperation("storage:getAll", scope, () => {
|
|
544
|
+
assertValidScope(scope);
|
|
545
|
+
const result: Record<string, string> = {};
|
|
546
|
+
if (scope === StorageScope.Memory) {
|
|
547
|
+
memoryStore.forEach((value, key) => {
|
|
548
|
+
if (typeof value === "string") result[key] = value;
|
|
549
|
+
});
|
|
550
|
+
return result;
|
|
551
|
+
}
|
|
552
|
+
const keys = getStorageModule().getAllKeys(scope);
|
|
553
|
+
if (keys.length === 0) return result;
|
|
554
|
+
const values = getStorageModule().getBatch(keys, scope);
|
|
555
|
+
keys.forEach((key, idx) => {
|
|
556
|
+
const val = decodeNativeBatchValue(values[idx]);
|
|
557
|
+
if (val !== undefined) result[key] = val;
|
|
397
558
|
});
|
|
398
559
|
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
560
|
});
|
|
407
|
-
return result;
|
|
408
561
|
},
|
|
409
562
|
size: (scope: StorageScope): number => {
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
563
|
+
return measureOperation("storage:size", scope, () => {
|
|
564
|
+
assertValidScope(scope);
|
|
565
|
+
if (scope === StorageScope.Memory) {
|
|
566
|
+
return memoryStore.size;
|
|
567
|
+
}
|
|
568
|
+
return getStorageModule().size(scope);
|
|
569
|
+
});
|
|
415
570
|
},
|
|
416
571
|
setAccessControl: (level: AccessControl) => {
|
|
417
|
-
|
|
418
|
-
|
|
572
|
+
measureOperation("storage:setAccessControl", StorageScope.Secure, () => {
|
|
573
|
+
secureDefaultAccessControl = level;
|
|
574
|
+
getStorageModule().setSecureAccessControl(level);
|
|
575
|
+
});
|
|
419
576
|
},
|
|
420
577
|
setSecureWritesAsync: (enabled: boolean) => {
|
|
421
|
-
|
|
578
|
+
measureOperation(
|
|
579
|
+
"storage:setSecureWritesAsync",
|
|
580
|
+
StorageScope.Secure,
|
|
581
|
+
() => {
|
|
582
|
+
getStorageModule().setSecureWritesAsync(enabled);
|
|
583
|
+
},
|
|
584
|
+
);
|
|
422
585
|
},
|
|
423
586
|
flushSecureWrites: () => {
|
|
424
|
-
flushSecureWrites()
|
|
587
|
+
measureOperation("storage:flushSecureWrites", StorageScope.Secure, () => {
|
|
588
|
+
flushSecureWrites();
|
|
589
|
+
});
|
|
425
590
|
},
|
|
426
591
|
setKeychainAccessGroup: (group: string) => {
|
|
427
|
-
|
|
592
|
+
measureOperation(
|
|
593
|
+
"storage:setKeychainAccessGroup",
|
|
594
|
+
StorageScope.Secure,
|
|
595
|
+
() => {
|
|
596
|
+
getStorageModule().setKeychainAccessGroup(group);
|
|
597
|
+
},
|
|
598
|
+
);
|
|
599
|
+
},
|
|
600
|
+
setMetricsObserver: (observer?: StorageMetricsObserver) => {
|
|
601
|
+
metricsObserver = observer;
|
|
602
|
+
},
|
|
603
|
+
getMetricsSnapshot: (): Record<string, StorageMetricSummary> => {
|
|
604
|
+
const snapshot: Record<string, StorageMetricSummary> = {};
|
|
605
|
+
metricsCounters.forEach((value, key) => {
|
|
606
|
+
snapshot[key] = {
|
|
607
|
+
count: value.count,
|
|
608
|
+
totalDurationMs: value.totalDurationMs,
|
|
609
|
+
avgDurationMs:
|
|
610
|
+
value.count === 0 ? 0 : value.totalDurationMs / value.count,
|
|
611
|
+
maxDurationMs: value.maxDurationMs,
|
|
612
|
+
};
|
|
613
|
+
});
|
|
614
|
+
return snapshot;
|
|
615
|
+
},
|
|
616
|
+
resetMetrics: () => {
|
|
617
|
+
metricsCounters.clear();
|
|
618
|
+
},
|
|
619
|
+
import: (data: Record<string, string>, scope: StorageScope): void => {
|
|
620
|
+
measureOperation(
|
|
621
|
+
"storage:import",
|
|
622
|
+
scope,
|
|
623
|
+
() => {
|
|
624
|
+
assertValidScope(scope);
|
|
625
|
+
const keys = Object.keys(data);
|
|
626
|
+
if (keys.length === 0) return;
|
|
627
|
+
const values = keys.map((k) => data[k]!);
|
|
628
|
+
|
|
629
|
+
if (scope === StorageScope.Memory) {
|
|
630
|
+
keys.forEach((key, index) => {
|
|
631
|
+
memoryStore.set(key, values[index]);
|
|
632
|
+
});
|
|
633
|
+
keys.forEach((key) => notifyKeyListeners(memoryListeners, key));
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
if (scope === StorageScope.Secure) {
|
|
638
|
+
flushSecureWrites();
|
|
639
|
+
getStorageModule().setSecureAccessControl(secureDefaultAccessControl);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
getStorageModule().setBatch(keys, values, scope);
|
|
643
|
+
keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
|
|
644
|
+
},
|
|
645
|
+
Object.keys(data).length,
|
|
646
|
+
);
|
|
428
647
|
},
|
|
429
648
|
};
|
|
430
649
|
|
|
650
|
+
export function setWebSecureStorageBackend(
|
|
651
|
+
_backend?: WebSecureStorageBackend,
|
|
652
|
+
): void {
|
|
653
|
+
// Native platforms do not use web secure backends.
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
export function getWebSecureStorageBackend():
|
|
657
|
+
| WebSecureStorageBackend
|
|
658
|
+
| undefined {
|
|
659
|
+
return undefined;
|
|
660
|
+
}
|
|
661
|
+
|
|
431
662
|
export interface StorageItemConfig<T> {
|
|
432
663
|
key: string;
|
|
433
664
|
scope: StorageScope;
|
|
@@ -442,12 +673,18 @@ export interface StorageItemConfig<T> {
|
|
|
442
673
|
coalesceSecureWrites?: boolean;
|
|
443
674
|
namespace?: string;
|
|
444
675
|
biometric?: boolean;
|
|
676
|
+
biometricLevel?: BiometricLevel;
|
|
445
677
|
accessControl?: AccessControl;
|
|
446
678
|
}
|
|
447
679
|
|
|
448
680
|
export interface StorageItem<T> {
|
|
449
681
|
get: () => T;
|
|
682
|
+
getWithVersion: () => VersionedValue<T>;
|
|
450
683
|
set: (value: T | ((prev: T) => T)) => void;
|
|
684
|
+
setIfVersion: (
|
|
685
|
+
version: StorageVersion,
|
|
686
|
+
value: T | ((prev: T) => T),
|
|
687
|
+
) => boolean;
|
|
451
688
|
delete: () => void;
|
|
452
689
|
has: () => boolean;
|
|
453
690
|
subscribe: (callback: () => void) => () => void;
|
|
@@ -459,10 +696,12 @@ export interface StorageItem<T> {
|
|
|
459
696
|
|
|
460
697
|
type StorageItemInternal<T> = StorageItem<T> & {
|
|
461
698
|
_triggerListeners: () => void;
|
|
699
|
+
_invalidateParsedCacheOnly: () => void;
|
|
462
700
|
_hasValidation: boolean;
|
|
463
701
|
_hasExpiration: boolean;
|
|
464
702
|
_readCacheEnabled: boolean;
|
|
465
703
|
_isBiometric: boolean;
|
|
704
|
+
_defaultValue: T;
|
|
466
705
|
_secureAccessControl?: AccessControl;
|
|
467
706
|
};
|
|
468
707
|
|
|
@@ -498,8 +737,14 @@ export function createStorageItem<T = undefined>(
|
|
|
498
737
|
const serialize = config.serialize ?? defaultSerialize;
|
|
499
738
|
const deserialize = config.deserialize ?? defaultDeserialize;
|
|
500
739
|
const isMemory = config.scope === StorageScope.Memory;
|
|
501
|
-
const
|
|
502
|
-
config.
|
|
740
|
+
const resolvedBiometricLevel =
|
|
741
|
+
config.scope === StorageScope.Secure
|
|
742
|
+
? (config.biometricLevel ??
|
|
743
|
+
(config.biometric === true
|
|
744
|
+
? BiometricLevel.BiometryOnly
|
|
745
|
+
: BiometricLevel.None))
|
|
746
|
+
: BiometricLevel.None;
|
|
747
|
+
const isBiometric = resolvedBiometricLevel !== BiometricLevel.None;
|
|
503
748
|
const secureAccessControl = config.accessControl;
|
|
504
749
|
const validate = config.validate;
|
|
505
750
|
const onValidationError = config.onValidationError;
|
|
@@ -512,8 +757,7 @@ export function createStorageItem<T = undefined>(
|
|
|
512
757
|
const coalesceSecureWrites =
|
|
513
758
|
config.scope === StorageScope.Secure &&
|
|
514
759
|
config.coalesceSecureWrites === true &&
|
|
515
|
-
!isBiometric
|
|
516
|
-
secureAccessControl === undefined;
|
|
760
|
+
!isBiometric;
|
|
517
761
|
const defaultValue = config.defaultValue as T;
|
|
518
762
|
const nonMemoryScope: NonMemoryScope | null =
|
|
519
763
|
config.scope === StorageScope.Disk
|
|
@@ -603,14 +847,22 @@ export function createStorageItem<T = undefined>(
|
|
|
603
847
|
|
|
604
848
|
const writeStoredRaw = (rawValue: string): void => {
|
|
605
849
|
if (isBiometric) {
|
|
606
|
-
getStorageModule().
|
|
850
|
+
getStorageModule().setSecureBiometricWithLevel(
|
|
851
|
+
storageKey,
|
|
852
|
+
rawValue,
|
|
853
|
+
resolvedBiometricLevel,
|
|
854
|
+
);
|
|
607
855
|
return;
|
|
608
856
|
}
|
|
609
857
|
|
|
610
858
|
cacheRawValue(nonMemoryScope!, storageKey, rawValue);
|
|
611
859
|
|
|
612
860
|
if (coalesceSecureWrites) {
|
|
613
|
-
scheduleSecureWrite(
|
|
861
|
+
scheduleSecureWrite(
|
|
862
|
+
storageKey,
|
|
863
|
+
rawValue,
|
|
864
|
+
secureAccessControl ?? secureDefaultAccessControl,
|
|
865
|
+
);
|
|
614
866
|
return;
|
|
615
867
|
}
|
|
616
868
|
|
|
@@ -633,7 +885,11 @@ export function createStorageItem<T = undefined>(
|
|
|
633
885
|
cacheRawValue(nonMemoryScope!, storageKey, undefined);
|
|
634
886
|
|
|
635
887
|
if (coalesceSecureWrites) {
|
|
636
|
-
scheduleSecureWrite(
|
|
888
|
+
scheduleSecureWrite(
|
|
889
|
+
storageKey,
|
|
890
|
+
undefined,
|
|
891
|
+
secureAccessControl ?? secureDefaultAccessControl,
|
|
892
|
+
);
|
|
637
893
|
return;
|
|
638
894
|
}
|
|
639
895
|
|
|
@@ -694,7 +950,7 @@ export function createStorageItem<T = undefined>(
|
|
|
694
950
|
return resolved;
|
|
695
951
|
};
|
|
696
952
|
|
|
697
|
-
const
|
|
953
|
+
const getInternal = (): T => {
|
|
698
954
|
const raw = readStoredRaw();
|
|
699
955
|
|
|
700
956
|
if (!memoryExpiration && raw === lastRaw && hasLastValue) {
|
|
@@ -712,6 +968,7 @@ export function createStorageItem<T = undefined>(
|
|
|
712
968
|
onExpired?.(storageKey);
|
|
713
969
|
lastValue = ensureValidatedValue(defaultValue, false);
|
|
714
970
|
hasLastValue = true;
|
|
971
|
+
listeners.forEach((cb) => cb());
|
|
715
972
|
return lastValue;
|
|
716
973
|
}
|
|
717
974
|
}
|
|
@@ -753,6 +1010,7 @@ export function createStorageItem<T = undefined>(
|
|
|
753
1010
|
onExpired?.(storageKey);
|
|
754
1011
|
lastValue = ensureValidatedValue(defaultValue, false);
|
|
755
1012
|
hasLastValue = true;
|
|
1013
|
+
listeners.forEach((cb) => cb());
|
|
756
1014
|
return lastValue;
|
|
757
1015
|
}
|
|
758
1016
|
|
|
@@ -771,40 +1029,74 @@ export function createStorageItem<T = undefined>(
|
|
|
771
1029
|
return lastValue;
|
|
772
1030
|
};
|
|
773
1031
|
|
|
1032
|
+
const getCurrentVersion = (): StorageVersion => {
|
|
1033
|
+
const raw = readStoredRaw();
|
|
1034
|
+
return toVersionToken(raw);
|
|
1035
|
+
};
|
|
1036
|
+
|
|
1037
|
+
const get = (): T =>
|
|
1038
|
+
measureOperation("item:get", config.scope, () => getInternal());
|
|
1039
|
+
|
|
1040
|
+
const getWithVersion = (): VersionedValue<T> =>
|
|
1041
|
+
measureOperation("item:getWithVersion", config.scope, () => ({
|
|
1042
|
+
value: getInternal(),
|
|
1043
|
+
version: getCurrentVersion(),
|
|
1044
|
+
}));
|
|
1045
|
+
|
|
774
1046
|
const set = (valueOrFn: T | ((prev: T) => T)): void => {
|
|
775
|
-
|
|
1047
|
+
measureOperation("item:set", config.scope, () => {
|
|
1048
|
+
const newValue = isUpdater(valueOrFn)
|
|
1049
|
+
? valueOrFn(getInternal())
|
|
1050
|
+
: valueOrFn;
|
|
776
1051
|
|
|
777
|
-
|
|
1052
|
+
invalidateParsedCache();
|
|
778
1053
|
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
1054
|
+
if (validate && !validate(newValue)) {
|
|
1055
|
+
throw new Error(
|
|
1056
|
+
`Validation failed for key "${storageKey}" in scope "${StorageScope[config.scope]}".`,
|
|
1057
|
+
);
|
|
1058
|
+
}
|
|
784
1059
|
|
|
785
|
-
|
|
1060
|
+
writeValueWithoutValidation(newValue);
|
|
1061
|
+
});
|
|
786
1062
|
};
|
|
787
1063
|
|
|
1064
|
+
const setIfVersion = (
|
|
1065
|
+
version: StorageVersion,
|
|
1066
|
+
valueOrFn: T | ((prev: T) => T),
|
|
1067
|
+
): boolean =>
|
|
1068
|
+
measureOperation("item:setIfVersion", config.scope, () => {
|
|
1069
|
+
const currentVersion = getCurrentVersion();
|
|
1070
|
+
if (currentVersion !== version) {
|
|
1071
|
+
return false;
|
|
1072
|
+
}
|
|
1073
|
+
set(valueOrFn);
|
|
1074
|
+
return true;
|
|
1075
|
+
});
|
|
1076
|
+
|
|
788
1077
|
const deleteItem = (): void => {
|
|
789
|
-
|
|
1078
|
+
measureOperation("item:delete", config.scope, () => {
|
|
1079
|
+
invalidateParsedCache();
|
|
790
1080
|
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
1081
|
+
if (isMemory) {
|
|
1082
|
+
if (memoryExpiration) {
|
|
1083
|
+
memoryExpiration.delete(storageKey);
|
|
1084
|
+
}
|
|
1085
|
+
memoryStore.delete(storageKey);
|
|
1086
|
+
notifyKeyListeners(memoryListeners, storageKey);
|
|
1087
|
+
return;
|
|
794
1088
|
}
|
|
795
|
-
memoryStore.delete(storageKey);
|
|
796
|
-
notifyKeyListeners(memoryListeners, storageKey);
|
|
797
|
-
return;
|
|
798
|
-
}
|
|
799
1089
|
|
|
800
|
-
|
|
1090
|
+
removeStoredRaw();
|
|
1091
|
+
});
|
|
801
1092
|
};
|
|
802
1093
|
|
|
803
|
-
const hasItem = (): boolean =>
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
1094
|
+
const hasItem = (): boolean =>
|
|
1095
|
+
measureOperation("item:has", config.scope, () => {
|
|
1096
|
+
if (isMemory) return memoryStore.has(storageKey);
|
|
1097
|
+
if (isBiometric) return getStorageModule().hasSecureBiometric(storageKey);
|
|
1098
|
+
return getStorageModule().has(storageKey, config.scope);
|
|
1099
|
+
});
|
|
808
1100
|
|
|
809
1101
|
const subscribe = (callback: () => void): (() => void) => {
|
|
810
1102
|
ensureSubscription();
|
|
@@ -823,7 +1115,9 @@ export function createStorageItem<T = undefined>(
|
|
|
823
1115
|
|
|
824
1116
|
const storageItem: StorageItemInternal<T> = {
|
|
825
1117
|
get,
|
|
1118
|
+
getWithVersion,
|
|
826
1119
|
set,
|
|
1120
|
+
setIfVersion,
|
|
827
1121
|
delete: deleteItem,
|
|
828
1122
|
has: hasItem,
|
|
829
1123
|
subscribe,
|
|
@@ -833,10 +1127,14 @@ export function createStorageItem<T = undefined>(
|
|
|
833
1127
|
invalidateParsedCache();
|
|
834
1128
|
listeners.forEach((listener) => listener());
|
|
835
1129
|
},
|
|
1130
|
+
_invalidateParsedCacheOnly: () => {
|
|
1131
|
+
invalidateParsedCache();
|
|
1132
|
+
},
|
|
836
1133
|
_hasValidation: validate !== undefined,
|
|
837
1134
|
_hasExpiration: expiration !== undefined,
|
|
838
1135
|
_readCacheEnabled: readCache,
|
|
839
1136
|
_isBiometric: isBiometric,
|
|
1137
|
+
_defaultValue: defaultValue,
|
|
840
1138
|
...(secureAccessControl !== undefined
|
|
841
1139
|
? { _secureAccessControl: secureAccessControl }
|
|
842
1140
|
: {}),
|
|
@@ -848,6 +1146,7 @@ export function createStorageItem<T = undefined>(
|
|
|
848
1146
|
}
|
|
849
1147
|
|
|
850
1148
|
export { useStorage, useStorageSelector, useSetStorage } from "./storage-hooks";
|
|
1149
|
+
export { createIndexedDBBackend } from "./indexeddb-backend";
|
|
851
1150
|
|
|
852
1151
|
type BatchReadItem<T> = Pick<
|
|
853
1152
|
StorageItem<T>,
|
|
@@ -857,6 +1156,7 @@ type BatchReadItem<T> = Pick<
|
|
|
857
1156
|
_hasExpiration?: boolean;
|
|
858
1157
|
_readCacheEnabled?: boolean;
|
|
859
1158
|
_isBiometric?: boolean;
|
|
1159
|
+
_defaultValue?: unknown;
|
|
860
1160
|
_secureAccessControl?: AccessControl;
|
|
861
1161
|
};
|
|
862
1162
|
type BatchRemoveItem = Pick<StorageItem<unknown>, "key" | "scope" | "delete">;
|
|
@@ -870,159 +1170,198 @@ export function getBatch(
|
|
|
870
1170
|
items: readonly BatchReadItem<unknown>[],
|
|
871
1171
|
scope: StorageScope,
|
|
872
1172
|
): unknown[] {
|
|
873
|
-
|
|
1173
|
+
return measureOperation(
|
|
1174
|
+
"batch:get",
|
|
1175
|
+
scope,
|
|
1176
|
+
() => {
|
|
1177
|
+
assertBatchScope(items, scope);
|
|
874
1178
|
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
1179
|
+
if (scope === StorageScope.Memory) {
|
|
1180
|
+
return items.map((item) => item.get());
|
|
1181
|
+
}
|
|
878
1182
|
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
const useBatchCache = items.every((item) => item._readCacheEnabled === true);
|
|
1183
|
+
const useRawBatchPath = items.every((item) =>
|
|
1184
|
+
scope === StorageScope.Secure
|
|
1185
|
+
? canUseSecureRawBatchPath(item)
|
|
1186
|
+
: canUseRawBatchPath(item),
|
|
1187
|
+
);
|
|
1188
|
+
if (!useRawBatchPath) {
|
|
1189
|
+
return items.map((item) => item.get());
|
|
1190
|
+
}
|
|
888
1191
|
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
1192
|
+
const rawValues = new Array<string | undefined>(items.length);
|
|
1193
|
+
const keysToFetch: string[] = [];
|
|
1194
|
+
const keyIndexes: number[] = [];
|
|
892
1195
|
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
1196
|
+
items.forEach((item, index) => {
|
|
1197
|
+
if (scope === StorageScope.Secure) {
|
|
1198
|
+
if (hasPendingSecureWrite(item.key)) {
|
|
1199
|
+
rawValues[index] = readPendingSecureWrite(item.key);
|
|
1200
|
+
return;
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
900
1203
|
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
1204
|
+
if (item._readCacheEnabled === true) {
|
|
1205
|
+
if (hasCachedRawValue(scope, item.key)) {
|
|
1206
|
+
rawValues[index] = readCachedRawValue(scope, item.key);
|
|
1207
|
+
return;
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
907
1210
|
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
1211
|
+
keysToFetch.push(item.key);
|
|
1212
|
+
keyIndexes.push(index);
|
|
1213
|
+
});
|
|
911
1214
|
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
1215
|
+
if (keysToFetch.length > 0) {
|
|
1216
|
+
const fetchedValues = getStorageModule()
|
|
1217
|
+
.getBatch(keysToFetch, scope)
|
|
1218
|
+
.map((value) => decodeNativeBatchValue(value));
|
|
916
1219
|
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
1220
|
+
fetchedValues.forEach((value, index) => {
|
|
1221
|
+
const key = keysToFetch[index];
|
|
1222
|
+
const targetIndex = keyIndexes[index];
|
|
1223
|
+
if (key === undefined || targetIndex === undefined) {
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
rawValues[targetIndex] = value;
|
|
1227
|
+
cacheRawValue(scope, key, value);
|
|
1228
|
+
});
|
|
922
1229
|
}
|
|
923
|
-
rawValues[targetIndex] = value;
|
|
924
|
-
cacheRawValue(scope, key, value);
|
|
925
|
-
});
|
|
926
|
-
}
|
|
927
1230
|
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
1231
|
+
return items.map((item, index) => {
|
|
1232
|
+
const raw = rawValues[index];
|
|
1233
|
+
if (raw === undefined) {
|
|
1234
|
+
return asInternal(item as StorageItem<unknown>)._defaultValue;
|
|
1235
|
+
}
|
|
1236
|
+
return item.deserialize(raw);
|
|
1237
|
+
});
|
|
1238
|
+
},
|
|
1239
|
+
items.length,
|
|
1240
|
+
);
|
|
935
1241
|
}
|
|
936
1242
|
|
|
937
1243
|
export function setBatch<T>(
|
|
938
1244
|
items: readonly StorageBatchSetItem<T>[],
|
|
939
1245
|
scope: StorageScope,
|
|
940
1246
|
): void {
|
|
941
|
-
|
|
942
|
-
|
|
1247
|
+
measureOperation(
|
|
1248
|
+
"batch:set",
|
|
943
1249
|
scope,
|
|
944
|
-
|
|
1250
|
+
() => {
|
|
1251
|
+
assertBatchScope(
|
|
1252
|
+
items.map((batchEntry) => batchEntry.item),
|
|
1253
|
+
scope,
|
|
1254
|
+
);
|
|
945
1255
|
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
1256
|
+
if (scope === StorageScope.Memory) {
|
|
1257
|
+
// Determine if any item needs per-item handling (validation or TTL)
|
|
1258
|
+
const needsIndividualSets = items.some(({ item }) => {
|
|
1259
|
+
const internal = asInternal(item as StorageItem<unknown>);
|
|
1260
|
+
return internal._hasValidation || internal._hasExpiration;
|
|
1261
|
+
});
|
|
1262
|
+
|
|
1263
|
+
if (needsIndividualSets) {
|
|
1264
|
+
// Fall back to individual sets to preserve validation and TTL semantics
|
|
1265
|
+
items.forEach(({ item, value }) => item.set(value));
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
950
1268
|
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
items.forEach(({ item, value }) => item.set(value));
|
|
962
|
-
return;
|
|
963
|
-
}
|
|
1269
|
+
// Atomic write: update all values in memoryStore, invalidate caches, then batch-notify
|
|
1270
|
+
items.forEach(({ item, value }) => {
|
|
1271
|
+
memoryStore.set(item.key, value);
|
|
1272
|
+
asInternal(item as StorageItem<unknown>)._invalidateParsedCacheOnly();
|
|
1273
|
+
});
|
|
1274
|
+
items.forEach(({ item }) =>
|
|
1275
|
+
notifyKeyListeners(memoryListeners, item.key),
|
|
1276
|
+
);
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
964
1279
|
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
1280
|
+
if (scope === StorageScope.Secure) {
|
|
1281
|
+
const secureEntries = items.map(({ item, value }) => ({
|
|
1282
|
+
item,
|
|
1283
|
+
value,
|
|
1284
|
+
internal: asInternal(item),
|
|
1285
|
+
}));
|
|
1286
|
+
const canUseSecureBatchPath = secureEntries.every(({ internal }) =>
|
|
1287
|
+
canUseSecureRawBatchPath(internal),
|
|
1288
|
+
);
|
|
1289
|
+
if (!canUseSecureBatchPath) {
|
|
1290
|
+
items.forEach(({ item, value }) => item.set(value));
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
flushSecureWrites();
|
|
1295
|
+
const storageModule = getStorageModule();
|
|
1296
|
+
const groupedByAccessControl = new Map<
|
|
1297
|
+
number,
|
|
1298
|
+
{ keys: string[]; values: string[] }
|
|
1299
|
+
>();
|
|
1300
|
+
|
|
1301
|
+
secureEntries.forEach(({ item, value, internal }) => {
|
|
1302
|
+
const accessControl =
|
|
1303
|
+
internal._secureAccessControl ?? secureDefaultAccessControl;
|
|
1304
|
+
const existingGroup = groupedByAccessControl.get(accessControl);
|
|
1305
|
+
const group = existingGroup ?? { keys: [], values: [] };
|
|
1306
|
+
group.keys.push(item.key);
|
|
1307
|
+
group.values.push(item.serialize(value));
|
|
1308
|
+
if (!existingGroup) {
|
|
1309
|
+
groupedByAccessControl.set(accessControl, group);
|
|
1310
|
+
}
|
|
1311
|
+
});
|
|
1312
|
+
|
|
1313
|
+
groupedByAccessControl.forEach((group, accessControl) => {
|
|
1314
|
+
storageModule.setSecureAccessControl(accessControl);
|
|
1315
|
+
storageModule.setBatch(group.keys, group.values, scope);
|
|
1316
|
+
group.keys.forEach((key, index) =>
|
|
1317
|
+
cacheRawValue(scope, key, group.values[index]),
|
|
1318
|
+
);
|
|
1319
|
+
});
|
|
1320
|
+
return;
|
|
981
1321
|
}
|
|
982
|
-
});
|
|
983
1322
|
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
storageModule.setBatch(group.keys, group.values, scope);
|
|
987
|
-
group.keys.forEach((key, index) =>
|
|
988
|
-
cacheRawValue(scope, key, group.values[index]),
|
|
1323
|
+
const useRawBatchPath = items.every(({ item }) =>
|
|
1324
|
+
canUseRawBatchPath(asInternal(item)),
|
|
989
1325
|
);
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
const useRawBatchPath = items.every(({ item }) =>
|
|
995
|
-
canUseRawBatchPath(asInternal(item)),
|
|
996
|
-
);
|
|
997
|
-
if (!useRawBatchPath) {
|
|
998
|
-
items.forEach(({ item, value }) => item.set(value));
|
|
999
|
-
return;
|
|
1000
|
-
}
|
|
1326
|
+
if (!useRawBatchPath) {
|
|
1327
|
+
items.forEach(({ item, value }) => item.set(value));
|
|
1328
|
+
return;
|
|
1329
|
+
}
|
|
1001
1330
|
|
|
1002
|
-
|
|
1003
|
-
|
|
1331
|
+
const keys = items.map((entry) => entry.item.key);
|
|
1332
|
+
const values = items.map((entry) => entry.item.serialize(entry.value));
|
|
1004
1333
|
|
|
1005
|
-
|
|
1006
|
-
|
|
1334
|
+
getStorageModule().setBatch(keys, values, scope);
|
|
1335
|
+
keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
|
|
1336
|
+
},
|
|
1337
|
+
items.length,
|
|
1338
|
+
);
|
|
1007
1339
|
}
|
|
1008
1340
|
|
|
1009
1341
|
export function removeBatch(
|
|
1010
1342
|
items: readonly BatchRemoveItem[],
|
|
1011
1343
|
scope: StorageScope,
|
|
1012
1344
|
): void {
|
|
1013
|
-
|
|
1345
|
+
measureOperation(
|
|
1346
|
+
"batch:remove",
|
|
1347
|
+
scope,
|
|
1348
|
+
() => {
|
|
1349
|
+
assertBatchScope(items, scope);
|
|
1014
1350
|
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1351
|
+
if (scope === StorageScope.Memory) {
|
|
1352
|
+
items.forEach((item) => item.delete());
|
|
1353
|
+
return;
|
|
1354
|
+
}
|
|
1019
1355
|
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1356
|
+
const keys = items.map((item) => item.key);
|
|
1357
|
+
if (scope === StorageScope.Secure) {
|
|
1358
|
+
flushSecureWrites();
|
|
1359
|
+
}
|
|
1360
|
+
getStorageModule().removeBatch(keys, scope);
|
|
1361
|
+
keys.forEach((key) => cacheRawValue(scope, key, undefined));
|
|
1362
|
+
},
|
|
1363
|
+
items.length,
|
|
1364
|
+
);
|
|
1026
1365
|
}
|
|
1027
1366
|
|
|
1028
1367
|
export function registerMigration(version: number, migration: Migration): void {
|
|
@@ -1040,92 +1379,124 @@ export function registerMigration(version: number, migration: Migration): void {
|
|
|
1040
1379
|
export function migrateToLatest(
|
|
1041
1380
|
scope: StorageScope = StorageScope.Disk,
|
|
1042
1381
|
): number {
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
.
|
|
1047
|
-
|
|
1382
|
+
return measureOperation("migration:run", scope, () => {
|
|
1383
|
+
assertValidScope(scope);
|
|
1384
|
+
const currentVersion = readMigrationVersion(scope);
|
|
1385
|
+
const versions = Array.from(registeredMigrations.keys())
|
|
1386
|
+
.filter((version) => version > currentVersion)
|
|
1387
|
+
.sort((a, b) => a - b);
|
|
1388
|
+
|
|
1389
|
+
let appliedVersion = currentVersion;
|
|
1390
|
+
const context: MigrationContext = {
|
|
1391
|
+
scope,
|
|
1392
|
+
getRaw: (key) => getRawValue(key, scope),
|
|
1393
|
+
setRaw: (key, value) => setRawValue(key, value, scope),
|
|
1394
|
+
removeRaw: (key) => removeRawValue(key, scope),
|
|
1395
|
+
};
|
|
1048
1396
|
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1397
|
+
versions.forEach((version) => {
|
|
1398
|
+
const migration = registeredMigrations.get(version);
|
|
1399
|
+
if (!migration) {
|
|
1400
|
+
return;
|
|
1401
|
+
}
|
|
1402
|
+
migration(context);
|
|
1403
|
+
writeMigrationVersion(scope, version);
|
|
1404
|
+
appliedVersion = version;
|
|
1405
|
+
});
|
|
1056
1406
|
|
|
1057
|
-
|
|
1058
|
-
const migration = registeredMigrations.get(version);
|
|
1059
|
-
if (!migration) {
|
|
1060
|
-
return;
|
|
1061
|
-
}
|
|
1062
|
-
migration(context);
|
|
1063
|
-
writeMigrationVersion(scope, version);
|
|
1064
|
-
appliedVersion = version;
|
|
1407
|
+
return appliedVersion;
|
|
1065
1408
|
});
|
|
1066
|
-
|
|
1067
|
-
return appliedVersion;
|
|
1068
1409
|
}
|
|
1069
1410
|
|
|
1070
1411
|
export function runTransaction<T>(
|
|
1071
1412
|
scope: StorageScope,
|
|
1072
1413
|
transaction: (context: TransactionContext) => T,
|
|
1073
1414
|
): T {
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1415
|
+
return measureOperation("transaction:run", scope, () => {
|
|
1416
|
+
assertValidScope(scope);
|
|
1417
|
+
if (scope === StorageScope.Secure) {
|
|
1418
|
+
flushSecureWrites();
|
|
1419
|
+
}
|
|
1078
1420
|
|
|
1079
|
-
|
|
1421
|
+
const rollback = new Map<string, string | undefined>();
|
|
1080
1422
|
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1423
|
+
const rememberRollback = (key: string) => {
|
|
1424
|
+
if (rollback.has(key)) {
|
|
1425
|
+
return;
|
|
1426
|
+
}
|
|
1427
|
+
rollback.set(key, getRawValue(key, scope));
|
|
1428
|
+
};
|
|
1087
1429
|
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1430
|
+
const tx: TransactionContext = {
|
|
1431
|
+
scope,
|
|
1432
|
+
getRaw: (key) => getRawValue(key, scope),
|
|
1433
|
+
setRaw: (key, value) => {
|
|
1434
|
+
rememberRollback(key);
|
|
1435
|
+
setRawValue(key, value, scope);
|
|
1436
|
+
},
|
|
1437
|
+
removeRaw: (key) => {
|
|
1438
|
+
rememberRollback(key);
|
|
1439
|
+
removeRawValue(key, scope);
|
|
1440
|
+
},
|
|
1441
|
+
getItem: (item) => {
|
|
1442
|
+
assertBatchScope([item], scope);
|
|
1443
|
+
return item.get();
|
|
1444
|
+
},
|
|
1445
|
+
setItem: (item, value) => {
|
|
1446
|
+
assertBatchScope([item], scope);
|
|
1447
|
+
rememberRollback(item.key);
|
|
1448
|
+
item.set(value);
|
|
1449
|
+
},
|
|
1450
|
+
removeItem: (item) => {
|
|
1451
|
+
assertBatchScope([item], scope);
|
|
1452
|
+
rememberRollback(item.key);
|
|
1453
|
+
item.delete();
|
|
1454
|
+
},
|
|
1455
|
+
};
|
|
1114
1456
|
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
.
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1457
|
+
try {
|
|
1458
|
+
return transaction(tx);
|
|
1459
|
+
} catch (error) {
|
|
1460
|
+
const rollbackEntries = Array.from(rollback.entries()).reverse();
|
|
1461
|
+
if (scope === StorageScope.Memory) {
|
|
1462
|
+
rollbackEntries.forEach(([key, previousValue]) => {
|
|
1463
|
+
if (previousValue === undefined) {
|
|
1464
|
+
removeRawValue(key, scope);
|
|
1465
|
+
} else {
|
|
1466
|
+
setRawValue(key, previousValue, scope);
|
|
1467
|
+
}
|
|
1468
|
+
});
|
|
1469
|
+
} else {
|
|
1470
|
+
const keysToSet: string[] = [];
|
|
1471
|
+
const valuesToSet: string[] = [];
|
|
1472
|
+
const keysToRemove: string[] = [];
|
|
1473
|
+
|
|
1474
|
+
rollbackEntries.forEach(([key, previousValue]) => {
|
|
1475
|
+
if (previousValue === undefined) {
|
|
1476
|
+
keysToRemove.push(key);
|
|
1477
|
+
} else {
|
|
1478
|
+
keysToSet.push(key);
|
|
1479
|
+
valuesToSet.push(previousValue);
|
|
1480
|
+
}
|
|
1481
|
+
});
|
|
1482
|
+
|
|
1483
|
+
if (scope === StorageScope.Secure) {
|
|
1484
|
+
flushSecureWrites();
|
|
1125
1485
|
}
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1486
|
+
if (keysToSet.length > 0) {
|
|
1487
|
+
getStorageModule().setBatch(keysToSet, valuesToSet, scope);
|
|
1488
|
+
keysToSet.forEach((key, index) =>
|
|
1489
|
+
cacheRawValue(scope, key, valuesToSet[index]),
|
|
1490
|
+
);
|
|
1491
|
+
}
|
|
1492
|
+
if (keysToRemove.length > 0) {
|
|
1493
|
+
getStorageModule().removeBatch(keysToRemove, scope);
|
|
1494
|
+
keysToRemove.forEach((key) => cacheRawValue(scope, key, undefined));
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
throw error;
|
|
1498
|
+
}
|
|
1499
|
+
});
|
|
1129
1500
|
}
|
|
1130
1501
|
|
|
1131
1502
|
export type SecureAuthStorageConfig<K extends string = string> = Record<
|
|
@@ -1133,6 +1504,7 @@ export type SecureAuthStorageConfig<K extends string = string> = Record<
|
|
|
1133
1504
|
{
|
|
1134
1505
|
ttlMs?: number;
|
|
1135
1506
|
biometric?: boolean;
|
|
1507
|
+
biometricLevel?: BiometricLevel;
|
|
1136
1508
|
accessControl?: AccessControl;
|
|
1137
1509
|
}
|
|
1138
1510
|
>;
|
|
@@ -1156,6 +1528,9 @@ export function createSecureAuthStorage<K extends string>(
|
|
|
1156
1528
|
...(itemConfig.biometric !== undefined
|
|
1157
1529
|
? { biometric: itemConfig.biometric }
|
|
1158
1530
|
: {}),
|
|
1531
|
+
...(itemConfig.biometricLevel !== undefined
|
|
1532
|
+
? { biometricLevel: itemConfig.biometricLevel }
|
|
1533
|
+
: {}),
|
|
1159
1534
|
...(itemConfig.accessControl !== undefined
|
|
1160
1535
|
? { accessControl: itemConfig.accessControl }
|
|
1161
1536
|
: {}),
|