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
@@ -29,7 +29,15 @@ Object.defineProperty(exports, "createIndexedDBBackend", {
29
29
  });
30
30
  exports.createSecureAuthStorage = createSecureAuthStorage;
31
31
  exports.createStorageItem = createStorageItem;
32
+ exports.flushWebStorageBackends = flushWebStorageBackends;
32
33
  exports.getBatch = getBatch;
34
+ Object.defineProperty(exports, "getStorageErrorCode", {
35
+ enumerable: true,
36
+ get: function () {
37
+ return _storageRuntime.getStorageErrorCode;
38
+ }
39
+ });
40
+ exports.getWebDiskStorageBackend = getWebDiskStorageBackend;
33
41
  exports.getWebSecureStorageBackend = getWebSecureStorageBackend;
34
42
  exports.isKeychainLockedError = isKeychainLockedError;
35
43
  Object.defineProperty(exports, "migrateFromMMKV", {
@@ -43,6 +51,7 @@ exports.registerMigration = registerMigration;
43
51
  exports.removeBatch = removeBatch;
44
52
  exports.runTransaction = runTransaction;
45
53
  exports.setBatch = setBatch;
54
+ exports.setWebDiskStorageBackend = setWebDiskStorageBackend;
46
55
  exports.setWebSecureStorageBackend = setWebSecureStorageBackend;
47
56
  exports.storage = void 0;
48
57
  Object.defineProperty(exports, "useSetStorage", {
@@ -65,6 +74,8 @@ Object.defineProperty(exports, "useStorageSelector", {
65
74
  });
66
75
  var _Storage = require("./Storage.types");
67
76
  var _internal = require("./internal");
77
+ var _webStorageBackend = require("./web-storage-backend");
78
+ var _storageRuntime = require("./storage-runtime");
68
79
  var _migration = require("./migration");
69
80
  var _storageHooks = require("./storage-hooks");
70
81
  var _indexeddbBackend = require("./indexeddb-backend");
@@ -81,20 +92,23 @@ const registeredMigrations = new Map();
81
92
  const runMicrotask = typeof queueMicrotask === "function" ? queueMicrotask : task => {
82
93
  Promise.resolve().then(task);
83
94
  };
95
+ const now = typeof performance !== "undefined" && typeof performance.now === "function" ? () => performance.now() : () => Date.now();
84
96
  const memoryStore = new Map();
85
97
  const memoryListeners = new Map();
86
98
  const webScopeListeners = new Map([[_Storage.StorageScope.Disk, new Map()], [_Storage.StorageScope.Secure, new Map()]]);
87
99
  const scopedRawCache = new Map([[_Storage.StorageScope.Disk, new Map()], [_Storage.StorageScope.Secure, new Map()]]);
88
100
  const webScopeKeyIndex = new Map([[_Storage.StorageScope.Disk, new Set()], [_Storage.StorageScope.Secure, new Set()]]);
89
101
  const hydratedWebScopeKeyIndex = new Set();
102
+ const pendingDiskWrites = new Map();
103
+ let diskFlushScheduled = false;
104
+ let diskWritesAsync = false;
90
105
  const pendingSecureWrites = new Map();
91
106
  let secureFlushScheduled = false;
92
107
  let secureDefaultAccessControl = _Storage.AccessControl.WhenUnlocked;
93
108
  const SECURE_WEB_PREFIX = "__secure_";
94
109
  const BIOMETRIC_WEB_PREFIX = "__bio_";
95
110
  let hasWarnedAboutWebBiometricFallback = false;
96
- let hasWebStorageEventSubscription = false;
97
- let webStorageSubscriberCount = 0;
111
+ let hasWindowStorageEventSubscription = false;
98
112
  let metricsObserver;
99
113
  const metricsCounters = new Map();
100
114
  function recordMetric(operation, scope, durationMs, keysCount = 1) {
@@ -121,61 +135,54 @@ function measureOperation(operation, scope, fn, keysCount = 1) {
121
135
  if (!metricsObserver) {
122
136
  return fn();
123
137
  }
124
- const start = Date.now();
138
+ const start = now();
125
139
  try {
126
140
  return fn();
127
141
  } finally {
128
- recordMetric(operation, scope, Date.now() - start, keysCount);
142
+ recordMetric(operation, scope, now() - start, keysCount);
129
143
  }
130
144
  }
131
- function createLocalStorageWebSecureBackend() {
132
- return {
133
- getItem: key => globalThis.localStorage?.getItem(key) ?? null,
134
- setItem: (key, value) => globalThis.localStorage?.setItem(key, value),
135
- removeItem: key => globalThis.localStorage?.removeItem(key),
136
- clear: () => globalThis.localStorage?.clear(),
137
- getAllKeys: () => {
138
- const storage = globalThis.localStorage;
139
- if (!storage) return [];
140
- const keys = [];
141
- for (let index = 0; index < storage.length; index += 1) {
142
- const key = storage.key(index);
143
- if (key) {
144
- keys.push(key);
145
- }
146
- }
147
- return keys;
148
- }
149
- };
145
+ function createDefaultDiskBackend() {
146
+ return (0, _webStorageBackend.createLocalStorageWebBackend)({
147
+ name: "localStorage:disk",
148
+ includeKey: key => !key.startsWith(SECURE_WEB_PREFIX) && !key.startsWith(BIOMETRIC_WEB_PREFIX)
149
+ });
150
150
  }
151
- let webSecureStorageBackend = createLocalStorageWebSecureBackend();
152
- let cachedSecureBrowserStorage;
153
- let cachedSecureBackendRef;
154
- function getBrowserStorage(scope) {
155
- if (scope === _Storage.StorageScope.Disk) {
156
- return globalThis.localStorage;
151
+ function createDefaultSecureBackend() {
152
+ return (0, _webStorageBackend.createLocalStorageWebBackend)({
153
+ name: "localStorage:secure",
154
+ includeKey: key => key.startsWith(SECURE_WEB_PREFIX) || key.startsWith(BIOMETRIC_WEB_PREFIX)
155
+ });
156
+ }
157
+ let webDiskStorageBackend = createDefaultDiskBackend();
158
+ let webSecureStorageBackend = createDefaultSecureBackend();
159
+ const externalSyncUnsubscribers = new Map();
160
+ function getBackendName(scope, backend) {
161
+ const scopeName = scope === _Storage.StorageScope.Disk ? "disk" : "secure";
162
+ return backend?.name ?? `web:${scopeName}`;
163
+ }
164
+ function getWebSecureEncryptionStatus(backend) {
165
+ return backend?.name === "localStorage:secure" ? "unavailable" : "unknown";
166
+ }
167
+ function createWebStorageError(scope, operation, error, backend) {
168
+ const backendName = getBackendName(scope, backend);
169
+ const message = error instanceof Error ? error.message : String(error ?? "Unknown error");
170
+ return new Error(`NitroStorage(web): ${operation} failed for ${backendName}: ${message}`);
171
+ }
172
+ function withWebBackendOperation(scope, operation, fn) {
173
+ const backend = scope === _Storage.StorageScope.Disk ? webDiskStorageBackend : webSecureStorageBackend;
174
+ if (!backend) {
175
+ throw new Error(`NitroStorage(web): ${operation} failed because no ${scope === _Storage.StorageScope.Disk ? "disk" : "secure"} backend is configured.`);
157
176
  }
158
- if (scope === _Storage.StorageScope.Secure) {
159
- if (!webSecureStorageBackend) {
160
- return undefined;
161
- }
162
- if (cachedSecureBackendRef === webSecureStorageBackend && cachedSecureBrowserStorage) {
163
- return cachedSecureBrowserStorage;
164
- }
165
- cachedSecureBackendRef = webSecureStorageBackend;
166
- cachedSecureBrowserStorage = {
167
- setItem: (key, value) => webSecureStorageBackend.setItem(key, value),
168
- getItem: key => webSecureStorageBackend.getItem(key) ?? null,
169
- removeItem: key => webSecureStorageBackend.removeItem(key),
170
- clear: () => webSecureStorageBackend.clear(),
171
- key: index => webSecureStorageBackend.getAllKeys()[index] ?? null,
172
- get length() {
173
- return webSecureStorageBackend.getAllKeys().length;
174
- }
175
- };
176
- return cachedSecureBrowserStorage;
177
+ try {
178
+ ensureExternalSyncSubscriptions();
179
+ return fn(backend);
180
+ } catch (error) {
181
+ throw createWebStorageError(scope, operation, error, backend);
177
182
  }
178
- return undefined;
183
+ }
184
+ function getWebBackend(scope) {
185
+ return scope === _Storage.StorageScope.Disk ? webDiskStorageBackend : webSecureStorageBackend;
179
186
  }
180
187
  function toSecureStorageKey(key) {
181
188
  return `${SECURE_WEB_PREFIX}${key}`;
@@ -196,28 +203,21 @@ function hydrateWebScopeKeyIndex(scope) {
196
203
  if (hydratedWebScopeKeyIndex.has(scope)) {
197
204
  return;
198
205
  }
199
- const storage = getBrowserStorage(scope);
206
+ const backend = getWebBackend(scope);
200
207
  const keyIndex = getWebScopeKeyIndex(scope);
201
208
  keyIndex.clear();
202
- if (storage) {
203
- for (let index = 0; index < storage.length; index += 1) {
204
- const key = storage.key(index);
205
- if (!key) {
206
- continue;
207
- }
208
- if (scope === _Storage.StorageScope.Disk) {
209
- if (!key.startsWith(SECURE_WEB_PREFIX) && !key.startsWith(BIOMETRIC_WEB_PREFIX)) {
210
- keyIndex.add(key);
211
- }
212
- continue;
213
- }
214
- if (key.startsWith(SECURE_WEB_PREFIX)) {
215
- keyIndex.add(fromSecureStorageKey(key));
216
- continue;
217
- }
218
- if (key.startsWith(BIOMETRIC_WEB_PREFIX)) {
219
- keyIndex.add(fromBiometricStorageKey(key));
220
- }
209
+ const keys = backend?.getAllKeys() ?? [];
210
+ for (const key of keys) {
211
+ if (scope === _Storage.StorageScope.Disk) {
212
+ keyIndex.add(key);
213
+ continue;
214
+ }
215
+ if (key.startsWith(SECURE_WEB_PREFIX)) {
216
+ keyIndex.add(fromSecureStorageKey(key));
217
+ continue;
218
+ }
219
+ if (key.startsWith(BIOMETRIC_WEB_PREFIX)) {
220
+ keyIndex.add(fromBiometricStorageKey(key));
221
221
  }
222
222
  }
223
223
  hydratedWebScopeKeyIndex.add(scope);
@@ -226,65 +226,85 @@ function ensureWebScopeKeyIndex(scope) {
226
226
  hydrateWebScopeKeyIndex(scope);
227
227
  return getWebScopeKeyIndex(scope);
228
228
  }
229
- function handleWebStorageEvent(event) {
230
- const key = event.key;
229
+ function applyExternalChangeEvent(scope, key, newValue) {
231
230
  if (key === null) {
232
- clearScopeRawCache(_Storage.StorageScope.Disk);
233
- clearScopeRawCache(_Storage.StorageScope.Secure);
234
- ensureWebScopeKeyIndex(_Storage.StorageScope.Disk).clear();
235
- ensureWebScopeKeyIndex(_Storage.StorageScope.Secure).clear();
236
- notifyAllListeners(getScopedListeners(_Storage.StorageScope.Disk));
237
- notifyAllListeners(getScopedListeners(_Storage.StorageScope.Secure));
231
+ clearScopeRawCache(scope);
232
+ ensureWebScopeKeyIndex(scope).clear();
233
+ notifyAllListeners(getScopedListeners(scope));
238
234
  return;
239
235
  }
240
- if (key.startsWith(SECURE_WEB_PREFIX)) {
236
+ if (scope === _Storage.StorageScope.Secure && key.startsWith(SECURE_WEB_PREFIX)) {
241
237
  const plainKey = fromSecureStorageKey(key);
242
- if (event.newValue === null) {
238
+ if (newValue === null) {
243
239
  ensureWebScopeKeyIndex(_Storage.StorageScope.Secure).delete(plainKey);
244
240
  cacheRawValue(_Storage.StorageScope.Secure, plainKey, undefined);
245
241
  } else {
246
242
  ensureWebScopeKeyIndex(_Storage.StorageScope.Secure).add(plainKey);
247
- cacheRawValue(_Storage.StorageScope.Secure, plainKey, event.newValue);
243
+ cacheRawValue(_Storage.StorageScope.Secure, plainKey, newValue);
248
244
  }
249
245
  notifyKeyListeners(getScopedListeners(_Storage.StorageScope.Secure), plainKey);
250
246
  return;
251
247
  }
252
- if (key.startsWith(BIOMETRIC_WEB_PREFIX)) {
248
+ if (scope === _Storage.StorageScope.Secure && key.startsWith(BIOMETRIC_WEB_PREFIX)) {
253
249
  const plainKey = fromBiometricStorageKey(key);
254
- if (event.newValue === null) {
255
- if (getBrowserStorage(_Storage.StorageScope.Secure)?.getItem(toSecureStorageKey(plainKey)) === null) {
250
+ if (newValue === null) {
251
+ if (withWebBackendOperation(_Storage.StorageScope.Secure, "external-sync:getItem", backend => backend.getItem(toSecureStorageKey(plainKey))) === null) {
256
252
  ensureWebScopeKeyIndex(_Storage.StorageScope.Secure).delete(plainKey);
257
253
  }
258
254
  cacheRawValue(_Storage.StorageScope.Secure, plainKey, undefined);
259
255
  } else {
260
256
  ensureWebScopeKeyIndex(_Storage.StorageScope.Secure).add(plainKey);
261
- cacheRawValue(_Storage.StorageScope.Secure, plainKey, event.newValue);
257
+ cacheRawValue(_Storage.StorageScope.Secure, plainKey, newValue);
262
258
  }
263
259
  notifyKeyListeners(getScopedListeners(_Storage.StorageScope.Secure), plainKey);
264
260
  return;
265
261
  }
266
- if (event.newValue === null) {
267
- ensureWebScopeKeyIndex(_Storage.StorageScope.Disk).delete(key);
268
- cacheRawValue(_Storage.StorageScope.Disk, key, undefined);
262
+ if (newValue === null) {
263
+ ensureWebScopeKeyIndex(scope).delete(key);
264
+ cacheRawValue(scope, key, undefined);
269
265
  } else {
270
- ensureWebScopeKeyIndex(_Storage.StorageScope.Disk).add(key);
271
- cacheRawValue(_Storage.StorageScope.Disk, key, event.newValue);
266
+ ensureWebScopeKeyIndex(scope).add(key);
267
+ cacheRawValue(scope, key, newValue);
272
268
  }
273
- notifyKeyListeners(getScopedListeners(_Storage.StorageScope.Disk), key);
269
+ notifyKeyListeners(getScopedListeners(scope), key);
274
270
  }
275
- function ensureWebStorageEventSubscription() {
276
- webStorageSubscriberCount += 1;
277
- if (webStorageSubscriberCount === 1 && typeof window !== "undefined" && typeof window.addEventListener === "function") {
278
- window.addEventListener("storage", handleWebStorageEvent);
279
- hasWebStorageEventSubscription = true;
271
+ function handleWebStorageEvent(event) {
272
+ const key = event.key;
273
+ if (key === null) {
274
+ applyExternalChangeEvent(_Storage.StorageScope.Disk, null, null);
275
+ applyExternalChangeEvent(_Storage.StorageScope.Secure, null, null);
276
+ return;
277
+ }
278
+ if (key.startsWith(SECURE_WEB_PREFIX) || key.startsWith(BIOMETRIC_WEB_PREFIX)) {
279
+ applyExternalChangeEvent(_Storage.StorageScope.Secure, key, event.newValue);
280
+ return;
281
+ }
282
+ applyExternalChangeEvent(_Storage.StorageScope.Disk, key, event.newValue);
283
+ }
284
+ function subscribeToBackendChanges(scope) {
285
+ if (externalSyncUnsubscribers.has(scope)) {
286
+ return;
287
+ }
288
+ const backend = getWebBackend(scope);
289
+ if (!backend?.subscribe) {
290
+ return;
280
291
  }
292
+ const unsubscribe = backend.subscribe(event => {
293
+ applyExternalChangeEvent(scope, event.key, event.newValue);
294
+ });
295
+ externalSyncUnsubscribers.set(scope, unsubscribe);
281
296
  }
282
- function maybeCleanupWebStorageSubscription() {
283
- webStorageSubscriberCount = Math.max(0, webStorageSubscriberCount - 1);
284
- if (webStorageSubscriberCount === 0 && hasWebStorageEventSubscription && typeof window !== "undefined") {
285
- window.removeEventListener("storage", handleWebStorageEvent);
286
- hasWebStorageEventSubscription = false;
297
+ function resetBackendChangeSubscription(scope) {
298
+ externalSyncUnsubscribers.get(scope)?.();
299
+ externalSyncUnsubscribers.delete(scope);
300
+ }
301
+ function ensureExternalSyncSubscriptions() {
302
+ if (!hasWindowStorageEventSubscription && typeof window !== "undefined" && typeof window.addEventListener === "function") {
303
+ window.addEventListener("storage", handleWebStorageEvent);
304
+ hasWindowStorageEventSubscription = true;
287
305
  }
306
+ subscribeToBackendChanges(_Storage.StorageScope.Disk);
307
+ subscribeToBackendChanges(_Storage.StorageScope.Secure);
288
308
  }
289
309
  function getScopedListeners(scope) {
290
310
  return webScopeListeners.get(scope);
@@ -340,12 +360,49 @@ function addKeyListener(registry, key, listener) {
340
360
  function readPendingSecureWrite(key) {
341
361
  return pendingSecureWrites.get(key)?.value;
342
362
  }
363
+ function readPendingDiskWrite(key) {
364
+ return pendingDiskWrites.get(key)?.value;
365
+ }
366
+ function hasPendingDiskWrite(key) {
367
+ return pendingDiskWrites.has(key);
368
+ }
343
369
  function hasPendingSecureWrite(key) {
344
370
  return pendingSecureWrites.has(key);
345
371
  }
372
+ function clearPendingDiskWrite(key) {
373
+ pendingDiskWrites.delete(key);
374
+ }
346
375
  function clearPendingSecureWrite(key) {
347
376
  pendingSecureWrites.delete(key);
348
377
  }
378
+ function flushDiskWrites() {
379
+ diskFlushScheduled = false;
380
+ if (pendingDiskWrites.size === 0) {
381
+ return;
382
+ }
383
+ const writes = Array.from(pendingDiskWrites.values());
384
+ pendingDiskWrites.clear();
385
+ const keysToSet = [];
386
+ const valuesToSet = [];
387
+ const keysToRemove = [];
388
+ writes.forEach(({
389
+ key,
390
+ value
391
+ }) => {
392
+ if (value === undefined) {
393
+ keysToRemove.push(key);
394
+ return;
395
+ }
396
+ keysToSet.push(key);
397
+ valuesToSet.push(value);
398
+ });
399
+ if (keysToSet.length > 0) {
400
+ WebStorage.setBatch(keysToSet, valuesToSet, _Storage.StorageScope.Disk);
401
+ }
402
+ if (keysToRemove.length > 0) {
403
+ WebStorage.removeBatch(keysToRemove, _Storage.StorageScope.Disk);
404
+ }
405
+ }
349
406
  function flushSecureWrites() {
350
407
  secureFlushScheduled = false;
351
408
  if (pendingSecureWrites.size === 0) {
@@ -384,6 +441,17 @@ function flushSecureWrites() {
384
441
  WebStorage.removeBatch(keysToRemove, _Storage.StorageScope.Secure);
385
442
  }
386
443
  }
444
+ function scheduleDiskWrite(key, value) {
445
+ pendingDiskWrites.set(key, {
446
+ key,
447
+ value
448
+ });
449
+ if (diskFlushScheduled) {
450
+ return;
451
+ }
452
+ diskFlushScheduled = true;
453
+ runMicrotask(flushDiskWrites);
454
+ }
387
455
  function scheduleSecureWrite(key, value, accessControl) {
388
456
  const pendingWrite = {
389
457
  key,
@@ -404,117 +472,124 @@ const WebStorage = {
404
472
  equals: other => other === WebStorage,
405
473
  dispose: () => {},
406
474
  set: (key, value, scope) => {
407
- const storage = getBrowserStorage(scope);
408
- if (!storage) {
475
+ if (scope !== _Storage.StorageScope.Disk && scope !== _Storage.StorageScope.Secure) {
409
476
  return;
410
477
  }
411
478
  const storageKey = scope === _Storage.StorageScope.Secure ? toSecureStorageKey(key) : key;
412
- storage.setItem(storageKey, value);
413
- if (scope === _Storage.StorageScope.Disk || scope === _Storage.StorageScope.Secure) {
414
- ensureWebScopeKeyIndex(scope).add(key);
415
- notifyKeyListeners(getScopedListeners(scope), key);
416
- }
479
+ withWebBackendOperation(scope, "set", backend => {
480
+ backend.setItem(storageKey, value);
481
+ });
482
+ ensureWebScopeKeyIndex(scope).add(key);
483
+ notifyKeyListeners(getScopedListeners(scope), key);
417
484
  },
418
485
  get: (key, scope) => {
419
- const storage = getBrowserStorage(scope);
486
+ if (scope !== _Storage.StorageScope.Disk && scope !== _Storage.StorageScope.Secure) {
487
+ return undefined;
488
+ }
420
489
  const storageKey = scope === _Storage.StorageScope.Secure ? toSecureStorageKey(key) : key;
421
- return storage?.getItem(storageKey) ?? undefined;
490
+ const value = withWebBackendOperation(scope, "get", backend => backend.getItem(storageKey));
491
+ return value ?? undefined;
422
492
  },
423
493
  remove: (key, scope) => {
424
- const storage = getBrowserStorage(scope);
425
- if (!storage) {
494
+ if (scope !== _Storage.StorageScope.Disk && scope !== _Storage.StorageScope.Secure) {
426
495
  return;
427
496
  }
428
497
  if (scope === _Storage.StorageScope.Secure) {
429
- storage.removeItem(toSecureStorageKey(key));
430
- storage.removeItem(toBiometricStorageKey(key));
498
+ withWebBackendOperation(scope, "remove", backend => {
499
+ if (backend.removeMany) {
500
+ backend.removeMany([toSecureStorageKey(key), toBiometricStorageKey(key)]);
501
+ return;
502
+ }
503
+ backend.removeItem(toSecureStorageKey(key));
504
+ backend.removeItem(toBiometricStorageKey(key));
505
+ });
431
506
  } else {
432
- storage.removeItem(key);
433
- }
434
- if (scope === _Storage.StorageScope.Disk || scope === _Storage.StorageScope.Secure) {
435
- ensureWebScopeKeyIndex(scope).delete(key);
436
- notifyKeyListeners(getScopedListeners(scope), key);
507
+ withWebBackendOperation(scope, "remove", backend => {
508
+ backend.removeItem(key);
509
+ });
437
510
  }
511
+ ensureWebScopeKeyIndex(scope).delete(key);
512
+ notifyKeyListeners(getScopedListeners(scope), key);
438
513
  },
439
514
  clear: scope => {
440
- const storage = getBrowserStorage(scope);
441
- if (!storage) {
515
+ if (scope !== _Storage.StorageScope.Disk && scope !== _Storage.StorageScope.Secure) {
442
516
  return;
443
517
  }
444
- if (scope === _Storage.StorageScope.Secure) {
445
- const keysToRemove = [];
446
- for (let i = 0; i < storage.length; i++) {
447
- const key = storage.key(i);
448
- if (key?.startsWith(SECURE_WEB_PREFIX) || key?.startsWith(BIOMETRIC_WEB_PREFIX)) {
449
- keysToRemove.push(key);
450
- }
451
- }
452
- keysToRemove.forEach(key => storage.removeItem(key));
453
- } else if (scope === _Storage.StorageScope.Disk) {
454
- const keysToRemove = [];
455
- for (let i = 0; i < storage.length; i++) {
456
- const key = storage.key(i);
457
- if (key && !key.startsWith(SECURE_WEB_PREFIX) && !key.startsWith(BIOMETRIC_WEB_PREFIX)) {
458
- keysToRemove.push(key);
459
- }
460
- }
461
- keysToRemove.forEach(key => storage.removeItem(key));
462
- } else {
463
- storage.clear();
464
- }
465
- if (scope === _Storage.StorageScope.Disk || scope === _Storage.StorageScope.Secure) {
466
- ensureWebScopeKeyIndex(scope).clear();
467
- notifyAllListeners(getScopedListeners(scope));
468
- }
518
+ withWebBackendOperation(scope, "clear", backend => {
519
+ backend.clear();
520
+ });
521
+ ensureWebScopeKeyIndex(scope).clear();
522
+ notifyAllListeners(getScopedListeners(scope));
469
523
  },
470
524
  setBatch: (keys, values, scope) => {
471
- const storage = getBrowserStorage(scope);
472
- if (!storage) {
525
+ if (scope !== _Storage.StorageScope.Disk && scope !== _Storage.StorageScope.Secure) {
473
526
  return;
474
527
  }
528
+ const entries = [];
475
529
  keys.forEach((key, index) => {
476
530
  const value = values[index];
477
531
  if (value === undefined) {
478
532
  return;
479
533
  }
480
- const storageKey = scope === _Storage.StorageScope.Secure ? toSecureStorageKey(key) : key;
481
- storage.setItem(storageKey, value);
534
+ entries.push([scope === _Storage.StorageScope.Secure ? toSecureStorageKey(key) : key, value]);
482
535
  });
483
- if (scope === _Storage.StorageScope.Disk || scope === _Storage.StorageScope.Secure) {
484
- const keyIndex = ensureWebScopeKeyIndex(scope);
485
- keys.forEach(key => keyIndex.add(key));
486
- const listeners = getScopedListeners(scope);
487
- keys.forEach(key => notifyKeyListeners(listeners, key));
488
- }
536
+ withWebBackendOperation(scope, "setBatch", backend => {
537
+ if (backend.setMany) {
538
+ backend.setMany(entries);
539
+ return;
540
+ }
541
+ entries.forEach(([storageKey, value]) => {
542
+ backend.setItem(storageKey, value);
543
+ });
544
+ });
545
+ const keyIndex = ensureWebScopeKeyIndex(scope);
546
+ keys.forEach(key => keyIndex.add(key));
547
+ const listeners = getScopedListeners(scope);
548
+ keys.forEach(key => notifyKeyListeners(listeners, key));
489
549
  },
490
550
  getBatch: (keys, scope) => {
491
- const storage = getBrowserStorage(scope);
492
- return keys.map(key => {
493
- const storageKey = scope === _Storage.StorageScope.Secure ? toSecureStorageKey(key) : key;
494
- return storage?.getItem(storageKey) ?? undefined;
551
+ if (scope !== _Storage.StorageScope.Disk && scope !== _Storage.StorageScope.Secure) {
552
+ return keys.map(() => undefined);
553
+ }
554
+ const storageKeys = keys.map(key => scope === _Storage.StorageScope.Secure ? toSecureStorageKey(key) : key);
555
+ const values = withWebBackendOperation(scope, "getBatch", backend => {
556
+ if (backend.getMany) {
557
+ return backend.getMany(storageKeys);
558
+ }
559
+ return storageKeys.map(storageKey => backend.getItem(storageKey));
495
560
  });
561
+ return values.map(value => value ?? undefined);
496
562
  },
497
563
  removeBatch: (keys, scope) => {
498
- const storage = getBrowserStorage(scope);
499
- if (!storage) {
564
+ if (scope !== _Storage.StorageScope.Disk && scope !== _Storage.StorageScope.Secure) {
500
565
  return;
501
566
  }
502
567
  if (scope === _Storage.StorageScope.Secure) {
503
- keys.forEach(key => {
504
- storage.removeItem(toSecureStorageKey(key));
505
- storage.removeItem(toBiometricStorageKey(key));
568
+ const storageKeys = keys.flatMap(key => [toSecureStorageKey(key), toBiometricStorageKey(key)]);
569
+ withWebBackendOperation(scope, "removeBatch", backend => {
570
+ if (backend.removeMany) {
571
+ backend.removeMany(storageKeys);
572
+ return;
573
+ }
574
+ storageKeys.forEach(storageKey => {
575
+ backend.removeItem(storageKey);
576
+ });
506
577
  });
507
578
  } else {
508
- keys.forEach(key => {
509
- storage.removeItem(key);
579
+ withWebBackendOperation(scope, "removeBatch", backend => {
580
+ if (backend.removeMany) {
581
+ backend.removeMany(keys);
582
+ return;
583
+ }
584
+ keys.forEach(key => {
585
+ backend.removeItem(key);
586
+ });
510
587
  });
511
588
  }
512
- if (scope === _Storage.StorageScope.Disk || scope === _Storage.StorageScope.Secure) {
513
- const keyIndex = ensureWebScopeKeyIndex(scope);
514
- keys.forEach(key => keyIndex.delete(key));
515
- const listeners = getScopedListeners(scope);
516
- keys.forEach(key => notifyKeyListeners(listeners, key));
517
- }
589
+ const keyIndex = ensureWebScopeKeyIndex(scope);
590
+ keys.forEach(key => keyIndex.delete(key));
591
+ const listeners = getScopedListeners(scope);
592
+ keys.forEach(key => notifyKeyListeners(listeners, key));
518
593
  },
519
594
  removeByPrefix: (prefix, scope) => {
520
595
  if (scope !== _Storage.StorageScope.Disk && scope !== _Storage.StorageScope.Secure) {
@@ -531,11 +606,13 @@ const WebStorage = {
531
606
  return () => {};
532
607
  },
533
608
  has: (key, scope) => {
534
- const storage = getBrowserStorage(scope);
535
609
  if (scope === _Storage.StorageScope.Secure) {
536
- return storage?.getItem(toSecureStorageKey(key)) !== null || storage?.getItem(toBiometricStorageKey(key)) !== null;
610
+ return withWebBackendOperation(scope, "has", backend => backend.getItem(toSecureStorageKey(key))) !== null || withWebBackendOperation(scope, "has", backend => backend.getItem(toBiometricStorageKey(key))) !== null;
611
+ }
612
+ if (scope !== _Storage.StorageScope.Disk) {
613
+ return false;
537
614
  }
538
- return storage?.getItem(key) !== null;
615
+ return withWebBackendOperation(scope, "has", backend => backend.getItem(key)) !== null;
539
616
  },
540
617
  getAllKeys: scope => {
541
618
  if (scope !== _Storage.StorageScope.Disk && scope !== _Storage.StorageScope.Secure) {
@@ -566,40 +643,43 @@ const WebStorage = {
566
643
  hasWarnedAboutWebBiometricFallback = true;
567
644
  console.warn("[NitroStorage] Biometric storage is not supported on web. Using localStorage.");
568
645
  }
569
- getBrowserStorage(_Storage.StorageScope.Secure)?.setItem(toBiometricStorageKey(key), value);
646
+ withWebBackendOperation(_Storage.StorageScope.Secure, "setSecureBiometric", backend => backend.setItem(toBiometricStorageKey(key), value));
570
647
  ensureWebScopeKeyIndex(_Storage.StorageScope.Secure).add(key);
571
648
  notifyKeyListeners(getScopedListeners(_Storage.StorageScope.Secure), key);
572
649
  },
573
650
  getSecureBiometric: key => {
574
- return getBrowserStorage(_Storage.StorageScope.Secure)?.getItem(toBiometricStorageKey(key)) ?? undefined;
651
+ const value = withWebBackendOperation(_Storage.StorageScope.Secure, "getSecureBiometric", backend => backend.getItem(toBiometricStorageKey(key)));
652
+ return value ?? undefined;
575
653
  },
576
654
  deleteSecureBiometric: key => {
577
- const storage = getBrowserStorage(_Storage.StorageScope.Secure);
578
- storage?.removeItem(toBiometricStorageKey(key));
579
- if (storage?.getItem(toSecureStorageKey(key)) === null) {
655
+ withWebBackendOperation(_Storage.StorageScope.Secure, "deleteSecureBiometric", backend => backend.removeItem(toBiometricStorageKey(key)));
656
+ if (withWebBackendOperation(_Storage.StorageScope.Secure, "deleteSecureBiometric:getItem", backend => backend.getItem(toSecureStorageKey(key))) === null) {
580
657
  ensureWebScopeKeyIndex(_Storage.StorageScope.Secure).delete(key);
581
658
  }
582
659
  notifyKeyListeners(getScopedListeners(_Storage.StorageScope.Secure), key);
583
660
  },
584
661
  hasSecureBiometric: key => {
585
- return getBrowserStorage(_Storage.StorageScope.Secure)?.getItem(toBiometricStorageKey(key)) !== null;
662
+ return withWebBackendOperation(_Storage.StorageScope.Secure, "hasSecureBiometric", backend => backend.getItem(toBiometricStorageKey(key))) !== null;
586
663
  },
587
664
  clearSecureBiometric: () => {
588
- const storage = getBrowserStorage(_Storage.StorageScope.Secure);
589
- if (!storage) return;
590
- const keysToNotify = [];
591
- const toRemove = [];
592
- for (let i = 0; i < storage.length; i++) {
593
- const k = storage.key(i);
594
- if (k?.startsWith(BIOMETRIC_WEB_PREFIX)) {
595
- toRemove.push(k);
596
- keysToNotify.push(fromBiometricStorageKey(k));
597
- }
665
+ const storageKeys = withWebBackendOperation(_Storage.StorageScope.Secure, "clearSecureBiometric:getAllKeys", backend => backend.getAllKeys());
666
+ const keysToNotify = storageKeys.filter(key => key.startsWith(BIOMETRIC_WEB_PREFIX)).map(key => fromBiometricStorageKey(key));
667
+ if (keysToNotify.length === 0) {
668
+ return;
598
669
  }
599
- toRemove.forEach(k => storage.removeItem(k));
670
+ withWebBackendOperation(_Storage.StorageScope.Secure, "clearSecureBiometric", backend => {
671
+ const biometricKeys = keysToNotify.map(key => toBiometricStorageKey(key));
672
+ if (backend.removeMany) {
673
+ backend.removeMany(biometricKeys);
674
+ return;
675
+ }
676
+ biometricKeys.forEach(storageKey => {
677
+ backend.removeItem(storageKey);
678
+ });
679
+ });
600
680
  const keyIndex = ensureWebScopeKeyIndex(_Storage.StorageScope.Secure);
601
681
  keysToNotify.forEach(key => {
602
- if (storage.getItem(toSecureStorageKey(key)) === null) {
682
+ if (withWebBackendOperation(_Storage.StorageScope.Secure, "clearSecureBiometric:getItem", backend => backend.getItem(toSecureStorageKey(key))) === null) {
603
683
  keyIndex.delete(key);
604
684
  }
605
685
  });
@@ -613,6 +693,9 @@ function getRawValue(key, scope) {
613
693
  const value = memoryStore.get(key);
614
694
  return typeof value === "string" ? value : undefined;
615
695
  }
696
+ if (scope === _Storage.StorageScope.Disk && hasPendingDiskWrite(key)) {
697
+ return readPendingDiskWrite(key);
698
+ }
616
699
  if (scope === _Storage.StorageScope.Secure && hasPendingSecureWrite(key)) {
617
700
  return readPendingSecureWrite(key);
618
701
  }
@@ -625,6 +708,15 @@ function setRawValue(key, value, scope) {
625
708
  notifyKeyListeners(memoryListeners, key);
626
709
  return;
627
710
  }
711
+ if (scope === _Storage.StorageScope.Disk) {
712
+ cacheRawValue(scope, key, value);
713
+ if (diskWritesAsync) {
714
+ scheduleDiskWrite(key, value);
715
+ return;
716
+ }
717
+ flushDiskWrites();
718
+ clearPendingDiskWrite(key);
719
+ }
628
720
  if (scope === _Storage.StorageScope.Secure) {
629
721
  flushSecureWrites();
630
722
  clearPendingSecureWrite(key);
@@ -639,6 +731,15 @@ function removeRawValue(key, scope) {
639
731
  notifyKeyListeners(memoryListeners, key);
640
732
  return;
641
733
  }
734
+ if (scope === _Storage.StorageScope.Disk) {
735
+ cacheRawValue(scope, key, undefined);
736
+ if (diskWritesAsync) {
737
+ scheduleDiskWrite(key, undefined);
738
+ return;
739
+ }
740
+ flushDiskWrites();
741
+ clearPendingDiskWrite(key);
742
+ }
642
743
  if (scope === _Storage.StorageScope.Secure) {
643
744
  flushSecureWrites();
644
745
  clearPendingSecureWrite(key);
@@ -665,6 +766,10 @@ const storage = exports.storage = {
665
766
  notifyAllListeners(memoryListeners);
666
767
  return;
667
768
  }
769
+ if (scope === _Storage.StorageScope.Disk) {
770
+ flushDiskWrites();
771
+ pendingDiskWrites.clear();
772
+ }
668
773
  if (scope === _Storage.StorageScope.Secure) {
669
774
  flushSecureWrites();
670
775
  pendingSecureWrites.clear();
@@ -693,6 +798,9 @@ const storage = exports.storage = {
693
798
  return;
694
799
  }
695
800
  const keyPrefix = (0, _internal.prefixKey)(namespace, "");
801
+ if (scope === _Storage.StorageScope.Disk) {
802
+ flushDiskWrites();
803
+ }
696
804
  if (scope === _Storage.StorageScope.Secure) {
697
805
  flushSecureWrites();
698
806
  }
@@ -714,6 +822,12 @@ const storage = exports.storage = {
714
822
  return measureOperation("storage:has", scope, () => {
715
823
  (0, _internal.assertValidScope)(scope);
716
824
  if (scope === _Storage.StorageScope.Memory) return memoryStore.has(key);
825
+ if (scope === _Storage.StorageScope.Disk) {
826
+ flushDiskWrites();
827
+ }
828
+ if (scope === _Storage.StorageScope.Secure) {
829
+ flushSecureWrites();
830
+ }
717
831
  return WebStorage.has(key, scope);
718
832
  });
719
833
  },
@@ -721,6 +835,12 @@ const storage = exports.storage = {
721
835
  return measureOperation("storage:getAllKeys", scope, () => {
722
836
  (0, _internal.assertValidScope)(scope);
723
837
  if (scope === _Storage.StorageScope.Memory) return Array.from(memoryStore.keys());
838
+ if (scope === _Storage.StorageScope.Disk) {
839
+ flushDiskWrites();
840
+ }
841
+ if (scope === _Storage.StorageScope.Secure) {
842
+ flushSecureWrites();
843
+ }
724
844
  return WebStorage.getAllKeys(scope);
725
845
  });
726
846
  },
@@ -730,6 +850,12 @@ const storage = exports.storage = {
730
850
  if (scope === _Storage.StorageScope.Memory) {
731
851
  return Array.from(memoryStore.keys()).filter(key => key.startsWith(prefix));
732
852
  }
853
+ if (scope === _Storage.StorageScope.Disk) {
854
+ flushDiskWrites();
855
+ }
856
+ if (scope === _Storage.StorageScope.Secure) {
857
+ flushSecureWrites();
858
+ }
733
859
  return WebStorage.getKeysByPrefix(prefix, scope);
734
860
  });
735
861
  },
@@ -749,6 +875,12 @@ const storage = exports.storage = {
749
875
  });
750
876
  return result;
751
877
  }
878
+ if (scope === _Storage.StorageScope.Disk) {
879
+ flushDiskWrites();
880
+ }
881
+ if (scope === _Storage.StorageScope.Secure) {
882
+ flushSecureWrites();
883
+ }
752
884
  const values = WebStorage.getBatch(keys, scope);
753
885
  keys.forEach((key, index) => {
754
886
  const value = values[index];
@@ -769,6 +901,12 @@ const storage = exports.storage = {
769
901
  });
770
902
  return result;
771
903
  }
904
+ if (scope === _Storage.StorageScope.Disk) {
905
+ flushDiskWrites();
906
+ }
907
+ if (scope === _Storage.StorageScope.Secure) {
908
+ flushSecureWrites();
909
+ }
772
910
  const keys = WebStorage.getAllKeys(scope);
773
911
  if (keys.length === 0) return {};
774
912
  const values = WebStorage.getBatch(keys, scope);
@@ -785,6 +923,12 @@ const storage = exports.storage = {
785
923
  return measureOperation("storage:size", scope, () => {
786
924
  (0, _internal.assertValidScope)(scope);
787
925
  if (scope === _Storage.StorageScope.Memory) return memoryStore.size;
926
+ if (scope === _Storage.StorageScope.Disk) {
927
+ flushDiskWrites();
928
+ }
929
+ if (scope === _Storage.StorageScope.Secure) {
930
+ flushSecureWrites();
931
+ }
788
932
  return WebStorage.size(scope);
789
933
  });
790
934
  },
@@ -795,6 +939,19 @@ const storage = exports.storage = {
795
939
  setSecureWritesAsync: _enabled => {
796
940
  recordMetric("storage:setSecureWritesAsync", _Storage.StorageScope.Secure, 0);
797
941
  },
942
+ setDiskWritesAsync: enabled => {
943
+ measureOperation("storage:setDiskWritesAsync", _Storage.StorageScope.Disk, () => {
944
+ diskWritesAsync = enabled;
945
+ if (!enabled) {
946
+ flushDiskWrites();
947
+ }
948
+ });
949
+ },
950
+ flushDiskWrites: () => {
951
+ measureOperation("storage:flushDiskWrites", _Storage.StorageScope.Disk, () => {
952
+ flushDiskWrites();
953
+ });
954
+ },
798
955
  flushSecureWrites: () => {
799
956
  measureOperation("storage:flushSecureWrites", _Storage.StorageScope.Secure, () => {
800
957
  flushSecureWrites();
@@ -821,6 +978,69 @@ const storage = exports.storage = {
821
978
  resetMetrics: () => {
822
979
  metricsCounters.clear();
823
980
  },
981
+ getCapabilities: () => ({
982
+ platform: "web",
983
+ backend: {
984
+ disk: getBackendName(_Storage.StorageScope.Disk, webDiskStorageBackend),
985
+ secure: getBackendName(_Storage.StorageScope.Secure, webSecureStorageBackend)
986
+ },
987
+ writeBuffering: {
988
+ disk: true,
989
+ secure: true
990
+ },
991
+ errorClassification: true
992
+ }),
993
+ getSecurityCapabilities: () => {
994
+ const secureBackend = getBackendName(_Storage.StorageScope.Secure, webSecureStorageBackend);
995
+ return {
996
+ platform: "web",
997
+ secureStorage: {
998
+ backend: secureBackend,
999
+ encrypted: getWebSecureEncryptionStatus(webSecureStorageBackend),
1000
+ accessControl: "unavailable",
1001
+ keychainAccessGroup: "unavailable",
1002
+ hardwareBacked: "unavailable"
1003
+ },
1004
+ biometric: {
1005
+ storage: "unavailable",
1006
+ prompt: "unavailable",
1007
+ biometryOnly: "unavailable",
1008
+ biometryOrPasscode: "unavailable"
1009
+ },
1010
+ metadata: {
1011
+ perKey: true,
1012
+ listsWithoutValues: true,
1013
+ persistsTimestamps: false
1014
+ }
1015
+ };
1016
+ },
1017
+ getSecureMetadata: key => {
1018
+ return measureOperation("storage:getSecureMetadata", _Storage.StorageScope.Secure, () => {
1019
+ flushSecureWrites();
1020
+ const biometricProtected = WebStorage.hasSecureBiometric(key);
1021
+ const exists = biometricProtected || WebStorage.has(key, _Storage.StorageScope.Secure);
1022
+ let kind = "missing";
1023
+ if (exists) {
1024
+ kind = biometricProtected ? "biometric" : "secure";
1025
+ }
1026
+ return {
1027
+ key,
1028
+ exists,
1029
+ kind,
1030
+ backend: getBackendName(_Storage.StorageScope.Secure, webSecureStorageBackend),
1031
+ encrypted: getWebSecureEncryptionStatus(webSecureStorageBackend),
1032
+ hardwareBacked: "unavailable",
1033
+ biometricProtected,
1034
+ valueExposed: false
1035
+ };
1036
+ });
1037
+ },
1038
+ getAllSecureMetadata: () => {
1039
+ return measureOperation("storage:getAllSecureMetadata", _Storage.StorageScope.Secure, () => {
1040
+ flushSecureWrites();
1041
+ return WebStorage.getAllKeys(_Storage.StorageScope.Secure).map(key => storage.getSecureMetadata(key));
1042
+ });
1043
+ },
824
1044
  getString: (key, scope) => {
825
1045
  return measureOperation("storage:getString", scope, () => {
826
1046
  return getRawValue(key, scope);
@@ -853,21 +1073,50 @@ const storage = exports.storage = {
853
1073
  flushSecureWrites();
854
1074
  WebStorage.setSecureAccessControl(secureDefaultAccessControl);
855
1075
  }
1076
+ if (scope === _Storage.StorageScope.Disk) {
1077
+ flushDiskWrites();
1078
+ }
856
1079
  WebStorage.setBatch(keys, values, scope);
857
1080
  keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
858
1081
  }, keys.length);
859
1082
  }
860
1083
  };
861
1084
  function setWebSecureStorageBackend(backend) {
862
- webSecureStorageBackend = backend ?? createLocalStorageWebSecureBackend();
863
- cachedSecureBrowserStorage = undefined;
864
- cachedSecureBackendRef = undefined;
1085
+ pendingSecureWrites.clear();
1086
+ webSecureStorageBackend = backend ?? createDefaultSecureBackend();
1087
+ resetBackendChangeSubscription(_Storage.StorageScope.Secure);
865
1088
  hydratedWebScopeKeyIndex.delete(_Storage.StorageScope.Secure);
866
1089
  clearScopeRawCache(_Storage.StorageScope.Secure);
1090
+ ensureExternalSyncSubscriptions();
867
1091
  }
868
1092
  function getWebSecureStorageBackend() {
869
1093
  return webSecureStorageBackend;
870
1094
  }
1095
+ function setWebDiskStorageBackend(backend) {
1096
+ pendingDiskWrites.clear();
1097
+ webDiskStorageBackend = backend ?? createDefaultDiskBackend();
1098
+ resetBackendChangeSubscription(_Storage.StorageScope.Disk);
1099
+ hydratedWebScopeKeyIndex.delete(_Storage.StorageScope.Disk);
1100
+ clearScopeRawCache(_Storage.StorageScope.Disk);
1101
+ ensureExternalSyncSubscriptions();
1102
+ }
1103
+ function getWebDiskStorageBackend() {
1104
+ return webDiskStorageBackend;
1105
+ }
1106
+ async function flushWebStorageBackends() {
1107
+ flushDiskWrites();
1108
+ flushSecureWrites();
1109
+ const flushes = [];
1110
+ const diskFlush = webDiskStorageBackend?.flush;
1111
+ const secureFlush = webSecureStorageBackend?.flush;
1112
+ if (diskFlush) {
1113
+ flushes.push(diskFlush());
1114
+ }
1115
+ if (secureFlush) {
1116
+ flushes.push(secureFlush());
1117
+ }
1118
+ await Promise.all(flushes);
1119
+ }
871
1120
  function canUseRawBatchPath(item) {
872
1121
  return item._hasExpiration === false && item._hasValidation === false && item._isBiometric !== true && item._secureAccessControl === undefined;
873
1122
  }
@@ -895,6 +1144,7 @@ function createStorageItem(config) {
895
1144
  const expirationTtlMs = expiration?.ttlMs;
896
1145
  const memoryExpiration = expiration && isMemory ? new Map() : null;
897
1146
  const readCache = !isMemory && config.readCache === true;
1147
+ const coalesceDiskWrites = config.scope === _Storage.StorageScope.Disk && config.coalesceDiskWrites === true;
898
1148
  const coalesceSecureWrites = config.scope === _Storage.StorageScope.Secure && config.coalesceSecureWrites === true && !isBiometric;
899
1149
  const defaultValue = config.defaultValue;
900
1150
  const nonMemoryScope = config.scope === _Storage.StorageScope.Disk ? _Storage.StorageScope.Disk : config.scope === _Storage.StorageScope.Secure ? _Storage.StorageScope.Secure : null;
@@ -925,7 +1175,7 @@ function createStorageItem(config) {
925
1175
  unsubscribe = addKeyListener(memoryListeners, storageKey, listener);
926
1176
  return;
927
1177
  }
928
- ensureWebStorageEventSubscription();
1178
+ ensureExternalSyncSubscriptions();
929
1179
  unsubscribe = addKeyListener(getScopedListeners(nonMemoryScope), storageKey, listener);
930
1180
  };
931
1181
  const readStoredRaw = () => {
@@ -942,6 +1192,12 @@ function createStorageItem(config) {
942
1192
  }
943
1193
  return memoryStore.get(storageKey);
944
1194
  }
1195
+ if (nonMemoryScope === _Storage.StorageScope.Disk) {
1196
+ const pending = pendingDiskWrites.get(storageKey);
1197
+ if (pending !== undefined) {
1198
+ return pending.value;
1199
+ }
1200
+ }
945
1201
  if (nonMemoryScope === _Storage.StorageScope.Secure && !isBiometric) {
946
1202
  const pending = pendingSecureWrites.get(storageKey);
947
1203
  if (pending !== undefined) {
@@ -968,6 +1224,13 @@ function createStorageItem(config) {
968
1224
  return;
969
1225
  }
970
1226
  cacheRawValue(nonMemoryScope, storageKey, rawValue);
1227
+ if (nonMemoryScope === _Storage.StorageScope.Disk) {
1228
+ if (coalesceDiskWrites || diskWritesAsync) {
1229
+ scheduleDiskWrite(storageKey, rawValue);
1230
+ return;
1231
+ }
1232
+ clearPendingDiskWrite(storageKey);
1233
+ }
971
1234
  if (coalesceSecureWrites) {
972
1235
  scheduleSecureWrite(storageKey, rawValue, secureAccessControl ?? secureDefaultAccessControl);
973
1236
  return;
@@ -983,6 +1246,13 @@ function createStorageItem(config) {
983
1246
  return;
984
1247
  }
985
1248
  cacheRawValue(nonMemoryScope, storageKey, undefined);
1249
+ if (nonMemoryScope === _Storage.StorageScope.Disk) {
1250
+ if (coalesceDiskWrites || diskWritesAsync) {
1251
+ scheduleDiskWrite(storageKey, undefined);
1252
+ return;
1253
+ }
1254
+ clearPendingDiskWrite(storageKey);
1255
+ }
986
1256
  if (coalesceSecureWrites) {
987
1257
  scheduleSecureWrite(storageKey, undefined, secureAccessControl ?? secureDefaultAccessControl);
988
1258
  return;
@@ -1143,6 +1413,18 @@ function createStorageItem(config) {
1143
1413
  const hasItem = () => measureOperation("item:has", config.scope, () => {
1144
1414
  if (isMemory) return memoryStore.has(storageKey);
1145
1415
  if (isBiometric) return WebStorage.hasSecureBiometric(storageKey);
1416
+ if (nonMemoryScope === _Storage.StorageScope.Disk) {
1417
+ const pending = pendingDiskWrites.get(storageKey);
1418
+ if (pending !== undefined) {
1419
+ return pending.value !== undefined;
1420
+ }
1421
+ }
1422
+ if (nonMemoryScope === _Storage.StorageScope.Secure) {
1423
+ const pending = pendingSecureWrites.get(storageKey);
1424
+ if (pending !== undefined) {
1425
+ return pending.value !== undefined;
1426
+ }
1427
+ }
1146
1428
  return WebStorage.has(storageKey, config.scope);
1147
1429
  });
1148
1430
  const subscribe = callback => {
@@ -1153,9 +1435,6 @@ function createStorageItem(config) {
1153
1435
  if (listeners.size === 0 && unsubscribe) {
1154
1436
  unsubscribe();
1155
1437
  unsubscribe = null;
1156
- if (!isMemory) {
1157
- maybeCleanupWebStorageSubscription();
1158
- }
1159
1438
  }
1160
1439
  };
1161
1440
  };
@@ -1203,6 +1482,13 @@ function getBatch(items, scope) {
1203
1482
  const keysToFetch = [];
1204
1483
  const keyIndexes = [];
1205
1484
  items.forEach((item, index) => {
1485
+ if (scope === _Storage.StorageScope.Disk) {
1486
+ const pending = pendingDiskWrites.get(item.key);
1487
+ if (pending !== undefined) {
1488
+ rawValues[index] = pending.value;
1489
+ return;
1490
+ }
1491
+ }
1206
1492
  if (scope === _Storage.StorageScope.Secure) {
1207
1493
  const pending = pendingSecureWrites.get(item.key);
1208
1494
  if (pending !== undefined) {
@@ -1319,6 +1605,7 @@ function setBatch(items, scope) {
1319
1605
  });
1320
1606
  return;
1321
1607
  }
1608
+ flushDiskWrites();
1322
1609
  const useRawBatchPath = items.every(({
1323
1610
  item
1324
1611
  }) => canUseRawBatchPath(asInternal(item)));
@@ -1343,6 +1630,9 @@ function removeBatch(items, scope) {
1343
1630
  return;
1344
1631
  }
1345
1632
  const keys = items.map(item => item.key);
1633
+ if (scope === _Storage.StorageScope.Disk) {
1634
+ flushDiskWrites();
1635
+ }
1346
1636
  if (scope === _Storage.StorageScope.Secure) {
1347
1637
  flushSecureWrites();
1348
1638
  }
@@ -1388,6 +1678,9 @@ function migrateToLatest(scope = _Storage.StorageScope.Disk) {
1388
1678
  function runTransaction(scope, transaction) {
1389
1679
  return measureOperation("transaction:run", scope, () => {
1390
1680
  (0, _internal.assertValidScope)(scope);
1681
+ if (scope === _Storage.StorageScope.Disk) {
1682
+ flushDiskWrites();
1683
+ }
1391
1684
  if (scope === _Storage.StorageScope.Secure) {
1392
1685
  flushSecureWrites();
1393
1686
  }
@@ -1454,6 +1747,9 @@ function runTransaction(scope, transaction) {
1454
1747
  valuesToSet.push(previousValue);
1455
1748
  }
1456
1749
  });
1750
+ if (scope === _Storage.StorageScope.Disk) {
1751
+ flushDiskWrites();
1752
+ }
1457
1753
  if (scope === _Storage.StorageScope.Secure) {
1458
1754
  flushSecureWrites();
1459
1755
  }
@@ -1499,7 +1795,7 @@ function createSecureAuthStorage(config, options) {
1499
1795
  }
1500
1796
  return result;
1501
1797
  }
1502
- function isKeychainLockedError(_err) {
1503
- return false;
1798
+ function isKeychainLockedError(err) {
1799
+ return (0, _storageRuntime.isLockedStorageErrorCode)((0, _storageRuntime.getStorageErrorCode)(err));
1504
1800
  }
1505
1801
  //# sourceMappingURL=index.web.js.map