react-native-nitro-storage 0.4.4 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +237 -862
  2. package/SECURITY.md +26 -0
  3. package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +61 -10
  4. package/docs/api-reference.md +217 -0
  5. package/docs/batch-transactions-migrations.md +186 -0
  6. package/docs/benchmarks.md +37 -0
  7. package/docs/mmkv-migration.md +80 -0
  8. package/docs/react-hooks.md +113 -0
  9. package/docs/recipes.md +281 -0
  10. package/docs/secure-storage.md +171 -0
  11. package/docs/web-backends.md +141 -0
  12. package/ios/IOSStorageAdapterCpp.mm +44 -14
  13. package/lib/commonjs/index.js +271 -5
  14. package/lib/commonjs/index.js.map +1 -1
  15. package/lib/commonjs/index.web.js +498 -202
  16. package/lib/commonjs/index.web.js.map +1 -1
  17. package/lib/commonjs/indexeddb-backend.js +129 -7
  18. package/lib/commonjs/indexeddb-backend.js.map +1 -1
  19. package/lib/commonjs/storage-runtime.js +41 -0
  20. package/lib/commonjs/storage-runtime.js.map +1 -0
  21. package/lib/commonjs/web-storage-backend.js +90 -0
  22. package/lib/commonjs/web-storage-backend.js.map +1 -0
  23. package/lib/module/index.js +263 -5
  24. package/lib/module/index.js.map +1 -1
  25. package/lib/module/index.web.js +490 -202
  26. package/lib/module/index.web.js.map +1 -1
  27. package/lib/module/indexeddb-backend.js +129 -7
  28. package/lib/module/indexeddb-backend.js.map +1 -1
  29. package/lib/module/storage-runtime.js +36 -0
  30. package/lib/module/storage-runtime.js.map +1 -0
  31. package/lib/module/web-storage-backend.js +86 -0
  32. package/lib/module/web-storage-backend.js.map +1 -0
  33. package/lib/typescript/index.d.ts +14 -7
  34. package/lib/typescript/index.d.ts.map +1 -1
  35. package/lib/typescript/index.web.d.ts +15 -8
  36. package/lib/typescript/index.web.d.ts.map +1 -1
  37. package/lib/typescript/indexeddb-backend.d.ts +6 -2
  38. package/lib/typescript/indexeddb-backend.d.ts.map +1 -1
  39. package/lib/typescript/storage-runtime.d.ts +48 -0
  40. package/lib/typescript/storage-runtime.d.ts.map +1 -0
  41. package/lib/typescript/web-storage-backend.d.ts +30 -0
  42. package/lib/typescript/web-storage-backend.d.ts.map +1 -0
  43. package/package.json +21 -8
  44. package/src/index.ts +330 -20
  45. package/src/index.web.ts +673 -245
  46. package/src/indexeddb-backend.ts +147 -6
  47. package/src/storage-runtime.ts +129 -0
  48. 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,54 @@ 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 getWebSecureEncryptionStatus(backend) {
93
+ return backend?.name === "localStorage:secure" ? "unavailable" : "unknown";
94
+ }
95
+ function createWebStorageError(scope, operation, error, backend) {
96
+ const backendName = getBackendName(scope, backend);
97
+ const message = error instanceof Error ? error.message : String(error ?? "Unknown error");
98
+ return new Error(`NitroStorage(web): ${operation} failed for ${backendName}: ${message}`);
99
+ }
100
+ function withWebBackendOperation(scope, operation, fn) {
101
+ const backend = scope === StorageScope.Disk ? webDiskStorageBackend : webSecureStorageBackend;
102
+ if (!backend) {
103
+ throw new Error(`NitroStorage(web): ${operation} failed because no ${scope === StorageScope.Disk ? "disk" : "secure"} backend is configured.`);
93
104
  }
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;
105
+ try {
106
+ ensureExternalSyncSubscriptions();
107
+ return fn(backend);
108
+ } catch (error) {
109
+ throw createWebStorageError(scope, operation, error, backend);
113
110
  }
114
- return undefined;
111
+ }
112
+ function getWebBackend(scope) {
113
+ return scope === StorageScope.Disk ? webDiskStorageBackend : webSecureStorageBackend;
115
114
  }
116
115
  function toSecureStorageKey(key) {
117
116
  return `${SECURE_WEB_PREFIX}${key}`;
@@ -132,28 +131,21 @@ function hydrateWebScopeKeyIndex(scope) {
132
131
  if (hydratedWebScopeKeyIndex.has(scope)) {
133
132
  return;
134
133
  }
135
- const storage = getBrowserStorage(scope);
134
+ const backend = getWebBackend(scope);
136
135
  const keyIndex = getWebScopeKeyIndex(scope);
137
136
  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
- }
137
+ const keys = backend?.getAllKeys() ?? [];
138
+ for (const key of keys) {
139
+ if (scope === StorageScope.Disk) {
140
+ keyIndex.add(key);
141
+ continue;
142
+ }
143
+ if (key.startsWith(SECURE_WEB_PREFIX)) {
144
+ keyIndex.add(fromSecureStorageKey(key));
145
+ continue;
146
+ }
147
+ if (key.startsWith(BIOMETRIC_WEB_PREFIX)) {
148
+ keyIndex.add(fromBiometricStorageKey(key));
157
149
  }
158
150
  }
159
151
  hydratedWebScopeKeyIndex.add(scope);
@@ -162,65 +154,85 @@ function ensureWebScopeKeyIndex(scope) {
162
154
  hydrateWebScopeKeyIndex(scope);
163
155
  return getWebScopeKeyIndex(scope);
164
156
  }
165
- function handleWebStorageEvent(event) {
166
- const key = event.key;
157
+ function applyExternalChangeEvent(scope, key, newValue) {
167
158
  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));
159
+ clearScopeRawCache(scope);
160
+ ensureWebScopeKeyIndex(scope).clear();
161
+ notifyAllListeners(getScopedListeners(scope));
174
162
  return;
175
163
  }
