fix(web): default the socket to TLS, and cache-bust the deployed bundles

?ws= now takes a host:port and the page supplies the scheme: wss:// when it
is itself served over https, ws:// when over http. An explicit ws:// from an
https page is UPGRADED rather than passed through, because an https origin
refuses it as mixed content -- so passing it through can only produce a
failure that reads like 'the server is not running'. The scheme is not the
caller's to get wrong.

And the deploy now stamps ?v=<sha> onto both 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
made a shipped fix look like it had not shipped, twice, during the first real
browser debugging session.

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 built against a base does not inherit the base's query. Stamping only
the import specifier would refresh the wrapper and keep the stale binary --
exactly the failure this is here to end.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EoW5BB87ZZdm96SvBTWawH
This commit is contained in:
Edward Langley
2026-09-19 21:17:08 -07:00
parent 6e1ba77d70
commit 94d26d8705
4 changed files with 76 additions and 18 deletions
+32 -3
View File
@@ -29,11 +29,12 @@
// default an in-page Web Worker hosting the whole App over OPFS
// (improvise-9x6). No server process; this is what makes a
// GitHub Pages copy of this directory a working application.
// ?ws=URL a websocket to `improvise-ws-server`. That process is how a
// ?ws=host:port
// a websocket to `improvise-ws-server`. That process is how a
// document opened in the TUI is shared, so the path is kept; the
// presence of the parameter is the whole selection mechanism,
// because a second knob naming the same fact is a second thing to
// get wrong.
// get wrong. The scheme defaults to `wss://` — see `socketUrl`.
//
// Neither is reachable silently. Every transport writes what it is doing to
// `#status` — "starting", "connected", "cannot reach …", "disconnected" — so
@@ -42,7 +43,35 @@
import init, { Client } from "./pkg/improvise_wasm_client.js";
const params = new URLSearchParams(location.search);
const WS_URL = params.get("ws"); // present => socket, absent => in-page worker
// present => socket, absent => in-page worker. The value is a host:port, and
// the scheme is NOT the caller's to get wrong: see `socketUrl`.
const WS_PEER = params.get("ws");
/**
* The websocket URL for `?ws=`, defaulting to TLS.
*
* `wss://` is the default and a bare `host:port` gets it, because the address
* that works from where this page is actually served is the one it should be
* easiest to write. This page ships to GitHub Pages, which is https, and an
* https origin refuses a `ws://` address as mixed content — so a `ws://`
* default would hand the browser a URL that cannot work and report it as a
* connection failure, which reads like "the server is not running".
*
* An explicit `ws://` is honoured only where it can function: a page served
* over plain http, which is the local `python3 -m http.server` case that pairs
* with `improvise-ws-server` (which speaks plain `ws://` only — it has no TLS,
* so reaching it from https means terminating TLS in front of it). From an
* https page an explicit `ws://` is upgraded rather than passed through, since
* passing it through can only produce a failure the user cannot act on.
*/
function socketUrl(peer) {
const secure = location.protocol === "https:";
if (/^wss:\/\//i.test(peer)) return peer;
if (/^ws:\/\//i.test(peer)) return secure ? peer.replace(/^ws:/i, "wss:") : peer;
return `${secure ? "wss" : "ws"}://${peer}`;
}
const WS_URL = WS_PEER ? socketUrl(WS_PEER) : null;
const DOC = params.get("doc") ?? "demo.improv";
const SEED = params.get("seed") ?? "demo.improv";