react-native-nitro-storage 0.4.4 → 0.4.5

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.
Files changed (39) hide show
  1. package/README.md +107 -7
  2. package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +61 -10
  3. package/ios/IOSStorageAdapterCpp.mm +44 -14
  4. package/lib/commonjs/index.js +221 -5
  5. package/lib/commonjs/index.js.map +1 -1
  6. package/lib/commonjs/index.web.js +444 -202
  7. package/lib/commonjs/index.web.js.map +1 -1
  8. package/lib/commonjs/indexeddb-backend.js +129 -7
  9. package/lib/commonjs/indexeddb-backend.js.map +1 -1
  10. package/lib/commonjs/storage-runtime.js +41 -0
  11. package/lib/commonjs/storage-runtime.js.map +1 -0
  12. package/lib/commonjs/web-storage-backend.js +90 -0
  13. package/lib/commonjs/web-storage-backend.js.map +1 -0
  14. package/lib/module/index.js +213 -5
  15. package/lib/module/index.js.map +1 -1
  16. package/lib/module/index.web.js +436 -202
  17. package/lib/module/index.web.js.map +1 -1
  18. package/lib/module/indexeddb-backend.js +129 -7
  19. package/lib/module/indexeddb-backend.js.map +1 -1
  20. package/lib/module/storage-runtime.js +36 -0
  21. package/lib/module/storage-runtime.js.map +1 -0
  22. package/lib/module/web-storage-backend.js +86 -0
  23. package/lib/module/web-storage-backend.js.map +1 -0
  24. package/lib/typescript/index.d.ts +11 -7
  25. package/lib/typescript/index.d.ts.map +1 -1
  26. package/lib/typescript/index.web.d.ts +12 -8
  27. package/lib/typescript/index.web.d.ts.map +1 -1
  28. package/lib/typescript/indexeddb-backend.d.ts +6 -2
  29. package/lib/typescript/indexeddb-backend.d.ts.map +1 -1
  30. package/lib/typescript/storage-runtime.d.ts +16 -0
  31. package/lib/typescript/storage-runtime.d.ts.map +1 -0
  32. package/lib/typescript/web-storage-backend.d.ts +30 -0
  33. package/lib/typescript/web-storage-backend.d.ts.map +1 -0
  34. package/package.json +1 -1
  35. package/src/index.ts +264 -20
  36. package/src/index.web.ts +597 -245
  37. package/src/indexeddb-backend.ts +147 -6
  38. package/src/storage-runtime.ts +94 -0
  39. package/src/web-storage-backend.ts +129 -0
package/src/index.web.ts CHANGED
@@ -11,9 +11,32 @@ import {
11
11
  prefixKey,
12
12
  isNamespaced,
13
13
  } from "./internal";
14
+ import {
15
+ createLocalStorageWebBackend,
16
+ type WebDiskStorageBackend,
17
+ type WebSecureStorageBackend,
18
+ type WebStorageBackend,
19
+ type WebStorageChangeEvent,
20
+ } from "./web-storage-backend";
21
+ import {
22
+ getStorageErrorCode,
23
+ isLockedStorageErrorCode,
24
+ type StorageCapabilities,
25
+ type StorageErrorCode,
26
+ } from "./storage-runtime";
14
27
 