176
- if (key.startsWith(SECURE_WEB_PREFIX)) {
164
+ if (scope === StorageScope.Secure && key.startsWith(SECURE_WEB_PREFIX)) {
177
165
  const plainKey = fromSecureStorageKey(key);
178
- if (event.newValue === null) {
166
+ if (newValue === null) {
179
167
  ensureWebScopeKeyIndex(StorageScope.Secure).delete(plainKey);
180
168
  cacheRawValue(StorageScope.Secure, plainKey, undefined);
181
169
  } else {
182
170
  ensureWebScopeKeyIndex(StorageScope.Secure).add(plainKey);
183
- cacheRawValue(StorageScope.Secure, plainKey, event.newValue);
171
+ cacheRawValue(StorageScope.Secure, plainKey, newValue);
184
172
  }
185
173
  notifyKeyListeners(getScopedListeners(StorageScope.Secure), plainKey);
186
174
  return;
187
175
  }
188
- if (key.startsWith(BIOMETRIC_WEB_PREFIX)) {
176
+ if (scope === StorageScope.Secure && key.startsWith(BIOMETRIC_WEB_PREFIX)) {
189
177
  const plainKey = fromBiometricStorageKey(key);
190
- if (event.newValue === null) {
191
- if (getBrowserStorage(StorageScope.Secure)?.getItem(toSecureStorageKey(plainKey)) === null) {
178
+ if (newValue === null) {
179
+ if (withWebBackendOperation(StorageScope.Secure, "external-sync:getItem", backend => backend.getItem(toSecureStorageKey(plainKey))) === null) {
192
180
  ensureWebScopeKeyIndex(StorageScope.Secure).delete(plainKey);
193
181
  }
194
182
  cacheRawValue(StorageScope.Secure, plainKey, undefined);
195
183
  } else {
196
184
  ensureWebScopeKeyIndex(StorageScope.Secure).add(plainKey);
197
- cacheRawValue(StorageScope.Secure, plainKey, event.newValue);
185
+ cacheRawValue(StorageScope.Secure, plainKey, newValue);
198
186
  }
199
187
  notifyKeyListeners(getScopedListeners(StorageScope.Secure), plainKey);
200
188
  return;
201
189
  }
202
- if (event.newValue === null) {
203
- ensureWebScopeKeyIndex(StorageScope.Disk).delete(key);
204
- cacheRawValue(StorageScope.Disk, key, undefined);
190
+ if (newValue === null) {
191
+ ensureWebScopeKeyIndex(scope).delete(key);
192
+ cacheRawValue(scope, key, undefined);
205
193
  } else {
206
- ensureWebScopeKeyIndex(StorageScope.Disk).add(key);
207
- cacheRawValue(StorageScope.Disk, key, event.newValue);
194
+ ensureWebScopeKeyIndex(scope).add(key);
195
+ cacheRawValue(scope, key, newValue);
208
196
  }
209
- notifyKeyListeners(getScopedListeners(StorageScope.Disk), key);
197
+ notifyKeyListeners(getScopedListeners(scope), key);
210
198
  }
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;
199
+ function handleWebStorageEvent(event) {
200
+ const key = event.key;
201
+ if (key === null) {
202
+ applyExternalChangeEvent(StorageScope.Disk, null, null);
203
+ applyExternalChangeEvent(StorageScope.Secure, null, null);
204
+ return;
205
+ }
206
+ if (key.startsWith(SECURE_WEB_PREFIX) || key.startsWith(BIOMETRIC_WEB_PREFIX)) {
207
+ applyExternalChangeEvent(StorageScope.Secure, key, event.newValue);
208
+ return;
216
209
  }
210
+ applyExternalChangeEvent(StorageScope.Disk, key, event.newValue);
211
+ }
212
+ function subscribeToBackendChanges(scope) {
213
+ if (externalSyncUnsubscribers.has(scope)) {
214
+ return;
215
+ }
216
+ const backend = getWebBackend(scope);
217
+ if (!backend?.subscribe) {
218
+ return;
219
+ }
220
+ const unsubscribe = backend.subscribe(event => {
221
+ applyExternalChangeEvent(scope, event.key, event.newValue);
222
+ });
223
+ externalSyncUnsubscribers.set(scope, unsubscribe);
224
+ }
225
+ function resetBackendChangeSubscription(scope) {
226
+ externalSyncUnsubscribers.get(scope)?.();
227
+ externalSyncUnsubscribers.delete(scope);
217
228
  }
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;
229
+ function ensureExternalSyncSubscriptions() {
230
+ if (!hasWindowStorageEventSubscription && typeof window !== "undefined" && typeof window.addEventListener === "function") {
231
+ window.addEventListener("storage", handleWebStorageEvent);
232
+ hasWindowStorageEventSubscription = true;
223
233
  }
234
+ subscribeToBackendChanges(StorageScope.Disk);
235
+ subscribeToBackendChanges(StorageScope.Secure);
224
236
  }
225
237
  function getScopedListeners(scope) {
226
238
  return webScopeListeners.get(scope);
@@ -276,12 +288,49 @@ function addKeyListener(registry, key, listener) {
276
288
  function readPendingSecureWrite(key) {
277
289
  return pendingSecureWrites.get(key)?.value;
278
290
  }
291
+ function readPendingDiskWrite(key) {
292
+ return pendingDiskWrites.get(key)?.value;
293
+ }
294
+ function hasPendingDiskWrite(key) {
295
+ return pendingDiskWrites.has(key);
296
+ }
279
297
  function hasPendingSecureWrite(key) {
280
298
  return pendingSecureWrites.has(key);
281
299
  }
300
+ function clearPendingDiskWrite(key) {
301
+ pendingDiskWrites.delete(key);
302
+ }
282
303
  function clearPendingSecureWrite(key) {
283
304
  pendingSecureWrites.delete(key);
284
305
  }
306
+ function flushDiskWrites() {
307
+ diskFlushScheduled = false;
308
+ if (pendingDiskWrites.size === 0) {
309
+ return;
310
+ }
311
+ const writes = Array.from(pendingDiskWrites.values());
312
+ pendingDiskWrites.clear();
313
+ const keysToSet = [];
314
+ const valuesToSet = [];
315
+ const keysToRemove = [];
316
+ writes.forEach(({
317
+ key,
318
+ value
319
+ }) => {
320
+ if (value === undefined) {
321
+ keysToRemove.push(key);
322
+ return;
323
+ }
324
+ keysToSet.push(key);
325
+ valuesToSet.push(value);
326
+ });
327
+ if (keysToSet.length > 0) {
328
+ WebStorage.setBatch(keysToSet, valuesToSet, StorageScope.Disk);
329
+ }
330
+ if (keysToRemove.length > 0) {
331
+ WebStorage.removeBatch(keysToRemove, StorageScope.Disk);
332
+ }
333
+ }
285
334
  function flushSecureWrites() {
286
335
  secureFlushScheduled = false;
287
336
  if (pendingSecureWrites.size === 0) {
@@ -320,6 +369,17 @@ function flushSecureWrites() {
320
369
  WebStorage.removeBatch(keysToRemove, StorageScope.Secure);
321
370
  }
322
371
  }
372
+ function scheduleDiskWrite(key, value) {
373
+ pendingDiskWrites.set(key, {
374
+ key,
375
+ value
376
+ });
377
+ if (diskFlushScheduled) {
378
+ return;
379
+ }
380
+ diskFlushScheduled = true;
381
+ runMicrotask(flushDiskWrites);
382
+ }
323
383
  function scheduleSecureWrite(key, value, accessControl) {
324
384
  const pendingWrite = {
325
385
  key,
@@ -340,117 +400,124 @@ const WebStorage = {
340
400
  equals: other => other === WebStorage,
341
401
  dispose: () => {},
342
402
  set: (key, value, scope) => {
343
- const storage = getBrowserStorage(scope);
344
- if (!storage) {
403
+ if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
345
404
  return;
346
405
  }
347
406
  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
- }
407
+ withWebBackendOperation(scope, "set", backend => {
408
+ backend.setItem(storageKey, value);
409
+ });
410
+ ensureWebScopeKeyIndex(scope).add(key);
411
+ notifyKeyListeners(getScopedListeners(scope), key);
353
412
  },
354
413
  get: (key, scope) => {
355
- const storage = getBrowserStorage(scope);
414
+ if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
415
+ return undefined;
416
+ }
356
417
  const storageKey = scope === StorageScope.Secure ? toSecureStorageKey(key) : key;
357
- return storage?.getItem(storageKey) ?? undefined;
418
+ const value = withWebBackendOperation(scope, "get", backend => backend.getItem(storageKey));
419
+ return value ?? undefined;
358
420
  },
359
421
  remove: (key, scope) => {
360
- const storage = getBrowserStorage(scope);
361
- if (!storage) {
422
+ if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
362
423
  return;
363
424
  }
364
425
  if (scope === StorageScope.Secure) {
365
- storage.removeItem(toSecureStorageKey(key));
366
- storage.removeItem(toBiometricStorageKey(key));
426
+ withWebBackendOperation(scope, "remove", backend => {
427
+ if (backend.removeMany) {
428
+ backend.removeMany([toSecureStorageKey(key), toBiometricStorageKey(key)]);
429
+ return;
430
+ }
431
+ backend.removeItem(toSecureStorageKey(key));
432
+ backend.removeItem(toBiometricStorageKey(key));
433
+ });
367
434
  } else {
368
- storage.removeItem(key);
369
- }
370
- if (scope === StorageScope.Disk || scope === StorageScope.Secure) {
371
- ensureWebScopeKeyIndex(scope).delete(key);
372
- notifyKeyListeners(getScopedListeners(scope), key);
435
+ withWebBackendOperation(scope, "remove", backend => {
436
+ backend.removeItem(key);
437
+ });
373
438
  }
439
+ ensureWebScopeKeyIndex(scope).delete(key);
440
+ notifyKeyListeners(getScopedListeners(scope), key);
374
441
  },
375
442
  clear: scope => {
376
- const storage = getBrowserStorage(scope);
377
- if (!storage) {
443
+ if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
378
444
  return;
379
445
  }
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
- }
446
+ withWebBackendOperation(scope, "clear", backend => {
447
+ backend.clear();
448
+ });
449
+ ensureWebScopeKeyIndex(scope).clear();
450
+ notifyAllListeners(getScopedListeners(scope));
405
451
  },
406
452
  setBatch: (keys, values, scope) => {
407
- const storage = getBrowserStorage(scope);
408
- if (!storage) {
453
+ if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
409
454
  return;
410
455
  }
456
+ const entries = [];
411
457
  keys.forEach((key, index) => {
412
458
  const value = values[index];
413
459
  if (value === undefined) {
414
460
  return;
415
461
  }
416
- const storageKey = scope === StorageScope.Secure ? toSecureStorageKey(key) : key;
417
- storage.setItem(storageKey, value);
462
+ entries.push([scope === StorageScope.Secure ? toSecureStorageKey(key) : key, value]);
418
463
  });
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
- }
464
+ withWebBackendOperation(scope, "setBatch", backend => {
465
+ if (backend.setMany) {
466
+ backend.setMany(entries);
467
+ return;
468
+ }
469
+ entries.forEach(([storageKey, value]) => {
470
+ backend.setItem(storageKey, value);
471
+ });
472
+ });
473
+ const keyIndex = ensureWebScopeKeyIndex(scope);
474
+ keys.forEach(key => keyIndex.add(key));
475
+ const listeners = getScopedListeners(scope);
476
+ keys.forEach(key => notifyKeyListeners(listeners, key));
425
477
  },
426
478
  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;
479
+ if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
480
+ return keys.map(() => undefined);
481
+ }
482
+ const storageKeys = keys.map(key => scope === StorageScope.Secure ? toSecureStorageKey(key) : key);
483
+ const values = withWebBackendOperation(scope, "getBatch", backend => {
484
+ if (backend.getMany) {
485
+ return backend.getMany(storageKeys);
486
+ }
487
+ return storageKeys.map(storageKey => backend.getItem(storageKey));
431
488
  });
489
+ return values.map(value => value ?? undefined);
432
490
  },
433
491
  removeBatch: (keys, scope) => {
434
- const storage = getBrowserStorage(scope);
435
- if (!storage) {
492
+ if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
436
493
  return;
437
494
  }
438
495
  if (scope === StorageScope.Secure) {
439
- keys.forEach(key => {
440
- storage.removeItem(toSecureStorageKey(key));
441
- storage.removeItem(toBiometricStorageKey(key));
496
+ const storageKeys = keys.flatMap(key => [toSecureStorageKey(key), toBiometricStorageKey(key)]);
497
+ withWebBackendOperation(scope, "removeBatch", backend => {
498
+ if (backend.removeMany) {
499
+ backend.removeMany(storageKeys);
500
+ return;
501
+ }
502
+ storageKeys.forEach(storageKey => {
503
+ backend.removeItem(storageKey);
504
+ });
442
505
  });
443
506
  } else {
444
- keys.forEach(key => {
445
- storage.removeItem(key);
507
+ withWebBackendOperation(scope, "removeBatch", backend => {
508
+ if (backend.removeMany) {
509
+ backend.removeMany(keys);
510
+ return;
511
+ }
512
+ keys.forEach(key => {
513
+ backend.removeItem(key);
514
+ });
446
515
  });
447
516
  }
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
- }
517
+ const keyIndex = ensureWebScopeKeyIndex(scope);
518
+ keys.forEach(key => keyIndex.delete(key));
519
+ const listeners = getScopedListeners(scope);
520
+ keys.forEach(key => notifyKeyListeners(listeners, key));
454
521
  },
455
522
  removeByPrefix: (prefix, scope) => {
456
523
  if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
@@ -467,11 +534,13 @@ const WebStorage = {
467
534
  return () => {};
468
535
  },
469
536
  has: (key, scope) => {
470
- const storage = getBrowserStorage(scope);
471
537
  if (scope === StorageScope.Secure) {
472
- return storage?.getItem(toSecureStorageKey(key)) !== null || storage?.getItem(toBiometricStorageKey(key)) !== null;
538
+ return withWebBackendOperation(scope, "has", backend => backend.getItem(toSecureStorageKey(key))) !== null || withWebBackendOperation(scope, "has", backend => backend.getItem(toBiometricStorageKey(key))) !== null;
473
539
  }
474
- return storage?.getItem(key) !== null;
540
+ if (scope !== StorageScope.Disk) {
541
+ return false;
542
+ }
543
+ return withWebBackendOperation(scope, "has", backend => backend.getItem(key)) !== null;
475
544
  },
476
545
  getAllKeys: scope => {
477
546
  if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
@@ -502,40 +571,43 @@ const WebStorage = {
502
571
  hasWarnedAboutWebBiometricFallback = true;
503
572
  console.warn("[NitroStorage] Biometric storage is not supported on web. Using localStorage.");
504
573
  }
505
- getBrowserStorage(StorageScope.Secure)?.setItem(toBiometricStorageKey(key), value);
574
+ withWebBackendOperation(StorageScope.Secure, "setSecureBiometric", backend => backend.setItem(toBiometricStorageKey(key), value));
506
575
  ensureWebScopeKeyIndex(StorageScope.Secure).add(key);
507
576
  notifyKeyListeners(getScopedListeners(StorageScope.Secure), key);
508
577
  },
509
578
  getSecureBiometric: key => {
510
- return getBrowserStorage(StorageScope.Secure)?.getItem(toBiometricStorageKey(key)) ?? undefined;
579
+ const value = withWebBackendOperation(StorageScope.Secure, "getSecureBiometric", backend => backend.getItem(toBiometricStorageKey(key)));
580
+ return value ?? undefined;
511
581
  },
512
582
  deleteSecureBiometric: key => {
513
- const storage = getBrowserStorage(StorageScope.Secure);
514
- storage?.removeItem(toBiometricStorageKey(key));
515
- if (storage?.getItem(toSecureStorageKey(key)) === null) {
583
+ withWebBackendOperation(StorageScope.Secure, "deleteSecureBiometric", backend => backend.removeItem(toBiometricStorageKey(key)));
584
+ if (withWebBackendOperation(StorageScope.Secure, "deleteSecureBiometric:getItem", backend => backend.getItem(toSecureStorageKey(key))) === null) {
516
585
  ensureWebScopeKeyIndex(StorageScope.Secure).delete(key);
517
586
  }
518
587
  notifyKeyListeners(getScopedListeners(StorageScope.Secure), key);
519
588
  },
520
589
  hasSecureBiometric: key => {
521
- return getBrowserStorage(StorageScope.Secure)?.getItem(toBiometricStorageKey(key)) !== null;
590
+ return withWebBackendOperation(StorageScope.Secure, "hasSecureBiometric", backend => backend.getItem(toBiometricStorageKey(key))) !== null;
522
591
  },
523
592
  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
- }
593
+ const storageKeys = withWebBackendOperation(StorageScope.Secure, "clearSecureBiometric:getAllKeys", backend => backend.getAllKeys());
594
+ const keysToNotify = storageKeys.filter(key => key.startsWith(BIOMETRIC_WEB_PREFIX)).map(key => fromBiometricStorageKey(key));
595
+ if (keysToNotify.length === 0) {
596
+ return;
534
597
  }
535
- toRemove.forEach(k => storage.removeItem(k));
598
+ withWebBackendOperation(StorageScope.Secure, "clearSecureBiometric", backend => {
599
+ const biometricKeys = keysToNotify.map(key => toBiometricStorageKey(key));
600
+ if (backend.removeMany) {
601
+ backend.removeMany(biometricKeys);
602
+ return;
603
+ }
604
+ biometricKeys.forEach(storageKey => {
605
+ backend.removeItem(storageKey);
606
+ });
607
+ });
536
608
  const keyIndex = ensureWebScopeKeyIndex(StorageScope.Secure);
537
609
  keysToNotify.forEach(key => {
538
- if (storage.getItem(toSecureStorageKey(key)) === null) {
610
+ if (withWebBackendOperation(StorageScope.Secure, "clearSecureBiometric:getItem", backend => backend.getItem(toSecureStorageKey(key))) === null) {
539
611
  keyIndex.delete(key);
540
612
  }
541
613
  });
@@ -549,6 +621,9 @@ function getRawValue(key, scope) {
549
621
  const value = memoryStore.get(key);
550
622
  return typeof value === "string" ? value : undefined;
551
623
  }
624
+ if (scope === StorageScope.Disk && hasPendingDiskWrite(key)) {
625
+ return readPendingDiskWrite(key);
626
+ }
552
627
  if (scope === StorageScope.Secure && hasPendingSecureWrite(key)) {
553
628
  return readPendingSecureWrite(key);
554
629
  }
@@ -561,6 +636,15 @@ function setRawValue(key, value, scope) {
561
636
  notifyKeyListeners(memoryListeners, key);
562
637
  return;
563
638
  }
639
+ if (scope === StorageScope.Disk) {
640
+ cacheRawValue(scope, key, value);
641
+ if (diskWritesAsync) {
642
+ scheduleDiskWrite(key, value);
643
+ return;
644
+ }
645
+ flushDiskWrites();
646
+ clearPendingDiskWrite(key);
647
+ }
564
648
  if (scope === StorageScope.Secure) {
565
649
  flushSecureWrites();
566
650
  clearPendingSecureWrite(key);
@@ -575,6 +659,15 @@ function removeRawValue(key, scope) {
575
659
  notifyKeyListeners(memoryListeners, key);
576
660
  return;
577
661
  }
662
+ if (scope === StorageScope.Disk) {
663
+ cacheRawValue(scope, key, undefined);
664
+ if (diskWritesAsync) {
665
+ scheduleDiskWrite(key, undefined);
666
+ return;
667
+ }
668
+ flushDiskWrites();
669
+ clearPendingDiskWrite(key);
670
+ }
578
671
  if (scope === StorageScope.Secure) {
579
672
  flushSecureWrites();
580
673
  clearPendingSecureWrite(key);
@@ -601,6 +694,10 @@ export const storage = {
601
694
  notifyAllListeners(memoryListeners);
602
695
  return;
603
696
  }
697
+ if (scope === StorageScope.Disk) {
698
+ flushDiskWrites();
699
+ pendingDiskWrites.clear();
700
+ }
604
701
  if (scope === StorageScope.Secure) {
605
702
  flushSecureWrites();
606
703
  pendingSecureWrites.clear();
@@ -629,6 +726,9 @@ export const storage = {
629
726
  return;
630
727
  }
631
728
  const keyPrefix = prefixKey(namespace, "");
729
+ if (scope === StorageScope.Disk) {
730
+ flushDiskWrites();
731
+ }
632
732
  if (scope === StorageScope.Secure) {
633
733
  flushSecureWrites();
634
734
  }
@@ -650,6 +750,12 @@ export const storage = {
650
750
  return measureOperation("storage:has", scope, () => {
651
751
  assertValidScope(scope);
652
752
  if (scope === StorageScope.Memory) return memoryStore.has(key);
753
+ if (scope === StorageScope.Disk) {
754
+ flushDiskWrites();
755
+ }
756
+ if (scope === StorageScope.Secure) {
757
+ flushSecureWrites();
758
+ }
653
759
  return WebStorage.has(key, scope);
654
760
  });
655
761
  },
@@ -657,6 +763,12 @@ export const storage = {
657
763
  return measureOperation("storage:getAllKeys", scope, () => {
658
764
  assertValidScope(scope);
659
765
  if (scope === StorageScope.Memory) return Array.from(memoryStore.keys());
766
+ if (scope === StorageScope.Disk) {
767
+ flushDiskWrites();
768
+ }
769
+ if (scope === StorageScope.Secure) {
770
+ flushSecureWrites();
771
+ }
660
772
  return WebStorage.getAllKeys(scope);
661
773
  });
662
774
  },
@@ -666,6 +778,12 @@ export const storage = {
666
778
  if (scope === StorageScope.Memory) {
667
779
  return Array.from(memoryStore.keys()).filter(key => key.startsWith(prefix));
668
780
  }
781
+ if (scope === StorageScope.Disk) {
782
+ flushDiskWrites();
783
+ }
784
+ if (scope === StorageScope.Secure) {
785
+ flushSecureWrites();
786
+ }
669
787
  return WebStorage.getKeysByPrefix(prefix, scope);
670
788
  });
671
789
  },
@@ -685,6 +803,12 @@ export const storage = {
685
803
  });
686
804
  return result;
687
805
  }
806
+ if (scope === StorageScope.Disk) {
807
+ flushDiskWrites();
808
+ }
809
+ if (scope === StorageScope.Secure) {
810
+ flushSecureWrites();
811
+ }
688
812
  const values = WebStorage.getBatch(keys, scope);
689
813
  keys.forEach((key, index) => {
690
814
  const value = values[index];
@@ -705,6 +829,12 @@ export const storage = {
705
829
  });
706
830
  return result;
707
831
  }
832
+ if (scope === StorageScope.Disk) {
833
+ flushDiskWrites();
834
+ }
835
+ if (scope === StorageScope.Secure) {
836
+ flushSecureWrites();
837
+ }
708
838
  const keys = WebStorage.getAllKeys(scope);
709
839
  if (keys.length === 0) return {};
710
840
  const values = WebStorage.getBatch(keys, scope);
@@ -721,6 +851,12 @@ export const storage = {
721
851
  return measureOperation("storage:size", scope, () => {
722
852
  assertValidScope(scope);
723
853
  if (scope === StorageScope.Memory) return memoryStore.size;
854
+ if (scope === StorageScope.Disk) {
855
+ flushDiskWrites();
856
+ }
857
+ if (scope === StorageScope.Secure) {
858
+ flushSecureWrites();
859
+ }
724
860
  return WebStorage.size(scope);
725
861
  });
726
862
  },
@@ -731,6 +867,19 @@ export const storage = {
731
867
  setSecureWritesAsync: _enabled => {
732
868
  recordMetric("storage:setSecureWritesAsync", StorageScope.Secure, 0);
733
869
  },
870
+ setDiskWritesAsync: enabled => {
871
+ measureOperation("storage:setDiskWritesAsync", StorageScope.Disk, () => {
872
+ diskWritesAsync = enabled;
873
+ if (!enabled) {
874
+ flushDiskWrites();
875
+ }
876
+ });
877
+ },
878
+ flushDiskWrites: () => {
879
+ measureOperation("storage:flushDiskWrites", StorageScope.Disk, () => {
880
+ flushDiskWrites();
881
+ });
882
+ },
734
883
  flushSecureWrites: () => {
735
884
  measureOperation("storage:flushSecureWrites", StorageScope.Secure, () => {
736
885
  flushSecureWrites();
@@ -757,6 +906,69 @@ export const storage = {
757
906
  resetMetrics: () => {
758
907
  metricsCounters.clear();
759
908
  },
909
+ getCapabilities: () => ({
910
+ platform: "web",
911
+ backend: {
912
+ disk: getBackendName(StorageScope.Disk, webDiskStorageBackend),
913
+ secure: getBackendName(StorageScope.Secure, webSecureStorageBackend)
914
+ },
915
+ writeBuffering: {
916
+ disk: true,
917
+ secure: true
918
+ },
919
+ errorClassification: true
920
+ }),
921
+ getSecurityCapabilities: () => {
922
+ const secureBackend = getBackendName(StorageScope.Secure, webSecureStorageBackend);
923
+ return {
924
+ platform: "web",
925
+ secureStorage: {
926
+ backend: secureBackend,
927
+ encrypted: getWebSecureEncryptionStatus(webSecureStorageBackend),
928
+ accessControl: "unavailable",
929
+ keychainAccessGroup: "unavailable",
930
+ hardwareBacked: "unavailable"
931
+ },
932
+ biometric: {
933
+ storage: "unavailable",
934
+ prompt: "unavailable",
935
+ biometryOnly: "unavailable",
936
+ biometryOrPasscode: "unavailable"
937
+ },
938
+ metadata: {
939
+ perKey: true,
940
+ listsWithoutValues: true,
941
+ persistsTimestamps: false
942
+ }
943
+ };
944
+ },
945
+ getSecureMetadata: key => {
946
+ return measureOperation("storage:getSecureMetadata", StorageScope.Secure, () => {
947
+ flushSecureWrites();
948
+ const biometricProtected = WebStorage.hasSecureBiometric(key);
949
+ const exists = biometricProtected || WebStorage.has(key, StorageScope.Secure);
950
+ let kind = "missing";
951
+ if (exists) {
952
+ kind = biometricProtected ? "biometric" : "secure";
953
+ }
954
+ return {
955
+ key,
956
+ exists,
957
+ kind,
958
+ backend: getBackendName(StorageScope.Secure, webSecureStorageBackend),
959
+ encrypted: getWebSecureEncryptionStatus(webSecureStorageBackend),
960
+ hardwareBacked: "unavailable",
961
+ biometricProtected,
962
+ valueExposed: false
963
+ };
964
+ });
965
+ },
966
+ getAllSecureMetadata: () => {
967
+ return measureOperation("storage:getAllSecureMetadata", StorageScope.Secure, () => {
968
+ flushSecureWrites();
969
+ return WebStorage.getAllKeys(StorageScope.Secure).map(key => storage.getSecureMetadata(key));
970
+ });
971
+ },
760
972
  getString: (key, scope) => {
761
973
  return measureOperation("storage:getString", scope, () => {
762
974
  return getRawValue(key, scope);
@@ -789,21 +1001,50 @@ export const storage = {
789
1001
  flushSecureWrites();
790
1002
  WebStorage.setSecureAccessControl(secureDefaultAccessControl);
791
1003
  }
1004
+ if (scope === StorageScope.Disk) {
1005
+ flushDiskWrites();
1006
+ }
792
1007
  WebStorage.setBatch(keys, values, scope);
793
1008
  keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
794
1009
  }, keys.length);
795
1010
  }
796
1011
  };
797
1012
  export function setWebSecureStorageBackend(backend) {
798
- webSecureStorageBackend = backend ?? createLocalStorageWebSecureBackend();
799
- cachedSecureBrowserStorage = undefined;
800
- cachedSecureBackendRef = undefined;
1013
+ pendingSecureWrites.clear();
1014
+ webSecureStorageBackend = backend ?? createDefaultSecureBackend();
1015
+ resetBackendChangeSubscription(StorageScope.Secure);
801
1016
  hydratedWebScopeKeyIndex.delete(StorageScope.Secure);
802
1017
  clearScopeRawCache(StorageScope.Secure);
1018
+ ensureExternalSyncSubscriptions();
803
1019
  }
804
1020
  export function getWebSecureStorageBackend() {
805
1021
  return webSecureStorageBackend;
806
1022
  }
1023
+ export function setWebDiskStorageBackend(backend) {
1024
+ pendingDiskWrites.clear();
1025
+ webDiskStorageBackend = backend ?? createDefaultDiskBackend();
1026
+ resetBackendChangeSubscription(StorageScope.Disk);
1027
+ hydratedWebScopeKeyIndex.delete(StorageScope.Disk);
1028
+ clearScopeRawCache(StorageScope.Disk);
1029
+ ensureExternalSyncSubscriptions();
1030
+ }
1031
+ export function getWebDiskStorageBackend() {
1032
+ return webDiskStorageBackend;
1033
+ }
1034
+ export async function flushWebStorageBackends() {
1035
+ flushDiskWrites();
1036
+ flushSecureWrites();
1037
+ const flushes = [];
1038
+ const diskFlush = webDiskStorageBackend?.flush;
1039
+ const secureFlush = webSecureStorageBackend?.flush;
1040
+ if (diskFlush) {
1041
+ flushes.push(diskFlush());
1042
+ }
1043
+ if (secureFlush) {
1044
+ flushes.push(secureFlush());
1045
+ }
1046
+ await Promise.all(flushes);
1047
+ }
807
1048
  function canUseRawBatchPath(item) {
808
1049
  return item._hasExpiration === false && item._hasValidation === false && item._isBiometric !== true && item._secureAccessControl === undefined;
809
1050
  }
@@ -831,6 +1072,7 @@ export function createStorageItem(config) {
831
1072
  const expirationTtlMs = expiration?.ttlMs;
832
1073
  const memoryExpiration = expiration && isMemory ? new Map() : null;
833
1074
  const readCache = !isMemory && config.readCache === true;
1075
+ const coalesceDiskWrites = config.scope === StorageScope.Disk && config.coalesceDiskWrites === true;
834
1076
  const coalesceSecureWrites = config.scope === StorageScope.Secure && config.coalesceSecureWrites === true && !isBiometric;
835
1077
  const defaultValue = config.defaultValue;
836
1078
  const nonMemoryScope = config.scope === StorageScope.Disk ? StorageScope.Disk : config.scope === StorageScope.Secure ? StorageScope.Secure : null;
@@ -861,7 +1103,7 @@ export function createStorageItem(config) {
861
1103
  unsubscribe = addKeyListener(memoryListeners, storageKey, listener);
862
1104
  return;
863
1105
  }
864
- ensureWebStorageEventSubscription();
1106
+ ensureExternalSyncSubscriptions();
865
1107
  unsubscribe = addKeyListener(getScopedListeners(nonMemoryScope), storageKey, listener);
866
1108
  };
867
1109
  const readStoredRaw = () => {
@@ -878,6 +1120,12 @@ export function createStorageItem(config) {
878
1120
  }
879
1121
  return memoryStore.get(storageKey);
880
1122
  }
1123
+ if (nonMemoryScope === StorageScope.Disk) {
1124
+ const pending = pendingDiskWrites.get(storageKey);
1125
+ if (pending !== undefined) {
1126
+ return pending.value;
1127
+ }
1128
+ }
881
1129
  if (nonMemoryScope === StorageScope.Secure && !isBiometric) {
882
1130
  const pending = pendingSecureWrites.get(storageKey);
883
1131
  if (pending !== undefined) {
@@ -904,6 +1152,13 @@ export function createStorageItem(config) {
904
1152
  return;
905
1153
  }
906
1154
  cacheRawValue(nonMemoryScope, storageKey, rawValue);
1155
+ if (nonMemoryScope === StorageScope.Disk) {
1156
+ if (coalesceDiskWrites || diskWritesAsync) {
1157
+ scheduleDiskWrite(storageKey, rawValue);
1158
+ return;
1159
+ }
1160
+ clearPendingDiskWrite(storageKey);
1161
+ }
907
1162
  if (coalesceSecureWrites) {
908
1163
  scheduleSecureWrite(storageKey, rawValue, secureAccessControl ?? secureDefaultAccessControl);
909
1164
  return;
@@ -919,6 +1174,13 @@ export function createStorageItem(config) {
919
1174
  return;
920
1175
  }
921
1176
  cacheRawValue(nonMemoryScope, storageKey, undefined);
1177
+ if (nonMemoryScope === StorageScope.Disk) {
1178
+ if (coalesceDiskWrites || diskWritesAsync) {
1179
+ scheduleDiskWrite(storageKey, undefined);
1180
+ return;
1181
+ }
1182
+ clearPendingDiskWrite(storageKey);
1183
+ }
922
1184
  if (coalesceSecureWrites) {
923
1185
  scheduleSecureWrite(storageKey, undefined, secureAccessControl ?? secureDefaultAccessControl);
924
1186
  return;
@@ -1079,6 +1341,18 @@ export function createStorageItem(config) {
1079
1341
  const hasItem = () => measureOperation("item:has", config.scope, () => {
1080
1342
  if (isMemory) return memoryStore.has(storageKey);
1081
1343
  if (isBiometric) return WebStorage.hasSecureBiometric(storageKey);
1344
+ if (nonMemoryScope === StorageScope.Disk) {
1345
+ const pending = pendingDiskWrites.get(storageKey);
1346
+ if (pending !== undefined) {
1347
+ return pending.value !== undefined;
1348
+ }
1349
+ }
1350
+ if (nonMemoryScope === StorageScope.Secure) {
1351
+ const pending = pendingSecureWrites.get(storageKey);
1352
+ if (pending !== undefined) {
1353
+ return pending.value !== undefined;
1354
+ }
1355
+ }
1082
1356
  return WebStorage.has(storageKey, config.scope);
1083
1357
  });
1084
1358
  const subscribe = callback => {
@@ -1089,9 +1363,6 @@ export function createStorageItem(config) {
1089
1363
  if (listeners.size === 0 && unsubscribe) {
1090
1364
  unsubscribe();
1091
1365
  unsubscribe = null;
1092
- if (!isMemory) {
1093
- maybeCleanupWebStorageSubscription();
1094
- }
1095
1366
  }
1096
1367
  };
1097
1368
  };
@@ -1141,6 +1412,13 @@ export function getBatch(items, scope) {
1141
1412
  const keysToFetch = [];
1142
1413
  const keyIndexes = [];
1143
1414
  items.forEach((item, index) => {
1415
+ if (scope === StorageScope.Disk) {
1416
+ const pending = pendingDiskWrites.get(item.key);
1417
+ if (pending !== undefined) {
1418
+ rawValues[index] = pending.value;
1419
+ return;
1420
+ }
1421
+ }
1144
1422
  if (scope === StorageScope.Secure) {
1145
1423
  const pending = pendingSecureWrites.get(item.key);
1146
1424
  if (pending !== undefined) {
@@ -1257,6 +1535,7 @@ export function setBatch(items, scope) {
1257
1535
  });
1258
1536
  return;
1259
1537
  }
1538
+ flushDiskWrites();
1260
1539
  const useRawBatchPath = items.every(({
1261
1540
  item
1262
1541
  }) => canUseRawBatchPath(asInternal(item)));
@@ -1281,6 +1560,9 @@ export function removeBatch(items, scope) {
1281
1560
  return;
1282
1561
  }
1283
1562
  const keys = items.map(item => item.key);
1563
+ if (scope === StorageScope.Disk) {
1564
+ flushDiskWrites();
1565
+ }
1284
1566
  if (scope === StorageScope.Secure) {
1285
1567
  flushSecureWrites();
1286
1568
  }
@@ -1326,6 +1608,9 @@ export function migrateToLatest(scope = StorageScope.Disk) {
1326
1608
  export function runTransaction(scope, transaction) {
1327
1609
  return measureOperation("transaction:run", scope, () => {
1328
1610
  assertValidScope(scope);
1611
+ if (scope === StorageScope.Disk) {
1612
+ flushDiskWrites();
1613
+ }
1329
1614
  if (scope === StorageScope.Secure) {
1330
1615
  flushSecureWrites();
1331
1616
  }
@@ -1392,6 +1677,9 @@ export function runTransaction(scope, transaction) {
1392
1677
  valuesToSet.push(previousValue);
1393
1678
  }
1394
1679
  });
1680
+ if (scope === StorageScope.Disk) {
1681
+ flushDiskWrites();
1682
+ }
1395
1683
  if (scope === StorageScope.Secure) {
1396
1684
  flushSecureWrites();
1397
1685
  }
@@ -1437,7 +1725,7 @@ export function createSecureAuthStorage(config, options) {
1437
1725
  }
1438
1726
  return result;
1439
1727
  }
1440
- export function isKeychainLockedError(_err) {
1441
- return false;
1728
+ export function isKeychainLockedError(err) {
1729
+ return isLockedStorageErrorCode(getStorageErrorCode(err));
1442
1730
  }
1443
1731
  //# sourceMappingURL=index.web.js.map