220944e26c69c6066a68d33ecd206efeb58f1784
54 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a550201fae |
feat(web): the page spawns the worker, opens a demo, and reads a key
Three pieces of the standalone deployment, all in web/.
improvise-djm — app.js picks its peer instead of assuming a socket. The
wasm bundle is the same artifact either way (improvise-xks6), so the
difference is confined to one factory: `?ws=` selects the websocket, its
absence spawns `new Worker('./worker.js?doc=&seed=', {type:'module'})`.
The websocket path is KEPT, because improvise-ws-server is how a
TUI-hosted document is shared and nothing else can reach it; the presence
of the parameter is the whole selection mechanism, since a second knob
naming the same fact is a second thing to get wrong.
improvise-bck — web/demo.improv, built against an assertion rather than
by flipping an axis. examples/demo.improv leaves Customer/Date on None,
so every cell is an Aggregate and nothing is editable. The new demo puts
exactly one category on each visible axis (Product on row, _Measure on
column, Quarter on page) and carries Profit = Revenue - Cost, so a
visitor sees the formula move beside the cell they typed in, with no
keystroke and no scrolling. Two tests hold it: tests/web_demo_model.rs
projects the whole grid through grid_of and asserts every CellId is
Record (ADR 002 — that is what `editable` means), and post_message.rs
drives the shipped bytes through the real seed -> VFS -> open -> Snapshot
chain. The first was proved discriminating by pointing it at
examples/demo.improv, where it names the aggregate at (0,0).
One category per axis is asserted too: GridCache slash-joins a
multi-level tuple (gotcha 44), so a second category on an axis puts that
known gap on the front page.
improvise-hwpi — all four. A keydown listener on window feeds
Client::on_key_event, so the classifier is reachable at last; a frame
that will not fold now writes to #status instead of only the console; the
socket has an error listener, so "not running" no longer reads like
"still connecting"; and close() has a re-entry guard. Plus one the ticket
could not have predicted: a worker whose start() rejects is caught inside
worker.js and fires no `error` on the main thread, so the same silence
was about to be rebuilt in the new path — a first-frame watchdog covers
both deployments, because "the peer never answered" is the one condition
that means the same thing on each.
Verified: cargo test --workspace 1159 passing (1155 + 4), clippy clean,
fmt clean, actionlint clean, ./web/build.sh end to end (345 KB / 123 KB
gzipped client, 1.33 MB / 498 KB gzipped worker), every call present in
the generated .d.ts with the right arity, every relative reference in the
assembled site resolving. NOT verified: there is no browser here. No line
of JavaScript in this commit has ever run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EoW5BB87ZZdm96SvBTWawH
|
||
|
|
bdce1636bb |
feat(worker-server): the whole App in a Web Worker, over postMessage + OPFS
improvise-9x6: the server half of the standalone static-web deployment (epic improvise-tm6). `improvise-worker-server` hosts the authoritative `App`, the command pipeline, the projection layer and VFS persistence behind `self.onmessage`/`self.postMessage`. It is small because everything it needs already existed: the frames are `improvise_protocol::frame`'s (improvise-xks6), the session loop is `Host` (previous commit), the basis check is `Sessions::apply`/`Checked` (improvise-ondj), and persistence is `Storage` + `OpfsFs` (improvise-6mq, i34). What is genuinely new is three things. `vfs.rs` — how a document gets INTO a browser's empty filesystem. Loading is unchanged: it is `persistence::load_from` on a `Storage`, the same call the native binary makes. Seeding is the new problem, and it is a `Storage::write`, so a fetched demo, an upload and a saved document take the same parse path and a seed never clobbers an edit. `list` is the document picker a browser has no OS file manager for. `worker.rs` — the loop: one port, one client, the `Viewport` handshake (dropped rather than closed, since there is no connection to close), and the filter from `Addressed`. Every decision is here and every test points at it. `bindings.rs` — `cfg(wasm32)`, decision-free, and the only part no test here can reach: it awaits `OpfsFs::open`/`attach`/`refresh`/`commit_deletes` and forwards strings. Two gaps found and reported rather than papered over. No client on any transport can ask its host to save — `effect::Save` is `ViewOnly`, so the wire vocabulary derived from the model effects cannot name it — and a wire `ModelCommand::SetCell` never sets `model_state.dirty`, because dirtiness is raised by a separate `MarkDirty` effect the key path emits. So the worker persists on its own behalf, keyed off the command variant rather than off a flag that is never set; `touches_document` carries the argument. `ClientFrame::Key` is still answered with nothing. A worker CAN call `reduce_key` — it holds the `App` — but nothing in the projection stream carries the host's own `ViewState`, so a half-wired arm diverges rather than under-delivers. `a_key_resolved_on_the_host_moves_state_no_client_can_hear` demonstrates it against a real `App` and `Sessions` (improvise-bpq2). 22 tests, all host-side: 16 in the crate and 6 in `tests/post_message.rs`, which drives the worker with nothing but the JSON a `MessagePort` carries. Bundle: 1,334,892 bytes raw, 496,965 gzipped — 485 KB against a 3 MB budget. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EoW5BB87ZZdm96SvBTWawH |
||
|
|
188f3e94ef |
ci(pages): build the wasm bundle and publish web/ to GitHub Pages
improvise-d31's finished artifact depends on improvise-9x6 (the worker-hosted server) through improvise-djm. The *pipeline* does not, and building it now is what stops the delivery half from being a mystery when the worker lands: the swap from websocket transport to worker transport then changes what is bundled, not how it is built and shipped. Two jobs in ci.yml rather than a second workflow. `pages-build` runs on every push and pull request, so a change that breaks `web/build.sh` or the bundle fails on the PR that made it. `pages-deploy` runs only on a push to main and `needs: [test, wasm, pages-build]`, so nothing that failed `cargo test` or the existing wasm32 gate can reach the published site — the deploy depends on those checks instead of repeating them. Deliberately not path-filtered. The bundle is improvise-wasm-client plus improvise-protocol, improvise-core and improvise-formula, so a filter meaning "skip when the bundle cannot have changed" would have to name most of the workspace, and getting it wrong leaves a stale site with nothing saying so. wasm-pack is a pinned release tarball, v0.15.0 — the version `nix develop` now supplies — so the local run that produced 346 KB of wasm (120 KB gzipped) is evidence about the CI run. The devShell was missing wasm-pack entirely: the `wasm` job only runs `cargo check`, which does not need it, so nothing had ever required the one tool that turns a passing check into a loadable page. What this cannot do yet, said on the page itself rather than routed around: web/app.js is a thin client that holds no model, so a copy served from a static host paints an empty grid until it is pointed at an improvise-ws-server with ?ws=. The notice is static HTML, not something app.js decides, because it is true on every load and a page that cannot reach a server cannot be trusted to explain why. README says the same and names the deployed URL. Verified: cargo test --workspace 1134 passed / 0 failed, clippy clean, fmt clean, actionlint clean, ./web/build.sh succeeds and the assemble step produces a 388 KB site whose every reference resolves inside it. NOT verified: the workflow has never run — GitHub Pages is not enabled on the repository (GET /repos/fiddlerwoaroof/improvise/pages is 404), which is improvise-3gy. The deploy job asks configure-pages to enable it; if the token may not, that step is where the run fails and the fix is the repository setting. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EoW5BB87ZZdm96SvBTWawH |
||
|
|
c72832f542 |
feat(wasm-client,web): the DOM renderer for the browser grid (improvise-cr3)
The last blocker of improvise-1ey. Mostly assembly: `Client` already had the fold, the key classifier and `set_cell`, `CellDisplay::id` already said what a cell is (ADR 002), and `flatten_cat_tree` already owned the category panel's flattening. What was missing was the three joints between them. `render.rs` is the one that matters. The DOM half is JavaScript and JavaScript here is untested, so every question with a *silent* wrong answer is answered in Rust under test and crosses as decided data: `editable` is a fact about the `CellId` variant (Record only — an Aggregate is a drill target and `Drill` is not on the wire yet, and `None` is "no projected cell", not "aggregate"), the selection is placed by subtracting the scroll offset because `GridCache` is viewport-indexed while the cursor is not, and the category panel is `flatten_cat_tree` rather than a second walk of the cache. `socket.rs` frames what `improvise-ws-server` actually speaks, which is not what `wire.rs` speaks: it has `Viewport` (the handshake — a snapshot built at extent zero is an empty grid), it batches `Commands`, and it has no `Key` variant. The two enums are duplicated because a native crate cannot be a wasm dependency; `improvise-ws-server` is a dev-dependency so the compatibility is checked through its own types, and a companion test proves the check is discriminating on the variant names that would drift. `web/` is the shell, and it is thin on purpose: four string calls in and out, no protocol knowledge, no framework. Gates: 1047 passing (was 1031), clippy and fmt clean, and `cargo check -p improvise-wasm-client --target wasm32-unknown-unknown` clean. Verified end to end headlessly — the wasm-pack artifact driven from node over a real websocket to a real `improvise-ws-server`: 1,200 read, "98765" typed, "98,765" projected back. The DOM painting itself is unverified; there is no headless browser here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EoW5BB87ZZdm96SvBTWawH |