Skip to content

Run the desktop's model layer in the browser - #557

Open
gschier wants to merge 6 commits into
mainfrom
claude/web-sqlite-host
Open

Run the desktop's model layer in the browser#557
gschier wants to merge 6 commits into
mainfrom
claude/web-sqlite-host

Conversation

@gschier

@gschier gschier commented Aug 15, 2026

Copy link
Copy Markdown
Member

Slice 1 of web.yaak.app: the real apps/yaak-client boots in a plain browser tab and stores its data through yaak-models compiled to wasm — the desktop's own queries, 70 migrations, cascade rules, duplicate naming and first-run bootstrap — rather than a TypeScript port of them. Follows #556.

  • crates/yaak-web (@yaakapp-internal/web): boot() registers an IndexedDB-backed VFS and calls init_standalone; rpc(cmd, payload, label) answers the models_* commands through ClientDb and returns the model_writes it caused; blob get/put via blob_manager. Built like yaak-templates with pkg/ committed. Needs a wasm-capable clang to rebuild; the build script keeps pkg/ and says so when there isn't one, so a desktop npm run bootstrap never depends on it
  • models_ops moves from crates/yaak into yaak-models (the send engine doesn't build for wasm)
  • The database lives in a SharedWorker — one process holds the data and pushes writes to every tab, as on the desktop. Falls back to a dedicated worker + Web Lock where SharedWorker is unavailable, and a second tab is told so
  • packages/platform/src/web sheds the IndexedDB model layer from the earlier draft; commands.ts forwards model commands to the worker and keeps the fixed answers and refusals-with-a-reason
  • YAAK_TARGET=web selects the browser entry (index.web.ts); Settings and workspace-switch open in-tab when multiWindow is false. Desktop bundle unchanged (verified: no @tauri-apps in the web bundle, Tauri host intact in the desktop one)

Verified in Chrome, dev and production builds: bootstrap, CRUD across every model type, serde defaults, engine-format ids, Rust copy naming, folder cascade with per-descendant events, single-event workspace delete, reload persistence, two tabs coherent via the worker, Send declined with a toast, 8/8 reloads rendering in ~100 ms.

Not in scope: sending (slice 2, hosted proxy), plugins, git/sync, encryption. Known gaps and what slice 2 needs from this layer are in packages/platform/src/web/README.md.

The browser host now stores data through yaak-models compiled to wasm — the
same queries, migrations, cascade rules, duplicate naming and first-run
bootstrap the desktop and CLI use — instead of a TypeScript port of them.

- crates/yaak-web: the wasm crate. boot() registers an IndexedDB-backed VFS
  and calls init_standalone; rpc(cmd, payload, label) answers the models_*
  commands via ClientDb and returns the model_writes it caused; blob get/put
  through blob_manager. Built like yaak-templates (pkg/ committed); the
  build script keeps pkg/ and says so when no wasm-capable clang is present,
  so a desktop bootstrap never depends on one.
- models_ops moves from crates/yaak into yaak-models so the wasm crate can
  use it without the send engine.
- The database lives in a SharedWorker (packages/platform/src/web/worker.ts):
  one process holds the data and pushes writes to every tab, as on the
  desktop. Where SharedWorker is missing or its script cannot be fetched, a
  dedicated worker guarded by a Web Lock takes over and a second tab is told
  so. The worker imports the wasm lazily so a tab's connect is answered
  instantly; the tab reconnects if it isn't.
- packages/platform/src/web loses models.ts, schema.ts, db.ts and the
  BroadcastChannel; commands.ts forwards model commands to the worker and
  keeps the fixed answers and refusals.

Verified in Chrome, dev and production builds: bootstrap, CRUD across every
model type, serde defaults, engine-format ids, Rust copy naming, folder
cascade with per-descendant events, single-event workspace delete, reload
persistence, two tabs coherent, Send declined with a toast, 8/8 reloads
rendering in ~100 ms.
@gschier

gschier commented Aug 15, 2026

Copy link
Copy Markdown
Member Author

@greptile-apps

@greptile-apps

greptile-apps Bot commented Aug 15, 2026

Copy link
Copy Markdown

Greptile Summary

The PR runs the desktop model layer in browsers through a WASM-backed database owned by a SharedWorker.

  • Adds the yaak-web WASM package and moves reusable model operations into yaak-models.
  • Adds the browser platform entry, command bridge, worker protocol, and browser-specific storage.
  • Uses a SharedWorker and lifetime-held Web Lock to maintain a single database owner across tabs.
  • Adjusts browser navigation behavior and build configuration while preserving the desktop entry.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
packages/platform/src/web/worker.ts Implements the SharedWorker database owner, lifetime Web Lock, WASM boot sequence, RPC handling, and cross-tab event broadcasts.
packages/platform/src/web/connection.ts Connects browser tabs to the SharedWorker and reports unsupported browsers or worker startup failures without creating a competing fallback owner.
packages/platform/src/web/commands.ts Routes supported browser commands to fixed handlers and returns explicit capability errors for unsupported operations.
crates/yaak-web/src/lib.rs Exposes the WASM model database, RPC, and blob operations backed by the IndexedDB SQLite VFS.
crates/yaak-models/src/models_ops.rs Relocates model operations into the shared crate so desktop and browser hosts use the same behavior.
apps/yaak-client/vite.config.ts Selects and bundles the browser platform entry and worker/WASM assets for the web target.

Sequence Diagram

sequenceDiagram
    participant Tab as Browser Tab
    participant SW as SharedWorker
    participant Lock as Web Lock
    participant WASM as yaak-web WASM
    participant DB as IndexedDB SQLite VFS
    Tab->>SW: connect(port)
    SW-->>Tab: hello
    SW->>Lock: acquire database-owner lock
    Lock-->>SW: lock granted
    SW->>WASM: boot()
    WASM->>DB: open and migrate database
    WASM-->>SW: ready
    SW-->>Tab: ready
    Tab->>SW: RPC command
    SW->>WASM: rpc(command, payload, tab label)
    WASM->>DB: query or mutation
    WASM-->>SW: result and model writes
    SW-->>Tab: result
    SW-->>Tab: broadcast model_writes
Loading

Reviews (5): Last reviewed commit: "Simplify the worker connection" | Re-trigger Greptile

Comment thread packages/platform/src/web/connection.ts Outdated
CI runs `cargo test --all`, which compiled yaak-web for the host and failed
in sqlite-wasm-rs's C shim. The crate is now `#![cfg(target_arch = "wasm32")]`
with its browser-only dependencies target-scoped, so it is empty natively.

Review caught a race in the worker fallback: a shared worker that starts
after the tab has given up on it and taken a dedicated worker would open the
database with no lock. The Web Lock now guards every worker, shared or not —
requested with a short timeout so a reloading tab's dying predecessor is
waited out, then reported. The tab also closes the port it abandoned. Verified
in production builds: 8/8 shared and 3/3 dedicated reloads render, and a
second tab in dedicated mode is told the database is in use.
wasm-bindgen glue, ts-rs bindings and lockfiles are build output; reviewing
them spends tokens on code nobody wrote. Same list vite.config.ts excludes
from lint.
@gschier

gschier commented Aug 15, 2026

Copy link
Copy Markdown
Member Author

@greptile-apps

Comment thread packages/platform/src/web/worker.ts Outdated
Without them nothing can promise a second tab won't open a second SQLite
over the same pages. Refusing with a clear message beats hoping; the
browsers affected (iOS Safari < 15.4, Android Chrome < 69) are already
behind what the app needs.
@gschier

gschier commented Aug 15, 2026

Copy link
Copy Markdown
Member Author

@greptile-apps

Comment thread packages/platform/src/web/connection.ts Outdated
Every review finding on the browser host was in the same forty lines: the
fallback from a SharedWorker to a per-tab worker. Two kinds of worker that
can both come up is a race, and each fix moved it rather than removed it.

The fallback existed for a browser we don't target (Android Chrome, no
SharedWorker) and a tooling limitation. Without it the invariant holds by
browser guarantee — one SharedWorker per origin — with the Web Lock covering
the one overlap the browser doesn't rule out, a reloading tab's dying
predecessor. Reconnect-on-silence stays, but reconnects to the same kind.
Browsers without SharedWorker or Web Locks get a clear message.

Verified in production builds: 8/8 reloads render, two tabs coherent,
unsupported browsers see the message.
@gschier

gschier commented Aug 15, 2026

Copy link
Copy Markdown
Member Author

@greptile-apps

Drop the reconnect-and-retry loop. The one thing it recovered from — a
module worker missing connect events during a top-level-await import — is
fixed at the source by importing the wasm lazily, and it has not fired since.
A worker that stays silent past a generous timeout now shows a message
rather than being retried; a reload is the right remedy anyway. Pending
requests no longer keep copies for replay, and blob bodies are transferred.

Also correct the browser-support wording: every current browser, desktop
and mobile, has SharedWorker and Web Locks (Chrome for Android since 148),
so this is not a targeting decision — only browsers years out of date are
told they can't run Yaak.
@gschier

gschier commented Aug 16, 2026

Copy link
Copy Markdown
Member Author

@greptile-apps

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