15
28
  export { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
16
29
  export { migrateFromMMKV } from "./migration";
30
+ export {
31
+ getStorageErrorCode,
32
+ type StorageCapabilities,
33
+ type StorageErrorCode,
34
+ } from "./storage-runtime";
35
+ export type {
36
+ WebStorageBackend,
37
+ WebStorageChangeEvent,
38
+ WebStorageScope,
39
+ } from "./web-storage-backend";
17
40
 
18
41
  export type Validator<T> = (value: unknown) => value is T;
19
42
  export type ExpirationConfig = {
@@ -37,14 +60,6 @@ export type StorageMetricSummary = {
37
60
  avgDurationMs: number;
38
61
  maxDurationMs: number;
39
62
  };
40
- export type WebSecureStorageBackend = {
41
- getItem: (key: string) => string | null;
42
- setItem: (key: string, value: string) => void;
43
- removeItem: (key: string) => void;
44
- clear: () => void;
45
- getAllKeys: () => string[];
46
- };
47
-
48
63
  export type MigrationContext = {
49
64
  scope: StorageScope;
50
65
  getRaw: (key: string) => string | undefined;
@@ -91,19 +106,15 @@ function typedKeys<K extends string, V>(record: Record<K, V>): K[] {
91
106
  return Object.keys(record) as K[];
92
107
  }
93
108
  type NonMemoryScope = StorageScope.Disk | StorageScope.Secure;
109
+ type PendingDiskWrite = {
110
+ key: string;
111
+ value: string | undefined;
112
+ };
94
113
  type PendingSecureWrite = {
95
114
  key: string;
96
115
  value: string | undefined;
97
116
  accessControl?: AccessControl;
98
117
  };
99
- type BrowserStorageLike = {
100
- setItem: (key: string, value: string) => void;
101
- getItem: (key: string) => string | null;
102
- removeItem: (key: string) => void;
103
- clear: () => void;
104
- key: (index: number) => string | null;
105
- readonly length: number;
106
- };
107
118
 
108
119
  const registeredMigrations = new Map<number, Migration>();
109
120
  const runMicrotask =
@@ -112,6 +123,10 @@ const runMicrotask =
112
123
  : (task: () => void) => {
113
124
  Promise.resolve().then(task);
114
125
  };
126
+ const now =
127
+ typeof performance !== "undefined" && typeof performance.now === "function"
128
+ ? () => performance.now()
129
+ : () => Date.now();
115
130
 
116
131
  export interface Storage {
117
132
  name: string;
@@ -161,14 +176,16 @@ const webScopeKeyIndex = new Map<NonMemoryScope, Set<string>>([
161
176
  [StorageScope.Secure, new Set()],
162
177
  ]);
163
178
  const hydratedWebScopeKeyIndex = new Set<NonMemoryScope>();
179
+ const pendingDiskWrites = new Map<string, PendingDiskWrite>();
180
+ let diskFlushScheduled = false;
181
+ let diskWritesAsync = false;
164
182
  const pendingSecureWrites = new Map<string, PendingSecureWrite>();
165
183
  let secureFlushScheduled = false;
166
184
  let secureDefaultAccessControl: AccessControl = AccessControl.WhenUnlocked;
167
185
  const SECURE_WEB_PREFIX = "__secure_";
168
186
  const BIOMETRIC_WEB_PREFIX = "__bio_";
169
187
  let hasWarnedAboutWebBiometricFallback = false;
170
- let hasWebStorageEventSubscription = false;
171
- let webStorageSubscriberCount = 0;
188
+ let hasWindowStorageEventSubscription = false;
172
189
  let metricsObserver: StorageMetricsObserver | undefined;
173
190
  const metricsCounters = new Map<
174
191
  string,
@@ -205,69 +222,86 @@ function measureOperation<T>(
205
222
  if (!metricsObserver) {
206
223
  return fn();
207
224
  }
208
- const start = Date.now();
225
+ const start = now();
209
226
  try {
210
227
  return fn();
211
228
  } finally {
212
- recordMetric(operation, scope, Date.now() - start, keysCount);
229
+ recordMetric(operation, scope, now() - start, keysCount);
213
230
  }
214
231
  }
215
232
 
216
- function createLocalStorageWebSecureBackend(): WebSecureStorageBackend {
217
- return {
218
- getItem: (key) => globalThis.localStorage?.getItem(key) ?? null,
219
- setItem: (key, value) => globalThis.localStorage?.setItem(key, value),
220
- removeItem: (key) => globalThis.localStorage?.removeItem(key),
221
- clear: () => globalThis.localStorage?.clear(),
222
- getAllKeys: () => {
223
- const storage = globalThis.localStorage;
224
- if (!storage) return [];
225
- const keys: string[] = [];
226
- for (let index = 0; index < storage.length; index += 1) {
227
- const key = storage.key(index);
228
- if (key) {
229
- keys.push(key);
230
- }
231
- }
232
- return keys;
233
- },
234
- };
233
+ function createDefaultDiskBackend(): WebDiskStorageBackend {
234
+ return createLocalStorageWebBackend({
235
+ name: "localStorage:disk",
236
+ includeKey: (key) =>
237
+ !key.startsWith(SECURE_WEB_PREFIX) &&
238
+ !key.startsWith(BIOMETRIC_WEB_PREFIX),
239
+ });
235
240
  }
236
241
 
242
+ function createDefaultSecureBackend(): WebSecureStorageBackend {
243
+ return createLocalStorageWebBackend({
244
+ name: "localStorage:secure",
245
+ includeKey: (key) =>
246
+ key.startsWith(SECURE_WEB_PREFIX) || key.startsWith(BIOMETRIC_WEB_PREFIX),
247
+ });
248
+ }
249
+
250
+ let webDiskStorageBackend: WebDiskStorageBackend | undefined =
251
+ createDefaultDiskBackend();
237
252
  let webSecureStorageBackend: WebSecureStorageBackend | undefined =
238
- createLocalStorageWebSecureBackend();
253
+ createDefaultSecureBackend();
254
+ const externalSyncUnsubscribers = new Map<NonMemoryScope, () => void>();
255
+
256
+ function getBackendName(
257
+ scope: NonMemoryScope,
258
+ backend: WebStorageBackend | undefined,
259
+ ): string {
260
+ const scopeName = scope === StorageScope.Disk ? "disk" : "secure";
261
+ return backend?.name ?? `web:${scopeName}`;
262
+ }
239
263
 
240
- let cachedSecureBrowserStorage: BrowserStorageLike | undefined;
241
- let cachedSecureBackendRef: WebSecureStorageBackend | undefined;
264
+ function createWebStorageError(
265
+ scope: NonMemoryScope,
266
+ operation: string,
267
+ error: unknown,
268
+ backend: WebStorageBackend | undefined,
269
+ ): Error {
270
+ const backendName = getBackendName(scope, backend);
271
+ const message =
272
+ error instanceof Error ? error.message : String(error ?? "Unknown error");
273
+ return new Error(
274
+ `NitroStorage(web): ${operation} failed for ${backendName}: ${message}`,
275
+ );
276
+ }
242
277
 
243
- function getBrowserStorage(scope: number): BrowserStorageLike | undefined {
244
- if (scope === StorageScope.Disk) {
245
- return globalThis.localStorage;
278
+ function withWebBackendOperation<T>(
279
+ scope: NonMemoryScope,
280
+ operation: string,
281
+ fn: (backend: WebStorageBackend) => T,
282
+ ): T {
283
+ const backend =
284
+ scope === StorageScope.Disk
285
+ ? webDiskStorageBackend
286
+ : webSecureStorageBackend;
287
+ if (!backend) {
288
+ throw new Error(
289
+ `NitroStorage(web): ${operation} failed because no ${scope === StorageScope.Disk ? "disk" : "secure"} backend is configured.`,
290
+ );
246
291
  }
247
- if (scope === StorageScope.Secure) {
248
- if (!webSecureStorageBackend) {
249
- return undefined;
250
- }
251
- if (
252
- cachedSecureBackendRef === webSecureStorageBackend &&
253
- cachedSecureBrowserStorage
254
- ) {
255
- return cachedSecureBrowserStorage;
256
- }
257
- cachedSecureBackendRef = webSecureStorageBackend;
258
- cachedSecureBrowserStorage = {
259
- setItem: (key, value) => webSecureStorageBackend!.setItem(key, value),
260
- getItem: (key) => webSecureStorageBackend!.getItem(key) ?? null,
261
- removeItem: (key) => webSecureStorageBackend!.removeItem(key),
262
- clear: () => webSecureStorageBackend!.clear(),
263
- key: (index) => webSecureStorageBackend!.getAllKeys()[index] ?? null,
264
- get length() {
265
- return webSecureStorageBackend!.getAllKeys().length;
266
- },
267
- };
268
- return cachedSecureBrowserStorage;
292
+
293
+ try {
294
+ ensureExternalSyncSubscriptions();
295
+ return fn(backend);
296
+ } catch (error) {
297
+ throw createWebStorageError(scope, operation, error, backend);
269
298
  }
270
- return undefined;
299
+ }
300
+
301
+ function getWebBackend(scope: NonMemoryScope): WebStorageBackend | undefined {
302
+ return scope === StorageScope.Disk
303
+ ? webDiskStorageBackend
304
+ : webSecureStorageBackend;
271
305
  }
272
306
 
273
307
  function toSecureStorageKey(key: string): string {
@@ -295,32 +329,22 @@ function hydrateWebScopeKeyIndex(scope: NonMemoryScope): void {
295
329
  return;
296
330
  }
297
331
 
298
- const storage = getBrowserStorage(scope);
332
+ const backend = getWebBackend(scope);
299
333
  const keyIndex = getWebScopeKeyIndex(scope);
300
334
  keyIndex.clear();
301
- if (storage) {
302
- for (let index = 0; index < storage.length; index += 1) {
303
- const key = storage.key(index);
304
- if (!key) {
305
- continue;
306
- }
307
- if (scope === StorageScope.Disk) {
308
- if (
309
- !key.startsWith(SECURE_WEB_PREFIX) &&
310
- !key.startsWith(BIOMETRIC_WEB_PREFIX)
311
- ) {
312
- keyIndex.add(key);
313
- }
314
- continue;
315
- }
335
+ const keys = backend?.getAllKeys() ?? [];
336
+ for (const key of keys) {
337
+ if (scope === StorageScope.Disk) {
338
+ keyIndex.add(key);
339
+ continue;
340
+ }
316
341
 
317
- if (key.startsWith(SECURE_WEB_PREFIX)) {
318
- keyIndex.add(fromSecureStorageKey(key));
319
- continue;
320
- }
321
- if (key.startsWith(BIOMETRIC_WEB_PREFIX)) {
322
- keyIndex.add(fromBiometricStorageKey(key));
323
- }
342
+ if (key.startsWith(SECURE_WEB_PREFIX)) {
343
+ keyIndex.add(fromSecureStorageKey(key));
344
+ continue;
345
+ }
346
+ if (key.startsWith(BIOMETRIC_WEB_PREFIX)) {
347
+ keyIndex.add(fromBiometricStorageKey(key));
324
348
  }
325
349
  }
326
350
  hydratedWebScopeKeyIndex.add(scope);
@@ -331,37 +355,39 @@ function ensureWebScopeKeyIndex(scope: NonMemoryScope): Set<string> {
331
355
  return getWebScopeKeyIndex(scope);
332
356
  }
333
357
 
334
- function handleWebStorageEvent(event: StorageEvent): void {
335
- const key = event.key;
358
+ function applyExternalChangeEvent(
359
+ scope: NonMemoryScope,
360
+ key: string | null,
361
+ newValue: string | null,
362
+ ): void {
336
363
  if (key === null) {
337
- clearScopeRawCache(StorageScope.Disk);
338
- clearScopeRawCache(StorageScope.Secure);
339
- ensureWebScopeKeyIndex(StorageScope.Disk).clear();
340
- ensureWebScopeKeyIndex(StorageScope.Secure).clear();
341
- notifyAllListeners(getScopedListeners(StorageScope.Disk));
342
- notifyAllListeners(getScopedListeners(StorageScope.Secure));
364
+ clearScopeRawCache(scope);
365
+ ensureWebScopeKeyIndex(scope).clear();
366
+ notifyAllListeners(getScopedListeners(scope));
343
367
  return;
344
368
  }
345
369
 
346
- if (key.startsWith(SECURE_WEB_PREFIX)) {
370
+ if (scope === StorageScope.Secure && key.startsWith(SECURE_WEB_PREFIX)) {
347
371
  const plainKey = fromSecureStorageKey(key);
348
- if (event.newValue === null) {
372
+ if (newValue === null) {
349
373
  ensureWebScopeKeyIndex(StorageScope.Secure).delete(plainKey);
350
374
  cacheRawValue(StorageScope.Secure, plainKey, undefined);
351
375
  } else {
352
376
  ensureWebScopeKeyIndex(StorageScope.Secure).add(plainKey);
353
- cacheRawValue(StorageScope.Secure, plainKey, event.newValue);
377
+ cacheRawValue(StorageScope.Secure, plainKey, newValue);
354
378
  }
355
379
  notifyKeyListeners(getScopedListeners(StorageScope.Secure), plainKey);
356
380
  return;
357
381
  }
358
382
 
359
- if (key.startsWith(BIOMETRIC_WEB_PREFIX)) {
383
+ if (scope === StorageScope.Secure && key.startsWith(BIOMETRIC_WEB_PREFIX)) {
360
384
  const plainKey = fromBiometricStorageKey(key);
361
- if (event.newValue === null) {
385
+ if (newValue === null) {
362
386
  if (
363
- getBrowserStorage(StorageScope.Secure)?.getItem(
364
- toSecureStorageKey(plainKey),
387
+ withWebBackendOperation(
388
+ StorageScope.Secure,
389
+ "external-sync:getItem",
390
+ (backend) => backend.getItem(toSecureStorageKey(plainKey)),
365
391
  ) === null
366
392
  ) {
367
393
  ensureWebScopeKeyIndex(StorageScope.Secure).delete(plainKey);
@@ -369,44 +395,74 @@ function handleWebStorageEvent(event: StorageEvent): void {
369
395
  cacheRawValue(StorageScope.Secure, plainKey, undefined);
370
396
  } else {
371
397
  ensureWebScopeKeyIndex(StorageScope.Secure).add(plainKey);
372
- cacheRawValue(StorageScope.Secure, plainKey, event.newValue);
398
+ cacheRawValue(StorageScope.Secure, plainKey, newValue);
373
399
  }
374
400
  notifyKeyListeners(getScopedListeners(StorageScope.Secure), plainKey);
375
401
  return;
376
402
  }
377
403
 
378
- if (event.newValue === null) {
379
- ensureWebScopeKeyIndex(StorageScope.Disk).delete(key);
380
- cacheRawValue(StorageScope.Disk, key, undefined);
404
+ if (newValue === null) {
405
+ ensureWebScopeKeyIndex(scope).delete(key);
406
+ cacheRawValue(scope, key, undefined);
381
407
  } else {
382
- ensureWebScopeKeyIndex(StorageScope.Disk).add(key);
383
- cacheRawValue(StorageScope.Disk, key, event.newValue);
408
+ ensureWebScopeKeyIndex(scope).add(key);
409
+ cacheRawValue(scope, key, newValue);
384
410
  }
385
- notifyKeyListeners(getScopedListeners(StorageScope.Disk), key);
411
+ notifyKeyListeners(getScopedListeners(scope), key);
386
412
  }
387
413
 
388
- function ensureWebStorageEventSubscription(): void {
389
- webStorageSubscriberCount += 1;
414
+ function handleWebStorageEvent(event: StorageEvent): void {
415
+ const key = event.key;
416
+ if (key === null) {
417
+ applyExternalChangeEvent(StorageScope.Disk, null, null);
418
+ applyExternalChangeEvent(StorageScope.Secure, null, null);
419
+ return;
420
+ }
421
+
390
422
  if (
391
- webStorageSubscriberCount === 1 &&
392
- typeof window !== "undefined" &&
393
- typeof window.addEventListener === "function"
423
+ key.startsWith(SECURE_WEB_PREFIX) ||
424
+ key.startsWith(BIOMETRIC_WEB_PREFIX)
394
425
  ) {
395
- window.addEventListener("storage", handleWebStorageEvent);
396
- hasWebStorageEventSubscription = true;
426
+ applyExternalChangeEvent(StorageScope.Secure, key, event.newValue);
427
+ return;
397
428
  }
429
+
430
+ applyExternalChangeEvent(StorageScope.Disk, key, event.newValue);
398
431
  }
399
432
 
400
- function maybeCleanupWebStorageSubscription(): void {
401
- webStorageSubscriberCount = Math.max(0, webStorageSubscriberCount - 1);
433
+ function subscribeToBackendChanges(scope: NonMemoryScope): void {
434
+ if (externalSyncUnsubscribers.has(scope)) {
435
+ return;
436
+ }
437
+
438
+ const backend = getWebBackend(scope);
439
+ if (!backend?.subscribe) {
440
+ return;
441
+ }
442
+
443
+ const unsubscribe = backend.subscribe((event: WebStorageChangeEvent) => {
444
+ applyExternalChangeEvent(scope, event.key, event.newValue);
445
+ });
446
+ externalSyncUnsubscribers.set(scope, unsubscribe);
447
+ }
448
+
449
+ function resetBackendChangeSubscription(scope: NonMemoryScope): void {
450
+ externalSyncUnsubscribers.get(scope)?.();
451
+ externalSyncUnsubscribers.delete(scope);
452
+ }
453
+
454
+ function ensureExternalSyncSubscriptions(): void {
402
455
  if (
403
- webStorageSubscriberCount === 0 &&
404
- hasWebStorageEventSubscription &&
405
- typeof window !== "undefined"
456
+ !hasWindowStorageEventSubscription &&
457
+ typeof window !== "undefined" &&
458
+ typeof window.addEventListener === "function"
406
459
  ) {
407
- window.removeEventListener("storage", handleWebStorageEvent);
408
- hasWebStorageEventSubscription = false;
460
+ window.addEventListener("storage", handleWebStorageEvent);
461
+ hasWindowStorageEventSubscription = true;
409
462
  }
463
+
464
+ subscribeToBackendChanges(StorageScope.Disk);
465
+ subscribeToBackendChanges(StorageScope.Secure);
410
466
  }
411
467
 
412
468
  function getScopedListeners(scope: NonMemoryScope): KeyListenerRegistry {
@@ -487,14 +543,58 @@ function readPendingSecureWrite(key: string): string | undefined {
487
543
  return pendingSecureWrites.get(key)?.value;
488
544
  }
489
545
 
546
+ function readPendingDiskWrite(key: string): string | undefined {
547
+ return pendingDiskWrites.get(key)?.value;
548
+ }
549
+
550
+ function hasPendingDiskWrite(key: string): boolean {
551
+ return pendingDiskWrites.has(key);
552
+ }
553
+
490
554
  function hasPendingSecureWrite(key: string): boolean {
491
555
  return pendingSecureWrites.has(key);
492
556
  }
493
557
 
558
+ function clearPendingDiskWrite(key: string): void {
559
+ pendingDiskWrites.delete(key);
560
+ }
561
+
494
562
  function clearPendingSecureWrite(key: string): void {
495
563
  pendingSecureWrites.delete(key);
496
564
  }
497
565
 
566
+ function flushDiskWrites(): void {
567
+ diskFlushScheduled = false;
568
+
569
+ if (pendingDiskWrites.size === 0) {
570
+ return;
571
+ }
572
+
573
+ const writes = Array.from(pendingDiskWrites.values());
574
+ pendingDiskWrites.clear();
575
+
576
+ const keysToSet: string[] = [];
577
+ const valuesToSet: string[] = [];
578
+ const keysToRemove: string[] = [];
579
+
580
+ writes.forEach(({ key, value }) => {
581
+ if (value === undefined) {
582
+ keysToRemove.push(key);
583
+ return;
584
+ }
585
+
586
+ keysToSet.push(key);
587
+ valuesToSet.push(value);
588
+ });
589
+
590
+ if (keysToSet.length > 0) {
591
+ WebStorage.setBatch(keysToSet, valuesToSet, StorageScope.Disk);
592
+ }
593
+ if (keysToRemove.length > 0) {
594
+ WebStorage.removeBatch(keysToRemove, StorageScope.Disk);
595
+ }
596
+ }
597
+
498
598
  function flushSecureWrites(): void {
499
599
  secureFlushScheduled = false;
500
600
 
@@ -535,6 +635,15 @@ function flushSecureWrites(): void {
535
635
  }
536
636
  }
537
637
 
638
+ function scheduleDiskWrite(key: string, value: string | undefined): void {
639
+ pendingDiskWrites.set(key, { key, value });
640
+ if (diskFlushScheduled) {
641
+ return;
642
+ }
643
+ diskFlushScheduled = true;
644
+ runMicrotask(flushDiskWrites);
645
+ }
646
+
538
647
  function scheduleSecureWrite(
539
648
  key: string,
540
649
  value: string | undefined,
@@ -557,131 +666,142 @@ const WebStorage: Storage = {
557
666
  equals: (other) => other === WebStorage,
558
667
  dispose: () => {},
559
668
  set: (key: string, value: string, scope: number) => {
560
- const storage = getBrowserStorage(scope);
561
- if (!storage) {
669
+ if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
562
670
  return;
563
671
  }
564
672
  const storageKey =
565
673
  scope === StorageScope.Secure ? toSecureStorageKey(key) : key;
566
- storage.setItem(storageKey, value);
567
- if (scope === StorageScope.Disk || scope === StorageScope.Secure) {
568
- ensureWebScopeKeyIndex(scope).add(key);
569
- notifyKeyListeners(getScopedListeners(scope), key);
570
- }
674
+ withWebBackendOperation(scope, "set", (backend) => {
675
+ backend.setItem(storageKey, value);
676
+ });
677
+ ensureWebScopeKeyIndex(scope).add(key);
678
+ notifyKeyListeners(getScopedListeners(scope), key);
571
679
  },
572
680
  get: (key: string, scope: number) => {
573
- const storage = getBrowserStorage(scope);
681
+ if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
682
+ return undefined;
683
+ }
574
684
  const storageKey =
575
685
  scope === StorageScope.Secure ? toSecureStorageKey(key) : key;
576
- return storage?.getItem(storageKey) ?? undefined;
686
+ const value = withWebBackendOperation(scope, "get", (backend) =>
687
+ backend.getItem(storageKey),
688
+ );
689
+ return value ?? undefined;
577
690
  },
578
691
  remove: (key: string, scope: number) => {
579
- const storage = getBrowserStorage(scope);
580
- if (!storage) {
692
+ if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
581
693
  return;
582
694
  }
583
695
  if (scope === StorageScope.Secure) {
584
- storage.removeItem(toSecureStorageKey(key));
585
- storage.removeItem(toBiometricStorageKey(key));
696
+ withWebBackendOperation(scope, "remove", (backend) => {
697
+ if (backend.removeMany) {
698
+ backend.removeMany([
699
+ toSecureStorageKey(key),
700
+ toBiometricStorageKey(key),
701
+ ]);
702
+ return;
703
+ }
704
+ backend.removeItem(toSecureStorageKey(key));
705
+ backend.removeItem(toBiometricStorageKey(key));
706
+ });
586
707
  } else {
587
- storage.removeItem(key);
588
- }
589
- if (scope === StorageScope.Disk || scope === StorageScope.Secure) {
590
- ensureWebScopeKeyIndex(scope).delete(key);
591
- notifyKeyListeners(getScopedListeners(scope), key);
708
+ withWebBackendOperation(scope, "remove", (backend) => {
709
+ backend.removeItem(key);
710
+ });
592
711
  }
712
+ ensureWebScopeKeyIndex(scope).delete(key);
713
+ notifyKeyListeners(getScopedListeners(scope), key);
593
714
  },
594
715
  clear: (scope: number) => {
595
- const storage = getBrowserStorage(scope);
596
- if (!storage) {
716
+ if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
597
717
  return;
598
718
  }
599
- if (scope === StorageScope.Secure) {
600
- const keysToRemove: string[] = [];
601
- for (let i = 0; i < storage.length; i++) {
602
- const key = storage.key(i);
603
- if (
604
- key?.startsWith(SECURE_WEB_PREFIX) ||
605
- key?.startsWith(BIOMETRIC_WEB_PREFIX)
606
- ) {
607
- keysToRemove.push(key);
608
- }
609
- }
610
- keysToRemove.forEach((key) => storage.removeItem(key));
611
- } else if (scope === StorageScope.Disk) {
612
- const keysToRemove: string[] = [];
613
- for (let i = 0; i < storage.length; i++) {
614
- const key = storage.key(i);
615
- if (
616
- key &&
617
- !key.startsWith(SECURE_WEB_PREFIX) &&
618
- !key.startsWith(BIOMETRIC_WEB_PREFIX)
619
- ) {
620
- keysToRemove.push(key);
621
- }
622
- }
623
- keysToRemove.forEach((key) => storage.removeItem(key));
624
- } else {
625
- storage.clear();
626
- }
627
- if (scope === StorageScope.Disk || scope === StorageScope.Secure) {
628
- ensureWebScopeKeyIndex(scope).clear();
629
- notifyAllListeners(getScopedListeners(scope));
630
- }
719
+ withWebBackendOperation(scope, "clear", (backend) => {
720
+ backend.clear();
721
+ });
722
+ ensureWebScopeKeyIndex(scope).clear();
723
+ notifyAllListeners(getScopedListeners(scope));
631
724
  },
632
725
  setBatch: (keys: string[], values: string[], scope: number) => {
633
- const storage = getBrowserStorage(scope);
634
- if (!storage) {
726
+ if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
635
727
  return;
636
728
  }
637
729
 
730
+ const entries: (readonly [string, string])[] = [];
638
731
  keys.forEach((key, index) => {
639
732
  const value = values[index];
640
733
  if (value === undefined) {
641
734
  return;
642
735
  }
643
- const storageKey =
644
- scope === StorageScope.Secure ? toSecureStorageKey(key) : key;
645
- storage.setItem(storageKey, value);
736
+ entries.push([
737
+ scope === StorageScope.Secure ? toSecureStorageKey(key) : key,
738
+ value,
739
+ ]);
646
740
  });
647
- if (scope === StorageScope.Disk || scope === StorageScope.Secure) {
648
- const keyIndex = ensureWebScopeKeyIndex(scope);
649
- keys.forEach((key) => keyIndex.add(key));
650
- const listeners = getScopedListeners(scope);
651
- keys.forEach((key) => notifyKeyListeners(listeners, key));
652
- }
741
+ withWebBackendOperation(scope, "setBatch", (backend) => {
742
+ if (backend.setMany) {
743
+ backend.setMany(entries);
744
+ return;
745
+ }
746
+ entries.forEach(([storageKey, value]) => {
747
+ backend.setItem(storageKey, value);
748
+ });
749
+ });
750
+ const keyIndex = ensureWebScopeKeyIndex(scope);
751
+ keys.forEach((key) => keyIndex.add(key));
752
+ const listeners = getScopedListeners(scope);
753
+ keys.forEach((key) => notifyKeyListeners(listeners, key));
653
754
  },
654
755
  getBatch: (keys: string[], scope: number) => {
655
- const storage = getBrowserStorage(scope);
656
- return keys.map((key) => {
657
- const storageKey =
658
- scope === StorageScope.Secure ? toSecureStorageKey(key) : key;
659
- return storage?.getItem(storageKey) ?? undefined;
756
+ if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
757
+ return keys.map(() => undefined);
758
+ }
759
+ const storageKeys = keys.map((key) =>
760
+ scope === StorageScope.Secure ? toSecureStorageKey(key) : key,
761
+ );
762
+ const values = withWebBackendOperation(scope, "getBatch", (backend) => {
763
+ if (backend.getMany) {
764
+ return backend.getMany(storageKeys);
765
+ }
766
+ return storageKeys.map((storageKey) => backend.getItem(storageKey));
660
767
  });
768
+ return values.map((value) => value ?? undefined);
661
769
  },
662
770
  removeBatch: (keys: string[], scope: number) => {
663
- const storage = getBrowserStorage(scope);
664
- if (!storage) {
771
+ if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
665
772
  return;
666
773
  }
667
774
 
668
775
  if (scope === StorageScope.Secure) {
669
- keys.forEach((key) => {
670
- storage.removeItem(toSecureStorageKey(key));
671
- storage.removeItem(toBiometricStorageKey(key));
776
+ const storageKeys = keys.flatMap((key) => [
777
+ toSecureStorageKey(key),
778
+ toBiometricStorageKey(key),
779
+ ]);
780
+ withWebBackendOperation(scope, "removeBatch", (backend) => {
781
+ if (backend.removeMany) {
782
+ backend.removeMany(storageKeys);
783
+ return;
784
+ }
785
+ storageKeys.forEach((storageKey) => {
786
+ backend.removeItem(storageKey);
787
+ });
672
788
  });
673
789
  } else {
674
- keys.forEach((key) => {
675
- storage.removeItem(key);
790
+ withWebBackendOperation(scope, "removeBatch", (backend) => {
791
+ if (backend.removeMany) {
792
+ backend.removeMany(keys);
793
+ return;
794
+ }
795
+ keys.forEach((key) => {
796
+ backend.removeItem(key);
797
+ });
676
798
  });
677
799
  }
678
800
 
679
- if (scope === StorageScope.Disk || scope === StorageScope.Secure) {
680
- const keyIndex = ensureWebScopeKeyIndex(scope);
681
- keys.forEach((key) => keyIndex.delete(key));
682
- const listeners = getScopedListeners(scope);
683
- keys.forEach((key) => notifyKeyListeners(listeners, key));
684
- }
801
+ const keyIndex = ensureWebScopeKeyIndex(scope);
802
+ keys.forEach((key) => keyIndex.delete(key));
803
+ const listeners = getScopedListeners(scope);
804
+ keys.forEach((key) => notifyKeyListeners(listeners, key));
685
805
  },
686
806
  removeByPrefix: (prefix: string, scope: number) => {
687
807
  if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
@@ -703,14 +823,24 @@ const WebStorage: Storage = {
703
823
  return () => {};
704
824
  },
705
825
  has: (key: string, scope: number) => {
706
- const storage = getBrowserStorage(scope);
707
826
  if (scope === StorageScope.Secure) {
708
827
  return (
709
- storage?.getItem(toSecureStorageKey(key)) !== null ||
710
- storage?.getItem(toBiometricStorageKey(key)) !== null
828
+ withWebBackendOperation(scope, "has", (backend) =>
829
+ backend.getItem(toSecureStorageKey(key)),
830
+ ) !== null ||
831
+ withWebBackendOperation(scope, "has", (backend) =>
832
+ backend.getItem(toBiometricStorageKey(key)),
833
+ ) !== null
711
834
  );
712
835
  }
713
- return storage?.getItem(key) !== null;
836
+ if (scope !== StorageScope.Disk) {
837
+ return false;
838
+ }
839
+ return (
840
+ withWebBackendOperation(scope, "has", (backend) =>
841
+ backend.getItem(key),
842
+ ) !== null
843
+ );
714
844
  },
715
845
  getAllKeys: (scope: number) => {
716
846
  if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
@@ -753,51 +883,85 @@ const WebStorage: Storage = {
753
883
  "[NitroStorage] Biometric storage is not supported on web. Using localStorage.",
754
884
  );
755
885
  }
756
- getBrowserStorage(StorageScope.Secure)?.setItem(
757
- toBiometricStorageKey(key),
758
- value,
886
+ withWebBackendOperation(
887
+ StorageScope.Secure,
888
+ "setSecureBiometric",
889
+ (backend) => backend.setItem(toBiometricStorageKey(key), value),
759
890
  );
760
891
  ensureWebScopeKeyIndex(StorageScope.Secure).add(key);
761
892
  notifyKeyListeners(getScopedListeners(StorageScope.Secure), key);
762
893
  },
763
894
  getSecureBiometric: (key: string) => {
764
- return (
765
- getBrowserStorage(StorageScope.Secure)?.getItem(
766
- toBiometricStorageKey(key),
767
- ) ?? undefined
895
+ const value = withWebBackendOperation(
896
+ StorageScope.Secure,
897
+ "getSecureBiometric",
898
+ (backend) => backend.getItem(toBiometricStorageKey(key)),
768
899
  );
900
+ return value ?? undefined;
769
901
  },
770
902
  deleteSecureBiometric: (key: string) => {
771
- const storage = getBrowserStorage(StorageScope.Secure);
772
- storage?.removeItem(toBiometricStorageKey(key));
773
- if (storage?.getItem(toSecureStorageKey(key)) === null) {
903
+ withWebBackendOperation(
904
+ StorageScope.Secure,
905
+ "deleteSecureBiometric",
906
+ (backend) => backend.removeItem(toBiometricStorageKey(key)),
907
+ );
908
+ if (
909
+ withWebBackendOperation(
910
+ StorageScope.Secure,
911
+ "deleteSecureBiometric:getItem",
912
+ (backend) => backend.getItem(toSecureStorageKey(key)),
913
+ ) === null
914
+ ) {
774
915
  ensureWebScopeKeyIndex(StorageScope.Secure).delete(key);
775
916
  }
776
917
  notifyKeyListeners(getScopedListeners(StorageScope.Secure), key);
777
918
  },
778
919
  hasSecureBiometric: (key: string) => {
779
920
  return (
780
- getBrowserStorage(StorageScope.Secure)?.getItem(
781
- toBiometricStorageKey(key),
921
+ withWebBackendOperation(
922
+ StorageScope.Secure,
923
+ "hasSecureBiometric",
924
+ (backend) => backend.getItem(toBiometricStorageKey(key)),
782
925
  ) !== null
783
926
  );
784
927
  },
785
928
  clearSecureBiometric: () => {
786
- const storage = getBrowserStorage(StorageScope.Secure);
787
- if (!storage) return;
788
- const keysToNotify: string[] = [];
789
- const toRemove: string[] = [];
790
- for (let i = 0; i < storage.length; i++) {
791
- const k = storage.key(i);
792
- if (k?.startsWith(BIOMETRIC_WEB_PREFIX)) {
793
- toRemove.push(k);
794
- keysToNotify.push(fromBiometricStorageKey(k));
795
- }
929
+ const storageKeys = withWebBackendOperation(
930
+ StorageScope.Secure,
931
+ "clearSecureBiometric:getAllKeys",
932
+ (backend) => backend.getAllKeys(),
933
+ );
934
+ const keysToNotify = storageKeys
935
+ .filter((key) => key.startsWith(BIOMETRIC_WEB_PREFIX))
936
+ .map((key) => fromBiometricStorageKey(key));
937
+ if (keysToNotify.length === 0) {
938
+ return;
796
939
  }
797
- toRemove.forEach((k) => storage.removeItem(k));
940
+ withWebBackendOperation(
941
+ StorageScope.Secure,
942
+ "clearSecureBiometric",
943
+ (backend) => {
944
+ const biometricKeys = keysToNotify.map((key) =>
945
+ toBiometricStorageKey(key),
946
+ );
947
+ if (backend.removeMany) {
948
+ backend.removeMany(biometricKeys);
949
+ return;
950
+ }
951
+ biometricKeys.forEach((storageKey) => {
952
+ backend.removeItem(storageKey);
953
+ });
954
+ },
955
+ );
798
956
  const keyIndex = ensureWebScopeKeyIndex(StorageScope.Secure);
799
957
  keysToNotify.forEach((key) => {
800
- if (storage.getItem(toSecureStorageKey(key)) === null) {
958
+ if (
959
+ withWebBackendOperation(
960
+ StorageScope.Secure,
961
+ "clearSecureBiometric:getItem",
962
+ (backend) => backend.getItem(toSecureStorageKey(key)),
963
+ ) === null
964
+ ) {
801
965
  keyIndex.delete(key);
802
966
  }
803
967
  });
@@ -813,6 +977,10 @@ function getRawValue(key: string, scope: StorageScope): string | undefined {
813
977
  return typeof value === "string" ? value : undefined;
814
978
  }
815
979
 
980
+ if (scope === StorageScope.Disk && hasPendingDiskWrite(key)) {
981
+ return readPendingDiskWrite(key);
982
+ }
983
+
816
984
  if (scope === StorageScope.Secure && hasPendingSecureWrite(key)) {
817
985
  return readPendingSecureWrite(key);
818
986
  }
@@ -828,6 +996,17 @@ function setRawValue(key: string, value: string, scope: StorageScope): void {
828
996
  return;
829
997
  }
830
998
 
999
+ if (scope === StorageScope.Disk) {
1000
+ cacheRawValue(scope, key, value);
1001
+ if (diskWritesAsync) {
1002
+ scheduleDiskWrite(key, value);
1003
+ return;
1004
+ }
1005
+
1006
+ flushDiskWrites();
1007
+ clearPendingDiskWrite(key);
1008
+ }
1009
+
831
1010
  if (scope === StorageScope.Secure) {
832
1011
  flushSecureWrites();
833
1012
  clearPendingSecureWrite(key);
@@ -845,6 +1024,17 @@ function removeRawValue(key: string, scope: StorageScope): void {
845
1024
  return;
846
1025
  }
847
1026
 
1027
+ if (scope === StorageScope.Disk) {
1028
+ cacheRawValue(scope, key, undefined);
1029
+ if (diskWritesAsync) {
1030
+ scheduleDiskWrite(key, undefined);
1031
+ return;
1032
+ }
1033
+
1034
+ flushDiskWrites();
1035
+ clearPendingDiskWrite(key);
1036
+ }
1037
+
848
1038
  if (scope === StorageScope.Secure) {
849
1039
  flushSecureWrites();
850
1040
  clearPendingSecureWrite(key);
@@ -877,6 +1067,11 @@ export const storage = {
877
1067
  return;
878
1068
  }
879
1069
 
1070
+ if (scope === StorageScope.Disk) {
1071
+ flushDiskWrites();
1072
+ pendingDiskWrites.clear();
1073
+ }
1074
+
880
1075
  if (scope === StorageScope.Secure) {
881
1076
  flushSecureWrites();
882
1077
  pendingSecureWrites.clear();
@@ -912,6 +1107,9 @@ export const storage = {
912
1107
  }
913
1108
 
914
1109
  const keyPrefix = prefixKey(namespace, "");
1110
+ if (scope === StorageScope.Disk) {
1111
+ flushDiskWrites();
1112
+ }
915
1113
  if (scope === StorageScope.Secure) {
916
1114
  flushSecureWrites();
917
1115
  }
@@ -933,6 +1131,12 @@ export const storage = {
933
1131
  return measureOperation("storage:has", scope, () => {
934
1132
  assertValidScope(scope);
935
1133
  if (scope === StorageScope.Memory) return memoryStore.has(key);
1134
+ if (scope === StorageScope.Disk) {
1135
+ flushDiskWrites();
1136
+ }
1137
+ if (scope === StorageScope.Secure) {
1138
+ flushSecureWrites();
1139
+ }
936
1140
  return WebStorage.has(key, scope);
937
1141
  });
938
1142
  },
@@ -940,6 +1144,12 @@ export const storage = {
940
1144
  return measureOperation("storage:getAllKeys", scope, () => {
941
1145
  assertValidScope(scope);
942
1146
  if (scope === StorageScope.Memory) return Array.from(memoryStore.keys());
1147
+ if (scope === StorageScope.Disk) {
1148
+ flushDiskWrites();
1149
+ }
1150
+ if (scope === StorageScope.Secure) {
1151
+ flushSecureWrites();
1152
+ }
943
1153
  return WebStorage.getAllKeys(scope);
944
1154
  });
945
1155
  },
@@ -951,6 +1161,12 @@ export const storage = {
951
1161
  key.startsWith(prefix),
952
1162
  );
953
1163
  }
1164
+ if (scope === StorageScope.Disk) {
1165
+ flushDiskWrites();
1166
+ }
1167
+ if (scope === StorageScope.Secure) {
1168
+ flushSecureWrites();
1169
+ }
954
1170
  return WebStorage.getKeysByPrefix(prefix, scope);
955
1171
  });
956
1172
  },
@@ -975,6 +1191,12 @@ export const storage = {
975
1191
  return result;
976
1192
  }
977
1193
 
1194
+ if (scope === StorageScope.Disk) {
1195
+ flushDiskWrites();
1196
+ }
1197
+ if (scope === StorageScope.Secure) {
1198
+ flushSecureWrites();
1199
+ }
978
1200
  const values = WebStorage.getBatch(keys, scope);
979
1201
  keys.forEach((key, index) => {
980
1202
  const value = values[index];
@@ -995,6 +1217,12 @@ export const storage = {
995
1217
  });
996
1218
  return result;
997
1219
  }
1220
+ if (scope === StorageScope.Disk) {
1221
+ flushDiskWrites();
1222
+ }
1223
+ if (scope === StorageScope.Secure) {
1224
+ flushSecureWrites();
1225
+ }
998
1226
  const keys = WebStorage.getAllKeys(scope);
999
1227
  if (keys.length === 0) return {};
1000
1228
  const values = WebStorage.getBatch(keys, scope);
@@ -1011,6 +1239,12 @@ export const storage = {
1011
1239
  return measureOperation("storage:size", scope, () => {
1012
1240
  assertValidScope(scope);
1013
1241
  if (scope === StorageScope.Memory) return memoryStore.size;
1242
+ if (scope === StorageScope.Disk) {
1243
+ flushDiskWrites();
1244
+ }
1245
+ if (scope === StorageScope.Secure) {
1246
+ flushSecureWrites();
1247
+ }
1014
1248
  return WebStorage.size(scope);
1015
1249
  });
1016
1250
  },
@@ -1021,6 +1255,19 @@ export const storage = {
1021
1255
  setSecureWritesAsync: (_enabled: boolean) => {
1022
1256
  recordMetric("storage:setSecureWritesAsync", StorageScope.Secure, 0);
1023
1257
  },
1258
+ setDiskWritesAsync: (enabled: boolean) => {
1259
+ measureOperation("storage:setDiskWritesAsync", StorageScope.Disk, () => {
1260
+ diskWritesAsync = enabled;
1261
+ if (!enabled) {
1262
+ flushDiskWrites();
1263
+ }
1264
+ });
1265
+ },
1266
+ flushDiskWrites: () => {
1267
+ measureOperation("storage:flushDiskWrites", StorageScope.Disk, () => {
1268
+ flushDiskWrites();
1269
+ });
1270
+ },
1024
1271
  flushSecureWrites: () => {
1025
1272
  measureOperation("storage:flushSecureWrites", StorageScope.Secure, () => {
1026
1273
  flushSecureWrites();
@@ -1048,6 +1295,18 @@ export const storage = {
1048
1295
  resetMetrics: () => {
1049
1296
  metricsCounters.clear();
1050
1297
  },
1298
+ getCapabilities: (): StorageCapabilities => ({
1299
+ platform: "web",
1300
+ backend: {
1301
+ disk: getBackendName(StorageScope.Disk, webDiskStorageBackend),
1302
+ secure: getBackendName(StorageScope.Secure, webSecureStorageBackend),
1303
+ },
1304
+ writeBuffering: {
1305
+ disk: true,
1306
+ secure: true,
1307
+ },
1308
+ errorClassification: true,
1309
+ }),
1051
1310
  getString: (key: string, scope: StorageScope): string | undefined => {
1052
1311
  return measureOperation("storage:getString", scope, () => {
1053
1312
  return getRawValue(key, scope);
@@ -1085,6 +1344,9 @@ export const storage = {
1085
1344
  flushSecureWrites();
1086
1345
  WebStorage.setSecureAccessControl(secureDefaultAccessControl);
1087
1346
  }
1347
+ if (scope === StorageScope.Disk) {
1348
+ flushDiskWrites();
1349
+ }
1088
1350
 
1089
1351
  WebStorage.setBatch(keys, values, scope);
1090
1352
  keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
@@ -1097,11 +1359,12 @@ export const storage = {
1097
1359
  export function setWebSecureStorageBackend(
1098
1360
  backend?: WebSecureStorageBackend,
1099
1361
  ): void {
1100
- webSecureStorageBackend = backend ?? createLocalStorageWebSecureBackend();
1101
- cachedSecureBrowserStorage = undefined;
1102
- cachedSecureBackendRef = undefined;
1362
+ pendingSecureWrites.clear();
1363
+ webSecureStorageBackend = backend ?? createDefaultSecureBackend();
1364
+ resetBackendChangeSubscription(StorageScope.Secure);
1103
1365
  hydratedWebScopeKeyIndex.delete(StorageScope.Secure);
1104
1366
  clearScopeRawCache(StorageScope.Secure);
1367
+ ensureExternalSyncSubscriptions();
1105
1368
  }
1106
1369
 
1107
1370
  export function getWebSecureStorageBackend():
@@ -1110,6 +1373,39 @@ export function getWebSecureStorageBackend():
1110
1373
  return webSecureStorageBackend;
1111
1374
  }
1112
1375
 
1376
+ export function setWebDiskStorageBackend(
1377
+ backend?: WebDiskStorageBackend,
1378
+ ): void {
1379
+ pendingDiskWrites.clear();
1380
+ webDiskStorageBackend = backend ?? createDefaultDiskBackend();
1381
+ resetBackendChangeSubscription(StorageScope.Disk);
1382
+ hydratedWebScopeKeyIndex.delete(StorageScope.Disk);
1383
+ clearScopeRawCache(StorageScope.Disk);
1384
+ ensureExternalSyncSubscriptions();
1385
+ }
1386
+
1387
+ export function getWebDiskStorageBackend(): WebDiskStorageBackend | undefined {
1388
+ return webDiskStorageBackend;
1389
+ }
1390
+
1391
+ export async function flushWebStorageBackends(): Promise<void> {
1392
+ flushDiskWrites();
1393
+ flushSecureWrites();
1394
+
1395
+ const flushes: Promise<void>[] = [];
1396
+ const diskFlush = webDiskStorageBackend?.flush;
1397
+ const secureFlush = webSecureStorageBackend?.flush;
1398
+
1399
+ if (diskFlush) {
1400
+ flushes.push(diskFlush());
1401
+ }
1402
+ if (secureFlush) {
1403
+ flushes.push(secureFlush());
1404
+ }
1405
+
1406
+ await Promise.all(flushes);
1407
+ }
1408
+
1113
1409
  export interface StorageItemConfig<T> {
1114
1410
  key: string;
1115
1411
  scope: StorageScope;
@@ -1121,6 +1417,7 @@ export interface StorageItemConfig<T> {
1121
1417
  expiration?: ExpirationConfig;
1122
1418
  onExpired?: (key: string) => void;
1123
1419
  readCache?: boolean;
1420
+ coalesceDiskWrites?: boolean;
1124
1421
  coalesceSecureWrites?: boolean;
1125
1422
  namespace?: string;
1126
1423
  biometric?: boolean;
@@ -1205,6 +1502,8 @@ export function createStorageItem<T = undefined>(
1205
1502
  const memoryExpiration =
1206
1503
  expiration && isMemory ? new Map<string, number>() : null;
1207
1504
  const readCache = !isMemory && config.readCache === true;
1505
+ const coalesceDiskWrites =
1506
+ config.scope === StorageScope.Disk && config.coalesceDiskWrites === true;
1208
1507
  const coalesceSecureWrites =
1209
1508
  config.scope === StorageScope.Secure &&
1210
1509
  config.coalesceSecureWrites === true &&
@@ -1250,7 +1549,7 @@ export function createStorageItem<T = undefined>(
1250
1549
  return;
1251
1550
  }
1252
1551
 
1253
- ensureWebStorageEventSubscription();
1552
+ ensureExternalSyncSubscriptions();
1254
1553
  unsubscribe = addKeyListener(
1255
1554
  getScopedListeners(nonMemoryScope!),
1256
1555
  storageKey,
@@ -1273,6 +1572,13 @@ export function createStorageItem<T = undefined>(
1273
1572
  return memoryStore.get(storageKey);
1274
1573
  }
1275
1574
 
1575
+ if (nonMemoryScope === StorageScope.Disk) {
1576
+ const pending = pendingDiskWrites.get(storageKey);
1577
+ if (pending !== undefined) {
1578
+ return pending.value;
1579
+ }
1580
+ }
1581
+
1276
1582
  if (nonMemoryScope === StorageScope.Secure && !isBiometric) {
1277
1583
  const pending = pendingSecureWrites.get(storageKey);
1278
1584
  if (pending !== undefined) {
@@ -1309,6 +1615,15 @@ export function createStorageItem<T = undefined>(
1309
1615
 
1310
1616
  cacheRawValue(nonMemoryScope!, storageKey, rawValue);
1311
1617
 
1618
+ if (nonMemoryScope === StorageScope.Disk) {
1619
+ if (coalesceDiskWrites || diskWritesAsync) {
1620
+ scheduleDiskWrite(storageKey, rawValue);
1621
+ return;
1622
+ }
1623
+
1624
+ clearPendingDiskWrite(storageKey);
1625
+ }
1626
+
1312
1627
  if (coalesceSecureWrites) {
1313
1628
  scheduleSecureWrite(
1314
1629
  storageKey,
@@ -1333,6 +1648,15 @@ export function createStorageItem<T = undefined>(
1333
1648
 
1334
1649
  cacheRawValue(nonMemoryScope!, storageKey, undefined);
1335
1650
 
1651
+ if (nonMemoryScope === StorageScope.Disk) {
1652
+ if (coalesceDiskWrites || diskWritesAsync) {
1653
+ scheduleDiskWrite(storageKey, undefined);
1654
+ return;
1655
+ }
1656
+
1657
+ clearPendingDiskWrite(storageKey);
1658
+ }
1659
+
1336
1660
  if (coalesceSecureWrites) {
1337
1661
  scheduleSecureWrite(
1338
1662
  storageKey,
@@ -1543,6 +1867,18 @@ export function createStorageItem<T = undefined>(
1543
1867
  measureOperation("item:has", config.scope, () => {
1544
1868
  if (isMemory) return memoryStore.has(storageKey);
1545
1869
  if (isBiometric) return WebStorage.hasSecureBiometric(storageKey);
1870
+ if (nonMemoryScope === StorageScope.Disk) {
1871
+ const pending = pendingDiskWrites.get(storageKey);
1872
+ if (pending !== undefined) {
1873
+ return pending.value !== undefined;
1874
+ }
1875
+ }
1876
+ if (nonMemoryScope === StorageScope.Secure) {
1877
+ const pending = pendingSecureWrites.get(storageKey);
1878
+ if (pending !== undefined) {
1879
+ return pending.value !== undefined;
1880
+ }
1881
+ }
1546
1882
  return WebStorage.has(storageKey, config.scope);
1547
1883
  });
1548
1884
 
@@ -1554,9 +1890,6 @@ export function createStorageItem<T = undefined>(
1554
1890
  if (listeners.size === 0 && unsubscribe) {
1555
1891
  unsubscribe();
1556
1892
  unsubscribe = null;
1557
- if (!isMemory) {
1558
- maybeCleanupWebStorageSubscription();
1559
- }
1560
1893
  }
1561
1894
  };
1562
1895
  };
@@ -1642,6 +1975,14 @@ export function getBatch(
1642
1975
  const keyIndexes: number[] = [];
1643
1976
 
1644
1977
  items.forEach((item, index) => {
1978
+ if (scope === StorageScope.Disk) {
1979
+ const pending = pendingDiskWrites.get(item.key);
1980
+ if (pending !== undefined) {
1981
+ rawValues[index] = pending.value;
1982
+ return;
1983
+ }
1984
+ }
1985
+
1645
1986
  if (scope === StorageScope.Secure) {
1646
1987
  const pending = pendingSecureWrites.get(item.key);
1647
1988
  if (pending !== undefined) {
@@ -1766,6 +2107,8 @@ export function setBatch<T>(
1766
2107
  return;
1767
2108
  }
1768
2109
 
2110
+ flushDiskWrites();
2111
+
1769
2112
  const useRawBatchPath = items.every(({ item }) =>
1770
2113
  canUseRawBatchPath(asInternal(item)),
1771
2114
  );
@@ -1799,6 +2142,9 @@ export function removeBatch(
1799
2142
  }
1800
2143
 
1801
2144
  const keys = items.map((item) => item.key);
2145
+ if (scope === StorageScope.Disk) {
2146
+ flushDiskWrites();
2147
+ }
1802
2148
  if (scope === StorageScope.Secure) {
1803
2149
  flushSecureWrites();
1804
2150
  }
@@ -1862,6 +2208,9 @@ export function runTransaction<T>(
1862
2208
  ): T {
1863
2209
  return measureOperation("transaction:run", scope, () => {
1864
2210
  assertValidScope(scope);
2211
+ if (scope === StorageScope.Disk) {
2212
+ flushDiskWrites();
2213
+ }
1865
2214
  if (scope === StorageScope.Secure) {
1866
2215
  flushSecureWrites();
1867
2216
  }
@@ -1937,6 +2286,9 @@ export function runTransaction<T>(
1937
2286
  }
1938
2287
  });
1939
2288
 
2289
+ if (scope === StorageScope.Disk) {
2290
+ flushDiskWrites();
2291
+ }
1940
2292
  if (scope === StorageScope.Secure) {
1941
2293
  flushSecureWrites();
1942
2294
  }
@@ -2000,6 +2352,6 @@ export function createSecureAuthStorage<K extends string>(
2000
2352
  return result as Record<K, StorageItem<string>>;
2001
2353
  }
2002
2354
 
2003
- export function isKeychainLockedError(_err: unknown): boolean {
2004
- return false;
2355
+ export function isKeychainLockedError(err: unknown): boolean {
2356
+ return isLockedStorageErrorCode(getStorageErrorCode(err));
2005
2357
  }