Merge main into wave/a-grid-frame
Two conflicts, both expected.
**`invalidate_relabelled` (improvise-hkt6, WU-K).** Both sides kept: the
signature and the item-tuple comparison are this branch's — `old` is an
`AxisFrame` now, and a group header appearing or collapsing must not blank
cells it did not rename — and the totals blanking and its whole argument are
main's. `grid_updates` still diffs the totals against the *framed* mirror,
which is what makes a renamed column whose total did not change send one;
`a_relabelled_column_drops_its_total`,
`a_relabelled_row_leaves_the_totals_alone` and
`a_renamed_column_whose_total_did_not_change_still_gets_its_total_back` all
pass against the new frame.
**`lib.rs`'s composition test.** Main's deletion taken: 18189c1 moved
`client_showing`, `published_basis` and
`a_client_composes_a_write_from_the_id_it_was_given` into `socket.rs` when
it consolidated `set_cell_at` and `typed`.
Main's new `GridUpdate::RowLabels`/`ColLabels` call sites are converted, and
five doc comments naming the retired spellings with them — a fact stated in
several places is a grep, not an edit.
1327 passing, 0 failing. clippy/fmt clean, wasm gate clean, both bundles
build.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+32
-3
@@ -39,6 +39,9 @@
|
||||
// Neither is reachable silently. Every transport writes what it is doing to
|
||||
// `#status` — "starting", "connected", "cannot reach …", "disconnected" — so
|
||||
// a peer that never arrives reads differently from one that went away.
|
||||
// "disconnected" means exactly the second: a socket that opened and then
|
||||
// closed. A socket that never opened keeps saying "cannot reach …", because
|
||||
// the close event there is the tail of the failure and not news.
|
||||
|
||||
import init, { Client } from "./pkg/improvise_wasm_client.js";
|
||||
|
||||
@@ -206,8 +209,17 @@ function workerTransport() {
|
||||
/** A websocket to `improvise-ws-server`: how a TUI-hosted document is shared. */
|
||||
function socketTransport() {
|
||||
const socket = new WebSocket(WS_URL);
|
||||
const unreachable = `cannot reach ${WS_URL} — is improvise-ws-server running?`;
|
||||
// Did this socket ever open? The page has to record that itself, because
|
||||
// nothing on the close event carries it: `readyState` is already CLOSED by
|
||||
// the time a close listener runs, whichever happened, and a refused
|
||||
// connection and a mid-session network drop are BOTH code 1006 with
|
||||
// `wasClean: false`. So "never connected" and "was connected and went away"
|
||||
// are distinguishable only by remembering.
|
||||
let opened = false;
|
||||
const ready = new Promise((resolve) => {
|
||||
socket.addEventListener("open", () => {
|
||||
opened = true;
|
||||
setShellStatus(`connected · ${WS_URL}`);
|
||||
resolve();
|
||||
});
|
||||
@@ -215,10 +227,27 @@ function socketTransport() {
|
||||
socket.addEventListener("message", (event) => onFrame(event.data));
|
||||
// Without this, a connection that never opens left #status blank and
|
||||
// "server not running" read exactly like "still connecting" (improvise-hwpi).
|
||||
socket.addEventListener("error", () =>
|
||||
setShellFailure(`cannot reach ${WS_URL} — is improvise-ws-server running?`),
|
||||
socket.addEventListener("error", () => setShellFailure(unreachable));
|
||||
// A socket that never opened did not "disconnect" — it failed to connect,
|
||||
// and the close event is the tail of that failure rather than a second,
|
||||
// vaguer thing that happened (improvise-taxx). Writing "disconnected" here
|
||||
// unconditionally replaced the `error` listener's precise message with the
|
||||
// least informative of the two facts.
|
||||
//
|
||||
// Same family as the watchdog bug and NOT the same fix: the watchdog is
|
||||
// disarmed because it is for silence only, whereas suppressing `close`
|
||||
// outright would hide a genuine mid-session disconnect, which is the one
|
||||
// thing this listener is really for. Hence the split rather than a guard.
|
||||
// Both arms go through `setShellFailure`, which DISARMS the watchdog. That
|
||||
// matters on the opened path: the watchdog is armed after `transport.ready`
|
||||
// resolves — i.e. on `open` — so a socket that opened and then dropped
|
||||
// before its first frame reaches here with the timer still pending, and
|
||||
// "disconnected" is more precise than the guess it would otherwise fire 8s
|
||||
// later. On the never-opened path there is no timer yet and the clear is a
|
||||
// no-op. Neither arm leaves one armed.
|
||||
socket.addEventListener("close", () =>
|
||||
setShellFailure(opened ? "disconnected" : unreachable),
|
||||
);
|
||||
socket.addEventListener("close", () => setShellStatus("disconnected"));
|
||||
setShellStatus(`connecting to ${WS_URL}…`);
|
||||
return {
|
||||
send: (text) => {
|
||||
|
||||
Executable
+72
@@ -0,0 +1,72 @@
|
||||
#!/bin/sh
|
||||
# Assemble the static site from `web/` into `site/`.
|
||||
#
|
||||
# `web/` is the *source* directory: it also holds build.sh, assemble.sh and
|
||||
# .gitignore, which have no business being served. This copies what ships.
|
||||
#
|
||||
# Extracted from the CI workflow so that the Pages job and the browser harness
|
||||
# assemble the same tree — a harness that runs against a different arrangement
|
||||
# of files is testing something nobody deploys (improvise-vvl3). Same reason
|
||||
# `web/build.sh` exists rather than two copies of the wasm-pack invocation.
|
||||
#
|
||||
# Usage: web/assemble.sh [OUT_DIR] [CACHE_BUST_TOKEN]
|
||||
#
|
||||
# The token cache-busts the two generated bundles. Pages serves everything
|
||||
# `max-age=600` and the wasm-pack filenames carry no content hash, so for ten
|
||||
# minutes after a deploy a returning visitor runs the PREVIOUS build — which
|
||||
# during improvise-9x6 debugging made a shipped fix look like it had not
|
||||
# shipped, twice. CI passes the commit SHA; a local run defaults to `dev`.
|
||||
#
|
||||
# Both halves are stamped, because a fresh glue file does NOT pull a fresh
|
||||
# .wasm: the glue locates it with `new URL('..._bg.wasm', import.meta.url)`,
|
||||
# and a URL constructed against a base does not inherit the base's query
|
||||
# string. Stamping only the import specifier would refresh the wrapper and
|
||||
# keep the stale binary — the failure mode this is here to end.
|
||||
set -eu
|
||||
|
||||
root=$(cd "$(dirname "$0")/.." && pwd)
|
||||
out=${1:-"$root/site"}
|
||||
token=${2:-dev}
|
||||
|
||||
test -d "$root/web/pkg" || { echo "web/pkg missing — run web/build.sh first" >&2; exit 1; }
|
||||
test -d "$root/web/worker-pkg" || { echo "web/worker-pkg missing — run web/build.sh first" >&2; exit 1; }
|
||||
|
||||
rm -rf "$out"
|
||||
mkdir -p "$out"
|
||||
cp -f "$root/web/index.html" "$root/web/app.js" "$root/web/style.css" "$root/web/worker.js" "$out/"
|
||||
# The document the page opens on a browser that has never seen one.
|
||||
# web/worker.js fetches it by the ?seed= URL and hands the bytes to Rust,
|
||||
# which writes them through the VFS — so leaving it out is an empty grid, not
|
||||
# a 404 the page could report.
|
||||
cp -f "$root/web/demo.improv" "$out/"
|
||||
cp -rf "$root/web/pkg" "$out/pkg"
|
||||
cp -rf "$root/web/worker-pkg" "$out/worker-pkg"
|
||||
rm -f "$out/pkg/.gitignore" "$out/pkg/package.json"
|
||||
rm -f "$out/worker-pkg/.gitignore" "$out/worker-pkg/package.json"
|
||||
|
||||
# BSD sed (macOS) needs an argument to -i; GNU sed must not be given one.
|
||||
sedi() { sed "$1" "$2" > "$2.tmp" && mv -f "$2.tmp" "$2"; }
|
||||
|
||||
sedi "s|\./pkg/improvise_wasm_client\.js|&?v=${token}|" "$out/app.js"
|
||||
sedi "s|\./worker-pkg/improvise_worker_server\.js|&?v=${token}|" "$out/worker.js"
|
||||
sedi "s|'improvise_wasm_client_bg\.wasm'|'improvise_wasm_client_bg.wasm?v=${token}'|" \
|
||||
"$out/pkg/improvise_wasm_client.js"
|
||||
sedi "s|'improvise_worker_server_bg\.wasm'|'improvise_worker_server_bg.wasm?v=${token}'|" \
|
||||
"$out/worker-pkg/improvise_worker_server.js"
|
||||
|
||||
# One `grep` over all four files succeeds when ANY one of them matches, so
|
||||
# three failed substitutions could still produce a "successful" assembly under
|
||||
# `set -e` (found by Copilot on PR #24). These checks are the only guard
|
||||
# against the generated glue changing shape, so each stamped reference is
|
||||
# verified on its own and names itself when it is the one missing.
|
||||
stamped() {
|
||||
grep -q "$2" "$1" || {
|
||||
echo "assemble: $1 is missing the cache-bust stamp: $2" >&2
|
||||
exit 1
|
||||
}
|
||||
grep -n "$2" "$1"
|
||||
}
|
||||
stamped "$out/app.js" "pkg/improvise_wasm_client.js?v=${token}"
|
||||
stamped "$out/worker.js" "worker-pkg/improvise_worker_server.js?v=${token}"
|
||||
stamped "$out/pkg/improvise_wasm_client.js" "improvise_wasm_client_bg.wasm?v=${token}"
|
||||
stamped "$out/worker-pkg/improvise_worker_server.js" "improvise_worker_server_bg.wasm?v=${token}"
|
||||
+7
-3
@@ -65,9 +65,13 @@
|
||||
<code>wss://</code></strong>, because this page is served over https and
|
||||
an https origin refuses a <code>ws://</code> address as mixed content.
|
||||
Note that <code>improvise-ws-server</code> speaks plain
|
||||
<code>ws://</code> only, so reaching it from here means terminating TLS
|
||||
in front of it. Served over <code>http://localhost</code> the same page
|
||||
defaults to <code>ws://</code> and pairs with it directly.
|
||||
<code>ws://</code> only, so reaching it from <em>this</em> page means
|
||||
terminating TLS in front of it with a reverse proxy or a tunnel — the
|
||||
README has a worked Caddy example. Served over
|
||||
<code>http://localhost</code> the same page defaults to
|
||||
<code>ws://</code> and pairs with it directly, with nothing in between;
|
||||
that is the case the pairing is really for. Sharing a document is what
|
||||
the server is for — not running one is what everything above is for.
|
||||
</p>
|
||||
|
||||
<main>
|
||||
|
||||
Reference in New Issue
Block a user