@bayonai/offline 0.7.0 → 0.9.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.
package/CHANGELOG.md CHANGED
@@ -1,6 +1,43 @@
1
1
  # Changelog
2
2
 
3
- ## 0.7.0 - Unreleased
3
+ ## 0.9.0 - 2026-09-26
4
+
5
+ - Added independent read-cache freshness and retention windows with explicit
6
+ `fresh`, `stale`, and `miss` outcomes and selectable refresh-failure behavior.
7
+ - Added retained fallback and stale in-flight response protection to forced
8
+ `loadReadCacheResource` refreshes.
9
+ - Added payload-free read-cache outcome callbacks for host-owned telemetry.
10
+ - Preserved existing `maxAgeMs` behavior for callers that do not opt into the
11
+ new policy. Forced refreshes retain fallback data while invalidating older
12
+ in-flight cache writes.
13
+ - Isolated pending-write listing and clearing by `keyPrefix` when logical
14
+ queues share one physical object store.
15
+ - Fixed NodeNext declaration resolution and supplied matching declarations for
16
+ both ESM and CommonJS consumers.
17
+ - Restored offline retry-control text when a reload attempt throws.
18
+
19
+ ## 0.8.0 - 2026-09-25
20
+
21
+ - Added authoritative-result reconciliation to `createDurableCommandStore`,
22
+ including rebasing remaining optimistic commands without a transient revert.
23
+ - Exposed stable durable operation IDs to remote executors for idempotent
24
+ command handling and prevented older refreshes from replacing confirmations.
25
+ - Propagated injected clocks through ordered replay so deferred-command timing
26
+ stays deterministic in hosts and tests.
27
+ - Added a host-owned retry policy so terminal failures remain inspectable
28
+ without replaying or blocking later commands, while connectivity failures
29
+ retain their optimistic projection and continue through ordered recovery.
30
+ - Made IndexedDB mutation failures truthful: unavailable, rejected, and
31
+ cancelled timed-out writes now reject with `OfflineStorageError` instead of
32
+ reporting false durability.
33
+ - Prevented delayed database opens from mutating after timeout, stale store
34
+ listings from replacing newer command state, and listener failures from
35
+ interrupting command dispatch.
36
+ - Preserved retryable optimistic projections while later commands enter the
37
+ queue, added collision-resistant fallback IDs where Web Crypto UUIDs are
38
+ unavailable, and restored the package's 100% coverage release gate.
39
+
40
+ ## 0.7.0 - 2026-09-16
4
41
 
5
42
  - Added `createDurableCommandStore`, a framework-neutral store that projects
6
43
  accepted commands immediately while persisting them before remote execution.
package/README.md CHANGED
@@ -25,8 +25,15 @@ import {
25
25
  createReadCacheKey,
26
26
  loadReadCacheResource,
27
27
  pruneReadCache,
28
+ type ReadCachePolicy,
28
29
  } from "@bayonai/offline";
29
30
 
