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
@@ -2,8 +2,11 @@
2
2
 
3
3
  import { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
4
4
  import { MIGRATION_VERSION_KEY, isStoredEnvelope, assertBatchScope, assertValidScope, serializeWithPrimitiveFastPath, deserializeWithPrimitiveFastPath, toVersionToken, prefixKey, isNamespaced } from "./internal";
5
+ import { createLocalStorageWebBackend } from "./web-storage-backend";
6
+ import { getStorageErrorCode, isLockedStorageErrorCode } from "./storage-runtime";
5
7
  export { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
6
8
  export { migrateFromMMKV } from "./migration";
9
+ export { getStorageErrorCode } from "./storage-runtime";
7
10
  function asInternal(item) {
8
11
  return item;
9
12
  }
@@ -17,20 +20,23 @@ const registeredMigrations = new Map();
17
20
  const runMicrotask = typeof queueMicrotask === "function" ? queueMicrotask : task => {
18
21
  Promise.resolve().then(task);
19
22
  };
23
+ const now = typeof performance !== "undefined" && typeof performance.now === "function" ? () => performance.now() : () => Date.now();
20
24
  const memoryStore = new Map();
21
25
  const memoryListeners = new Map();
22
26
  const webScopeListeners = new Map([[StorageScope.Disk, new Map()], [StorageScope.Secure, new Map()]]);
23
27
  const scopedRawCache = new Map([[StorageScope.Disk, new Map()], [StorageScope.Secure, new Map()]]);
24
28
  const webScopeKeyIndex = new Map([[StorageScope.Disk, new Set()], [StorageScope.Secure, new Set()]]);
25
29
  const hydratedWebScopeKeyIndex = new Set();
30
+ const pendingDiskWrites = new Map();
31
+ let diskFlushScheduled = false;
32
+ let diskWritesAsync = false;
26
33
  const pendingSecureWrites = new Map();
27
34
  let secureFlushScheduled = false;
28
35
  let secureDefaultAccessControl = AccessControl.WhenUnlocked;
29
36
  const SECURE_WEB_PREFIX = "__secure_";
30
37
  const BIOMETRIC_WEB_PREFIX = "__bio_";
31
38
  let hasWarnedAboutWebBiometricFallback = false;
32
- let hasWebStorageEventSubscription = false;
33
- let webStorageSubscriberCount = 0;
39
+ let hasWindowStorageEventSubscription = false;
34
40
  let metricsObserver;
35
41
  const metricsCounters = new Map();
36
42
  function recordMetric(operation, scope, durationMs, keysCount = 1) {
@@ -57,61 +63,51 @@ function measureOperation(operation, scope, fn, keysCount = 1) {
57
63
  if (!metricsObserver) {
58
64
  return fn();
59
65
  }
60
- const start = Date.now();
66
+ const start = now();
61
67
  try {
62
68
  return fn();
63
69
  } finally {
64
- recordMetric(operation, scope, Date.now() - start, keysCount);
70
+ recordMetric(operation, scope, now() - start, keysCount);
65
71
  }
66
72
  }
67
- function createLocalStorageWebSecureBackend() {
68
- return {
69
- getItem: key => globalThis.localStorage?.getItem(key) ?? null,
70
- setItem: (key, value) => globalThis.localStorage?.setItem(key, value),
71
- removeItem: key => globalThis.localStorage?.removeItem(key),
72
- clear: () => globalThis.localStorage?.clear(),
73
- getAllKeys: () => {
74
- const storage = globalThis.localStorage;
75
- if (!storage) return [];
76
- const keys = [];
77
- for (let index = 0; index < storage.length; index += 1) {
78
- const key = storage.key(index);
79
- if (key) {
80
- keys.push(key);
81
- }
82
- }
83
- return keys;
84
- }
85
- };
73
+ function createDefaultDiskBackend() {
74
+ return createLocalStorageWebBackend({
75
+ name: "localStorage:disk",
76
+ includeKey: key => !key.startsWith(SECURE_WEB_PREFIX) && !key.startsWith(BIOMETRIC_WEB_PREFIX)
77
+ });
86
78
  }
87
- let webSecureStorageBackend = createLocalStorageWebSecureBackend();
88
- let cachedSecureBrowserStorage;
89
- let cachedSecureBackendRef;
90
- function getBrowserStorage(scope) {
91
- if (scope === StorageScope.Disk) {
92
- return globalThis.localStorage;
79
+ function createDefaultSecureBackend() {
80
+ return createLocalStorageWebBackend({
81
+ name: "localStorage:secure",
82
+ includeKey: key => key.startsWith(SECURE_WEB_PREFIX) || key.startsWith(BIOMETRIC_WEB_PREFIX)
83
+ });
84
+ }
85
+ let webDiskStorageBackend = createDefaultDiskBackend();
86
+ let webSecureStorageBackend = createDefaultSecureBackend();
87
+ const externalSyncUnsubscribers = new Map();
88
+ function getBackendName(scope, backend) {
89
+ const scopeName = scope === StorageScope.Disk ? "disk" : "secure";
90
+ return backend?.name ?? `web:${scopeName}`;
91
+ }
92
+ function createWebStorageError(scope, operation, error, backend) {
93
+ const backendName = getBackendName(scope, backend);
94
+ const message = error instanceof Error ? error.message : String(error ?? "Unknown error");
95
+ return new Error(`NitroStorage(web): ${operation} failed for ${backendName}: ${message}`);
96
+ }
97
+ function withWebBackendOperation(scope, operation, fn) {
98
+ const backend = scope === StorageScope.Disk ? webDiskStorageBackend : webSecureStorageBackend;
99
+ if (!backend) {
100
+ throw new Error(`NitroStorage(web): ${operation} failed because no ${scope === StorageScope.Disk ? "disk" : "secure"} backend is configured.`);
93
101
  }
94
- if (scope === StorageScope.Secure) {
95
- if (!webSecureStorageBackend) {
96
- return undefined;
97
- }
98
- if (cachedSecureBackendRef === webSecureStorageBackend && cachedSecureBrowserStorage) {
99
- return cachedSecureBrowserStorage;
100
- }
101
- cachedSecureBackendRef = webSecureStorageBackend;
102
- cachedSecureBrowserStorage = {
103
- setItem: (key, value) => webSecureStorageBackend.setItem(key, value),
104
- getItem: key => webSecureStorageBackend.getItem(key) ?? null,
105
- removeItem: key => webSecureStorageBackend.removeItem(key),
106
- clear: () => webSecureStorageBackend.clear(),
107
- key: index => webSecureStorageBackend.getAllKeys()[index] ?? null,
108
- get length() {
109
- return webSecureStorageBackend.getAllKeys().length;
110
- }
111
- };
112
- return cachedSecureBrowserStorage;
102
+ try {
103
+ ensureExternalSyncSubscriptions();
104
+ return fn(backend);
105
+ } catch (error) {
106
+ throw createWebStorageError(scope, operation, error, backend);
113
107
  }
114
- return undefined;
108
+ }
109
+ function getWebBackend(scope) {
110
+ return scope === StorageScope.Disk ? webDiskStorageBackend : webSecureStorageBackend;
115
111
  }
116
112
  function toSecureStorageKey(key) {
117
113
  return `${SECURE_WEB_PREFIX}${key}`;
@@ -132,28 +128,21 @@ function hydrateWebScopeKeyIndex(scope) {
132
128
  if (hydratedWebScopeKeyIndex.has(scope)) {
133
129
  return;
134
130
  }
135
- const storage = getBrowserStorage(scope);
131
+ const backend = getWebBackend(scope);
136
132
  const keyIndex = getWebScopeKeyIndex(scope);
137
133
  keyIndex.clear();
138
- if (storage) {
139
- for (let index = 0; index < storage.length; index += 1) {
140
- const key = storage.key(index);
141
- if (!key) {
142
- continue;
143
- }
144
- if (scope === StorageScope.Disk) {
145
- if (!key.startsWith(SECURE_WEB_PREFIX) && !key.startsWith(BIOMETRIC_WEB_PREFIX)) {
146
- keyIndex.add(key);
147
- }
148
- continue;
149
- }
150
- if (key.startsWith(SECURE_WEB_PREFIX)) {
151
- keyIndex.add(fromSecureStorageKey(key));
152
- continue;
153
- }
154
- if (key.startsWith(BIOMETRIC_WEB_PREFIX)) {
155
- keyIndex.add(fromBiometricStorageKey(key));
156
- }
134
+ const keys = backend?.getAllKeys() ?? [];
135
+ for (const key of keys) {
136
+ if (scope === StorageScope.Disk) {
137
+ keyIndex.add(key);
138
+ continue;
139
+ }
140
+ if (key.startsWith(SECURE_WEB_PREFIX)) {
141
+ keyIndex.add(fromSecureStorageKey(key));
142
+ continue;
143
+ }
144
+ if (key.startsWith(BIOMETRIC_WEB_PREFIX)) {
145
+ keyIndex.add(fromBiometricStorageKey(key));
157
146
  }
158
147
  }
159
148
  hydratedWebScopeKeyIndex.add(scope);
@@ -162,65 +151,85 @@ function ensureWebScopeKeyIndex(scope) {
162
151
  hydrateWebScopeKeyIndex(scope);
163
152
  return getWebScopeKeyIndex(scope);
164
153
  }
165
- function handleWebStorageEvent(event) {
166
- const key = event.key;
154
+ function applyExternalChangeEvent(scope, key, newValue) {
167
155
  if (key === null) {
168
- clearScopeRawCache(StorageScope.Disk);
169
- clearScopeRawCache(StorageScope.Secure);
170
- ensureWebScopeKeyIndex(StorageScope.Disk).clear();
171
- ensureWebScopeKeyIndex(StorageScope.Secure).clear();
172
- notifyAllListeners(getScopedListeners(StorageScope.Disk));
173
- notifyAllListeners(getScopedListeners(StorageScope.Secure));
156
+ clearScopeRawCache(scope);
157
+ ensureWebScopeKeyIndex(scope).clear();
158
+ notifyAllListeners(getScopedListeners(scope));
174
159
  return;
175
160
  }
176
- if (key.startsWith(SECURE_WEB_PREFIX)) {
161
+ if (scope === StorageScope.Secure && key.startsWith(SECURE_WEB_PREFIX)) {
177
162
  const plainKey = fromSecureStorageKey(key);
178
- if (event.newValue === null) {
163
+ if (newValue === null) {
179
164
  ensureWebScopeKeyIndex(StorageScope.Secure).delete(plainKey);
180
165
  cacheRawValue(StorageScope.Secure, plainKey, undefined);
181
166
  } else {
182
167
  ensureWebScopeKeyIndex(StorageScope.Secure).add(plainKey);
183
- cacheRawValue(StorageScope.Secure, plainKey, event.newValue);
168
+ cacheRawValue(StorageScope.Secure, plainKey, newValue);
184
169
  }
185
170
  notifyKeyListeners(getScopedListeners(StorageScope.Secure), plainKey);
186
171
  return;
187
172
  }
188
- if (key.startsWith(BIOMETRIC_WEB_PREFIX)) {
173
+ if (scope === StorageScope.Secure && key.startsWith(BIOMETRIC_WEB_PREFIX)) {
189
174
  const plainKey = fromBiometricStorageKey(key);
190
- if (event.newValue === null) {
191
- if (getBrowserStorage(StorageScope.Secure)?.getItem(toSecureStorageKey(plainKey)) === null) {
175
+ if (newValue === null) {
176
+ if (withWebBackendOperation(StorageScope.Secure, "external-sync:getItem", backend => backend.getItem(toSecureStorageKey(plainKey))) === null) {
192
177
  ensureWebScopeKeyIndex(StorageScope.Secure).delete(plainKey);
193
178
  }
194
179
  cacheRawValue(StorageScope.Secure, plainKey, undefined);
195
180
  } else {
196
181
  ensureWebScopeKeyIndex(StorageScope.Secure).add(plainKey);
197
- cacheRawValue(StorageScope.Secure, plainKey, event.newValue);
182
+ cacheRawValue(StorageScope.Secure, plainKey, newValue);
198
183
  }
199
184
  notifyKeyListeners(getScopedListeners(StorageScope.Secure), plainKey);
200
185
  return;
201
186
  }
202
- if (event.newValue === null) {
203
- ensureWebScopeKeyIndex(StorageScope.Disk).delete(key);
204
- cacheRawValue(StorageScope.Disk, key, undefined);
187
+ if (newValue === null) {
188
+ ensureWebScopeKeyIndex(scope).delete(key);
189
+ cacheRawValue(scope, key, undefined);
205
190
  } else {
206
- ensureWebScopeKeyIndex(StorageScope.Disk).add(key);
207
- cacheRawValue(StorageScope.Disk, key, event.newValue);
191
+ ensureWebScopeKeyIndex(scope).add(key);
192
+ cacheRawValue(scope, key, newValue);
208
193
  }
209
- notifyKeyListeners(getScopedListeners(StorageScope.Disk), key);
194
+ notifyKeyListeners(getScopedListeners(scope), key);
210
195
  }
211
- function ensureWebStorageEventSubscription() {
212
- webStorageSubscriberCount += 1;
213
- if (webStorageSubscriberCount === 1 && typeof window !== "undefined" && typeof window.addEventListener === "function") {
214
- window.addEventListener("storage", handleWebStorageEvent);
215
- hasWebStorageEventSubscription = true;
196
+ function handleWebStorageEvent(event) {
197
+ const key = event.key;
198
+ if (key === null) {
199
+ applyExternalChangeEvent(StorageScope.Disk, null, null);
200
+ applyExternalChangeEvent(StorageScope.Secure, null, null);
201
+ return;
202
+ }
203
+ if (key.startsWith(SECURE_WEB_PREFIX) || key.startsWith(BIOMETRIC_WEB_PREFIX)) {
204
+ applyExternalChangeEvent(StorageScope.Secure, key, event.newValue);
205
+ return;
206
+ }
207
+ applyExternalChangeEvent(StorageScope.Disk, key, event.newValue);
208
+ }
209
+ function subscribeToBackendChanges(scope) {
210
+ if (externalSyncUnsubscribers.has(scope)) {
211
+ return;
212
+ }
213
+ const backend = getWebBackend(scope);
214
+ if (!backend?.subscribe) {
215
+ return;
216
216
  }
217
+ const unsubscribe = backend.subscribe(event => {
218
+ applyExternalChangeEvent(scope, event.key, event.newValue);
219
+ });
220
+ externalSyncUnsubscribers.set(scope, unsubscribe);
221
+ }
222
+ function resetBackendChangeSubscription(scope) {
223
+ externalSyncUnsubscribers.get(scope)?.();
224
+ externalSyncUnsubscribers.delete(scope);
217
225
  }
218
- function maybeCleanupWebStorageSubscription() {
219
- webStorageSubscriberCount = Math.max(0, webStorageSubscriberCount - 1);
220
- if (webStorageSubscriberCount === 0 && hasWebStorageEventSubscription && typeof window !== "undefined") {
221
- window.removeEventListener("storage", handleWebStorageEvent);
222
- hasWebStorageEventSubscription = false;
226
+ function ensureExternalSyncSubscriptions() {
227
+ if (!hasWindowStorageEventSubscription && typeof window !== "undefined" && typeof window.addEventListener === "function") {
228
+ window.addEventListener("storage", handleWebStorageEvent);
229
+ hasWindowStorageEventSubscription = true;
223
230
  }
231
+ subscribeToBackendChanges(StorageScope.Disk);
232
+ subscribeToBackendChanges(StorageScope.Secure);
224
233
  }
225
234
  function getScopedListeners(scope) {
226
235
  return webScopeListeners.get(scope);
@@ -276,12 +285,49 @@ function addKeyListener(registry, key, listener) {
276
285
  function readPendingSecureWrite(key) {
277
286
  return pendingSecureWrites.get(key)?.value;
278
287
  }
288
+ function readPendingDiskWrite(key) {
289
+ return pendingDiskWrites.get(key)?.value;
290
+ }
291
+ function hasPendingDiskWrite(key) {
292
+ return pendingDiskWrites.has(key);
293
+ }
279
294
  function hasPendingSecureWrite(key) {
280
295
  return pendingSecureWrites.has(key);
281
296
  }
297
+ function clearPendingDiskWrite(key) {
298
+ pendingDiskWrites.delete(key);
299
+ }
282
300
  function clearPendingSecureWrite(key) {
283
301
  pendingSecureWrites.delete(key);
284
302
  }
303
+ function flushDiskWrites() {
304
+ diskFlushScheduled = false;
305
+ if (pendingDiskWrites.size === 0) {
306
+ return;
307
+ }
308
+ const writes = Array.from(pendingDiskWrites.values());
309
+ pendingDiskWrites.clear();
310
+ const keysToSet = [];
311
+ const valuesToSet = [];
312
+ const keysToRemove = [];
313
+ writes.forEach(({
314
+ key,
315
+ value
316
+ }) => {
317
+ if (value === undefined) {
318
+ keysToRemove.push(key);
319
+ return;
320
+ }
321
+ keysToSet.push(key);
322
+ valuesToSet.push(value);
323
+ });
324
+ if (keysToSet.length > 0) {
325
+ WebStorage.setBatch(keysToSet, valuesToSet, StorageScope.Disk);
326
+ }
327
+ if (keysToRemove.length > 0) {
328
+ WebStorage.removeBatch(keysToRemove, StorageScope.Disk);
329
+ }
330
+ }
285
331
  function flushSecureWrites() {
286
332
  secureFlushScheduled = false;
287
333
  if (pendingSecureWrites.size === 0) {
@@ -320,6 +366,17 @@ function flushSecureWrites() {
320
366
  WebStorage.removeBatch(keysToRemove, StorageScope.Secure);
321
367
  }
322
368
  }
369
+ function scheduleDiskWrite(key, value) {
370
+ pendingDiskWrites.set(key, {
371
+ key,
372
+ value
373
+ });
374
+ if (diskFlushScheduled) {
375
+ return;
376
+ }
377
+ diskFlushScheduled = true;
378
+ runMicrotask(flushDiskWrites);
379
+ }
323
380
  function scheduleSecureWrite(key, value, accessControl) {
324
381
  const pendingWrite = {
325
382
  key,
@@ -340,117 +397,124 @@ const WebStorage = {
340
397
  equals: other => other === WebStorage,
341
398
  dispose: () => {},
342
399
  set: (key, value, scope) => {
343
- const storage = getBrowserStorage(scope);
344
- if (!storage) {
400
+ if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
345
401
  return;
346
402
  }
347
403
  const storageKey = scope === StorageScope.Secure ? toSecureStorageKey(key) : key;
348
- storage.setItem(storageKey, value);
349
- if (scope === StorageScope.Disk || scope === StorageScope.Secure) {
350
- ensureWebScopeKeyIndex(scope).add(key);
351
- notifyKeyListeners(getScopedListeners(scope), key);
352
- }
404
+ withWebBackendOperation(scope, "set", backend => {
405
+ backend.setItem(storageKey, value);
406
+ });
407
+ ensureWebScopeKeyIndex(scope).add(key);
408
+ notifyKeyListeners(getScopedListeners(scope), key);
353
409
  },
354
410
  get: (key, scope) => {
355
- const storage = getBrowserStorage(scope);
411
+ if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
412
+ return undefined;
413
+ }
356
414
  const storageKey = scope === StorageScope.Secure ? toSecureStorageKey(key) : key;
357
- return storage?.getItem(storageKey) ?? undefined;
415
+ const value = withWebBackendOperation(scope, "get", backend => backend.getItem(storageKey));
416
+ return value ?? undefined;
358
417
  },
359
418
  remove: (key, scope) => {
360
- const storage = getBrowserStorage(scope);
361
- if (!storage) {
419
+ if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
362
420
  return;
363
421
  }
364
422
  if (scope === StorageScope.Secure) {
365
- storage.removeItem(toSecureStorageKey(key));
366
- storage.removeItem(toBiometricStorageKey(key));
423
+ withWebBackendOperation(scope, "remove", backend => {
424
+ if (backend.removeMany) {
425
+ backend.removeMany([toSecureStorageKey(key), toBiometricStorageKey(key)]);
426
+ return;
427
+ }
428
+ backend.removeItem(toSecureStorageKey(key));
429
+ backend.removeItem(toBiometricStorageKey(key));
430
+ });
367
431
  } else {
368
- storage.removeItem(key);
369
- }
370
- if (scope === StorageScope.Disk || scope === StorageScope.Secure) {
371
- ensureWebScopeKeyIndex(scope).delete(key);
372
- notifyKeyListeners(getScopedListeners(scope), key);
432
+ withWebBackendOperation(scope, "remove", backend => {
433
+ backend.removeItem(key);
434
+ });
373
435
  }
436
+ ensureWebScopeKeyIndex(scope).delete(key);
437
+ notifyKeyListeners(getScopedListeners(scope), key);
374
438
  },
375
439
  clear: scope => {
376
- const storage = getBrowserStorage(scope);
377
- if (!storage) {
440
+ if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
378
441
  return;
379
442
  }
380
- if (scope === StorageScope.Secure) {
381
- const keysToRemove = [];
382
- for (let i = 0; i < storage.length; i++) {
383
- const key = storage.key(i);
384
- if (key?.startsWith(SECURE_WEB_PREFIX) || key?.startsWith(BIOMETRIC_WEB_PREFIX)) {
385
- keysToRemove.push(key);
386
- }
387
- }
388
- keysToRemove.forEach(key => storage.removeItem(key));
389
- } else if (scope === StorageScope.Disk) {
390
- const keysToRemove = [];
391
- for (let i = 0; i < storage.length; i++) {
392
- const key = storage.key(i);
393
- if (key && !key.startsWith(SECURE_WEB_PREFIX) && !key.startsWith(BIOMETRIC_WEB_PREFIX)) {
394
- keysToRemove.push(key);
395
- }
396
- }
397
- keysToRemove.forEach(key => storage.removeItem(key));
398
- } else {
399
- storage.clear();
400
- }
401
- if (scope === StorageScope.Disk || scope === StorageScope.Secure) {
402
- ensureWebScopeKeyIndex(scope).clear();
403
- notifyAllListeners(getScopedListeners(scope));
404
- }
443
+ withWebBackendOperation(scope, "clear", backend => {
444
+ backend.clear();
445
+ });
446
+ ensureWebScopeKeyIndex(scope).clear();
447
+ notifyAllListeners(getScopedListeners(scope));
405
448
  },
406
449
  setBatch: (keys, values, scope) => {
407
- const storage = getBrowserStorage(scope);
408
- if (!storage) {
450
+ if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
409
451
  return;
410
452
  }
453
+ const entries = [];
411
454
  keys.forEach((key, index) => {
412
455
  const value = values[index];
413
456
  if (value === undefined) {
414
457
  return;
415
458
  }
416
- const storageKey = scope === StorageScope.Secure ? toSecureStorageKey(key) : key;
417
- storage.setItem(storageKey, value);
459
+ entries.push([scope === StorageScope.Secure ? toSecureStorageKey(key) : key, value]);
418
460
  });
419
- if (scope === StorageScope.Disk || scope === StorageScope.Secure) {
420
- const keyIndex = ensureWebScopeKeyIndex(scope);
421
- keys.forEach(key => keyIndex.add(key));
422
- const listeners = getScopedListeners(scope);
423
- keys.forEach(key => notifyKeyListeners(listeners, key));
424
- }
461
+ withWebBackendOperation(scope, "setBatch", backend => {
462
+ if (backend.setMany) {
463
+ backend.setMany(entries);
464
+ return;
465
+ }
466
+ entries.forEach(([storageKey, value]) => {
467
+ backend.setItem(storageKey, value);
468
+ });
469
+ });
470
+ const keyIndex = ensureWebScopeKeyIndex(scope);
471
+ keys.forEach(key => keyIndex.add(key));
472
+ const listeners = getScopedListeners(scope);
473
+ keys.forEach(key => notifyKeyListeners(listeners, key));
425
474
  },
426
475
  getBatch: (keys, scope) => {
427
- const storage = getBrowserStorage(scope);
428
- return keys.map(key => {
429
- const storageKey = scope === StorageScope.Secure ? toSecureStorageKey(key) : key;
430
- return storage?.getItem(storageKey) ?? undefined;
476
+ if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
477
+ return keys.map(() => undefined);
478
+ }
479
+ const storageKeys = keys.map(key => scope === StorageScope.Secure ? toSecureStorageKey(key) : key);
480
+ const values = withWebBackendOperation(scope, "getBatch", backend => {
481
+ if (backend.getMany) {
482
+ return backend.getMany(storageKeys);
483
+ }
484
+ return storageKeys.map(storageKey => backend.getItem(storageKey));
431
485
  });
486
+ return values.map(value => value ?? undefined);
432
487
  },
433
488
  removeBatch: (keys, scope) => {
434
- const storage = getBrowserStorage(scope);
435
- if (!storage) {
489
+ if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
436
490
  return;
437
491
  }
438
492
  if (scope === StorageScope.Secure) {
439
- keys.forEach(key => {
440
- storage.removeItem(toSecureStorageKey(key));
441
- storage.removeItem(toBiometricStorageKey(key));
493
+ const storageKeys = keys.flatMap(key => [toSecureStorageKey(key), toBiometricStorageKey(key)]);
494
+ withWebBackendOperation(scope, "removeBatch", backend => {
495
+ if (backend.removeMany) {
496
+ backend.removeMany(storageKeys);
497
+ return;
498
+ }
499
+ storageKeys.forEach(storageKey => {
500
+ backend.removeItem(storageKey);
501
+ });
442
502
  });
443
503
  } else {
444
- keys.forEach(key => {
445
- storage.removeItem(key);
504
+ withWebBackendOperation(scope, "removeBatch", backend => {
505
+ if (backend.removeMany) {
506
+ backend.removeMany(keys);
507
+ return;
508
+ }
509
+ keys.forEach(key => {
510
+ backend.removeItem(key);
511
+ });
446
512
  });
447
513
  }
448
- if (scope === StorageScope.Disk || scope === StorageScope.Secure) {
449
- const keyIndex = ensureWebScopeKeyIndex(scope);
450
- keys.forEach(key => keyIndex.delete(key));
451
- const listeners = getScopedListeners(scope);
452
- keys.forEach(key => notifyKeyListeners(listeners, key));
453
- }
514
+ const keyIndex = ensureWebScopeKeyIndex(scope);
515
+ keys.forEach(key => keyIndex.delete(key));
516
+ const listeners = getScopedListeners(scope);
517
+ keys.forEach(key => notifyKeyListeners(listeners, key));
454
518
  },
455
519
  removeByPrefix: (prefix, scope) => {
456
520
  if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
@@ -467,11 +531,13 @@ const WebStorage = {
467
531
  return () => {};
468
532
  },
469
533
  has: (key, scope) => {
470
- const storage = getBrowserStorage(scope);
471
534
  if (scope === StorageScope.Secure) {
472
- return storage?.getItem(toSecureStorageKey(key)) !== null || storage?.getItem(toBiometricStorageKey(key)) !== null;
535
+ return withWebBackendOperation(scope, "has", backend => backend.getItem(toSecureStorageKey(key))) !== null || withWebBackendOperation(scope, "has", backend => backend.getItem(toBiometricStorageKey(key))) !== null;
473
536
  }
474
- return storage?.getItem(key) !== null;
537
+ if (scope !== StorageScope.Disk) {
538
+ return false;
539
+ }
540
+ return withWebBackendOperation(scope, "has", backend => backend.getItem(key)) !== null;
475
541
  },
476
542
  getAllKeys: scope => {
477
543
  if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
@@ -502,40 +568,43 @@ const WebStorage = {
502
568
  hasWarnedAboutWebBiometricFallback = true;
503
569
  console.warn("[NitroStorage] Biometric storage is not supported on web. Using localStorage.");
504
570
  }
505
- getBrowserStorage(StorageScope.Secure)?.setItem(toBiometricStorageKey(key), value);
571
+ withWebBackendOperation(StorageScope.Secure, "setSecureBiometric", backend => backend.setItem(toBiometricStorageKey(key), value));
506
572
  ensureWebScopeKeyIndex(StorageScope.Secure).add(key);
507
573
  notifyKeyListeners(getScopedListeners(StorageScope.Secure), key);
508
574
  },
509
575
  getSecureBiometric: key => {
510
- return getBrowserStorage(StorageScope.Secure)?.getItem(toBiometricStorageKey(key)) ?? undefined;
576
+ const value = withWebBackendOperation(StorageScope.Secure, "getSecureBiometric", backend => backend.getItem(toBiometricStorageKey(key)));
577
+ return value ?? undefined;
511
578
  },
512
579
  deleteSecureBiometric: key => {
513
- const storage = getBrowserStorage(StorageScope.Secure);
514
- storage?.removeItem(toBiometricStorageKey(key));
515
- if (storage?.getItem(toSecureStorageKey(key)) === null) {
580
+ withWebBackendOperation(StorageScope.Secure, "deleteSecureBiometric", backend => backend.removeItem(toBiometricStorageKey(key)));
581
+ if (withWebBackendOperation(StorageScope.Secure, "deleteSecureBiometric:getItem", backend => backend.getItem(toSecureStorageKey(key))) === null) {
516
582
  ensureWebScopeKeyIndex(StorageScope.Secure).delete(key);
517
583
  }
518
584
  notifyKeyListeners(getScopedListeners(StorageScope.Secure), key);
519
585
  },
520
586
  hasSecureBiometric: key => {
521
- return getBrowserStorage(StorageScope.Secure)?.getItem(toBiometricStorageKey(key)) !== null;
587
+ return withWebBackendOperation(StorageScope.Secure, "hasSecureBiometric", backend => backend.getItem(toBiometricStorageKey(key))) !== null;
522
588
  },
523
589
  clearSecureBiometric: () => {
524
- const storage = getBrowserStorage(StorageScope.Secure);
525
- if (!storage) return;
526
- const keysToNotify = [];
527
- const toRemove = [];
528
- for (let i = 0; i < storage.length; i++) {
529
- const k = storage.key(i);
530
- if (k?.startsWith(BIOMETRIC_WEB_PREFIX)) {
531
- toRemove.push(k);
532
- keysToNotify.push(fromBiometricStorageKey(k));
533
- }
590
+ const storageKeys = withWebBackendOperation(StorageScope.Secure, "clearSecureBiometric:getAllKeys", backend => backend.getAllKeys());
591
+ const keysToNotify = storageKeys.filter(key => key.startsWith(BIOMETRIC_WEB_PREFIX)).map(key => fromBiometricStorageKey(key));
592
+ if (keysToNotify.length === 0) {
593
+ return;
534
594
  }
535
- toRemove.forEach(k => storage.removeItem(k));
595
+ withWebBackendOperation(StorageScope.Secure, "clearSecureBiometric", backend => {
596
+ const biometricKeys = keysToNotify.map(key => toBiometricStorageKey(key));
597
+ if (backend.removeMany) {
598
+ backend.removeMany(biometricKeys);
599
+ return;
600
+ }
601
+ biometricKeys.forEach(storageKey => {
602
+ backend.removeItem(storageKey);
603
+ });
604
+ });
536
605
  const keyIndex = ensureWebScopeKeyIndex(StorageScope.Secure);
537
606
  keysToNotify.forEach(key => {
538
- if (storage.getItem(toSecureStorageKey(key)) === null) {
607
+ if (withWebBackendOperation(StorageScope.Secure, "clearSecureBiometric:getItem", backend => backend.getItem(toSecureStorageKey(key))) === null) {
539
608
  keyIndex.delete(key);
540
609
  }
541
610
  });
@@ -549,6 +618,9 @@ function getRawValue(key, scope) {
549
618
  const value = memoryStore.get(key);
550
619
  return typeof value === "string" ? value : undefined;
551
620
  }
621
+ if (scope === StorageScope.Disk && hasPendingDiskWrite(key)) {
622
+ return readPendingDiskWrite(key);
623
+ }
552
624
  if (scope === StorageScope.Secure && hasPendingSecureWrite(key)) {
553
625
  return readPendingSecureWrite(key);
554
626
  }
@@ -561,6 +633,15 @@ function setRawValue(key, value, scope) {
561
633
  notifyKeyListeners(memoryListeners, key);
562
634
  return;
563
635
  }
636
+ if (scope === StorageScope.Disk) {
637
+ cacheRawValue(scope, key, value);
638
+ if (diskWritesAsync) {
639
+ scheduleDiskWrite(key, value);
640
+ return;
641
+ }
642
+ flushDiskWrites();
643
+ clearPendingDiskWrite(key);
644
+ }
564
645
  if (scope === StorageScope.Secure) {
565
646
  flushSecureWrites();
566
647
  clearPendingSecureWrite(key);
@@ -575,6 +656,15 @@ function removeRawValue(key, scope) {
575
656
  notifyKeyListeners(memoryListeners, key);
576
657
  return;
577
658
  }
659
+ if (scope === StorageScope.Disk) {
660
+ cacheRawValue(scope, key, undefined);
661
+ if (diskWritesAsync) {
662
+ scheduleDiskWrite(key, undefined);
663
+ return;
664
+ }
665
+ flushDiskWrites();
666
+ clearPendingDiskWrite(key);
667
+ }
578
668
  if (scope === StorageScope.Secure) {
579
669
  flushSecureWrites();
580
670
  clearPendingSecureWrite(key);
@@ -601,6 +691,10 @@ export const storage = {
601
691
  notifyAllListeners(memoryListeners);
602
692
  return;
603
693
  }
694
+ if (scope === StorageScope.Disk) {
695
+ flushDiskWrites();
696
+ pendingDiskWrites.clear();
697
+ }
604
698
  if (scope === StorageScope.Secure) {
605
699
  flushSecureWrites();
606
700
  pendingSecureWrites.clear();
@@ -629,6 +723,9 @@ export const storage = {
629
723
  return;
630
724
  }
631
725
  const keyPrefix = prefixKey(namespace, "");
726
+ if (scope === StorageScope.Disk) {
727
+ flushDiskWrites();
728
+ }
632
729
  if (scope === StorageScope.Secure) {
633
730
  flushSecureWrites();
634
731
  }
@@ -650,6 +747,12 @@ export const storage = {
650
747
  return measureOperation("storage:has", scope, () => {
651
748
  assertValidScope(scope);
652
749
  if (scope === StorageScope.Memory) return memoryStore.has(key);
750
+ if (scope === StorageScope.Disk) {
751
+ flushDiskWrites();
752
+ }
753
+ if (scope === StorageScope.Secure) {
754
+ flushSecureWrites();
755
+ }
653
756
  return WebStorage.has(key, scope);
654
757
  });
655
758
  },
@@ -657,6 +760,12 @@ export const storage = {
657
760
  return measureOperation("storage:getAllKeys", scope, () => {
658
761
  assertValidScope(scope);
659
762
  if (scope === StorageScope.Memory) return Array.from(memoryStore.keys());
763
+ if (scope === StorageScope.Disk) {
764
+ flushDiskWrites();
765
+ }
766
+ if (scope === StorageScope.Secure) {
767
+ flushSecureWrites();
768
+ }
660
769
  return WebStorage.getAllKeys(scope);
661
770
  });
662
771
  },
@@ -666,6 +775,12 @@ export const storage = {
666
775
  if (scope === StorageScope.Memory) {
667
776
  return Array.from(memoryStore.keys()).filter(key => key.startsWith(prefix));
668
777
  }
778
+ if (scope === StorageScope.Disk) {
779
+ flushDiskWrites();
780
+ }
781
+ if (scope === StorageScope.Secure) {
782
+ flushSecureWrites();
783
+ }
669
784
  return WebStorage.getKeysByPrefix(prefix, scope);
670
785
  });
671
786
  },
@@ -685,6 +800,12 @@ export const storage = {
685
800
  });
686
801
  return result;
687
802
  }
803
+ if (scope === StorageScope.Disk) {
804
+ flushDiskWrites();
805
+ }
806
+ if (scope === StorageScope.Secure) {
807
+ flushSecureWrites();
808
+ }
688
809
  const values = WebStorage.getBatch(keys, scope);
689
810
  keys.forEach((key, index) => {
690
811
  const value = values[index];
@@ -705,6 +826,12 @@ export const storage = {
705
826
  });
706
827
  return result;
707
828
  }
829
+ if (scope === StorageScope.Disk) {
830
+ flushDiskWrites();
831
+ }
832
+ if (scope === StorageScope.Secure) {
833
+ flushSecureWrites();
834
+ }
708
835
  const keys = WebStorage.getAllKeys(scope);
709
836
  if (keys.length === 0) return {};
710
837
  const values = WebStorage.getBatch(keys, scope);
@@ -721,6 +848,12 @@ export const storage = {
721
848
  return measureOperation("storage:size", scope, () => {
722
849
  assertValidScope(scope);
723
850
  if (scope === StorageScope.Memory) return memoryStore.size;
851
+ if (scope === StorageScope.Disk) {
852
+ flushDiskWrites();
853
+ }
854
+ if (scope === StorageScope.Secure) {
855
+ flushSecureWrites();
856
+ }
724
857
  return WebStorage.size(scope);
725
858
  });
726
859
  },
@@ -731,6 +864,19 @@ export const storage = {
731
864
  setSecureWritesAsync: _enabled => {
732
865
  recordMetric("storage:setSecureWritesAsync", StorageScope.Secure, 0);
733
866
  },
867
+ setDiskWritesAsync: enabled => {
868
+ measureOperation("storage:setDiskWritesAsync", StorageScope.Disk, () => {
869
+ diskWritesAsync = enabled;
870
+ if (!enabled) {
871
+ flushDiskWrites();
872
+ }
873
+ });
874
+ },
875
+ flushDiskWrites: () => {
876
+ measureOperation("storage:flushDiskWrites", StorageScope.Disk, () => {
877
+ flushDiskWrites();
878
+ });
879
+ },
734
880
  flushSecureWrites: () => {
735
881
  measureOperation("storage:flushSecureWrites", StorageScope.Secure, () => {
736
882
  flushSecureWrites();
@@ -757,6 +903,18 @@ export const storage = {
757
903
  resetMetrics: () => {
758
904
  metricsCounters.clear();
759
905
  },
906
+ getCapabilities: () => ({
907
+ platform: "web",
908
+ backend: {
909
+ disk: getBackendName(StorageScope.Disk, webDiskStorageBackend),
910
+ secure: getBackendName(StorageScope.Secure, webSecureStorageBackend)
911
+ },
912
+ writeBuffering: {
913
+ disk: true,
914
+ secure: true
915
+ },
916
+ errorClassification: true
917
+ }),
760
918
  getString: (key, scope) => {
761
919
  return measureOperation("storage:getString", scope, () => {
762
920
  return getRawValue(key, scope);
@@ -789,21 +947,50 @@ export const storage = {
789
947
  flushSecureWrites();
790
948
  WebStorage.setSecureAccessControl(secureDefaultAccessControl);
791
949
  }
950
+ if (scope === StorageScope.Disk) {
951
+ flushDiskWrites();
952
+ }
792
953
  WebStorage.setBatch(keys, values, scope);
793
954
  keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
794
955
  }, keys.length);
795
956
  }
796
957
  };
797
958
  export function setWebSecureStorageBackend(backend) {
798
- webSecureStorageBackend = backend ?? createLocalStorageWebSecureBackend();
799
- cachedSecureBrowserStorage = undefined;
800
- cachedSecureBackendRef = undefined;
959
+ pendingSecureWrites.clear();
960
+ webSecureStorageBackend = backend ?? createDefaultSecureBackend();
961
+ resetBackendChangeSubscription(StorageScope.Secure);
801
962
  hydratedWebScopeKeyIndex.delete(StorageScope.Secure);
802
963
  clearScopeRawCache(StorageScope.Secure);
964
+ ensureExternalSyncSubscriptions();
803
965
  }
804
966
  export function getWebSecureStorageBackend() {
805
967
  return webSecureStorageBackend;
806
968
  }
969
+ export function setWebDiskStorageBackend(backend) {
970
+ pendingDiskWrites.clear();
971
+ webDiskStorageBackend = backend ?? createDefaultDiskBackend();
972
+ resetBackendChangeSubscription(StorageScope.Disk);
973
+ hydratedWebScopeKeyIndex.delete(StorageScope.Disk);
974
+ clearScopeRawCache(StorageScope.Disk);
975
+ ensureExternalSyncSubscriptions();
976
+ }
977
+ export function getWebDiskStorageBackend() {
978
+ return webDiskStorageBackend;
979
+ }
980
+ export async function flushWebStorageBackends() {
981
+ flushDiskWrites();
982
+ flushSecureWrites();
983
+ const flushes = [];
984
+ const diskFlush = webDiskStorageBackend?.flush;
985
+ const secureFlush = webSecureStorageBackend?.flush;
986
+ if (diskFlush) {
987
+ flushes.push(diskFlush());
988
+ }
989
+ if (secureFlush) {
990
+ flushes.push(secureFlush());
991
+ }
992
+ await Promise.all(flushes);
993
+ }
807
994
  function canUseRawBatchPath(item) {
808
995
  return item._hasExpiration === false && item._hasValidation === false && item._isBiometric !== true && item._secureAccessControl === undefined;
809
996
  }
@@ -831,6 +1018,7 @@ export function createStorageItem(config) {
831
1018
  const expirationTtlMs = expiration?.ttlMs;
832
1019
  const memoryExpiration = expiration && isMemory ? new Map() : null;
833
1020
  const readCache = !isMemory && config.readCache === true;
1021
+ const coalesceDiskWrites = config.scope === StorageScope.Disk && config.coalesceDiskWrites === true;
834
1022
  const coalesceSecureWrites = config.scope === StorageScope.Secure && config.coalesceSecureWrites === true && !isBiometric;
835
1023
  const defaultValue = config.defaultValue;
836
1024
  const nonMemoryScope = config.scope === StorageScope.Disk ? StorageScope.Disk : config.scope === StorageScope.Secure ? StorageScope.Secure : null;
@@ -861,7 +1049,7 @@ export function createStorageItem(config) {
861
1049
  unsubscribe = addKeyListener(memoryListeners, storageKey, listener);
862
1050
  return;
863
1051
  }
864
- ensureWebStorageEventSubscription();
1052
+ ensureExternalSyncSubscriptions();
865
1053
  unsubscribe = addKeyListener(getScopedListeners(nonMemoryScope), storageKey, listener);
866
1054
  };
867
1055
  const readStoredRaw = () => {
@@ -878,6 +1066,12 @@ export function createStorageItem(config) {
878
1066
  }
879
1067
  return memoryStore.get(storageKey);
880
1068
  }
1069
+ if (nonMemoryScope === StorageScope.Disk) {
1070
+ const pending = pendingDiskWrites.get(storageKey);
1071
+ if (pending !== undefined) {
1072
+ return pending.value;
1073
+ }
1074
+ }
881
1075
  if (nonMemoryScope === StorageScope.Secure && !isBiometric) {
882
1076
  const pending = pendingSecureWrites.get(storageKey);
883
1077
  if (pending !== undefined) {
@@ -904,6 +1098,13 @@ export function createStorageItem(config) {
904
1098
  return;
905
1099
  }
906
1100
  cacheRawValue(nonMemoryScope, storageKey, rawValue);
1101
+ if (nonMemoryScope === StorageScope.Disk) {
1102
+ if (coalesceDiskWrites || diskWritesAsync) {
1103
+ scheduleDiskWrite(storageKey, rawValue);
1104
+ return;
1105
+ }
1106
+ clearPendingDiskWrite(storageKey);
1107
+ }
907
1108
  if (coalesceSecureWrites) {
908
1109
  scheduleSecureWrite(storageKey, rawValue, secureAccessControl ?? secureDefaultAccessControl);
909
1110
  return;
@@ -919,6 +1120,13 @@ export function createStorageItem(config) {
919
1120
  return;
920
1121
  }
921
1122
  cacheRawValue(nonMemoryScope, storageKey, undefined);
1123
+ if (nonMemoryScope === StorageScope.Disk) {
1124
+ if (coalesceDiskWrites || diskWritesAsync) {
1125
+ scheduleDiskWrite(storageKey, undefined);
1126
+ return;
1127
+ }
1128
+ clearPendingDiskWrite(storageKey);
1129
+ }
922
1130
  if (coalesceSecureWrites) {
923
1131
  scheduleSecureWrite(storageKey, undefined, secureAccessControl ?? secureDefaultAccessControl);
924
1132
  return;
@@ -1079,6 +1287,18 @@ export function createStorageItem(config) {
1079
1287
  const hasItem = () => measureOperation("item:has", config.scope, () => {
1080
1288
  if (isMemory) return memoryStore.has(storageKey);
1081
1289
  if (isBiometric) return WebStorage.hasSecureBiometric(storageKey);
1290
+ if (nonMemoryScope === StorageScope.Disk) {
1291
+ const pending = pendingDiskWrites.get(storageKey);
1292
+ if (pending !== undefined) {
1293
+ return pending.value !== undefined;
1294
+ }
1295
+ }
1296
+ if (nonMemoryScope === StorageScope.Secure) {
1297
+ const pending = pendingSecureWrites.get(storageKey);
1298
+ if (pending !== undefined) {
1299
+ return pending.value !== undefined;
1300
+ }
1301
+ }
1082
1302
  return WebStorage.has(storageKey, config.scope);
1083
1303
  });
1084
1304
  const subscribe = callback => {
@@ -1089,9 +1309,6 @@ export function createStorageItem(config) {
1089
1309
  if (listeners.size === 0 && unsubscribe) {
1090
1310
  unsubscribe();
1091
1311
  unsubscribe = null;
1092
- if (!isMemory) {
1093
- maybeCleanupWebStorageSubscription();
1094
- }
1095
1312
  }
1096
1313
  };
1097
1314
  };
@@ -1141,6 +1358,13 @@ export function getBatch(items, scope) {
1141
1358
  const keysToFetch = [];
1142
1359
  const keyIndexes = [];
1143
1360
  items.forEach((item, index) => {
1361
+ if (scope === StorageScope.Disk) {
1362
+ const pending = pendingDiskWrites.get(item.key);
1363
+ if (pending !== undefined) {
1364
+ rawValues[index] = pending.value;
1365
+ return;
1366
+ }
1367
+ }
1144
1368
  if (scope === StorageScope.Secure) {
1145
1369
  const pending = pendingSecureWrites.get(item.key);
1146
1370
  if (pending !== undefined) {
@@ -1257,6 +1481,7 @@ export function setBatch(items, scope) {
1257
1481
  });
1258
1482
  return;
1259
1483
  }
1484
+ flushDiskWrites();
1260
1485
  const useRawBatchPath = items.every(({
1261
1486
  item
1262
1487
  }) => canUseRawBatchPath(asInternal(item)));
@@ -1281,6 +1506,9 @@ export function removeBatch(items, scope) {
1281
1506
  return;
1282
1507
  }
1283
1508
  const keys = items.map(item => item.key);
1509
+ if (scope === StorageScope.Disk) {
1510
+ flushDiskWrites();
1511
+ }
1284
1512
  if (scope === StorageScope.Secure) {
1285
1513
  flushSecureWrites();
1286
1514
  }
@@ -1326,6 +1554,9 @@ export function migrateToLatest(scope = StorageScope.Disk) {
1326
1554
  export function runTransaction(scope, transaction) {
1327
1555
  return measureOperation("transaction:run", scope, () => {
1328
1556
  assertValidScope(scope);
1557
+ if (scope === StorageScope.Disk) {
1558
+ flushDiskWrites();
1559
+ }
1329
1560
  if (scope === StorageScope.Secure) {
1330
1561
  flushSecureWrites();
1331
1562
  }
@@ -1392,6 +1623,9 @@ export function runTransaction(scope, transaction) {
1392
1623
  valuesToSet.push(previousValue);
1393
1624
  }
1394
1625
  });
1626
+ if (scope === StorageScope.Disk) {
1627
+ flushDiskWrites();
1628
+ }
1395
1629
  if (scope === StorageScope.Secure) {
1396
1630
  flushSecureWrites();
1397
1631
  }
@@ -1437,7 +1671,7 @@ export function createSecureAuthStorage(config, options) {
1437
1671
  }
1438
1672
  return result;
1439
1673
  }
1440
- export function isKeychainLockedError(_err) {
1441
- return false;
1674
+ export function isKeychainLockedError(err) {
1675
+ return isLockedStorageErrorCode(getStorageErrorCode(err));
1442
1676
  }
1443
1677
  //# sourceMappingURL=index.web.js.map