PATCH_CLIENT

Constant PATCH_CLIENT 

Source
pub const PATCH_CLIENT: &str = "// The patch interpreter, the router, the input capture and the id source. Shared by both modes.\n//\n// Mode A receives these ops from the server; Mode B\'s kernel produces them in the browser from its\n// own two renders. Same vocabulary, same interpreter \u{2014} which is not a convenience, it is the claim:\n// \"hand-written JavaScript never appears in the source \u{2014} it\'s compiler residue\", and there is one\n// piece of residue rather than one per mode.\n(() => {\n  // An element belongs to a namespace, and `createElement` only ever guesses HTML.\n  //\n  // Server-side rendering goes through the browser\'s own HTML parser, which knows that `<svg>`\n  // opens a different namespace and that `<foreignObject>` closes it again. This interpreter is the\n  // other half of the same page and has to know the same thing: an `svg` built as HTML is not an\n  // `SVGElement`, so it lays out as nothing and a chart that paints on first load vanishes the\n  // first time its data changes. `createElementNS` is also what keeps `linearGradient` and\n  // `clipPath` their own case, which `createElement` lowercases in an HTML document.\n  const HTML = \"http://www.w3.org/1999/xhtml\";\n  const SVG = \"http://www.w3.org/2000/svg\";\n\n  // Which namespace a *child* of this node is built in. Inherited, except at the two edges.\n  const within = (node) =>\n    node && node.namespaceURI === SVG && node.localName !== \"foreignObject\" ? SVG : HTML;\n\n  const build = (h, ns) => {\n    if (typeof h === \"string\") return document.createTextNode(h);\n    const tag = h[0];\n    // A patch can carry a subtree whose root is `svg` or a subtree that starts inside one, so the\n    // namespace comes from the tag when the tag opens one and from the destination otherwise.\n    const here = tag === \"svg\" ? SVG : ns || HTML;\n    const el = document.createElementNS(here, tag);\n    // Pairs, in the order the server wrote them: an element rebuilt here carries its attributes in\n    // the order the same element has in the server-rendered document.\n    const attrs = h[1];\n    for (let a = 0; a < attrs.length; a++) el.setAttribute(attrs[a][0], attrs[a][1]);\n    const kids = h[2];\n    const inner = here === SVG && tag !== \"foreignObject\" ? SVG : HTML;\n    for (let i = 0; i < kids.length; i++) el.appendChild(build(kids[i], inner));\n    return el;\n  };\n\n  // What this client has sent, received and applied. Read by the devtools panel and by nothing\n  // else \u{2014} no behaviour depends on it, which is the point: a counter that changes what the page\n  // does is a second implementation of the page.\n  const stats = {\n    frames: 0,\n    ops: 0,\n    bytes_in: 0,\n    bytes_out: 0,\n    sent: 0,\n    navigations: 0,\n    pending: 0,\n    connected: false,\n  };\n\n  // ---- where the caret and the scroll are, across a patch --------------------------------------\n  //\n  // A patch that replaces an ancestor of the focused element destroys it, and the browser\'s answer\n  // to \"what is focused now\" is `body`. So the page loses the caret in the middle of typing, and\n  // the list somebody had scrolled jumps back to the top. Neither is a defect of the diff \u{2014} the\n  // page really did change \u{2014} and neither is the program\'s to think about, so it is handled here,\n  // once, for both modes.\n  //\n  // The cost is proportional to *the patch* and not to the page: nothing is walked except the\n  // subtrees a replace is about to destroy, and the focused element is one lookup.\n\n  const indexPath = (from, node) => {\n    const path = [];\n    let at = node;\n    while (at && at !== from) {\n      const parent = at.parentNode;\n      if (!parent) return null;\n      path.unshift(Array.prototype.indexOf.call(parent.childNodes, at));\n      at = parent;\n    }\n    return at === from ? path : null;\n  };\n\n  const descend = (from, path) => {\n    let node = from;\n    for (let i = 0; i < path.length && node; i++) node = node.childNodes[path[i]];\n    return node;\n  };\n\n  // Enough of an element\'s identity to refuse to restore the caret into a *different* element that\n  // happens to have taken its place. A key, a name or an id is the program\'s own answer; the tag is\n  // the floor.\n  const identity = (el) =>\n    el.tagName +\n    \"|\" +\n    (el.getAttribute(\"data-b-k\") || \"\") +\n    \"|\" +\n    (el.getAttribute(\"name\") || \"\") +\n    \"|\" +\n    (el.id || \"\");\n\n  const caret = (root) => {\n    const el = document.activeElement;\n    if (!el || el === document.body || !root.contains(el)) return null;\n    const path = indexPath(root, el);\n    if (!path) return null;\n    const range =\n      typeof el.selectionStart === \"number\"\n        ? { start: el.selectionStart, end: el.selectionEnd, dir: el.selectionDirection }\n        : null;\n    return { el, path, range, id: identity(el), top: el.scrollTop, left: el.scrollLeft };\n  };\n\n  const restoreCaret = (root, was) => {\n    // Still there: the patch did not touch it, and re-focusing would move the caret for nothing.\n    if (!was || was.el.isConnected) return;\n    const now = descend(root, was.path);\n    if (!now || now.nodeType !== 1 || identity(now) !== was.id || !now.focus) return;\n    now.focus();\n    if (was.range && now.setSelectionRange) {\n      try {\n        now.setSelectionRange(was.range.start, was.range.end, was.range.dir);\n      } catch (e) {\n        // A type whose selection cannot be set (`number`, `email` in some browsers). The focus is\n        // the part that matters and it is already restored.\n      }\n    }\n    now.scrollTop = was.top;\n    now.scrollLeft = was.left;\n  };\n\n  // Scroll offsets inside a subtree about to be replaced, by position within it. Best effort by\n  // construction \u{2014} a replaced subtree is one whose shape may have changed \u{2014} so it is keyed by\n  // position and restored only where the position still holds something scrollable.\n  const scrollsIn = (node, path, out) => {\n    if (node.nodeType !== 1) return;\n    if (node.scrollTop || node.scrollLeft) {\n      out.push({ path, top: node.scrollTop, left: node.scrollLeft });\n    }\n    for (let i = 0; i < node.childNodes.length; i++) {\n      scrollsIn(node.childNodes[i], path.concat(i), out);\n    }\n  };\n\n  const apply = (root, ops) => {\n    const at = (path) => {\n      let node = root.firstElementChild;\n      for (let i = 0; i < path.length; i++) node = node.childNodes[path[i]];\n      return node;\n    };\n    const was = caret(root);\n    const scrolled = [];\n    for (let i = 0; i < ops.length; i++) {\n      if (ops[i][0] !== 0) continue; // only a replace rebuilds what was scrolled\n      const victim = at(ops[i][1]);\n      if (victim) scrollsIn(victim, [], (scrolled[i] = []));\n    }\n\n    for (let i = 0; i < ops.length; i++) {\n      const op = ops[i];\n      const path = op[1];\n      switch (op[0]) {\n        case 0: { // replace\n          const node = at(path);\n          // The namespace of what is being built comes from where it is going, which is the parent\n          // of what it replaces \u{2014} an op whose root is a `rect` says nothing about namespaces itself.\n          const next = build(op[2], within(node ? node.parentNode : root));\n          if (node) node.replaceWith(next);\n          else root.appendChild(next);\n          const kept = scrolled[i];\n          for (let s = 0; kept && s < kept.length; s++) {\n            const target = descend(next, kept[s].path);\n            if (target && target.nodeType === 1) {\n              target.scrollTop = kept[s].top;\n              target.scrollLeft = kept[s].left;\n            }\n          }\n          break;\n        }\n        case 1: at(path).textContent = op[2]; break;        // set text\n        case 2: at(path).setAttribute(op[2], op[3]); break;  // set attribute\n        case 3: at(path).removeAttribute(op[2]); break;      // remove attribute\n        case 4: { // insert child\n          const parent = at(path);\n          parent.insertBefore(build(op[3], within(parent)), parent.childNodes[op[2]] || null);\n          break;\n        }\n        case 5: { // remove child\n          const parent = at(path);\n          parent.removeChild(parent.childNodes[op[2]]);\n          break;\n        }\n        case 6: { // move child (from > to), which is what preserves focus and scroll on reorder\n          const parent = at(path);\n          parent.insertBefore(parent.childNodes[op[2]], parent.childNodes[op[3]]);\n          break;\n        }\n      }\n    }\n    restoreCaret(root, was);\n    stats.frames += 1;\n    stats.ops += ops.length;\n    announce(root, \"beck:traffic\", stats);\n  };\n\n  // \"Client-generated UUIDs are the small tell that browsers here are replicas, not terminals\":\n  // the client must be able to name a todo before the server confirms it exists.\n  const uuid7 = () => {\n    const now = Date.now();\n    const b = crypto.getRandomValues(new Uint8Array(16));\n    b[0] = now / 2 ** 40; b[1] = now / 2 ** 32; b[2] = now / 2 ** 24;\n    b[3] = now / 2 ** 16; b[4] = now / 2 ** 8;  b[5] = now;\n    b[6] = (b[6] & 0x0f) | 0x70;\n    b[8] = (b[8] & 0x3f) | 0x80;\n    let hex = \"\";\n    for (let i = 0; i < 16; i++) hex += b[i].toString(16).padStart(2, \"0\");\n    return hex.slice(0, 8) + \"-\" + hex.slice(8, 12) + \"-\" + hex.slice(12, 16) + \"-\" +\n           hex.slice(16, 20) + \"-\" + hex.slice(20);\n  };\n\n  // Handlers in `view` compiled to attributes, so no user JavaScript runs and `script-src` can\n  // stay near-empty. Three holes, and a command is filled *recursively* \u{2014} a command whose field is\n  // a record has its holes one level down, and a filler that only looked at the top would leave\n  // the literal `\"$id\"` in the log.\n  //\n  // | hole | filled with |\n  // |---|---|\n  // | `$id` | a fresh UUIDv7, minted here so the client can name a thing before the server has it |\n  // | `$value` | the value of the element the handler is on |\n  // | `$field:name` | the value of the form control called `name`, in the form being submitted |\n  const fill = (template, value, form) => {\n    const hole = (v) => {\n      if (v === \"$id\") return uuid7();\n      if (v === \"$value\") return value === null ? \"\" : value;\n      if (typeof v === \"string\" && v.startsWith(\"$field:\")) {\n        const named = form && form.elements[v.slice(\"$field:\".length)];\n        if (!named) return \"\";\n        if (named.type === \"checkbox\") return named.checked;\n        return named.value === undefined ? \"\" : named.value;\n      }\n      return v;\n    };\n    const walk = (node) => {\n      if (typeof node === \"string\") return hole(node);\n      if (Array.isArray(node)) return node.map(walk);\n      if (node && typeof node === \"object\") {\n        const out = {};\n        for (const key in node) out[key] = walk(node[key]);\n        return out;\n      }\n      return node;\n    };\n    return walk(JSON.parse(template));\n  };\n\n  // Declared handlers, captured once. `send` is whatever the mode does with a command: post it up\n  // the socket (Mode A), or apply it locally first and then post it (Mode B).\n  //\n  // Every event listened for is a `data-b-<event>` attribute the `ui:` macro wrote from an\n  // `on_<event>=` in the program, so this file names events and never commands.\n  const capture = (send) => {\n    const on = (kind, attribute, handler) =>\n      document.addEventListener(kind, (event) => {\n        const el = event.target.closest && event.target.closest(\"[\" + attribute + \"]\");\n        if (el) handler(el, event);\n      });\n\n    on(\"click\", \"data-b-click\", (el) => send(fill(el.getAttribute(\"data-b-click\"), null)));\n\n    on(\"keydown\", \"data-b-enter\", (el, event) => {\n      if (event.key !== \"Enter\") return;\n      const value = el.value || \"\";\n      if (!value.trim()) return;\n      send(fill(el.getAttribute(\"data-b-enter\"), value));\n      el.value = \"\";\n    });\n\n    // A form. The browser\'s own submit \u{2014} a button, or Enter in a single-line field \u{2014} so a page\n    // built out of `form:` and `input(name=\u{2026})` is one a keyboard and a screen reader already know\n    // how to drive, and `$field:name` is how the program names what was typed.\n    on(\"submit\", \"data-b-submit\", (el, event) => {\n      event.preventDefault();\n      send(fill(el.getAttribute(\"data-b-submit\"), null, el));\n      el.reset();\n    });\n\n    // A control that reports as it changes. `input` fires per keystroke and `change` on commit,\n    // which is the browser\'s distinction rather than one invented here.\n    on(\"input\", \"data-b-input\", (el) =>\n      send(fill(el.getAttribute(\"data-b-input\"), controlValue(el), el.form)));\n    on(\"change\", \"data-b-change\", (el) =>\n      send(fill(el.getAttribute(\"data-b-change\"), controlValue(el), el.form)));\n  };\n\n  const controlValue = (el) =>\n    el.type === \"checkbox\" ? el.checked : el.value === undefined ? \"\" : el.value;\n\n  // ---- the router -----------------------------------------------------------------------------\n  //\n  // A route is `session.path`, so navigating is not a fetch and not a route table: it is the same\n  // page function of a different session, and the only thing this has to do is (a) keep the address\n  // bar honest and (b) say where the client is. In Mode A that is a message and a patch back; in\n  // Mode B the kernel re-renders locally and the server is told only so that the `Session` it hands\n  // `validate` is the one the client\'s own `validate` saw.\n  //\n  // An ordinary `<a href>` \u{2014} no `data-b-` attribute, no `onclick`, nothing in the program that\n  // knows a router exists. Which is the point: a link that this file did not intercept is still a\n  // link, and the page it lands on is server-rendered at that path.\n\n  // Does this document have a URL of its own?\n  //\n  // A `srcdoc` iframe does not \u{2014} which is what the playground\'s clients are (docs/98) \u{2014} and neither\n  // does a blob. Their `location.pathname` is `srcdoc` or a UUID, so a client that read a route off\n  // it would report one no program could ever match, and nothing would say so.\n  const addressed = () => location.protocol === \"http:\" || location.protocol === \"https:\";\n\n  // Where this document is, as a route. The application\'s root when there is no address bar.\n  const here = () => (addressed() ? location.pathname : \"/\");\n\n  const route = (go) => {\n    // A document with no URL cannot navigate: `pushState` has no address bar to move, and the\n    // links inside it are not this page\'s to intercept.\n    if (!addressed()) return;\n    document.addEventListener(\"click\", (event) => {\n      if (event.defaultPrevented || event.button !== 0) return;\n      if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;\n      const a = event.target.closest && event.target.closest(\"a[href]\");\n      // `target`, `download` and `rel=external` are three ways of writing \"not an in-page\n      // navigation\", and each is the author\'s statement rather than a guess about intent.\n      if (!a || a.target || a.hasAttribute(\"download\") || a.getAttribute(\"rel\") === \"external\") {\n        return;\n      }\n      const url = new URL(a.getAttribute(\"href\"), location.href);\n      if (url.origin !== location.origin) return;\n      if (url.pathname === location.pathname && url.hash) return; // an anchor on this page\n      event.preventDefault();\n      if (url.pathname === location.pathname && url.search === location.search) return;\n      history.pushState(null, \"\", url.pathname + url.search + url.hash);\n      go(location.pathname);\n    });\n    // Back and forward. The address bar has already moved by the time this fires, so the route is\n    // read off `location` rather than carried in the history entry \u{2014} one source for where the\n    // client is, and it is the browser\'s.\n    window.addEventListener(\"popstate\", () => go(location.pathname));\n  };\n\n  // The transport, as a seam. A deployment opens a websocket to the origin it was served from;\n  // the playground hands the identical protocol a `MessageChannel` port to a worker in the same\n  // tab (docs/17 \u{a7}17.2). Everything above and below this function \u{2014} the frames, the resumption\n  // rule, the outbox \u{2014} is the same code either way, which is what makes a tab a *host* rather\n  // than a simulation.\n  //\n  // The contract, for whoever writes the third one: `dial(handlers)` returns `{send, ready}` and\n  // optionally `close`, and calls `handlers.open` *after* returning \u{2014} a transport that is ready\n  // the moment it is dialled has to defer, because `open` is where the `hello` frame is sent and\n  // it sends it through the object `dial` has not handed back yet. A transport without `close`\n  // is dropped rather than closed, so its connection lives until whatever owns it goes away.\n  //\n  // Where the mode\'s own artefacts come from, as a seam \u{2014} the kernel module and the component\'s\n  // bundle in Mode B. A deployment fetches them from the origin it was served from, on the two\n  // reserved routes `beck_rt::http` answers; the playground has no server behind the frame, so it\n  // hands over the bundle its worker derived and reads the kernel from the directory the page was\n  // deployed to (docs/98).\n  //\n  // The contract: `asset(name)` returns a promise of a `Response`, because that is what\n  // `WebAssembly.instantiateStreaming` takes and a synthetic one is a constructor call.\n  const asset = (name) => fetch(\"/\" + name);\n\n  const websocket = (handlers) => {\n    const url = (location.protocol === \"https:\" ? \"wss://\" : \"ws://\") + location.host + \"/socket\";\n    const socket = new WebSocket(url);\n    socket.onopen = handlers.open;\n    // The bytes are counted here rather than in `connect`, because they are a fact about *this*\n    // transport: a port transport hands over objects and never encodes one. A devtools panel\n    // reading a zero is therefore reading the truth about a playground tab rather than a bug.\n    socket.onmessage = (event) => {\n      stats.bytes_in += event.data.length;\n      handlers.message(JSON.parse(event.data));\n    };\n    socket.onclose = handlers.close;\n    socket.onerror = () => socket.close();\n    return {\n      send: (frame) => {\n        const text = JSON.stringify(frame);\n        stats.bytes_out += text.length;\n        socket.send(text);\n      },\n      ready: () => socket.readyState === 1,\n      close: () => socket.close(),\n    };\n  };\n\n  // One connection, resumable by `(subscription, seq)`. `on` is the frame handler the mode\n  // supplies.\n  //\n  // `state` is read at every open rather than at the first one, so a caller that keeps\n  // `state.seq` current resumes from where it actually is. A snapshot would make every reconnect\n  // ask for the gap since first paint and then apply it to a DOM that had already moved.\n  //\n  // A null `state.seq` is sent as an *absent* field, which is the protocol\'s \"I hold nothing\".\n  // Zero would mean \"I hold the frame as of zero\", which is true of a server-rendered document\n  // and false of a client that has only just started.\n  //\n  // `state.path` rides on the `hello` for a related reason: a route established by a second frame\n  // would leave every reconnection rendering the root\'s page until that frame arrived.\n  //\n  // The returned sender carries a `close`, because the reconnect below has to be stoppable by\n  // something other than the frame being destroyed. Nothing else holds the retry timer.\n  const connect = (state, on, opened) => {\n    let backoff = 250;\n    const outbox = [];\n    let link = null;\n    let retry = null;\n    let stopped = false;\n    const open = () => {\n      retry = null;\n      link = (beck.dial || websocket)({\n        open: () => {\n          backoff = 250;\n          stats.connected = true;\n          // The route rides on the `hello` for the reason above: a route established by a second\n          // frame would leave every reconnection rendering the root\'s page until it arrived.\n          const hello = {\n            t: \"hello\",\n            sub: state.sub,\n            actor: state.actor,\n            path: here(),\n          };\n          if (state.seq !== null && state.seq !== undefined) hello.seq = state.seq;\n          link.send(hello);\n          // Commands sent while disconnected are safe to repeat: each carries an id, and the\n          // server de-duplicates by it.\n          while (outbox.length) link.send(outbox.shift());\n          // Every open, not only the first: a client that has been away has a queue that predates\n          // this connection, and possibly this page load (`beck-mode-b.js`).\n          if (opened) opened();\n        },\n        message: on,\n        close: () => {\n          link = null;\n          stats.connected = false;\n          if (stopped) return;\n          retry = setTimeout(open, backoff);\n          backoff = Math.min(backoff * 2, 5000);\n        },\n      });\n    };\n    open();\n    const send = (frame) => {\n      // Counted here rather than in the transport, because a frame is a frame whichever one is\n      // under it. The *bytes* are not: they are counted in `websocket` below, since a port\n      // transport moves objects and has none.\n      stats.sent += 1;\n      if (link && link.ready()) link.send(frame);\n      else outbox.push(frame);\n    };\n    // Stop dialling. Both lines are load-bearing, for the two ways a client is closed: after the\n    // connection dropped there is a retry already armed, which is the `clearTimeout`; while it is\n    // still up the close travels through the transport and the handler above runs on the way out,\n    // which is the flag. A socket reports its own closing and cannot say whether it was asked for.\n    send.close = () => {\n      stopped = true;\n      clearTimeout(retry);\n      retry = null;\n      const closing = link;\n      link = null;\n      stats.connected = false;\n      if (closing && closing.close) closing.close();\n    };\n    return send;\n  };\n\n  // An event anybody can listen for, on any ancestor. `bubbles` because the natural place to\n  // listen is `document` \u{2014} a page showing \"reconnecting\u{2026}\" should not have to know which element\n  // the residue chose as its frame root.\n  const announce = (root, kind, detail) =>\n    root.dispatchEvent(new CustomEvent(kind, { detail, bubbles: true }));\n\n  // \"This component is live.\" `data-b-ready` is the mode\'s letter, so a stylesheet can hide a\n  // spinner with a selector and a script can wait for one attribute whichever mode it is in.\n  const ready = (root, mode) => {\n    if (root.dataset.bReady === mode) return;\n    root.dataset.bReady = mode;\n    announce(root, \"beck:ready\", { mode });\n  };\n\n  // What a devtools panel is allowed to know, and the only way it is allowed to know it: a mode\n  // registers what it can say about itself, and the panel reads. Nothing here computes a second\n  // account of anything.\n  const inspect = { stats, describe: () => ({}) };\n\n  // The panel is loaded on request, not on every page. `?devtools` turns it on and leaves the\n  // switch behind, so a reload \u{2014} and a route change \u{2014} keeps it; `?devtools=off` clears it.\n  const devtools = () => {\n    let want = null;\n    try {\n      const asked = new URL(location.href).searchParams.get(\"devtools\");\n      if (asked !== null) {\n        want = asked !== \"off\" && asked !== \"0\";\n        localStorage.setItem(\"beck:devtools\", want ? \"1\" : \"\");\n      } else {\n        want = localStorage.getItem(\"beck:devtools\") === \"1\";\n      }\n    } catch (e) {\n      want = false; // no `localStorage` in this context; the page is unaffected\n    }\n    if (!want) return;\n    const script = document.createElement(\"script\");\n    script.src = \"/beck-devtools.js\";\n    document.body.appendChild(script);\n  };\n\n  // `dial` is deliberately absent rather than null: a page that wants a different transport sets\n  // it on this object before the mode\'s script runs, and one that does not gets a websocket.\n  // `asset` is present and overridable for the same reason, and `shell` says whether this document\n  // may cache itself \u{2014} a frame with no origin of its own may not, and says so rather than failing a\n  // registration nobody reads.\n  window.beck = {\n    build, apply, uuid7, fill, capture, connect, announce, ready, route, here, stats, inspect,\n    devtools, asset, shell: true,\n  };\n})();\n";
Expand description

The patch interpreter and the socket, shared by both rendering modes (§5.1).

“Hand-written JavaScript never appears in the source — it’s compiler residue: the patch interpreter plus the compiled view. You stopped writing it the moment the page became a function.” These three files are that residue, and they hold no application logic.