31
+ const policy: ReadCachePolicy = {
32
+ freshForMs: 30_000,
33
+ retainForMs: 7 * 24 * 60 * 60_000,
34
+ refreshFailure: "use-retained",
35
+ };
36
+
30
37
  const cache = createBrowserReadCache({
31
38
  databaseName: "my-app:v1",
32
39
  storeName: "read-cache",
@@ -43,13 +50,41 @@ const projects = await loadReadCacheResource({
43
50
  cache,
44
51
  key,
45
52
  loadFresh: () => fetchProjects(),
46
- maxAgeMs: 30_000,
47
- onUpdate: ({ data, fromCache }) => {
48
- renderProjects(data, { fromCache });
53
+ onOutcome: (outcome) => telemetry.record("offline-read", { outcome }),
54
+ policy,
55
+ onUpdate: ({ data, status, fromCache }) => {
56
+ renderProjects(data, { fromCache, status });
49
57
  },
50
58
  });
51
59
  ```
52
60
 
61
+ `freshForMs` controls how long a cached value can avoid a remote read.
62
+ `retainForMs` independently controls how long it can be shown as an offline
63
+ fallback. A stale-but-retained value is emitted immediately and revalidated;
64
+ an expired value is not emitted and cannot mask a loader failure. The
65
+ `status` is `fresh`, `stale`, or `miss`: a stale update is retained data while
66
+ refreshing or after a failed refresh, and a miss is a successful loader result
67
+ with no retained cache. Successful remote results are `fresh` when replacing
68
+ stale data and `miss` when there was no usable retained data.
69
+
70
+ `onOutcome` emits only a fixed event name (`fresh-hit`, `stale-hit`,
71
+ `cache-miss`, `forced-refresh`, `loader-success`, or `loader-failure`). It never
72
+ includes cache keys, user identifiers, payloads, or error messages. Callback
73
+ exceptions are logged and isolated from the read.
74
+
75
+ When refresh fails, `refreshFailure: "use-retained"` returns retained data
76
+ with `status: "stale"` and the error; `"throw"` propagates the loader error.
77
+ This also applies to forced refresh. Set `forceFresh: true` on
78
+ `loadReadCacheResource` or `readThroughCache` to bypass freshness, retain
79
+ eligible data as the failure fallback, and advance the cache generation so an
80
+ older in-flight load cannot overwrite the refresh. `readThroughCache` returns
81
+ the `status` field alongside its existing `fromCache`, `stale`, and `error`
82
+ fields.
83
+
84
+ For compatibility, callers that omit `policy` retain the previous `maxAgeMs`
85
+ freshness behavior and have no age-based fallback expiry. New callers should
86
+ use `policy`; when supplied it takes precedence over the legacy `maxAgeMs`.
87
+
53
88
  After a successful write, use `readThroughCache({ forceFresh: true })` when
54
89
  the next read must be server-confirmed rather than a pre-write cached or
55
90
  in-flight response. Concurrent ordinary reads for the same cache/key share one
@@ -102,6 +137,11 @@ Failed sync results include a safe persisted `error` string and the original
102
137
  non-persisted `failure` value. Hosts may use `failure` for structured logging or
103
138
  domain error classification; it is never written into the pending-write store.
104
139
 
140
+ `keyPrefix` is an isolation boundary inside a shared object store. Listing,
141
+ recovery, and `clearAll()` affect only that prefix, so independent logical
142
+ queues may safely share the same IndexedDB database and store when their
143
+ prefixes are distinct.
144
+
105
145
  For a browser application that wants durable, silent recovery, mount the
106
146
  headless controller once for the active account or workspace:
107
147
 
@@ -202,8 +242,12 @@ package never imports an application model, database SDK, or framework.
202
242
  const commands = createDurableCommandStore({
203
243
  initialState: workspace,
204
244
  load: (scope) => workspaceApi.load(scope),
205
- execute: (command) => workspaceApi.execute(command),
245
+ execute: (command, { operationId }) =>
246
+ workspaceApi.execute(command, { operationId }),
206
247
  project: (state, pending) => projectWorkspace(state, pending),
248
+ reconcile: (state, command, result) =>
249
+ reconcileWorkspace(state, command, result),
250
+ shouldRetry: (category) => category === "connectivity",
207
251
  store,
208
252
  });
209
253
 
@@ -221,6 +265,31 @@ commands in order, and `close` releases the local subscription and delayed
221
265
  deadline timer. Local-storage failure rejects `accepted`; hosts must show that
222
266
  failure instead of claiming the command will recover.
223
267
 
268
+ Browser pending-write mutations reject with `OfflineStorageError` when
269
+ IndexedDB is unavailable, rejects an operation, or cancels a timed-out
270
+ transaction. This is intentional: successful queue acceptance always means
271
+ the command was durably stored. Read-cache access remains best-effort, and a
272
+ fresh remote result is still returned when optional cache invalidation or
273
+ persistence fails.
274
+
275
+ The executor receives the durable `operationId` on every attempt so the remote
276
+ handler can use it as an idempotency key. When execution succeeds, `reconcile`
277
+ folds the authoritative result into base state before the command is removed;
278
+ any later optimistic commands are projected over that confirmed state. A read
279
+ that started before this confirmation cannot replace it when it finishes.
280
+ Reconciliation is deliberately a host-owned, synchronous state transformation.
281
+ If it throws, the package logs the local bug and removes the already-confirmed
282
+ command rather than risking a duplicate remote effect.
283
+
284
+ `classifyError` and `shouldRetry` let a host distinguish recoverable failures
285
+ from terminal validation, authentication, permission, or conflict outcomes.
286
+ Retryable failures retain their optimistic projection while awaiting recovery.
287
+ Terminal failures roll back, stay available in `operations` for host feedback
288
+ or dismissal, and are excluded from later recovery lanes, so they neither retry
289
+ nor block a newer command. Because recovery survives restart, classification
290
+ must work from the package's persisted safe error message rather than relying
291
+ only on a non-serializable error object.
292
+
224
293
  Use generic record summaries when an app needs to render its own status:
225
294
 
226
295
  ```ts
@@ -277,17 +346,21 @@ opened at the same version.
277
346
 
278
347
  ```bash
279
348
  pnpm --filter @bayonai/offline test
349
+ pnpm --filter @bayonai/offline test:coverage
280
350
  pnpm --filter @bayonai/offline run build
281
- pnpm --filter @bayonai/offline pack --dry-run
351
+ pnpm --filter @bayonai/offline pack:smoke
282
352
  ```
283
353
 
284
354
  ## Publish Checklist
285
355
 
286
356
  1. Run the package tests and build.
287
- 2. Run `pnpm --filter @bayonai/offline pack --dry-run` and confirm the tarball
357
+ 2. Run the 100% coverage gate and production dependency audit.
358
+ 3. Run `pnpm --filter @bayonai/offline pack:smoke` and confirm the tarball
288
359
  includes only `package.json`, `dist`, `dist-cjs`, `README.md`,
289
360
  `CHANGELOG.md`, and `LICENSE`.
290
- 3. Confirm `package.json` metadata, version, repository, license, and
361
+ 4. Install the generated tarball into an isolated consumer and verify ESM,
362
+ CommonJS, and TypeScript declaration imports.
363
+ 5. Confirm `package.json` metadata, version, repository, license, and
291
364
  `publishConfig.access` are correct.
292
- 4. Publish from the package directory or with the package filter after the
365
+ 6. Publish from the package directory or with the package filter after the
293
366
  workspace lockfile is current.
@@ -8,9 +8,11 @@ export function createBrowserOfflineFallbackRecovery({ control, environment = cr
8
8
  let active = false;
9
9
  let retrying = false;
10
10
  let pullStartY = null;
11
+ const touchThreshold = pullThreshold === null ? Number.POSITIVE_INFINITY : pullThreshold;
11
12
  function retry() {
12
13
  if (retrying)
13
14
  return;
15
+ const previousText = control.textContent;
14
16
  retrying = true;
15
17
  control.disabled = true;
16
18
  control.textContent = retryingText;
@@ -19,9 +21,12 @@ export function createBrowserOfflineFallbackRecovery({ control, environment = cr
19
21
  }
20
22
  catch (error) {
21
23
  // A failed navigation must leave the recovery action visible and usable.
22
- console.error("[Bayon offline] Offline fallback reload failed.", { error });
24
+ console.error("[Bayon offline] Offline fallback reload failed.", {
25
+ error,
26
+ });
23
27
  retrying = false;
24
28
  control.disabled = false;
29
+ control.textContent = previousText;
25
30
  }
26
31
  }
27
32
  function handleTouchStart(event) {
@@ -32,7 +37,7 @@ export function createBrowserOfflineFallbackRecovery({ control, environment = cr
32
37
  const touchY = getSingleTouchY(event, "changedTouches");
33
38
  if (pullStartY !== null &&
34
39
  touchY !== null &&
35
- touchY - pullStartY >= (pullThreshold ?? Number.POSITIVE_INFINITY)) {
40
+ touchY - pullStartY >= touchThreshold) {
36
41
  retry();
37
42
  }
38
43
  pullStartY = null;
@@ -1,5 +1,5 @@
1
- import { type PendingWriteRecord, type PendingWriteStore, type PendingWriteSync, type SyncPendingWritesResult } from "./pendingWrites";
2
- import type { BrowserPendingWriteRecoveryCoordination } from "./browserCoordination";
1
+ import { type PendingWriteRecord, type PendingWriteStore, type PendingWriteSync, type SyncPendingWritesResult } from "./pendingWrites.js";
2
+ import type { BrowserPendingWriteRecoveryCoordination } from "./browserCoordination.js";
3
3
  export declare const initialPendingWriteRetryDelayMs = 2000;
4
4
  export declare const maxPendingWriteRetryDelayMs = 60000;
5
5
  export type BrowserPendingWriteRecoveryEnvironment = {
@@ -1,4 +1,4 @@
1
- import { type PendingWriteRecord, type PendingWriteStore } from "./pendingWrites";
1
+ import { type PendingWriteRecord, type PendingWriteStore } from "./pendingWrites.js";
2
2
  export type DurableCommandErrorCategory = "auth" | "conflict" | "connectivity" | "local-storage" | "permission" | "unexpected" | "validation";
3
3
  export type DurableCommandOperation<Command> = {
4
4
  command: Command;
@@ -14,6 +14,10 @@ export type DurableCommandDispatch = {
14
14
  accepted: Promise<void>;
15
15
  operationId: string;
16
16
  };
17
+ export type DurableCommandExecutionContext = {
18
+ /** Stable across retries so the remote command can be idempotent. */
19
+ operationId: string;
20
+ };
17
21
  export type DurableCommandStore<State, Command, Scope> = {
18
22
  cancel: (operationId: string) => Promise<boolean>;
19
23
  close: () => void;
@@ -30,14 +34,16 @@ export type DurableCommandStore<State, Command, Scope> = {
30
34
  * durable command lifecycle. A command becomes visible immediately, but is
31
35
  * never sent remotely until IndexedDB has accepted it.
32
36
  */
33
- export declare function createDurableCommandStore<State, Command, Scope>({ canCancel, classifyError, createId, execute, initialState, load, now, project, store, }: {
37
+ export declare function createDurableCommandStore<State, Command, Scope, Result = unknown>({ canCancel, classifyError, createId, execute, initialState, load, now, project, reconcile, shouldRetry, store, }: {
34
38
  canCancel?: (command: Command, now: string) => boolean;
35
39
  classifyError?: (error: unknown) => DurableCommandErrorCategory;
36
40
  createId?: () => string;
37
- execute: (command: Command) => Promise<unknown>;
41
+ execute: (command: Command, context: DurableCommandExecutionContext) => Promise<Result>;
38
42
  initialState: State;
39
43
  load: (scope: Scope) => Promise<State>;
40
44
  now?: () => string;
41
45
  project: (state: State, commands: readonly PendingWriteRecord<Command>[]) => State;
46
+ reconcile?: (state: State, command: Command, result: Result) => State;
47
+ shouldRetry?: (category: DurableCommandErrorCategory, command: Command) => boolean;
42
48
  store: PendingWriteStore<Command>;
43
49
  }): DurableCommandStore<State, Command, Scope>;
@@ -4,24 +4,59 @@ import { createOrderedPendingWriteExecutor, } from "./pendingWrites.js";
4
4
  * durable command lifecycle. A command becomes visible immediately, but is
5
5
  * never sent remotely until IndexedDB has accepted it.
6
6
  */
7
- export function createDurableCommandStore({ canCancel = () => true, classifyError = () => "unexpected", createId = createDurableCommandId, execute, initialState, load, now = () => new Date().toISOString(), project, store, }) {
7
+ export function createDurableCommandStore({ canCancel = () => true, classifyError = () => "unexpected", createId = createDurableCommandId, execute, initialState, load, now = () => new Date().toISOString(), project, reconcile, shouldRetry = () => true, store, }) {
8
8
  let baseState = initialState;
9
- let refreshGeneration = 0;
9
+ let baseStateGeneration = 0;
10
10
  let savedAtMillis = 0;
11
+ let publishGeneration = 0;
12
+ let storedRecords = [];
11
13
  let deferredRecoveryTimer = null;
14
+ const provisionalRecords = new Map();
15
+ const localStorageFailures = new Map();
12
16
  let snapshot = {
13
17
  operations: [],
14
18
  state: initialState,
15
19
  };
16
20
  const listeners = new Set();
17
21
  const executor = createOrderedPendingWriteExecutor({
22
+ now,
18
23
  scopeKey: "durable-command-store",
24
+ select: isActiveCommand,
19
25
  store,
20
- sync: execute,
26
+ sync: async (command, { recordId }) => {
27
+ const result = await execute(command, { operationId: recordId });
28
+ // A completed remote write is newer than any read already in flight.
29
+ // Reconciliation is local-only: a projector bug must never retry a
30
+ // command whose remote effect may already have committed.
31
+ baseStateGeneration += 1;
32
+ if (reconcile) {
33
+ try {
34
+ baseState = reconcile(baseState, command, result);
35
+ }
36
+ catch (error) {
37
+ console.error("[Bayon offline] Unable to reconcile a confirmed durable command.", { error, operationId: recordId });
38
+ }
39
+ }
40
+ },
21
41
  });
22
42
  function notify() {
23
- for (const listener of listeners)
24
- listener();
43
+ for (const listener of listeners) {
44
+ try {
45
+ listener();
46
+ }
47
+ catch (error) {
48
+ console.error("[Bayon offline] Durable-command listener failed.", {
49
+ error,
50
+ });
51
+ }
52
+ }
53
+ }
54
+ function classifyStoredError(error) {
55
+ return classifyError(new Error(error ?? "Unable to sync durable command."));
56
+ }
57
+ function isActiveCommand(record) {
58
+ return (record.syncStatus !== "failed" ||
59
+ shouldRetry(classifyStoredError(record.error), record.payload));
25
60
  }
26
61
  function scheduleDeferredRecovery(records) {
27
62
  if (deferredRecoveryTimer) {
@@ -37,32 +72,62 @@ export function createDurableCommandStore({ canCancel = () => true, classifyErro
37
72
  }, null);
38
73
  if (nextDeadline === null)
39
74
  return;
40
- deferredRecoveryTimer = setTimeout(() => void recover(), Math.max(0, nextDeadline - currentTime));
75
+ deferredRecoveryTimer = setTimeout(recoverInBackground, Math.max(0, nextDeadline - currentTime));
41
76
  }
42
- async function publish() {
43
- const records = await store.list();
77
+ function currentRecords() {
78
+ const records = new Map(storedRecords.map((record) => [record.id, record]));
79
+ for (const record of provisionalRecords.values()) {
80
+ records.set(record.id, record);
81
+ }
82
+ return [...records.values()].sort((left, right) => left.savedAt.localeCompare(right.savedAt) ||
83
+ left.id.localeCompare(right.id));
84
+ }
85
+ function applySnapshot(records) {
44
86
  snapshot = {
45
- operations: records.map((record) => ({
46
- command: record.payload,
47
- ...(record.error
48
- ? { error: classifyError(new Error(record.error)) }
49
- : {}),
50
- id: record.id,
51
- status: record.syncStatus === "failed"
52
- ? "failed"
53
- : record.syncStatus === "syncing"
54
- ? "syncing"
55
- : "accepted",
56
- })),
57
- state: project(baseState, records.filter((record) => record.syncStatus !== "failed")),
87
+ operations: [
88
+ ...records.map((record) => ({
89
+ command: record.payload,
90
+ ...(record.syncStatus === "failed"
91
+ ? { error: classifyStoredError(record.error) }
92
+ : {}),
93
+ id: record.id,
94
+ status: record.syncStatus === "failed"
95
+ ? "failed"
96
+ : record.syncStatus === "syncing"
97
+ ? "syncing"
98
+ : "accepted",
99
+ })),
100
+ ...[...localStorageFailures].map(([id, command]) => ({
101
+ command,
102
+ error: "local-storage",
103
+ id,
104
+ status: "failed",
105
+ })),
106
+ ],
107
+ state: project(baseState, records.filter(isActiveCommand)),
58
108
  };
59
109
  scheduleDeferredRecovery(records);
110
+ }
111
+ async function publish() {
112
+ const generation = ++publishGeneration;
113
+ const records = await store.list();
114
+ if (generation !== publishGeneration)
115
+ return;
116
+ storedRecords = records;
117
+ applySnapshot(currentRecords());
60
118
  notify();
61
119
  }
62
120
  async function recover() {
63
121
  await executor.recover();
64
122
  await publish();
65
123
  }
124
+ function recoverInBackground() {
125
+ void recover().catch((error) => {
126
+ console.error("[Bayon offline] Unable to recover durable commands.", {
127
+ error,
128
+ });
129
+ });
130
+ }
66
131
  const unsubscribe = store.subscribe(() => {
67
132
  void publish().catch((error) => {
68
133
  console.error("[Bayon offline] Unable to publish durable commands.", {
@@ -72,6 +137,11 @@ export function createDurableCommandStore({ canCancel = () => true, classifyErro
72
137
  });
73
138
  return {
74
139
  async cancel(operationId) {
140
+ if (localStorageFailures.delete(operationId)) {
141
+ applySnapshot(currentRecords());
142
+ notify();
143
+ return true;
144
+ }
75
145
  const record = await store.get(operationId);
76
146
  if (!record ||
77
147
  record.syncStatus === "syncing" ||
@@ -86,6 +156,7 @@ export function createDurableCommandStore({ canCancel = () => true, classifyErro
86
156
  close() {
87
157
  if (deferredRecoveryTimer)
88
158
  clearTimeout(deferredRecoveryTimer);
159
+ publishGeneration += 1;
89
160
  unsubscribe();
90
161
  },
91
162
  dispatch(command, options = {}) {
@@ -100,36 +171,20 @@ export function createDurableCommandStore({ canCancel = () => true, classifyErro
100
171
  snapshotId: operationId,
101
172
  syncStatus: "pending",
102
173
  };
103
- // Project before storage settles so controls never wait on storage I/O.
104
- snapshot = {
105
- operations: [
106
- ...snapshot.operations,
107
- { command, id: operationId, status: "accepted" },
108
- ],
109
- state: project(baseState, [
110
- ...snapshot.operations
111
- .filter((operation) => operation.status !== "failed")
112
- .map((operation) => ({
113
- id: operation.id,
114
- payload: operation.command,
115
- savedAt: record.savedAt,
116
- snapshotId: operation.id,
117
- syncStatus: "pending",
118
- })),
119
- record,
120
- ]),
121
- };
174
+ // Product decision: provisional records remain part of the same ordered
175
+ // projection as persisted retryable failures while storage settles.
176
+ provisionalRecords.set(operationId, record);
177
+ localStorageFailures.delete(operationId);
178
+ applySnapshot(currentRecords());
122
179
  notify();
123
180
  const accepted = store.set(record).then(async () => {
181
+ provisionalRecords.delete(operationId);
124
182
  await publish();
125
- void recover();
183
+ recoverInBackground();
126
184
  }, async (error) => {
127
- snapshot = {
128
- operations: snapshot.operations.map((operation) => operation.id === operationId
129
- ? { ...operation, error: "local-storage", status: "failed" }
130
- : operation),
131
- state: baseState,
132
- };
185
+ provisionalRecords.delete(operationId);
186
+ localStorageFailures.set(operationId, command);
187
+ applySnapshot(currentRecords());
133
188
  notify();
134
189
  throw error;
135
190
  });
@@ -140,9 +195,9 @@ export function createDurableCommandStore({ canCancel = () => true, classifyErro
140
195
  await recover();
141
196
  },
142
197
  async refresh(scope) {
143
- const generation = ++refreshGeneration;
198
+ const generation = ++baseStateGeneration;
144
199
  const loaded = await load(scope);
145
- if (generation !== refreshGeneration)
200
+ if (generation !== baseStateGeneration)
146
201
  return;
147
202
  baseState = loaded;
148
203
  await publish();
@@ -154,5 +209,15 @@ export function createDurableCommandStore({ canCancel = () => true, classifyErro
154
209
  };
155
210
  }
156
211
  function createDurableCommandId() {
157
- return globalThis.crypto?.randomUUID?.() ?? `command-${Date.now()}`;
212
+ return globalThis.crypto?.randomUUID?.() ?? createFallbackDurableCommandId();
213
+ }
214
+ let fallbackDurableCommandSequence = 0;
215
+ function createFallbackDurableCommandId() {
216
+ fallbackDurableCommandSequence += 1;
217
+ return [
218
+ "command",
219
+ Date.now().toString(36),
220
+ fallbackDurableCommandSequence.toString(36),
221
+ Math.random().toString(36).slice(2),
222
+ ].join("-");
158
223
  }
@@ -20,4 +20,7 @@ export type BrowserKeyValueDriverOptions = {
20
20
  operationTimeoutMs: number;
21
21
  storeName: string;
22
22
  };
23
+ export declare class OfflineStorageError extends Error {
24
+ name: string;
25
+ }
23
26
  export declare function createIdbKeyValueDriver({ databaseName, databaseVersion, indexedDB, operationTimeoutMs, storeName, }: BrowserKeyValueDriverOptions): BrowserKeyValueDriver | null;