MODE_B_CLIENT

Constant MODE_B_CLIENT 

Source
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]).