react-native-nitro-storage 0.5.7 → 0.5.9

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.
@@ -0,0 +1,2349 @@
1
+ import {
2
+ assertAccessControlLevel,
3
+ assertBiometricLevel,
4
+ canUseRawBatchPath,
5
+ canUseSecureRawBatchPath,
6
+ createKeyChange,
7
+ defaultDeserialize,
8
+ defaultSerialize,
9
+ isUpdater,
10
+ notifyAllListeners,
11
+ notifyKeyListeners,
12
+ now,
13
+ redactSecureKeyChange,
14
+ runMicrotask,
15
+ typedKeys,
16
+ type ExpirationConfig,
17
+ type KeyListenerRegistry,
18
+ type Migration,
19
+ type MigrationContext,
20
+ type NonMemoryScope,
21
+ type PendingDiskWrite,
22
+ type PendingSecureWrite,
23
+ type RollbackRecord,
24
+ type SecureAuthStorageConfig,
25
+ type StorageEventObserverOptions,
26
+ type StorageExportOptions,
27
+ type StorageMetricSummary,
28
+ type StorageMetricsObserver,
29
+ type StorageSelectorListener,
30
+ type StorageSelectorSubscribeOptions,
31
+ type StorageVersion,
32
+ type Validator,
33
+ type VersionedValue,
34
+ } from "./shared";
35
+ import { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
36
+ import {
37
+ MIGRATION_VERSION_KEY,
38
+ type StoredEnvelope,
39
+ isStoredEnvelope,
40
+ assertBatchScope,
41
+ assertValidScope,
42
+ toVersionToken,
43
+ prefixKey,
44
+ isNamespaced,
45
+ } from "./internal";
46
+ import type { SecureStorageMetadata } from "./storage-runtime";
47
+ import {
48
+ StorageEventRegistry,
49
+ type StorageBatchChangeEvent,
50
+ type StorageChangeEvent,
51
+ type StorageChangeOperation,
52
+ type StorageChangeSource,
53
+ type StorageEventListener,
54
+ type StorageKeyChangeEvent,
55
+ } from "./storage-events";
56
+ import type { StorageSetter } from "./storage-hooks";
57
+
58
+ export type TransactionContext = {
59
+ scope: StorageScope;
60
+ getRaw: (key: string) => string | undefined;
61
+ setRaw: (key: string, value: string) => void;
62
+ removeRaw: (key: string) => void;
63
+ getItem: <T>(item: Pick<StorageItem<T>, "scope" | "key" | "get">) => T;
64
+ setItem: <T>(
65
+ item: Pick<StorageItem<T>, "scope" | "key" | "set">,
66
+ value: T,
67
+ ) => void;
68
+ removeItem: (
69
+ item: Pick<StorageItem<unknown>, "scope" | "key" | "delete">,
70
+ ) => void;
71
+ };
72
+
73
+ export type StorageItemConfig<T> = {
74
+ key: string;
75
+ scope: StorageScope;
76
+ defaultValue?: T;
77
+ serialize?: (value: T) => string;
78
+ deserialize?: (value: string) => T;
79
+ validate?: Validator<T>;
80
+ onValidationError?: (invalidValue: unknown) => T;
81
+ expiration?: ExpirationConfig;
82
+ onExpired?: (key: string) => void;
83
+ readCache?: boolean;
84
+ coalesceDiskWrites?: boolean;
85
+ coalesceSecureWrites?: boolean;
86
+ namespace?: string;
87
+ biometric?: boolean;
88
+ biometricLevel?: BiometricLevel;
89
+ accessControl?: AccessControl;
90
+ };
91
+
92
+ export type StorageItem<T> = {
93
+ get: () => T;
94
+ getWithVersion: () => VersionedValue<T>;
95
+ set: StorageSetter<T>;
96
+ setIfVersion: (
97
+ version: StorageVersion,
98
+ value: T | ((prev: T) => T),
99
+ ) => boolean;
100
+ delete: () => void;
101
+ has: () => boolean;
102
+ subscribe: (callback: () => void) => () => void;
103
+ subscribeSelector: <TSelected>(
104
+ selector: (value: T) => TSelected,
105
+ listener: StorageSelectorListener<TSelected>,
106
+ options?: StorageSelectorSubscribeOptions<TSelected>,
107
+ ) => () => void;
108
+ serialize: (value: T) => string;
109
+ deserialize: (value: string) => T;
110
+ scope: StorageScope;
111
+ key: string;
112
+ };
113
+
114
+ type StorageItemInternal<T> = StorageItem<T> & {
115
+ _triggerListeners: () => void;
116
+ _invalidateParsedCacheOnly: () => void;
117
+ _hasValidation: boolean;
118
+ _hasExpiration: boolean;
119
+ _readCacheEnabled: boolean;
120
+ _isBiometric: boolean;
121
+ _biometricLevel: BiometricLevel;
122
+ _defaultValue: T;
123
+ _secureAccessControl?: AccessControl;
124
+ };
125
+
126
+ export type BatchReadItem<T> = Pick<
127
+ StorageItem<T>,
128
+ "key" | "scope" | "get" | "deserialize"
129
+ > & {
130
+ _hasValidation?: boolean;
131
+ _hasExpiration?: boolean;
132
+ _readCacheEnabled?: boolean;
133
+ _isBiometric?: boolean;
134
+ _defaultValue?: unknown;
135
+ _secureAccessControl?: AccessControl;
136
+ };
137
+ export type BatchRemoveItem = Pick<
138
+ StorageItem<unknown>,
139
+ "key" | "scope" | "delete"
140
+ >;
141
+ export type BatchValues<TItems extends readonly BatchReadItem<unknown>[]> = {
142
+ [Index in keyof TItems]: TItems[Index] extends BatchReadItem<infer Value>
143
+ ? Value
144
+ : never;
145
+ };
146
+
147
+ export type StorageBatchSetItem<T> = {
148
+ item: StorageItem<T>;
149
+ value: T;
150
+ };
151
+
152
+ export type StorageCoreBackend = {
153
+ get(key: string, scope: StorageScope): string | undefined;
154
+ set(key: string, value: string, scope: StorageScope): void;
155
+ remove(key: string, scope: StorageScope): void;
156
+ clear(scope: StorageScope): void;
157
+ has(key: string, scope: StorageScope): boolean;
158
+ getAllKeys(scope: StorageScope): string[];
159
+ getKeysByPrefix(prefix: string, scope: StorageScope): string[];
160
+ size(scope: StorageScope): number;
161
+ setBatch(keys: string[], values: string[], scope: StorageScope): void;
162
+ getBatch(keys: string[], scope: StorageScope): (string | undefined)[];
163
+ removeBatch(keys: string[], scope: StorageScope): void;
164
+ removeByPrefix(prefix: string, scope: StorageScope): void;
165
+ setSecureAccessControl(level: AccessControl): void;
166
+ getSecureBiometric(key: string): string | undefined;
167
+ setSecureBiometricWithLevel(
168
+ key: string,
169
+ value: string,
170
+ level: BiometricLevel,
171
+ ): void;
172
+ deleteSecureBiometric(key: string): void;
173
+ hasSecureBiometric(key: string): boolean;
174
+ clearSecureBiometric(): void;
175
+ };
176
+
177
+ export type StorageCoreAdapter = {
178
+ backend: StorageCoreBackend;
179
+ changeSource: StorageChangeSource;
180
+ applyAccessControlOnSecureRawWrite: boolean;
181
+ flushDiskWritesOnImport: boolean;
182
+ ensureScopeSubscription(scope: NonMemoryScope): void;
183
+ maybeCleanupScopeSubscription(scope: NonMemoryScope): void;
184
+ onWillEmitChanges(
185
+ scope: StorageScope,
186
+ keys: readonly string[],
187
+ operation: StorageChangeOperation,
188
+ source: StorageChangeSource,
189
+ ): void;
190
+ getSecureMetadataProfile(): Pick<
191
+ SecureStorageMetadata,
192
+ "backend" | "encrypted" | "hardwareBacked"
193
+ >;
194
+ };
195
+
196
+ export type StorageCoreInternals = {
197
+ getScopedListeners(scope: NonMemoryScope): KeyListenerRegistry;
198
+ cacheRawValue(
199
+ scope: NonMemoryScope,
200
+ key: string,
201
+ value: string | undefined,
202
+ ): void;
203
+ readCachedRawValue(scope: NonMemoryScope, key: string): string | undefined;
204
+ clearScopeRawCache(scope: NonMemoryScope): void;
205
+ clearPendingDiskWrite(key: string): void;
206
+ clearPendingSecureWrite(key: string): void;
207
+ clearAllPendingDiskWrites(): void;
208
+ clearAllPendingSecureWrites(): void;
209
+ flushDiskWrites(): void;
210
+ flushSecureWrites(): void;
211
+ emitKeyChange(
212
+ scope: StorageScope,
213
+ key: string,
214
+ oldValue: string | undefined,
215
+ newValue: string | undefined,
216
+ operation: StorageChangeOperation,
217
+ source: StorageChangeSource,
218
+ ): void;
219
+ hasEventObserver(): boolean;
220
+ hasScopeEventListeners(scope: StorageScope): boolean;
221
+ measureOperation<T>(
222
+ operation: string,
223
+ scope: StorageScope,
224
+ fn: () => T,
225
+ keysCount?: number,
226
+ ): T;
227
+ recordMetric(
228
+ operation: string,
229
+ scope: StorageScope,
230
+ durationMs: number,
231
+ keysCount?: number,
232
+ ): void;
233
+ getSecureDefaultAccessControl(): AccessControl;
234
+ setSecureDefaultAccessControl(level: AccessControl): void;
235
+ };
236
+
237
+ function asInternal<T>(item: StorageItem<T>): StorageItemInternal<T> {
238
+ return item as StorageItemInternal<T>;
239
+ }
240
+
241
+ export function createStorageCore(
242
+ buildAdapter: (internals: StorageCoreInternals) => StorageCoreAdapter,
243
+ ) {
244
+ const registeredMigrations = new Map<number, Migration>();
245
+ const memoryStore = new Map<string, unknown>();
246
+ const memoryListeners: KeyListenerRegistry = new Map();
247
+ const scopedListeners = new Map<NonMemoryScope, KeyListenerRegistry>([
248
+ [StorageScope.Disk, new Map()],
249
+ [StorageScope.Secure, new Map()],
250
+ ]);
251
+ const scopedRawCache = new Map<
252
+ NonMemoryScope,
253
+ Map<string, string | undefined>
254
+ >([
255
+ [StorageScope.Disk, new Map()],
256
+ [StorageScope.Secure, new Map()],
257
+ ]);
258
+ const pendingDiskWrites = new Map<string, PendingDiskWrite>();
259
+ let diskFlushScheduled = false;
260
+ let diskWritesAsync = false;
261
+ const pendingSecureWrites = new Map<string, PendingSecureWrite>();
262
+ let secureFlushScheduled = false;
263
+ let secureDefaultAccessControl: AccessControl = AccessControl.WhenUnlocked;
264
+ let metricsObserver: StorageMetricsObserver | undefined;
265
+ let eventObserver: StorageEventListener | undefined;
266
+ let eventObserverRedactSecureValues = true;
267
+ const metricsCounters = new Map<
268
+ string,
269
+ { count: number; totalDurationMs: number; maxDurationMs: number }
270
+ >();
271
+ const storageEvents = new StorageEventRegistry();
272
+
273
+ function recordMetric(
274
+ operation: string,
275
+ scope: StorageScope,
276
+ durationMs: number,
277
+ keysCount = 1,
278
+ ): void {
279
+ const existing = metricsCounters.get(operation);
280
+ if (!existing) {
281
+ metricsCounters.set(operation, {
282
+ count: 1,
283
+ totalDurationMs: durationMs,
284
+ maxDurationMs: durationMs,
285
+ });
286
+ } else {
287
+ existing.count += 1;
288
+ existing.totalDurationMs += durationMs;
289
+ existing.maxDurationMs = Math.max(existing.maxDurationMs, durationMs);
290
+ }
291
+
292
+ metricsObserver?.({
293
+ operation,
294
+ scope,
295
+ durationMs,
296
+ keysCount,
297
+ });
298
+ }
299
+
300
+ function measureOperation<T>(
301
+ operation: string,
302
+ scope: StorageScope,
303
+ fn: () => T,
304
+ keysCount = 1,
305
+ ): T {
306
+ if (!metricsObserver) {
307
+ return fn();
308
+ }
309
+ const start = now();
310
+ try {
311
+ return fn();
312
+ } finally {
313
+ recordMetric(operation, scope, now() - start, keysCount);
314
+ }
315
+ }
316
+
317
+ function getScopedListeners(scope: NonMemoryScope): KeyListenerRegistry {
318
+ return scopedListeners.get(scope)!;
319
+ }
320
+
321
+ function getScopeRawCache(
322
+ scope: NonMemoryScope,
323
+ ): Map<string, string | undefined> {
324
+ return scopedRawCache.get(scope)!;
325
+ }
326
+
327
+ function cacheRawValue(
328
+ scope: NonMemoryScope,
329
+ key: string,
330
+ value: string | undefined,
331
+ ): void {
332
+ getScopeRawCache(scope).set(key, value);
333
+ }
334
+
335
+ function readCachedRawValue(
336
+ scope: NonMemoryScope,
337
+ key: string,
338
+ ): string | undefined {
339
+ return getScopeRawCache(scope).get(key);
340
+ }
341
+
342
+ function clearScopeRawCache(scope: NonMemoryScope): void {
343
+ getScopeRawCache(scope).clear();
344
+ }
345
+
346
+ function addKeyListener(
347
+ registry: KeyListenerRegistry,
348
+ key: string,
349
+ listener: () => void,
350
+ ): () => void {
351
+ let listeners = registry.get(key);
352
+ if (!listeners) {
353
+ listeners = new Set();
354
+ registry.set(key, listeners);
355
+ }
356
+ listeners.add(listener);
357
+
358
+ return () => {
359
+ const keyListeners = registry.get(key);
360
+ if (!keyListeners) {
361
+ return;
362
+ }
363
+ keyListeners.delete(listener);
364
+ if (keyListeners.size === 0) {
365
+ registry.delete(key);
366
+ }
367
+ };
368
+ }
369
+
370
+ function getEventRawValue(
371
+ scope: StorageScope,
372
+ key: string,
373
+ ): string | undefined {
374
+ if (scope === StorageScope.Memory) {
375
+ const value = memoryStore.get(key);
376
+ return typeof value === "string" ? value : undefined;
377
+ }
378
+
379
+ return getRawValue(key, scope);
380
+ }
381
+
382
+ function shouldReadPreviousEventValues(scope: StorageScope): boolean {
383
+ if (storageEvents.hasListeners(scope)) {
384
+ return true;
385
+ }
386
+ if (!eventObserver) {
387
+ return false;
388
+ }
389
+ return scope !== StorageScope.Secure || !eventObserverRedactSecureValues;
390
+ }
391
+
392
+ function eventForGlobalObserver(
393
+ event: StorageChangeEvent,
394
+ ): StorageChangeEvent {
395
+ if (
396
+ !eventObserverRedactSecureValues ||
397
+ event.scope !== StorageScope.Secure
398
+ ) {
399
+ return event;
400
+ }
401
+
402
+ if (event.type === "key") {
403
+ return redactSecureKeyChange(event);
404
+ }
405
+
406
+ return {
407
+ ...event,
408
+ changes: event.changes.map(redactSecureKeyChange),
409
+ };
410
+ }
411
+
412
+ function emitKeyChange(
413
+ scope: StorageScope,
414
+ key: string,
415
+ oldValue: string | undefined,
416
+ newValue: string | undefined,
417
+ operation: StorageChangeOperation,
418
+ source: StorageChangeSource,
419
+ ): void {
420
+ adapter.onWillEmitChanges(scope, [key], operation, source);
421
+ const event = createKeyChange(
422
+ scope,
423
+ key,
424
+ oldValue,
425
+ newValue,
426
+ operation,
427
+ source,
428
+ );
429
+ storageEvents.emitKey(event);
430
+ eventObserver?.(eventForGlobalObserver(event));
431
+ }
432
+
433
+ function emitBatchChange(
434
+ scope: StorageScope,
435
+ operation: StorageChangeOperation,
436
+ source: StorageChangeSource,
437
+ changes: StorageKeyChangeEvent[],
438
+ ): void {
439
+ if (changes.length === 0) {
440
+ return;
441
+ }
442
+
443
+ adapter.onWillEmitChanges(
444
+ scope,
445
+ changes.map((change) => change.key),
446
+ operation,
447
+ source,
448
+ );
449
+
450
+ const event: StorageBatchChangeEvent = {
451
+ type: "batch",
452
+ scope,
453
+ operation,
454
+ source,
455
+ changes,
456
+ };
457
+ storageEvents.emitBatch(event);
458
+ eventObserver?.(eventForGlobalObserver(event));
459
+ }
460
+
461
+ function readPendingSecureWrite(key: string): string | undefined {
462
+ return pendingSecureWrites.get(key)?.value;
463
+ }
464
+
465
+ function readPendingDiskWrite(key: string): string | undefined {
466
+ return pendingDiskWrites.get(key)?.value;
467
+ }
468
+
469
+ function hasPendingDiskWrite(key: string): boolean {
470
+ return pendingDiskWrites.has(key);
471
+ }
472
+
473
+ function hasPendingSecureWrite(key: string): boolean {
474
+ return pendingSecureWrites.has(key);
475
+ }
476
+
477
+ function clearPendingDiskWrite(key: string): void {
478
+ pendingDiskWrites.delete(key);
479
+ }
480
+
481
+ function clearPendingSecureWrite(key: string): void {
482
+ pendingSecureWrites.delete(key);
483
+ }
484
+
485
+ function flushDiskWrites(): void {
486
+ diskFlushScheduled = false;
487
+
488
+ if (pendingDiskWrites.size === 0) {
489
+ return;
490
+ }
491
+
492
+ const writes = Array.from(pendingDiskWrites.values());
493
+ pendingDiskWrites.clear();
494
+
495
+ const keysToSet: string[] = [];
496
+ const valuesToSet: string[] = [];
497
+ const keysToRemove: string[] = [];
498
+
499
+ writes.forEach(({ key, value }) => {
500
+ if (value === undefined) {
501
+ keysToRemove.push(key);
502
+ return;
503
+ }
504
+
505
+ keysToSet.push(key);
506
+ valuesToSet.push(value);
507
+ });
508
+
509
+ if (keysToSet.length > 0) {
510
+ adapter.backend.setBatch(keysToSet, valuesToSet, StorageScope.Disk);
511
+ }
512
+ if (keysToRemove.length > 0) {
513
+ adapter.backend.removeBatch(keysToRemove, StorageScope.Disk);
514
+ }
515
+ }
516
+
517
+ function flushSecureWrites(): void {
518
+ secureFlushScheduled = false;
519
+
520
+ if (pendingSecureWrites.size === 0) {
521
+ return;
522
+ }
523
+
524
+ const writes = Array.from(pendingSecureWrites.values());
525
+ pendingSecureWrites.clear();
526
+
527
+ const groupedSetWrites = new Map<
528
+ AccessControl,
529
+ { keys: string[]; values: string[] }
530
+ >();
531
+ const keysToRemove: string[] = [];
532
+
533
+ writes.forEach(({ key, value, accessControl }) => {
534
+ if (value === undefined) {
535
+ keysToRemove.push(key);
536
+ } else {
537
+ const resolvedAccessControl =
538
+ accessControl ?? secureDefaultAccessControl;
539
+ const existingGroup = groupedSetWrites.get(resolvedAccessControl);
540
+ const group = existingGroup ?? { keys: [], values: [] };
541
+ group.keys.push(key);
542
+ group.values.push(value);
543
+ if (!existingGroup) {
544
+ groupedSetWrites.set(resolvedAccessControl, group);
545
+ }
546
+ }
547
+ });
548
+
549
+ groupedSetWrites.forEach((group, accessControl) => {
550
+ adapter.backend.setSecureAccessControl(accessControl);
551
+ adapter.backend.setBatch(group.keys, group.values, StorageScope.Secure);
552
+ });
553
+ if (keysToRemove.length > 0) {
554
+ adapter.backend.removeBatch(keysToRemove, StorageScope.Secure);
555
+ }
556
+ }
557
+
558
+ function scheduleDiskWrite(key: string, value: string | undefined): void {
559
+ pendingDiskWrites.set(key, { key, value });
560
+ if (diskFlushScheduled) {
561
+ return;
562
+ }
563
+ diskFlushScheduled = true;
564
+ runMicrotask(flushDiskWrites);
565
+ }
566
+
567
+ function scheduleSecureWrite(
568
+ key: string,
569
+ value: string | undefined,
570
+ accessControl?: AccessControl,
571
+ ): void {
572
+ const pendingWrite: PendingSecureWrite = { key, value };
573
+ if (accessControl !== undefined) {
574
+ pendingWrite.accessControl = accessControl;
575
+ }
576
+ pendingSecureWrites.set(key, pendingWrite);
577
+ if (secureFlushScheduled) {
578
+ return;
579
+ }
580
+ secureFlushScheduled = true;
581
+ runMicrotask(flushSecureWrites);
582
+ }
583
+
584
+ function getRawValue(key: string, scope: StorageScope): string | undefined {
585
+ assertValidScope(scope);
586
+ if (scope === StorageScope.Memory) {
587
+ const value = memoryStore.get(key);
588
+ return typeof value === "string" ? value : undefined;
589
+ }
590
+
591
+ if (scope === StorageScope.Disk && hasPendingDiskWrite(key)) {
592
+ return readPendingDiskWrite(key);
593
+ }
594
+
595
+ if (scope === StorageScope.Secure && hasPendingSecureWrite(key)) {
596
+ return readPendingSecureWrite(key);
597
+ }
598
+
599
+ return adapter.backend.get(key, scope);
600
+ }
601
+
602
+ function setRawValue(key: string, value: string, scope: StorageScope): void {
603
+ assertValidScope(scope);
604
+ const oldValue =
605
+ scope === StorageScope.Memory ? getEventRawValue(scope, key) : undefined;
606
+ if (scope === StorageScope.Memory) {
607
+ memoryStore.set(key, value);
608
+ notifyKeyListeners(memoryListeners, key);
609
+ emitKeyChange(scope, key, oldValue, value, "set", "memory");
610
+ return;
611
+ }
612
+
613
+ if (scope === StorageScope.Disk) {
614
+ cacheRawValue(scope, key, value);
615
+ if (diskWritesAsync) {
616
+ scheduleDiskWrite(key, value);
617
+ emitKeyChange(scope, key, oldValue, value, "set", adapter.changeSource);
618
+ return;
619
+ }
620
+
621
+ flushDiskWrites();
622
+ clearPendingDiskWrite(key);
623
+ }
624
+
625
+ if (scope === StorageScope.Secure) {
626
+ flushSecureWrites();
627
+ clearPendingSecureWrite(key);
628
+ if (adapter.applyAccessControlOnSecureRawWrite) {
629
+ adapter.backend.setSecureAccessControl(secureDefaultAccessControl);
630
+ }
631
+ }
632
+
633
+ adapter.backend.set(key, value, scope);
634
+ cacheRawValue(scope, key, value);
635
+ emitKeyChange(scope, key, oldValue, value, "set", adapter.changeSource);
636
+ }
637
+
638
+ function removeRawValue(key: string, scope: StorageScope): void {
639
+ assertValidScope(scope);
640
+ const oldValue = getEventRawValue(scope, key);
641
+ if (scope === StorageScope.Memory) {
642
+ memoryStore.delete(key);
643
+ notifyKeyListeners(memoryListeners, key);
644
+ emitKeyChange(scope, key, oldValue, undefined, "remove", "memory");
645
+ return;
646
+ }
647
+
648
+ if (scope === StorageScope.Disk) {
649
+ cacheRawValue(scope, key, undefined);
650
+ if (diskWritesAsync) {
651
+ scheduleDiskWrite(key, undefined);
652
+ emitKeyChange(
653
+ scope,
654
+ key,
655
+ oldValue,
656
+ undefined,
657
+ "remove",
658
+ adapter.changeSource,
659
+ );
660
+ return;
661
+ }
662
+
663
+ flushDiskWrites();
664
+ clearPendingDiskWrite(key);
665
+ }
666
+
667
+ if (scope === StorageScope.Secure) {
668
+ flushSecureWrites();
669
+ clearPendingSecureWrite(key);
670
+ }
671
+
672
+ adapter.backend.remove(key, scope);
673
+ cacheRawValue(scope, key, undefined);
674
+ emitKeyChange(
675
+ scope,
676
+ key,
677
+ oldValue,
678
+ undefined,
679
+ "remove",
680
+ adapter.changeSource,
681
+ );
682
+ }
683
+
684
+ function readMigrationVersion(scope: StorageScope): number {
685
+ const raw = getRawValue(MIGRATION_VERSION_KEY, scope);
686
+ if (raw === undefined) {
687
+ return 0;
688
+ }
689
+
690
+ const parsed = Number.parseInt(raw, 10);
691
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
692
+ }
693
+
694
+ function writeMigrationVersion(scope: StorageScope, version: number): void {
695
+ setRawValue(MIGRATION_VERSION_KEY, String(version), scope);
696
+ }
697
+
698
+ const internals: StorageCoreInternals = {
699
+ getScopedListeners,
700
+ cacheRawValue,
701
+ readCachedRawValue,
702
+ clearScopeRawCache,
703
+ clearPendingDiskWrite,
704
+ clearPendingSecureWrite,
705
+ clearAllPendingDiskWrites: () => {
706
+ pendingDiskWrites.clear();
707
+ },
708
+ clearAllPendingSecureWrites: () => {
709
+ pendingSecureWrites.clear();
710
+ },
711
+ flushDiskWrites,
712
+ flushSecureWrites,
713
+ emitKeyChange,
714
+ hasEventObserver: () => eventObserver !== undefined,
715
+ hasScopeEventListeners: (scope) => storageEvents.hasListeners(scope),
716
+ measureOperation,
717
+ recordMetric,
718
+ getSecureDefaultAccessControl: () => secureDefaultAccessControl,
719
+ setSecureDefaultAccessControl: (level) => {
720
+ secureDefaultAccessControl = level;
721
+ },
722
+ };
723
+
724
+ const adapter = buildAdapter(internals);
725
+
726
+ const storage = {
727
+ subscribe: (
728
+ scope: StorageScope,
729
+ listener: StorageEventListener,
730
+ ): (() => void) => {
731
+ assertValidScope(scope);
732
+ if (scope !== StorageScope.Memory) {
733
+ adapter.ensureScopeSubscription(scope);
734
+ const unsubscribe = storageEvents.subscribe(scope, listener);
735
+ return () => {
736
+ unsubscribe();
737
+ adapter.maybeCleanupScopeSubscription(scope);
738
+ };
739
+ }
740
+ return storageEvents.subscribe(scope, listener);
741
+ },
742
+ subscribeKey: (
743
+ scope: StorageScope,
744
+ key: string,
745
+ listener: StorageEventListener,
746
+ ): (() => void) => {
747
+ assertValidScope(scope);
748
+ if (scope !== StorageScope.Memory) {
749
+ adapter.ensureScopeSubscription(scope);
750
+ const unsubscribe = storageEvents.subscribeKey(scope, key, listener);
751
+ return () => {
752
+ unsubscribe();
753
+ adapter.maybeCleanupScopeSubscription(scope);
754
+ };
755
+ }
756
+ return storageEvents.subscribeKey(scope, key, listener);
757
+ },
758
+ subscribePrefix: (
759
+ scope: StorageScope,
760
+ prefix: string,
761
+ listener: StorageEventListener,
762
+ ): (() => void) => {
763
+ assertValidScope(scope);
764
+ if (scope !== StorageScope.Memory) {
765
+ adapter.ensureScopeSubscription(scope);
766
+ const unsubscribe = storageEvents.subscribePrefix(
767
+ scope,
768
+ prefix,
769
+ listener,
770
+ );
771
+ return () => {
772
+ unsubscribe();
773
+ adapter.maybeCleanupScopeSubscription(scope);
774
+ };
775
+ }
776
+ return storageEvents.subscribePrefix(scope, prefix, listener);
777
+ },
778
+ subscribeNamespace: (
779
+ namespace: string,
780
+ scope: StorageScope,
781
+ listener: StorageEventListener,
782
+ ): (() => void) => {
783
+ return storage.subscribePrefix(scope, prefixKey(namespace, ""), listener);
784
+ },
785
+ setEventObserver: (
786
+ observer?: StorageEventListener,
787
+ options: StorageEventObserverOptions = {},
788
+ ) => {
789
+ eventObserver = observer;
790
+ eventObserverRedactSecureValues = options.redactSecureValues !== false;
791
+ if (observer) {
792
+ adapter.ensureScopeSubscription(StorageScope.Disk);
793
+ adapter.ensureScopeSubscription(StorageScope.Secure);
794
+ return;
795
+ }
796
+ adapter.maybeCleanupScopeSubscription(StorageScope.Disk);
797
+ adapter.maybeCleanupScopeSubscription(StorageScope.Secure);
798
+ },
799
+ clear: (scope: StorageScope) => {
800
+ measureOperation("storage:clear", scope, () => {
801
+ const previousValues = shouldReadPreviousEventValues(scope)
802
+ ? storage.getAll(scope)
803
+ : {};
804
+ if (scope === StorageScope.Memory) {
805
+ memoryStore.clear();
806
+ notifyAllListeners(memoryListeners);
807
+ emitBatchChange(
808
+ scope,
809
+ "clear",
810
+ "memory",
811
+ Object.keys(previousValues).map((key) =>
812
+ createKeyChange(
813
+ scope,
814
+ key,
815
+ previousValues[key],
816
+ undefined,
817
+ "clear",
818
+ "memory",
819
+ ),
820
+ ),
821
+ );
822
+ return;
823
+ }
824
+
825
+ if (scope === StorageScope.Disk) {
826
+ flushDiskWrites();
827
+ pendingDiskWrites.clear();
828
+ }
829
+
830
+ if (scope === StorageScope.Secure) {
831
+ flushSecureWrites();
832
+ pendingSecureWrites.clear();
833
+ }
834
+
835
+ clearScopeRawCache(scope);
836
+ adapter.backend.clear(scope);
837
+ emitBatchChange(
838
+ scope,
839
+ "clear",
840
+ adapter.changeSource,
841
+ Object.keys(previousValues).map((key) =>
842
+ createKeyChange(
843
+ scope,
844
+ key,
845
+ previousValues[key],
846
+ undefined,
847
+ "clear",
848
+ adapter.changeSource,
849
+ ),
850
+ ),
851
+ );
852
+ });
853
+ },
854
+ clearAll: () => {
855
+ measureOperation(
856
+ "storage:clearAll",
857
+ StorageScope.Memory,
858
+ () => {
859
+ storage.clear(StorageScope.Memory);
860
+ storage.clear(StorageScope.Disk);
861
+ storage.clear(StorageScope.Secure);
862
+ },
863
+ 3,
864
+ );
865
+ },
866
+ clearNamespace: (namespace: string, scope: StorageScope) => {
867
+ measureOperation("storage:clearNamespace", scope, () => {
868
+ assertValidScope(scope);
869
+ if (scope === StorageScope.Memory) {
870
+ const affectedKeys = Array.from(memoryStore.keys()).filter((key) =>
871
+ isNamespaced(key, namespace),
872
+ );
873
+ const previousValues = affectedKeys.map((key) => ({
874
+ key,
875
+ value: getEventRawValue(scope, key),
876
+ }));
877
+
878
+ if (affectedKeys.length === 0) {
879
+ return;
880
+ }
881
+
882
+ affectedKeys.forEach((key) => {
883
+ memoryStore.delete(key);
884
+ });
885
+ affectedKeys.forEach((key) =>
886
+ notifyKeyListeners(memoryListeners, key),
887
+ );
888
+ emitBatchChange(
889
+ scope,
890
+ "clearNamespace",
891
+ "memory",
892
+ previousValues.map(({ key, value }) =>
893
+ createKeyChange(
894
+ scope,
895
+ key,
896
+ value,
897
+ undefined,
898
+ "clearNamespace",
899
+ "memory",
900
+ ),
901
+ ),
902
+ );
903
+ return;
904
+ }
905
+
906
+ const keyPrefix = prefixKey(namespace, "");
907
+ const previousValues = shouldReadPreviousEventValues(scope)
908
+ ? storage.getByPrefix(keyPrefix, scope)
909
+ : {};
910
+ if (scope === StorageScope.Disk) {
911
+ flushDiskWrites();
912
+ }
913
+ if (scope === StorageScope.Secure) {
914
+ flushSecureWrites();
915
+ }
916
+
917
+ const scopeCache = getScopeRawCache(scope);
918
+ for (const key of scopeCache.keys()) {
919
+ if (isNamespaced(key, namespace)) {
920
+ scopeCache.delete(key);
921
+ }
922
+ }
923
+ adapter.backend.removeByPrefix(keyPrefix, scope);
924
+ emitBatchChange(
925
+ scope,
926
+ "clearNamespace",
927
+ adapter.changeSource,
928
+ Object.keys(previousValues).map((key) =>
929
+ createKeyChange(
930
+ scope,
931
+ key,
932
+ previousValues[key],
933
+ undefined,
934
+ "clearNamespace",
935
+ adapter.changeSource,
936
+ ),
937
+ ),
938
+ );
939
+ });
940
+ },
941
+ clearBiometric: () => {
942
+ measureOperation("storage:clearBiometric", StorageScope.Secure, () => {
943
+ adapter.backend.clearSecureBiometric();
944
+ });
945
+ },
946
+ has: (key: string, scope: StorageScope): boolean => {
947
+ return measureOperation("storage:has", scope, () => {
948
+ assertValidScope(scope);
949
+ if (scope === StorageScope.Memory) {
950
+ return memoryStore.has(key);
951
+ }
952
+ if (scope === StorageScope.Disk) {
953
+ flushDiskWrites();
954
+ }
955
+ if (scope === StorageScope.Secure) {
956
+ flushSecureWrites();
957
+ }
958
+ return adapter.backend.has(key, scope);
959
+ });
960
+ },
961
+ getAllKeys: (scope: StorageScope): string[] => {
962
+ return measureOperation("storage:getAllKeys", scope, () => {
963
+ assertValidScope(scope);
964
+ if (scope === StorageScope.Memory) {
965
+ return Array.from(memoryStore.keys());
966
+ }
967
+ if (scope === StorageScope.Disk) {
968
+ flushDiskWrites();
969
+ }
970
+ if (scope === StorageScope.Secure) {
971
+ flushSecureWrites();
972
+ }
973
+ return adapter.backend.getAllKeys(scope);
974
+ });
975
+ },
976
+ getKeysByPrefix: (prefix: string, scope: StorageScope): string[] => {
977
+ return measureOperation("storage:getKeysByPrefix", scope, () => {
978
+ assertValidScope(scope);
979
+ if (scope === StorageScope.Memory) {
980
+ return Array.from(memoryStore.keys()).filter((key) =>
981
+ key.startsWith(prefix),
982
+ );
983
+ }
984
+ if (scope === StorageScope.Disk) {
985
+ flushDiskWrites();
986
+ }
987
+ if (scope === StorageScope.Secure) {
988
+ flushSecureWrites();
989
+ }
990
+ return adapter.backend.getKeysByPrefix(prefix, scope);
991
+ });
992
+ },
993
+ getByPrefix: (
994
+ prefix: string,
995
+ scope: StorageScope,
996
+ ): Record<string, string> => {
997
+ return measureOperation("storage:getByPrefix", scope, () => {
998
+ const result: Record<string, string> = {};
999
+ const keys = storage.getKeysByPrefix(prefix, scope);
1000
+ if (keys.length === 0) {
1001
+ return result;
1002
+ }
1003
+
1004
+ if (scope === StorageScope.Memory) {
1005
+ keys.forEach((key) => {
1006
+ const value = memoryStore.get(key);
1007
+ if (typeof value === "string") {
1008
+ result[key] = value;
1009
+ }
1010
+ });
1011
+ return result;
1012
+ }
1013
+
1014
+ if (scope === StorageScope.Disk) {
1015
+ flushDiskWrites();
1016
+ }
1017
+ if (scope === StorageScope.Secure) {
1018
+ flushSecureWrites();
1019
+ }
1020
+ const values = adapter.backend.getBatch(keys, scope);
1021
+ keys.forEach((key, idx) => {
1022
+ const value = values[idx];
1023
+ if (value !== undefined) {
1024
+ result[key] = value;
1025
+ }
1026
+ });
1027
+ return result;
1028
+ });
1029
+ },
1030
+ getAll: (scope: StorageScope): Record<string, string> => {
1031
+ return measureOperation("storage:getAll", scope, () => {
1032
+ assertValidScope(scope);
1033
+ const result: Record<string, string> = {};
1034
+ if (scope === StorageScope.Memory) {
1035
+ for (const key of memoryStore.keys()) {
1036
+ const value = memoryStore.get(key);
1037
+ if (typeof value === "string") result[key] = value;
1038
+ }
1039
+ return result;
1040
+ }
1041
+ if (scope === StorageScope.Disk) {
1042
+ flushDiskWrites();
1043
+ }
1044
+ if (scope === StorageScope.Secure) {
1045
+ flushSecureWrites();
1046
+ }
1047
+ const keys = adapter.backend.getAllKeys(scope);
1048
+ if (keys.length === 0) return result;
1049
+ const values = adapter.backend.getBatch(keys, scope);
1050
+ keys.forEach((key, idx) => {
1051
+ const val = values[idx];
1052
+ if (val !== undefined) result[key] = val;
1053
+ });
1054
+ return result;
1055
+ });
1056
+ },
1057
+ export: (
1058
+ scope: StorageScope,
1059
+ options: StorageExportOptions = {},
1060
+ ): Record<string, string> => {
1061
+ if (
1062
+ scope === StorageScope.Secure &&
1063
+ options.includeSecureValues !== true
1064
+ ) {
1065
+ throw new Error(
1066
+ "NitroStorage: exporting Secure scope exposes raw secret values. Pass { includeSecureValues: true } or use exportSecureUnsafe().",
1067
+ );
1068
+ }
1069
+ return measureOperation("storage:export", scope, () =>
1070
+ storage.getAll(scope),
1071
+ );
1072
+ },
1073
+ exportSecureUnsafe: (): Record<string, string> => {
1074
+ return measureOperation(
1075
+ "storage:exportSecureUnsafe",
1076
+ StorageScope.Secure,
1077
+ () => storage.getAll(StorageScope.Secure),
1078
+ );
1079
+ },
1080
+ size: (scope: StorageScope): number => {
1081
+ return measureOperation("storage:size", scope, () => {
1082
+ assertValidScope(scope);
1083
+ if (scope === StorageScope.Memory) {
1084
+ return memoryStore.size;
1085
+ }
1086
+ if (scope === StorageScope.Disk) {
1087
+ flushDiskWrites();
1088
+ }
1089
+ if (scope === StorageScope.Secure) {
1090
+ flushSecureWrites();
1091
+ }
1092
+ return adapter.backend.size(scope);
1093
+ });
1094
+ },
1095
+ setDiskWritesAsync: (enabled: boolean) => {
1096
+ measureOperation("storage:setDiskWritesAsync", StorageScope.Disk, () => {
1097
+ diskWritesAsync = enabled;
1098
+ if (!enabled) {
1099
+ flushDiskWrites();
1100
+ }
1101
+ });
1102
+ },
1103
+ flushDiskWrites: () => {
1104
+ measureOperation("storage:flushDiskWrites", StorageScope.Disk, () => {
1105
+ flushDiskWrites();
1106
+ });
1107
+ },
1108
+ flushSecureWrites: () => {
1109
+ measureOperation("storage:flushSecureWrites", StorageScope.Secure, () => {
1110
+ flushSecureWrites();
1111
+ });
1112
+ },
1113
+ setMetricsObserver: (observer?: StorageMetricsObserver) => {
1114
+ metricsObserver = observer;
1115
+ },
1116
+ getMetricsSnapshot: (): Record<string, StorageMetricSummary> => {
1117
+ const snapshot: Record<string, StorageMetricSummary> = {};
1118
+ metricsCounters.forEach((value, key) => {
1119
+ snapshot[key] = {
1120
+ count: value.count,
1121
+ totalDurationMs: value.totalDurationMs,
1122
+ avgDurationMs:
1123
+ value.count === 0 ? 0 : value.totalDurationMs / value.count,
1124
+ maxDurationMs: value.maxDurationMs,
1125
+ };
1126
+ });
1127
+ return snapshot;
1128
+ },
1129
+ resetMetrics: () => {
1130
+ metricsCounters.clear();
1131
+ },
1132
+ getSecureMetadata: (key: string): SecureStorageMetadata => {
1133
+ return measureOperation(
1134
+ "storage:getSecureMetadata",
1135
+ StorageScope.Secure,
1136
+ () => {
1137
+ flushSecureWrites();
1138
+ const profile = adapter.getSecureMetadataProfile();
1139
+ const biometricProtected = adapter.backend.hasSecureBiometric(key);
1140
+ const exists =
1141
+ biometricProtected || adapter.backend.has(key, StorageScope.Secure);
1142
+ let kind: SecureStorageMetadata["kind"] = "missing";
1143
+ if (exists) {
1144
+ kind = biometricProtected ? "biometric" : "secure";
1145
+ }
1146
+
1147
+ return {
1148
+ key,
1149
+ exists,
1150
+ kind,
1151
+ backend: profile.backend,
1152
+ encrypted: profile.encrypted,
1153
+ hardwareBacked: profile.hardwareBacked,
1154
+ biometricProtected,
1155
+ valueExposed: false,
1156
+ };
1157
+ },
1158
+ );
1159
+ },
1160
+ getAllSecureMetadata: (): SecureStorageMetadata[] => {
1161
+ return measureOperation(
1162
+ "storage:getAllSecureMetadata",
1163
+ StorageScope.Secure,
1164
+ () => {
1165
+ flushSecureWrites();
1166
+ return adapter.backend
1167
+ .getAllKeys(StorageScope.Secure)
1168
+ .map((key) => storage.getSecureMetadata(key));
1169
+ },
1170
+ );
1171
+ },
1172
+ getString: (key: string, scope: StorageScope): string | undefined => {
1173
+ return measureOperation("storage:getString", scope, () => {
1174
+ return getRawValue(key, scope);
1175
+ });
1176
+ },
1177
+ setString: (key: string, value: string, scope: StorageScope): void => {
1178
+ measureOperation("storage:setString", scope, () => {
1179
+ setRawValue(key, value, scope);
1180
+ });
1181
+ },
1182
+ deleteString: (key: string, scope: StorageScope): void => {
1183
+ measureOperation("storage:deleteString", scope, () => {
1184
+ removeRawValue(key, scope);
1185
+ });
1186
+ },
1187
+ import: (data: Record<string, string>, scope: StorageScope): void => {
1188
+ const keys = Object.keys(data);
1189
+ measureOperation(
1190
+ "storage:import",
1191
+ scope,
1192
+ () => {
1193
+ assertValidScope(scope);
1194
+ if (keys.length === 0) return;
1195
+ const values = keys.map((k) => data[k]!);
1196
+ const changes = keys.map((key, index) =>
1197
+ createKeyChange(
1198
+ scope,
1199
+ key,
1200
+ getEventRawValue(scope, key),
1201
+ values[index],
1202
+ "import",
1203
+ scope === StorageScope.Memory ? "memory" : adapter.changeSource,
1204
+ ),
1205
+ );
1206
+
1207
+ if (scope === StorageScope.Memory) {
1208
+ keys.forEach((key, index) => {
1209
+ memoryStore.set(key, values[index]);
1210
+ });
1211
+ keys.forEach((key) => notifyKeyListeners(memoryListeners, key));
1212
+ emitBatchChange(scope, "import", "memory", changes);
1213
+ return;
1214
+ }
1215
+
1216
+ if (scope === StorageScope.Secure) {
1217
+ flushSecureWrites();
1218
+ adapter.backend.setSecureAccessControl(secureDefaultAccessControl);
1219
+ }
1220
+ if (scope === StorageScope.Disk && adapter.flushDiskWritesOnImport) {
1221
+ flushDiskWrites();
1222
+ }
1223
+
1224
+ adapter.backend.setBatch(keys, values, scope);
1225
+ keys.forEach((key, index) =>
1226
+ cacheRawValue(scope, key, values[index]),
1227
+ );
1228
+ emitBatchChange(scope, "import", adapter.changeSource, changes);
1229
+ },
1230
+ keys.length,
1231
+ );
1232
+ },
1233
+ };
1234
+
1235
+ function createStorageItem<T = undefined>(
1236
+ config: StorageItemConfig<T>,
1237
+ ): StorageItem<T> {
1238
+ const storageKey = prefixKey(config.namespace, config.key);
1239
+ const serialize = config.serialize ?? defaultSerialize;
1240
+ const deserialize = config.deserialize ?? defaultDeserialize;
1241
+ const isMemory = config.scope === StorageScope.Memory;
1242
+ const resolvedBiometricLevel =
1243
+ config.scope === StorageScope.Secure
1244
+ ? (config.biometricLevel ??
1245
+ (config.biometric === true
1246
+ ? BiometricLevel.BiometryOnly
1247
+ : BiometricLevel.None))
1248
+ : BiometricLevel.None;
1249
+ const isBiometric = resolvedBiometricLevel !== BiometricLevel.None;
1250
+ const secureAccessControl = config.accessControl;
1251
+ const validate = config.validate;
1252
+ const onValidationError = config.onValidationError;
1253
+ const expiration = config.expiration;
1254
+ const onExpired = config.onExpired;
1255
+ const expirationTtlMs = expiration?.ttlMs;
1256
+ const memoryExpiration =
1257
+ expiration && isMemory ? new Map<string, number>() : null;
1258
+ const readCache = !isMemory && config.readCache === true;
1259
+ const coalesceDiskWrites =
1260
+ config.scope === StorageScope.Disk && config.coalesceDiskWrites === true;
1261
+ const coalesceSecureWrites =
1262
+ config.scope === StorageScope.Secure &&
1263
+ config.coalesceSecureWrites === true &&
1264
+ !isBiometric;
1265
+ const defaultValue = config.defaultValue as T;
1266
+ const nonMemoryScope: NonMemoryScope | null =
1267
+ config.scope === StorageScope.Disk
1268
+ ? StorageScope.Disk
1269
+ : config.scope === StorageScope.Secure
1270
+ ? StorageScope.Secure
1271
+ : null;
1272
+
1273
+ if (expiration && expiration.ttlMs <= 0) {
1274
+ throw new Error("expiration.ttlMs must be greater than 0.");
1275
+ }
1276
+ if (config.scope === StorageScope.Secure) {
1277
+ assertBiometricLevel(resolvedBiometricLevel);
1278
+ if (secureAccessControl !== undefined) {
1279
+ assertAccessControlLevel(secureAccessControl);
1280
+ }
1281
+ }
1282
+
1283
+ const listeners = new Set<() => void>();
1284
+ let unsubscribe: (() => void) | null = null;
1285
+ let lastRaw: unknown = undefined;
1286
+ let lastValue: T | undefined;
1287
+ let hasLastValue = false;
1288
+ let lastExpiresAt: number | null | undefined = undefined;
1289
+
1290
+ const invalidateParsedCache = () => {
1291
+ lastRaw = undefined;
1292
+ lastValue = undefined;
1293
+ hasLastValue = false;
1294
+ lastExpiresAt = undefined;
1295
+ };
1296
+
1297
+ const ensureSubscription = () => {
1298
+ if (unsubscribe) {
1299
+ return;
1300
+ }
1301
+
1302
+ const listener = () => {
1303
+ invalidateParsedCache();
1304
+ listeners.forEach((callback) => callback());
1305
+ };
1306
+
1307
+ if (isMemory) {
1308
+ unsubscribe = addKeyListener(memoryListeners, storageKey, listener);
1309
+ return;
1310
+ }
1311
+
1312
+ adapter.ensureScopeSubscription(nonMemoryScope!);
1313
+ unsubscribe = addKeyListener(
1314
+ getScopedListeners(nonMemoryScope!),
1315
+ storageKey,
1316
+ listener,
1317
+ );
1318
+ };
1319
+
1320
+ const readStoredRaw = (): unknown => {
1321
+ if (isMemory) {
1322
+ if (memoryExpiration) {
1323
+ const expiresAt = memoryExpiration.get(storageKey);
1324
+ if (expiresAt !== undefined && expiresAt <= Date.now()) {
1325
+ memoryExpiration.delete(storageKey);
1326
+ memoryStore.delete(storageKey);
1327
+ notifyKeyListeners(memoryListeners, storageKey);
1328
+ onExpired?.(storageKey);
1329
+ return undefined;
1330
+ }
1331
+ }
1332
+ return memoryStore.get(storageKey);
1333
+ }
1334
+
1335
+ if (nonMemoryScope === StorageScope.Disk) {
1336
+ const pending = pendingDiskWrites.get(storageKey);
1337
+ if (pending !== undefined) {
1338
+ return pending.value;
1339
+ }
1340
+ }
1341
+
1342
+ if (nonMemoryScope === StorageScope.Secure && !isBiometric) {
1343
+ const pending = pendingSecureWrites.get(storageKey);
1344
+ if (pending !== undefined) {
1345
+ return pending.value;
1346
+ }
1347
+ }
1348
+
1349
+ if (readCache) {
1350
+ const cache = getScopeRawCache(nonMemoryScope!);
1351
+ const cached = cache.get(storageKey);
1352
+ if (cached !== undefined || cache.has(storageKey)) {
1353
+ return cached;
1354
+ }
1355
+ }
1356
+
1357
+ if (isBiometric) {
1358
+ return adapter.backend.getSecureBiometric(storageKey);
1359
+ }
1360
+
1361
+ const raw = adapter.backend.get(storageKey, config.scope);
1362
+ cacheRawValue(nonMemoryScope!, storageKey, raw);
1363
+ return raw;
1364
+ };
1365
+
1366
+ const writeStoredRaw = (rawValue: string): void => {
1367
+ const oldValue = undefined;
1368
+ if (isBiometric) {
1369
+ adapter.backend.setSecureBiometricWithLevel(
1370
+ storageKey,
1371
+ rawValue,
1372
+ resolvedBiometricLevel,
1373
+ );
1374
+ emitKeyChange(
1375
+ config.scope,
1376
+ storageKey,
1377
+ oldValue,
1378
+ rawValue,
1379
+ "set",
1380
+ adapter.changeSource,
1381
+ );
1382
+ return;
1383
+ }
1384
+
1385
+ cacheRawValue(nonMemoryScope!, storageKey, rawValue);
1386
+
1387
+ if (nonMemoryScope === StorageScope.Disk) {
1388
+ if (coalesceDiskWrites || diskWritesAsync) {
1389
+ scheduleDiskWrite(storageKey, rawValue);
1390
+ emitKeyChange(
1391
+ config.scope,
1392
+ storageKey,
1393
+ oldValue,
1394
+ rawValue,
1395
+ "set",
1396
+ adapter.changeSource,
1397
+ );
1398
+ return;
1399
+ }
1400
+
1401
+ clearPendingDiskWrite(storageKey);
1402
+ }
1403
+
1404
+ if (coalesceSecureWrites) {
1405
+ scheduleSecureWrite(
1406
+ storageKey,
1407
+ rawValue,
1408
+ secureAccessControl ?? secureDefaultAccessControl,
1409
+ );
1410
+ emitKeyChange(
1411
+ config.scope,
1412
+ storageKey,
1413
+ oldValue,
1414
+ rawValue,
1415
+ "set",
1416
+ adapter.changeSource,
1417
+ );
1418
+ return;
1419
+ }
1420
+
1421
+ if (nonMemoryScope === StorageScope.Secure) {
1422
+ clearPendingSecureWrite(storageKey);
1423
+ if (adapter.applyAccessControlOnSecureRawWrite) {
1424
+ adapter.backend.setSecureAccessControl(
1425
+ secureAccessControl ?? secureDefaultAccessControl,
1426
+ );
1427
+ }
1428
+ }
1429
+
1430
+ adapter.backend.set(storageKey, rawValue, config.scope);
1431
+ emitKeyChange(
1432
+ config.scope,
1433
+ storageKey,
1434
+ oldValue,
1435
+ rawValue,
1436
+ "set",
1437
+ adapter.changeSource,
1438
+ );
1439
+ };
1440
+
1441
+ const removeStoredRaw = (): void => {
1442
+ const oldValue = getEventRawValue(config.scope, storageKey);
1443
+ if (isBiometric) {
1444
+ adapter.backend.deleteSecureBiometric(storageKey);
1445
+ emitKeyChange(
1446
+ config.scope,
1447
+ storageKey,
1448
+ oldValue,
1449
+ undefined,
1450
+ "remove",
1451
+ adapter.changeSource,
1452
+ );
1453
+ return;
1454
+ }
1455
+
1456
+ cacheRawValue(nonMemoryScope!, storageKey, undefined);
1457
+
1458
+ if (nonMemoryScope === StorageScope.Disk) {
1459
+ if (coalesceDiskWrites || diskWritesAsync) {
1460
+ scheduleDiskWrite(storageKey, undefined);
1461
+ emitKeyChange(
1462
+ config.scope,
1463
+ storageKey,
1464
+ oldValue,
1465
+ undefined,
1466
+ "remove",
1467
+ adapter.changeSource,
1468
+ );
1469
+ return;
1470
+ }
1471
+
1472
+ clearPendingDiskWrite(storageKey);
1473
+ }
1474
+
1475
+ if (coalesceSecureWrites) {
1476
+ scheduleSecureWrite(
1477
+ storageKey,
1478
+ undefined,
1479
+ secureAccessControl ?? secureDefaultAccessControl,
1480
+ );
1481
+ emitKeyChange(
1482
+ config.scope,
1483
+ storageKey,
1484
+ oldValue,
1485
+ undefined,
1486
+ "remove",
1487
+ adapter.changeSource,
1488
+ );
1489
+ return;
1490
+ }
1491
+
1492
+ if (nonMemoryScope === StorageScope.Secure) {
1493
+ clearPendingSecureWrite(storageKey);
1494
+ }
1495
+
1496
+ adapter.backend.remove(storageKey, config.scope);
1497
+ emitKeyChange(
1498
+ config.scope,
1499
+ storageKey,
1500
+ oldValue,
1501
+ undefined,
1502
+ "remove",
1503
+ adapter.changeSource,
1504
+ );
1505
+ };
1506
+
1507
+ const writeValueWithoutValidation = (value: T): void => {
1508
+ if (isMemory) {
1509
+ const oldValue = getEventRawValue(config.scope, storageKey);
1510
+ if (memoryExpiration) {
1511
+ memoryExpiration.set(storageKey, Date.now() + (expirationTtlMs ?? 0));
1512
+ }
1513
+ memoryStore.set(storageKey, value);
1514
+ notifyKeyListeners(memoryListeners, storageKey);
1515
+ emitKeyChange(
1516
+ config.scope,
1517
+ storageKey,
1518
+ oldValue,
1519
+ typeof value === "string" ? value : undefined,
1520
+ "set",
1521
+ "memory",
1522
+ );
1523
+ return;
1524
+ }
1525
+
1526
+ const serialized = serialize(value);
1527
+ if (expiration) {
1528
+ const envelope: StoredEnvelope = {
1529
+ __nitroStorageEnvelope: true,
1530
+ expiresAt: Date.now() + expiration.ttlMs,
1531
+ payload: serialized,
1532
+ };
1533
+ writeStoredRaw(JSON.stringify(envelope));
1534
+ return;
1535
+ }
1536
+
1537
+ writeStoredRaw(serialized);
1538
+ };
1539
+
1540
+ const resolveInvalidValue = (invalidValue: unknown): T => {
1541
+ if (onValidationError) {
1542
+ return onValidationError(invalidValue);
1543
+ }
1544
+
1545
+ return defaultValue;
1546
+ };
1547
+
1548
+ const ensureValidatedValue = (
1549
+ candidate: unknown,
1550
+ hadStoredValue: boolean,
1551
+ ): T => {
1552
+ if (!validate || validate(candidate)) {
1553
+ return candidate as T;
1554
+ }
1555
+
1556
+ const resolved = resolveInvalidValue(candidate);
1557
+ if (validate && !validate(resolved)) {
1558
+ return defaultValue;
1559
+ }
1560
+ if (hadStoredValue) {
1561
+ writeValueWithoutValidation(resolved);
1562
+ }
1563
+ return resolved;
1564
+ };
1565
+
1566
+ const getInternal = (): T => {
1567
+ const raw = readStoredRaw();
1568
+
1569
+ if (!memoryExpiration && raw === lastRaw && hasLastValue) {
1570
+ if (!expiration || lastExpiresAt === null) {
1571
+ return lastValue as T;
1572
+ }
1573
+
1574
+ if (typeof lastExpiresAt === "number") {
1575
+ if (lastExpiresAt > Date.now()) {
1576
+ return lastValue as T;
1577
+ }
1578
+
1579
+ removeStoredRaw();
1580
+ invalidateParsedCache();
1581
+ onExpired?.(storageKey);
1582
+ lastValue = ensureValidatedValue(defaultValue, false);
1583
+ hasLastValue = true;
1584
+ listeners.forEach((cb) => cb());
1585
+ return lastValue;
1586
+ }
1587
+ }
1588
+
1589
+ lastRaw = raw;
1590
+
1591
+ if (raw === undefined) {
1592
+ lastExpiresAt = undefined;
1593
+ lastValue = ensureValidatedValue(defaultValue, false);
1594
+ hasLastValue = true;
1595
+ return lastValue;
1596
+ }
1597
+
1598
+ if (isMemory) {
1599
+ lastExpiresAt = undefined;
1600
+ lastValue = ensureValidatedValue(raw, true);
1601
+ hasLastValue = true;
1602
+ return lastValue;
1603
+ }
1604
+
1605
+ if (typeof raw !== "string") {
1606
+ lastExpiresAt = undefined;
1607
+ lastValue = ensureValidatedValue(defaultValue, false);
1608
+ hasLastValue = true;
1609
+ return lastValue;
1610
+ }
1611
+
1612
+ let deserializableRaw = raw;
1613
+
1614
+ if (expiration) {
1615
+ let envelopeExpiresAt: number | null = null;
1616
+ try {
1617
+ const parsed = JSON.parse(raw) as unknown;
1618
+ if (isStoredEnvelope(parsed)) {
1619
+ envelopeExpiresAt = parsed.expiresAt;
1620
+ if (parsed.expiresAt <= Date.now()) {
1621
+ removeStoredRaw();
1622
+ invalidateParsedCache();
1623
+ onExpired?.(storageKey);
1624
+ lastValue = ensureValidatedValue(defaultValue, false);
1625
+ hasLastValue = true;
1626
+ listeners.forEach((cb) => cb());
1627
+ return lastValue;
1628
+ }
1629
+
1630
+ deserializableRaw = parsed.payload;
1631
+ }
1632
+ } catch {
1633
+ // Keep backward compatibility with legacy raw values.
1634
+ }
1635
+ lastExpiresAt = envelopeExpiresAt;
1636
+ } else {
1637
+ lastExpiresAt = undefined;
1638
+ }
1639
+
1640
+ lastValue = ensureValidatedValue(deserialize(deserializableRaw), true);
1641
+ hasLastValue = true;
1642
+ return lastValue;
1643
+ };
1644
+
1645
+ const getCurrentVersion = (): StorageVersion => {
1646
+ const raw = readStoredRaw();
1647
+ return toVersionToken(raw);
1648
+ };
1649
+
1650
+ const get = (): T =>
1651
+ measureOperation("item:get", config.scope, () => getInternal());
1652
+
1653
+ const getWithVersion = (): VersionedValue<T> =>
1654
+ measureOperation("item:getWithVersion", config.scope, () => ({
1655
+ value: getInternal(),
1656
+ version: getCurrentVersion(),
1657
+ }));
1658
+
1659
+ const set = (valueOrFn: T | ((prev: T) => T)): void => {
1660
+ measureOperation("item:set", config.scope, () => {
1661
+ const newValue = isUpdater(valueOrFn)
1662
+ ? valueOrFn(getInternal())
1663
+ : valueOrFn;
1664
+
1665
+ if (validate && !validate(newValue)) {
1666
+ throw new Error(
1667
+ `Validation failed for key "${storageKey}" in scope "${StorageScope[config.scope]}".`,
1668
+ );
1669
+ }
1670
+
1671
+ invalidateParsedCache();
1672
+ writeValueWithoutValidation(newValue);
1673
+ });
1674
+ };
1675
+
1676
+ const setIfVersion = (
1677
+ version: StorageVersion,
1678
+ valueOrFn: T | ((prev: T) => T),
1679
+ ): boolean =>
1680
+ measureOperation("item:setIfVersion", config.scope, () => {
1681
+ const currentVersion = getCurrentVersion();
1682
+ if (currentVersion !== version) {
1683
+ return false;
1684
+ }
1685
+ set(valueOrFn);
1686
+ return true;
1687
+ });
1688
+
1689
+ const deleteItem = (): void => {
1690
+ measureOperation("item:delete", config.scope, () => {
1691
+ invalidateParsedCache();
1692
+
1693
+ if (isMemory) {
1694
+ const oldValue = getEventRawValue(config.scope, storageKey);
1695
+ if (memoryExpiration) {
1696
+ memoryExpiration.delete(storageKey);
1697
+ }
1698
+ memoryStore.delete(storageKey);
1699
+ notifyKeyListeners(memoryListeners, storageKey);
1700
+ emitKeyChange(
1701
+ config.scope,
1702
+ storageKey,
1703
+ oldValue,
1704
+ undefined,
1705
+ "remove",
1706
+ "memory",
1707
+ );
1708
+ return;
1709
+ }
1710
+
1711
+ removeStoredRaw();
1712
+ });
1713
+ };
1714
+
1715
+ const hasItem = (): boolean =>
1716
+ measureOperation("item:has", config.scope, () => {
1717
+ if (isMemory) return memoryStore.has(storageKey);
1718
+ if (isBiometric) return adapter.backend.hasSecureBiometric(storageKey);
1719
+ if (nonMemoryScope === StorageScope.Disk) {
1720
+ const pending = pendingDiskWrites.get(storageKey);
1721
+ if (pending !== undefined) {
1722
+ return pending.value !== undefined;
1723
+ }
1724
+ }
1725
+ if (nonMemoryScope === StorageScope.Secure) {
1726
+ const pending = pendingSecureWrites.get(storageKey);
1727
+ if (pending !== undefined) {
1728
+ return pending.value !== undefined;
1729
+ }
1730
+ }
1731
+ return adapter.backend.has(storageKey, config.scope);
1732
+ });
1733
+
1734
+ const subscribe = (callback: () => void): (() => void) => {
1735
+ ensureSubscription();
1736
+ listeners.add(callback);
1737
+ return () => {
1738
+ listeners.delete(callback);
1739
+ if (listeners.size === 0 && unsubscribe) {
1740
+ unsubscribe();
1741
+ if (!isMemory) {
1742
+ adapter.maybeCleanupScopeSubscription(nonMemoryScope!);
1743
+ }
1744
+ unsubscribe = null;
1745
+ }
1746
+ };
1747
+ };
1748
+
1749
+ const subscribeSelector = <TSelected>(
1750
+ selector: (value: T) => TSelected,
1751
+ listener: StorageSelectorListener<TSelected>,
1752
+ options: StorageSelectorSubscribeOptions<TSelected> = {},
1753
+ ): (() => void) => {
1754
+ const isEqual = options.isEqual ?? Object.is;
1755
+ let currentValue = selector(getInternal());
1756
+
1757
+ if (options.fireImmediately === true) {
1758
+ listener(currentValue, currentValue);
1759
+ }
1760
+
1761
+ return subscribe(() => {
1762
+ const nextValue = selector(getInternal());
1763
+ if (isEqual(currentValue, nextValue)) {
1764
+ return;
1765
+ }
1766
+
1767
+ const previousValue = currentValue;
1768
+ currentValue = nextValue;
1769
+ listener(nextValue, previousValue);
1770
+ });
1771
+ };
1772
+
1773
+ const storageItem: StorageItemInternal<T> = {
1774
+ get,
1775
+ getWithVersion,
1776
+ set,
1777
+ setIfVersion,
1778
+ delete: deleteItem,
1779
+ has: hasItem,
1780
+ subscribe,
1781
+ subscribeSelector,
1782
+ serialize,
1783
+ deserialize,
1784
+ _triggerListeners: () => {
1785
+ invalidateParsedCache();
1786
+ listeners.forEach((listener) => listener());
1787
+ },
1788
+ _invalidateParsedCacheOnly: () => {
1789
+ invalidateParsedCache();
1790
+ },
1791
+ _hasValidation: validate !== undefined,
1792
+ _hasExpiration: expiration !== undefined,
1793
+ _readCacheEnabled: readCache,
1794
+ _isBiometric: isBiometric,
1795
+ _biometricLevel: resolvedBiometricLevel,
1796
+ _defaultValue: defaultValue,
1797
+ ...(secureAccessControl !== undefined
1798
+ ? { _secureAccessControl: secureAccessControl }
1799
+ : {}),
1800
+ scope: config.scope,
1801
+ key: storageKey,
1802
+ };
1803
+
1804
+ return storageItem;
1805
+ }
1806
+
1807
+ function getBatch<const TItems extends readonly BatchReadItem<unknown>[]>(
1808
+ items: TItems,
1809
+ scope: StorageScope,
1810
+ ): BatchValues<TItems> {
1811
+ return measureOperation(
1812
+ "batch:get",
1813
+ scope,
1814
+ () => {
1815
+ assertBatchScope(items, scope);
1816
+
1817
+ if (scope === StorageScope.Memory) {
1818
+ return items.map((item) => item.get());
1819
+ }
1820
+
1821
+ const useRawBatchPath = items.every((item) =>
1822
+ scope === StorageScope.Secure
1823
+ ? canUseSecureRawBatchPath(item)
1824
+ : canUseRawBatchPath(item),
1825
+ );
1826
+ if (!useRawBatchPath) {
1827
+ return items.map((item) => item.get());
1828
+ }
1829
+
1830
+ const rawValues = new Array<string | undefined>(items.length);
1831
+ const keysToFetch: string[] = [];
1832
+ const keyIndexes: number[] = [];
1833
+
1834
+ items.forEach((item, index) => {
1835
+ if (scope === StorageScope.Disk) {
1836
+ const pending = pendingDiskWrites.get(item.key);
1837
+ if (pending !== undefined) {
1838
+ rawValues[index] = pending.value;
1839
+ return;
1840
+ }
1841
+ }
1842
+
1843
+ if (scope === StorageScope.Secure) {
1844
+ const pending = pendingSecureWrites.get(item.key);
1845
+ if (pending !== undefined) {
1846
+ rawValues[index] = pending.value;
1847
+ return;
1848
+ }
1849
+ }
1850
+
1851
+ if (item._readCacheEnabled === true) {
1852
+ const cache = getScopeRawCache(scope);
1853
+ const cached = cache.get(item.key);
1854
+ if (cached !== undefined || cache.has(item.key)) {
1855
+ rawValues[index] = cached;
1856
+ return;
1857
+ }
1858
+ }
1859
+
1860
+ keysToFetch.push(item.key);
1861
+ keyIndexes.push(index);
1862
+ });
1863
+
1864
+ if (keysToFetch.length > 0) {
1865
+ const fetchedValues = adapter.backend.getBatch(keysToFetch, scope);
1866
+ fetchedValues.forEach((value, index) => {
1867
+ const key = keysToFetch[index];
1868
+ const targetIndex = keyIndexes[index];
1869
+ if (key === undefined || targetIndex === undefined) {
1870
+ return;
1871
+ }
1872
+ rawValues[targetIndex] = value;
1873
+ cacheRawValue(scope, key, value);
1874
+ });
1875
+ }
1876
+
1877
+ return items.map((item, index) => {
1878
+ const raw = rawValues[index];
1879
+ if (raw === undefined) {
1880
+ return asInternal(item as StorageItem<unknown>)._defaultValue;
1881
+ }
1882
+ return item.deserialize(raw);
1883
+ });
1884
+ },
1885
+ items.length,
1886
+ ) as BatchValues<TItems>;
1887
+ }
1888
+
1889
+ function setBatch<T>(
1890
+ items: readonly StorageBatchSetItem<T>[],
1891
+ scope: StorageScope,
1892
+ ): void {
1893
+ measureOperation(
1894
+ "batch:set",
1895
+ scope,
1896
+ () => {
1897
+ assertBatchScope(
1898
+ items.map((batchEntry) => batchEntry.item),
1899
+ scope,
1900
+ );
1901
+
1902
+ if (scope === StorageScope.Memory) {
1903
+ // Determine if any item needs per-item handling (validation or TTL)
1904
+ const needsIndividualSets = items.some(({ item }) => {
1905
+ const internal = asInternal(item as StorageItem<unknown>);
1906
+ return internal._hasValidation || internal._hasExpiration;
1907
+ });
1908
+
1909
+ if (needsIndividualSets) {
1910
+ // Fall back to individual sets to preserve validation and TTL semantics
1911
+ items.forEach(({ item, value }) => item.set(value));
1912
+ return;
1913
+ }
1914
+
1915
+ const changes = items.map(({ item, value }) =>
1916
+ createKeyChange(
1917
+ scope,
1918
+ item.key,
1919
+ getEventRawValue(scope, item.key),
1920
+ typeof value === "string" ? value : undefined,
1921
+ "setBatch",
1922
+ "memory",
1923
+ ),
1924
+ );
1925
+
1926
+ // Atomic write: update all values in memoryStore, invalidate caches, then batch-notify
1927
+ items.forEach(({ item, value }) => {
1928
+ memoryStore.set(item.key, value);
1929
+ asInternal(
1930
+ item as StorageItem<unknown>,
1931
+ )._invalidateParsedCacheOnly();
1932
+ });
1933
+ items.forEach(({ item }) =>
1934
+ notifyKeyListeners(memoryListeners, item.key),
1935
+ );
1936
+ emitBatchChange(scope, "setBatch", "memory", changes);
1937
+ return;
1938
+ }
1939
+
1940
+ if (scope === StorageScope.Secure) {
1941
+ const secureEntries = items.map(({ item, value }) => ({
1942
+ item,
1943
+ value,
1944
+ internal: asInternal(item),
1945
+ }));
1946
+ const canUseSecureBatchPath = secureEntries.every(({ internal }) =>
1947
+ canUseSecureRawBatchPath(internal),
1948
+ );
1949
+ if (!canUseSecureBatchPath) {
1950
+ items.forEach(({ item, value }) => item.set(value));
1951
+ return;
1952
+ }
1953
+
1954
+ flushSecureWrites();
1955
+ const keys = secureEntries.map(({ item }) => item.key);
1956
+ const oldValues = shouldReadPreviousEventValues(scope)
1957
+ ? adapter.backend.getBatch(keys, scope)
1958
+ : [];
1959
+ const groupedByAccessControl = new Map<
1960
+ number,
1961
+ { keys: string[]; values: string[] }
1962
+ >();
1963
+
1964
+ secureEntries.forEach(({ item, value, internal }) => {
1965
+ const accessControl =
1966
+ internal._secureAccessControl ?? secureDefaultAccessControl;
1967
+ const existingGroup = groupedByAccessControl.get(accessControl);
1968
+ const group = existingGroup ?? { keys: [], values: [] };
1969
+ group.keys.push(item.key);
1970
+ group.values.push(item.serialize(value));
1971
+ if (!existingGroup) {
1972
+ groupedByAccessControl.set(accessControl, group);
1973
+ }
1974
+ });
1975
+
1976
+ groupedByAccessControl.forEach((group, accessControl) => {
1977
+ adapter.backend.setSecureAccessControl(accessControl);
1978
+ adapter.backend.setBatch(group.keys, group.values, scope);
1979
+ group.keys.forEach((key, index) =>
1980
+ cacheRawValue(scope, key, group.values[index]),
1981
+ );
1982
+ });
1983
+ emitBatchChange(
1984
+ scope,
1985
+ "setBatch",
1986
+ adapter.changeSource,
1987
+ secureEntries.map(({ item, value }, index) =>
1988
+ createKeyChange(
1989
+ scope,
1990
+ item.key,
1991
+ oldValues[index],
1992
+ item.serialize(value),
1993
+ "setBatch",
1994
+ adapter.changeSource,
1995
+ ),
1996
+ ),
1997
+ );
1998
+ return;
1999
+ }
2000
+
2001
+ flushDiskWrites();
2002
+
2003
+ const useRawBatchPath = items.every(({ item }) =>
2004
+ canUseRawBatchPath(asInternal(item)),
2005
+ );
2006
+ if (!useRawBatchPath) {
2007
+ items.forEach(({ item, value }) => item.set(value));
2008
+ return;
2009
+ }
2010
+
2011
+ const keys = items.map((entry) => entry.item.key);
2012
+ const values = items.map((entry) => entry.item.serialize(entry.value));
2013
+ const oldValues = shouldReadPreviousEventValues(scope)
2014
+ ? adapter.backend.getBatch(keys, scope)
2015
+ : [];
2016
+
2017
+ adapter.backend.setBatch(keys, values, scope);
2018
+ keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
2019
+ emitBatchChange(
2020
+ scope,
2021
+ "setBatch",
2022
+ adapter.changeSource,
2023
+ keys.map((key, index) =>
2024
+ createKeyChange(
2025
+ scope,
2026
+ key,
2027
+ oldValues[index],
2028
+ values[index],
2029
+ "setBatch",
2030
+ adapter.changeSource,
2031
+ ),
2032
+ ),
2033
+ );
2034
+ },
2035
+ items.length,
2036
+ );
2037
+ }
2038
+
2039
+ function removeBatch(
2040
+ items: readonly BatchRemoveItem[],
2041
+ scope: StorageScope,
2042
+ ): void {
2043
+ measureOperation(
2044
+ "batch:remove",
2045
+ scope,
2046
+ () => {
2047
+ assertBatchScope(items, scope);
2048
+
2049
+ if (scope === StorageScope.Memory) {
2050
+ const changes = items.map((item) =>
2051
+ createKeyChange(
2052
+ scope,
2053
+ item.key,
2054
+ getEventRawValue(scope, item.key),
2055
+ undefined,
2056
+ "removeBatch",
2057
+ "memory",
2058
+ ),
2059
+ );
2060
+ items.forEach((item) => item.delete());
2061
+ emitBatchChange(scope, "removeBatch", "memory", changes);
2062
+ return;
2063
+ }
2064
+
2065
+ const keys = items.map((item) => item.key);
2066
+ if (scope === StorageScope.Disk) {
2067
+ flushDiskWrites();
2068
+ }
2069
+ if (scope === StorageScope.Secure) {
2070
+ flushSecureWrites();
2071
+ }
2072
+ const oldValues = shouldReadPreviousEventValues(scope)
2073
+ ? adapter.backend.getBatch(keys, scope)
2074
+ : [];
2075
+ adapter.backend.removeBatch(keys, scope);
2076
+ keys.forEach((key) => cacheRawValue(scope, key, undefined));
2077
+ emitBatchChange(
2078
+ scope,
2079
+ "removeBatch",
2080
+ adapter.changeSource,
2081
+ keys.map((key, index) =>
2082
+ createKeyChange(
2083
+ scope,
2084
+ key,
2085
+ oldValues[index],
2086
+ undefined,
2087
+ "removeBatch",
2088
+ adapter.changeSource,
2089
+ ),
2090
+ ),
2091
+ );
2092
+ },
2093
+ items.length,
2094
+ );
2095
+ }
2096
+
2097
+ function registerMigration(version: number, migration: Migration): void {
2098
+ if (!Number.isInteger(version) || version <= 0) {
2099
+ throw new Error("Migration version must be a positive integer.");
2100
+ }
2101
+
2102
+ if (registeredMigrations.has(version)) {
2103
+ throw new Error(`Migration version ${version} is already registered.`);
2104
+ }
2105
+
2106
+ registeredMigrations.set(version, migration);
2107
+ }
2108
+
2109
+ function migrateToLatest(scope: StorageScope = StorageScope.Disk): number {
2110
+ return measureOperation("migration:run", scope, () => {
2111
+ assertValidScope(scope);
2112
+ const currentVersion = readMigrationVersion(scope);
2113
+ const versions = Array.from(registeredMigrations.keys())
2114
+ .filter((version) => version > currentVersion)
2115
+ .sort((a, b) => a - b);
2116
+
2117
+ let appliedVersion = currentVersion;
2118
+ const context: MigrationContext = {
2119
+ scope,
2120
+ getRaw: (key) => getRawValue(key, scope),
2121
+ setRaw: (key, value) => setRawValue(key, value, scope),
2122
+ removeRaw: (key) => removeRawValue(key, scope),
2123
+ };
2124
+
2125
+ versions.forEach((version) => {
2126
+ const migration = registeredMigrations.get(version);
2127
+ if (!migration) {
2128
+ return;
2129
+ }
2130
+ migration(context);
2131
+ appliedVersion = version;
2132
+ });
2133
+
2134
+ if (appliedVersion !== currentVersion) {
2135
+ writeMigrationVersion(scope, appliedVersion);
2136
+ }
2137
+
2138
+ return appliedVersion;
2139
+ });
2140
+ }
2141
+
2142
+ function runTransaction<T>(
2143
+ scope: StorageScope,
2144
+ transaction: (context: TransactionContext) => T,
2145
+ ): T {
2146
+ return measureOperation("transaction:run", scope, () => {
2147
+ assertValidScope(scope);
2148
+ if (scope === StorageScope.Disk) {
2149
+ flushDiskWrites();
2150
+ }
2151
+ if (scope === StorageScope.Secure) {
2152
+ flushSecureWrites();
2153
+ }
2154
+
2155
+ const NOT_SET = Symbol();
2156
+ const rollback = new Map<string, RollbackRecord>();
2157
+
2158
+ const rememberRollback = (
2159
+ key: string,
2160
+ item?: Pick<StorageItem<unknown>, "key" | "scope">,
2161
+ ) => {
2162
+ if (rollback.has(key)) {
2163
+ return;
2164
+ }
2165
+ if (scope === StorageScope.Memory) {
2166
+ rollback.set(key, {
2167
+ kind: "memory",
2168
+ value: memoryStore.has(key) ? memoryStore.get(key) : NOT_SET,
2169
+ });
2170
+ } else {
2171
+ const internal = item
2172
+ ? (item as StorageItemInternal<unknown>)
2173
+ : undefined;
2174
+ if (
2175
+ scope === StorageScope.Secure &&
2176
+ internal?._isBiometric === true
2177
+ ) {
2178
+ rollback.set(key, {
2179
+ kind: "biometric",
2180
+ value: adapter.backend.getSecureBiometric(key),
2181
+ level: internal._biometricLevel,
2182
+ });
2183
+ return;
2184
+ }
2185
+ rollback.set(key, {
2186
+ kind: "raw",
2187
+ value: getRawValue(key, scope),
2188
+ ...(scope === StorageScope.Secure &&
2189
+ internal?._secureAccessControl !== undefined
2190
+ ? { accessControl: internal._secureAccessControl }
2191
+ : {}),
2192
+ });
2193
+ }
2194
+ };
2195
+
2196
+ const tx: TransactionContext = {
2197
+ scope,
2198
+ getRaw: (key) => getRawValue(key, scope),
2199
+ setRaw: (key, value) => {
2200
+ rememberRollback(key);
2201
+ setRawValue(key, value, scope);
2202
+ },
2203
+ removeRaw: (key) => {
2204
+ rememberRollback(key);
2205
+ removeRawValue(key, scope);
2206
+ },
2207
+ getItem: (item) => {
2208
+ assertBatchScope([item], scope);
2209
+ return item.get();
2210
+ },
2211
+ setItem: (item, value) => {
2212
+ assertBatchScope([item], scope);
2213
+ rememberRollback(item.key, item);
2214
+ item.set(value);
2215
+ },
2216
+ removeItem: (item) => {
2217
+ assertBatchScope([item], scope);
2218
+ rememberRollback(item.key, item);
2219
+ item.delete();
2220
+ },
2221
+ };
2222
+
2223
+ try {
2224
+ return transaction(tx);
2225
+ } catch (error) {
2226
+ const rollbackEntries = Array.from(rollback.entries()).reverse();
2227
+ if (scope === StorageScope.Memory) {
2228
+ rollbackEntries.forEach(([key, record]) => {
2229
+ if (record.value === NOT_SET) {
2230
+ memoryStore.delete(key);
2231
+ } else {
2232
+ memoryStore.set(key, record.value);
2233
+ }
2234
+ notifyKeyListeners(memoryListeners, key);
2235
+ });
2236
+ } else {
2237
+ const groupedKeysToSet = new Map<
2238
+ AccessControl,
2239
+ { keys: string[]; values: string[] }
2240
+ >();
2241
+ const keysToRemove: string[] = [];
2242
+
2243
+ rollbackEntries.forEach(([key, record]) => {
2244
+ if (record.kind === "biometric") {
2245
+ if (record.value === undefined) {
2246
+ adapter.backend.deleteSecureBiometric(key);
2247
+ } else {
2248
+ adapter.backend.setSecureBiometricWithLevel(
2249
+ key,
2250
+ record.value,
2251
+ record.level,
2252
+ );
2253
+ }
2254
+ return;
2255
+ }
2256
+ if (record.kind !== "raw") {
2257
+ return;
2258
+ }
2259
+ if (record.value === undefined) {
2260
+ keysToRemove.push(key);
2261
+ } else {
2262
+ const accessControl =
2263
+ record.accessControl ?? secureDefaultAccessControl;
2264
+ const existingGroup = groupedKeysToSet.get(accessControl);
2265
+ const group = existingGroup ?? { keys: [], values: [] };
2266
+ group.keys.push(key);
2267
+ group.values.push(record.value);
2268
+ if (!existingGroup) {
2269
+ groupedKeysToSet.set(accessControl, group);
2270
+ }
2271
+ }
2272
+ });
2273
+
2274
+ if (scope === StorageScope.Disk) {
2275
+ flushDiskWrites();
2276
+ }
2277
+ if (scope === StorageScope.Secure) {
2278
+ flushSecureWrites();
2279
+ }
2280
+ groupedKeysToSet.forEach((group, accessControl) => {
2281
+ if (scope === StorageScope.Secure) {
2282
+ adapter.backend.setSecureAccessControl(accessControl);
2283
+ }
2284
+ adapter.backend.setBatch(group.keys, group.values, scope);
2285
+ group.keys.forEach((key, index) =>
2286
+ cacheRawValue(scope, key, group.values[index]),
2287
+ );
2288
+ });
2289
+ if (keysToRemove.length > 0) {
2290
+ adapter.backend.removeBatch(keysToRemove, scope);
2291
+ keysToRemove.forEach((key) => cacheRawValue(scope, key, undefined));
2292
+ }
2293
+ }
2294
+ throw error;
2295
+ }
2296
+ });
2297
+ }
2298
+
2299
+ function createSecureAuthStorage<K extends string>(
2300
+ config: SecureAuthStorageConfig<K>,
2301
+ options?: { namespace?: string },
2302
+ ): Record<K, StorageItem<string>> {
2303
+ const ns = options?.namespace ?? "auth";
2304
+ const result: Partial<Record<K, StorageItem<string>>> = {};
2305
+
2306
+ for (const key of typedKeys(config)) {
2307
+ const itemConfig = config[key];
2308
+ const expirationConfig =
2309
+ itemConfig.ttlMs !== undefined
2310
+ ? { ttlMs: itemConfig.ttlMs }
2311
+ : undefined;
2312
+ result[key] = createStorageItem<string>({
2313
+ key,
2314
+ scope: StorageScope.Secure,
2315
+ defaultValue: "",
2316
+ namespace: ns,
2317
+ ...(itemConfig.biometric !== undefined
2318
+ ? { biometric: itemConfig.biometric }
2319
+ : {}),
2320
+ ...(itemConfig.biometricLevel !== undefined
2321
+ ? { biometricLevel: itemConfig.biometricLevel }
2322
+ : {}),
2323
+ ...(itemConfig.accessControl !== undefined
2324
+ ? { accessControl: itemConfig.accessControl }
2325
+ : {}),
2326
+ ...(expirationConfig !== undefined
2327
+ ? { expiration: expirationConfig }
2328
+ : {}),
2329
+ });
2330
+ }
2331
+
2332
+ return result as Record<K, StorageItem<string>>;
2333
+ }
2334
+
2335
+ return {
2336
+ storage,
2337
+ createStorageItem,
2338
+ getBatch,
2339
+ setBatch,
2340
+ removeBatch,
2341
+ registerMigration,
2342
+ migrateToLatest,
2343
+ runTransaction,
2344
+ createSecureAuthStorage,
2345
+ internals,
2346
+ };
2347
+ }
2348
+
2349
+ export type StorageCore = ReturnType<typeof createStorageCore>;