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";
+9 -9
View File
@@ -29,8 +29,8 @@
?doc= and ?seed= name the document and the file it is first seeded from.
To talk to a separate `improvise-ws-server` process instead — which is
how a document open in the TUI is shared — pass ?ws=wss://host:port
(or ws:// when this page is served over http://localhost).
how a document open in the TUI is shared — pass ?ws=host:port; the
scheme defaults to wss:// on https and ws:// on http.
-->
<header>
<h1>improvise</h1>
@@ -60,13 +60,13 @@
<code>?seed=url</code>. To open a document hosted by a separate
process instead, run
<code>improvise-ws-server model.improv --port 8787</code> and reload
with <code>?ws=wss://host:port</code>. From this https page the
address must be <code>wss://</code> — a <code>ws://</code> one is
blocked as mixed content — and
<code>improvise-ws-server</code> speaks plain <code>ws://</code> only,
so it needs TLS terminated in front of it. Served over
<code>http://localhost</code> the same page takes
<code>?ws=ws://127.0.0.1:8787</code> directly.
with <code>?ws=host:port</code>. <strong>The scheme defaults to
<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.
</p>
<main>