Skip to content

Add a storage Worker and its client to @optuna/storage - #1322

Merged
c-bata merged 4 commits into
optuna:mainfrom
c-bata:storage-worker-package
Aug 14, 2026
Merged

Add a storage Worker and its client to @optuna/storage#1322
c-bata merged 4 commits into
optuna:mainfrom
c-bata:storage-worker-package

Conversation

@c-bata

@c-bata c-bata commented Aug 14, 2026

Copy link
Copy Markdown
Member

Contributor License Agreement

This repository (optuna-dashboard) and Goptuna share common code.
This pull request may therefore be ported to Goptuna.
Make sure that you understand the consequences concerning licenses and check the box below if you accept the term before creating this pull request.

  • I agree this patch may be ported to Goptuna by other Goptuna contributors.

Reference Issues/PRs

Follow-up of #1319. First step of moving SQLite / Journal parsing off the UI thread.

What does this implement/fix? Explain your changes.

SQLite3Storage and JournalFileStorage run entirely in the caller's context:
sqlite-wasm initialization, sqlite3_deserialize(), every query, the UTF-8 decode and JSON parse of a whole Journal file, and the replay that rebuilds the Optuna objects. In the standalone app and in the VS Code Webview that caller is the UI thread, so selecting a large storage file freezes the page until parsing finishes.

This PR adds the pieces needed to run that work in a Web Worker. Nothing calls them yet.

OptunaStorage.close()

Nothing releases a storage today: selecting another file in the standalone app replaces a React state reference and leaves the SQLite connection and the parsed Journal model behind.

A Worker session has to be endable, so the interface gains close() and both backends implement it. It is idempotent and does not fail, including after a failed open.

An explicit sqlite-wasm source

SQLite3Storage relied on the URL sqlite-wasm derives from import.meta.url, which does not survive bundling into a Worker: a Worker started from a blob: URL cannot resolve anything relative to itself.

The constructor now takes the wasm as a URL or as bytes, locateFile is always set so the fallback is never evaluated, and a failed initialization closes the half-open database instead of leaking it.

The initializer is imported from the dependency's bundler-friendly entrypoint rather than its package entrypoint, which would pull in the optional Worker API this dashboard does not use.

The Worker and its client

storage_worker.ts owns one storage session. It detects the format from the first bytes of the transferred buffer, so callers no longer decide which backend to build, and answers getStudies() / getStudy() / close(). Backend errors cross the boundary as structured errors rather than exception objects.

StorageWorkerClient implements OptunaStorage, so a UI can use it like any other storage. It takes the Worker through a factory, because how a Worker is started differs per platform — a module Worker where the bundler supports one, a blob: URL in a VS Code Webview — and the factory returns a dispose() so the client releases whatever the platform allocated. The storage buffer is transferred, not copied.

Request and response types are derived from a single request-type-to-result map, responses carry the request type so a mismatch is reported instead of being handed to the caller as the wrong shape, and an unknown request is answered with an error rather than leaving the caller's promise pending forever.

Tests

  • worker_client.test.mjs drives the client against a fake Worker: request ID matching, buffer transfer, double close, terminate, rejecting everything pending when the Worker fails, and rejecting a response that answers a different request type.
  • storage_worker.test.mjs runs the real Worker on node:worker_threads with the existing fixtures: opening both formats, study summaries and details, transferring the wasm binary, an unsupported request, and a missing wasm asset.

Follow-ups (not in this PR)

  • the standalone app opening storages through the Worker
  • the VS Code Webview doing the same, plus the asset plumbing its Webview needs
  • surfacing the Journal parse warnings that OpenStorageResult already carries

c-bata added 2 commits August 14, 2026 11:31
Preparation for running the storage backends in a Web Worker.

`OptunaStorage` gains `close()`. Today nothing releases a storage: selecting
another file in the standalone app only replaces a React state reference, and
neither the SQLite connection nor the parsed Journal model is freed. A Worker
makes that explicit, so the interface needs a way to say "this session is over"
that both backends implement.

`SQLite3Storage` also takes where to load sqlite-wasm from, as a URL or as
bytes. It has been relying on the URL that sqlite-wasm derives from
`import.meta.url`, which does not survive being bundled into a Worker: a Worker
started from a blob: URL cannot resolve anything relative to itself. locateFile
is therefore always set, and initialization failures now close the half-open
database instead of leaking it.

The initializer is imported from the bundler-friendly entrypoint of the
dependency rather than its package entrypoint, which would pull in the optional
Worker API this dashboard does not use.
The Worker owns one storage session: it detects the format from the first bytes
of the transferred buffer, opens the matching backend, answers getStudies() /
getStudy() / close(), and reports failures as structured errors. The client
implements `OptunaStorage`, so a UI can treat it as any other storage, and takes
the Worker through a factory because how a Worker is started differs per
platform: a module Worker in a bundler that supports one, a blob: URL in a VS
Code Webview. The factory hands back a dispose() so the client can release
whatever the platform allocated, such as that blob: URL.

Request and response types are derived from one request type to result map, and
responses carry the request type so a mismatch is caught rather than handed to
the caller as the wrong shape. Requests the Worker does not know about are
answered with an error instead of silently leaving the caller's promise pending.

Nothing calls this yet: the standalone app and the VS Code extension keep
loading storages on the UI thread, and are moved over in later changes.

The client is also exported as `@optuna/storage/worker-client` so that a UI can
depend on it without importing the package root, which drags the SQLite backend
into the bundle.
@c-bata c-bata changed the title Storage worker package Introduce StorageWorkerClient Aug 14, 2026
c-bata added 2 commits August 14, 2026 13:14
Each of the three entry points now says, at the top of the file, which context
it belongs to and why the Worker entry is not part of `exports`. The comment is
repeated verbatim in all three: whichever file you open first, the split is
right there.

SQLiteWasmOptions records why the wasm location is passed in at all, what the
two forms are for, and why inlining it as base64 was not kept.

The protocol also exported more than it needed to. StorageWorkerRequestOf was an
alias for an Extract<> used in exactly one signature, StorageWorkerRequestWithoutId
described itself rather than what it is, and the package re-exported the whole
wire format although a consumer that reaches for it is talking to the Worker
without the client.
Setting locateFile unconditionally broke the standalone app: with no wasm
location given it returned the bare path "sqlite3.wasm", so the browser resolved
it against the page, the dev server answered with index.html, and instantiation
failed on the HTML magic bytes ("expected magic word 00 61 73 6d, found 3c 21 44
4f"). Journal files were unaffected, which is why only the rdb e2e case failed.

The default path is the point: without locateFile, sqlite-wasm resolves the wasm
as `new URL("sqlite3.wasm", import.meta.url)`, which the bundler rewrites to the
asset it emitted. A built bundle now takes that branch again. locateFile is set
only when this call knows better, that is when a URL was passed, or when the
bytes were, where it exists solely to keep the fallback from being evaluated in
a Worker that cannot resolve anything relative to itself.
o-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@c-bata
c-bata merged commit 5ee7947 into optuna:main Aug 14, 2026
10 checks passed
@c-bata
c-bata deleted the storage-worker-package branch August 14, 2026 04:39
@c-bata c-bata changed the title Introduce StorageWorkerClient Add a storage Worker and its client to @optuna/storage Aug 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant