pub const MODE_B_CLIENT: &str = "// Mode B\'s browser half: load the kernel, hold the state, render locally.\n//\n// This file is compiler residue in the same sense `beck-thin.js` is \u{2014} no todo, no command names,\n// no view logic. What it adds over the thin client is *where the rendering happens*: it hands the\n// kernel data patches and gets DOM patches back, so an interaction is a local fold rather than a\n// round trip (docs/05 \u{a7}5.1, docs/93).\n//\n// The wasm boundary is four exports and a length-prefixed byte buffer; see `crates/beck-wasm`.\n(() => {\n const root = document.getElementById(\"b-root\");\n if (!root) return;\n\n const actor = root.dataset.bActor || \"dev\";\n // What the provider said about this person, as the server verified it. The kernel renders the\n // view against a `Session` built from both, so a claims map missing here is a page that differs\n // from the one being hydrated \u{2014} see `crates/beck-wasm`\'s `Viewer`.\n let claims = {};\n try {\n claims = JSON.parse(root.dataset.bClaims || \"{}\");\n } catch (e) {\n beck.announce(root, \"beck:error\", { error: \"unreadable claims: \" + e });\n }\n const sub = beck.uuid7();\n // The position the server-rendered document reflects. It is *not* what this client resumes\n // from: a Mode A client resuming at `seq` is claiming to hold the page as of `seq`, and the\n // document is that page \u{2014} but a Mode B client holds the *state*, and it starts holding nothing\n // but `init`. So the first connection says it holds nothing (`seq: null`, which the protocol\n // reads as \"send me the world\") and `state.seq` moves only as frames arrive.\n const painted = Number(root.dataset.bSeq) || 0;\n const state = { sub, seq: null, actor };\n\n let wasm = null;\n // Commands proposed and not yet acknowledged, in order. Restored from the local copy on load,\n // and sent whenever a socket opens.\n let queued = [];\n let flush = () => {};\n const memory = () => new Uint8Array(wasm.memory.buffer);\n\n // A response is `<u32 len><bytes>` at the returned pointer, and the module holds it until freed.\n const read = (ptr) => {\n const mem = memory();\n const len = mem[ptr] | (mem[ptr + 1] << 8) | (mem[ptr + 2] << 16) | (mem[ptr + 3] << 24);\n const body = new TextDecoder().decode(mem.subarray(ptr + 4, ptr + 4 + len));\n wasm.beck_free(ptr);\n return JSON.parse(body);\n };\n\n const write = (bytes) => {\n const ptr = wasm.beck_alloc(bytes.length);\n memory().set(bytes, ptr);\n return ptr;\n };\n\n const call = (request) => {\n const bytes = new TextEncoder().encode(JSON.stringify(request));\n return read(wasm.beck_call(write(bytes), bytes.length));\n };\n\n const apply = (response) => {\n if (response.error) {\n beck.announce(root, \"beck:error\", response);\n return;\n }\n if (response.dom && response.dom.length) beck.apply(root, response.dom);\n save();\n };\n\n // ---- the local copy (D7 rung 2) ------------------------------------------\n //\n // \"A Mode B component holds a local copy of its state and queues commands while offline.\" The\n // copy is the kernel\'s confirmed state and its unsent commands; this is somewhere to put it.\n //\n // The key carries the program\'s wire id and the actor, so a deployment that changes the command\n // channel\'s types cannot restore a snapshot of the old one, and one person\'s queue is not\n // another\'s. The kernel refuses a mismatch as well \u{2014} twice, because a key is a convention and a\n // check is a rule.\n let store = null;\n const key = () => \"beck:\" + store.wire + \":\" + actor;\n\n // Writing costs the size of the *state*, not of the change, so it is coalesced: a burst of\n // events persists once. What would remove the cost rather than spreading it is an append-only\n // local log \u{2014} which is what D7\'s later rungs are about, and is not this.\n let pendingSave = null;\n const save = () => {\n if (!store || pendingSave) return;\n pendingSave = setTimeout(() => {\n pendingSave = null;\n try {\n const out = call({ op: \"snapshot\" });\n // A kernel that cannot produce one is a kernel that does not match this shim, and a client\n // that silently stops keeping a local copy is the failure mode worth being loud about.\n if (out.error || !out.snapshot) {\n beck.announce(root, \"beck:error\", out.error ? out : { error: \"no local copy\" });\n store = null;\n return;\n }\n localStorage.setItem(key(), JSON.stringify(out.snapshot));\n } catch (e) {\n // A full quota, a private window, a disabled store: the component still works, it just\n // will not survive a reload. Saying so once is better than failing an interaction.\n beck.announce(root, \"beck:error\", { error: \"cannot store locally: \" + e });\n store = null;\n }\n }, 200);\n };\n\n // The shell, cached, so that a reload with no network is a page rather than an error\n // (`docs/94` \u{a7}94.13). Registered before the kernel is fetched so a first visit primes the cache\n // while the network is there; a browser without service workers simply skips this and keeps\n // every other property of the mode.\n if (navigator.serviceWorker && beck.shell) {\n navigator.serviceWorker\n .register(\"/beck-sw.js\")\n .catch((e) => beck.announce(root, \"beck:error\", { error: \"no shell cache: \" + e }));\n }\n\n const start = async () => {\n // Through `beck.asset`, not `fetch`, for the same reason the socket goes through `beck.dial`:\n // in a playground tab the bundle comes from the worker that derived it and there is no origin\n // to fetch either of them from (docs/98).\n const [module, bundle] = await Promise.all([\n WebAssembly.instantiateStreaming(beck.asset(\"beck-kernel.wasm\"), {}),\n beck.asset(\"beck-bundle.bpk\").then((r) => r.arrayBuffer()),\n ]);\n wasm = module.instance.exports;\n\n // `<u32 len><viewer json><bundle>` \u{2014} the viewer first because the kernel needs it to build the\n // `Session` the view is rendered against.\n // The route is part of the viewer, and it is read off the address bar rather than restored\n // from the local copy: after a reload the URL is the browser\'s own answer to \"where am I\", and\n // a snapshot that disagreed with it would render a page the URL does not name. A document with\n // no URL of its own is at the root, which is `beck.here`\'s job to know.\n const name = new TextEncoder().encode(\n JSON.stringify({ actor, claims, path: beck.here() }),\n );\n const payload = new Uint8Array(4 + name.length + bundle.byteLength);\n new DataView(payload.buffer).setUint32(0, name.length, true);\n payload.set(name, 4);\n payload.set(new Uint8Array(bundle), 4 + name.length);\n const loaded = read(wasm.beck_load(write(payload), payload.length));\n if (loaded.error) {\n beck.announce(root, \"beck:error\", loaded);\n return;\n }\n\n store = { wire: loaded.wire };\n\n // Restore before connecting, so a browser with no network shows the state it had rather than\n // the empty one the fold starts from.\n let restored = false;\n const saved = localStorage.getItem(key());\n if (saved) {\n const out = call({ op: \"restore\", snapshot: JSON.parse(saved) });\n if (out.error) {\n // A snapshot of another program, or of another actor. Dropping it is the whole recovery:\n // the subscription is about to send this client a state anyway.\n localStorage.removeItem(key());\n } else {\n restored = true;\n state.seq = out.seq;\n if (out.dom && out.dom.length) beck.apply(root, out.dom);\n queued = out.queued || [];\n beck.stats.pending = queued.length;\n }\n }\n\n let hydrated = false;\n const send = beck.connect(state, (msg) => {\n // `s` is the whole accumulator (a fresh subscription); `d` is the difference. Everything\n // else is the protocol both modes share.\n if (msg.t === \"s\") {\n state.seq = msg.q;\n // The document was rendered from this state by the same `view`, so this client\'s first\n // render *is* what the DOM shows: adopt it, no DOM work, nothing can differ (docs/93\n // \u{a7}93.5). Otherwise an event landed between the render and this socket opening, and the\n // page on screen is not this state\'s page \u{2014} one rebuild, once.\n const adopt = !hydrated && msg.q === painted;\n hydrated = true;\n apply(call({ op: \"reset\", seq: msg.q, state: msg.v, adopt }));\n } else if (msg.t === \"d\") {\n state.seq = msg.q;\n apply(call({ op: \"data\", seq: msg.q, ops: msg.o }));\n } else if (msg.t === \"u\" || msg.t === \"w\") state.seq = msg.q;\n else if (msg.t === \"a\") {\n call({ op: \"settle\", id: msg.id, seq: msg.q });\n queued = queued.filter((q) => q.id !== msg.id);\n beck.stats.pending = queued.length;\n save();\n }\n else if (msg.t === \"n\") {\n // The server refused a command this client accepted \u{2014} a race rather than a bug, and the\n // correction is to drop the guess and re-render.\n apply(call({ op: \"refused\", id: msg.id }));\n beck.announce(root, \"beck:rejected\", msg);\n }\n }, () => flush());\n\n // A route change is a local render: the kernel moves `session.path` and re-renders from the\n // state it already holds, so the page changes with no round trip at all. The server is told\n // anyway \u{2014} not for the page, which it is not rendering, but so that the `Session` it hands\n // `validate` is the one this client\'s own `validate` saw. Both travel on the one socket, so\n // the navigation precedes the commands proposed from the page it produced.\n beck.route((path) => {\n beck.stats.navigations += 1;\n apply(call({ op: \"nav\", path }));\n send({ t: \"g\", path });\n });\n\n beck.capture((command) => {\n const id = beck.uuid7();\n const out = call({ op: \"propose\", id, command, at: Date.now() });\n if (out.accepted === false) {\n // Refused by the program\'s own `validate`, running here. No round trip, and the reason is\n // the program\'s `Rejection` rather than a string this file invented.\n beck.announce(root, \"beck:rejected\", { id, e: out.why });\n return;\n }\n apply(out);\n // D30: a gesture was folded into this client\'s own interface state and there is nothing to\n // send. Not queued either \u{2014} a queue is what survives a disconnection so the server hears\n // eventually, and there is no \"eventually\" for a thing the server has no decoder for.\n if (out.local === true) return;\n queued.push({ id, command });\n beck.stats.pending = queued.length;\n send({ t: \"c\", id, command });\n });\n\n // What Mode B can say about itself that Mode A cannot: the commands it is holding are *applied*\n // rather than merely sent, so \"pending\" here is the difference between what this browser shows\n // and what the server has agreed to.\n beck.inspect.describe = () => {\n const info = call({ op: \"info\" });\n return {\n mode: \"B\",\n seq: info.seq,\n actor,\n path: info.path,\n component: info.component,\n optimistic: info.optimistic,\n pending: queued.map((q) => q.id),\n in_flight: info.pending,\n };\n };\n beck.devtools();\n\n // Whatever this client owes the server \u{2014} from this session or from the last one \u{2014} goes up as\n // soon as there is a socket. Each carries the id it was proposed with, and the server\n // de-duplicates by it (\u{a7}4.3), so a command sent twice is appended once. That is the whole of\n // why an offline queue needs no agreement between the two sides.\n flush = () => queued.forEach((q) => send({ t: \"c\", id: q.id, command: q.command }));\n\n // The component is live: the kernel holds the bundle, the socket is open and interactions are\n // being captured. Before this, a click reaches nothing \u{2014} the handlers are installed at the end\n // of an asynchronous load \u{2014} so a page with a spinner, a devtools panel, or a test has to be\n // able to tell \"not yet\" from \"nothing happened\".\n beck.ready(root, \"b\");\n };\n\n start().catch((e) => beck.announce(root, \"beck:error\", { error: String(e) }));\n})();\n";Expand description
Mode B: load the kernel, hold the state, render locally ([beck_core::render]).