diff --git a/lib/host/playwright/boost-nav.spec.js b/lib/host/playwright/boost-nav.spec.js new file mode 100644 index 00000000..59506e39 --- /dev/null +++ b/lib/host/playwright/boost-nav.spec.js @@ -0,0 +1,37 @@ +// Regression for the boosted-navigation link-rebinding bug (reported on blog.rose-ash.com): +// home --boosted nav--> a post --click "edit"--> lands on /tags (a HOME footer link), +// not //edit. After a boost swap, the swapped-in links carry a STALE binding from +// the previous page. Run by run-boost-nav-check.sh against an ephemeral host server +// (serve.sh seeds /compose-demo + the home footer's /tags link). +const { test, expect } = require('playwright/test'); + +const BASE = process.env.SX_TEST_URL || 'http://127.0.0.1:8914'; + +async function waitReady(page) { + await expect(page.locator('html[data-sx-ready="true"]')).toHaveCount(1, { timeout: 45000 }); +} + +test.describe('boosted navigation (browser-only)', () => { + test('a post link clicked AFTER a boosted nav navigates to the right target (not a stale home link)', async ({ page }) => { + test.setTimeout(90000); + // 1) load HOME (its footer has a /tags link — the stale target the bug lands on) + await page.goto(BASE + '/'); + await waitReady(page); + await expect(page.locator('a[href="/tags"]')).toHaveCount(1); // home has the /tags link + + // 2) boosted nav HOME -> the composed post (no full reload) + await page.locator('a[href="/compose-demo/"]').first().click(); + await expect(page.locator('body')).toContainText('composition object', { timeout: 15000 }); + expect(page.url()).toContain('/compose-demo/'); + + // 3) click the post's "edit" link — brought in by the swap + await expect(page.locator('a[href="/compose-demo/edit"]')).toHaveCount(1); + await page.locator('a[href="/compose-demo/edit"]').click(); + await page.waitForTimeout(3000); + + // 4) it MUST navigate to the edit route (guarded -> the login view is fine, the URL is + // pushed to /compose-demo/edit), and MUST NOT land on the stale /tags link. + expect(page.url()).not.toContain('/tags'); + expect(page.url()).toContain('/compose-demo/edit'); + }); +}); diff --git a/lib/host/playwright/run-boost-nav-check.sh b/lib/host/playwright/run-boost-nav-check.sh new file mode 100755 index 00000000..2b7b5c3d --- /dev/null +++ b/lib/host/playwright/run-boost-nav-check.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +# Regression harness for the boosted-nav link-rebinding bug (composition step polish). +# Spins up an EPHEMERAL host server (this worktree's binary + lib + web + WASM), which on +# boot seeds /compose-demo and the home footer's /tags link, runs boost-nav.spec.js in the +# main worktree's Playwright, then tears down. No live-site dependency. +# +# bash lib/host/playwright/run-boost-nav-check.sh +# +# Requires: the OCaml binary built + Playwright + chromium in /root/rose-ash. +set -uo pipefail +cd "$(git rev-parse --show-toplevel)" +ROOT=$(pwd) + +PORT="${BOOST_PORT:-8914}" +PW_DIR="${PW_DIR:-/root/rose-ash}" +USER="admin"; PASS="boost-check-pw"; SECRET="boost-check-secret" +PDIR=$(mktemp -d) +SPEC_SRC="lib/host/playwright/boost-nav.spec.js" +SPEC_DST="$PW_DIR/tests/playwright/_boost-nav-check.spec.js" +SERVE_LOG=$(mktemp) + +cleanup() { + [ -n "${SVPID:-}" ] && kill "$SVPID" 2>/dev/null + local pid + pid=$(ss -lptn "sport = :$PORT" 2>/dev/null | grep -oE 'pid=[0-9]+' | head -1 | cut -d= -f2) + [ -n "$pid" ] && kill "$pid" 2>/dev/null + rm -f "$SPEC_DST" "$SERVE_LOG"; rm -rf "$PDIR" +} +trap cleanup EXIT + +echo "== starting ephemeral host server on :$PORT (persist=$PDIR) ==" +SX_SERVING_JIT=1 HOST_PORT="$PORT" SX_PERSIST_DIR="$PDIR" \ + SX_ADMIN_USER="$USER" SX_ADMIN_PASSWORD="$PASS" SX_SESSION_SECRET="$SECRET" \ + bash lib/host/serve.sh >"$SERVE_LOG" 2>&1 & +SVPID=$! +for i in $(seq 1 60); do + curl -sf -o /dev/null "http://127.0.0.1:$PORT/health" 2>/dev/null && break + sleep 1 + [ "$i" = "60" ] && { echo "server never came up:"; cat "$SERVE_LOG"; exit 1; } +done +echo "== server up ==" + +echo "== running Playwright ==" +cp "$ROOT/$SPEC_SRC" "$SPEC_DST" +cd "$PW_DIR" +SX_TEST_URL="http://127.0.0.1:$PORT" \ + node_modules/.bin/playwright test _boost-nav-check.spec.js --workers=1 \ + --config tests/playwright/playwright.config.js +RC=$? +echo "== done (exit $RC) ==" +exit $RC diff --git a/shared/static/wasm/sx/boot-helpers.sx b/shared/static/wasm/sx/boot-helpers.sx index cc487e7c..a07b52df 100644 --- a/shared/static/wasm/sx/boot-helpers.sx +++ b/shared/static/wasm/sx/boot-helpers.sx @@ -611,7 +611,15 @@ (not (event-modifier-key? e)) (prevent-default e) (let - ((boost-el (dom-query "[sx-boost]")) + (;; Read the href FRESH from the element at click time. A morph swap + ;; (innerHTML) REUSES DOM nodes in place — an from the previous page + ;; can be re-purposed as a different link, its href rewritten but this + ;; click closure (and its is-processed? mark, so boost-descendants skips + ;; re-binding) left intact. Capturing href in the closure then navigated + ;; to the STALE target (e.g. home's /tags for a swapped-in "edit" link). + ;; Reading the live attribute makes a reused node follow its CURRENT href. + (href (or (dom-get-attr link "href") href)) + (boost-el (dom-query "[sx-boost]")) (target-sel (if boost-el diff --git a/shared/static/wasm/sx/boot-helpers.sxbc b/shared/static/wasm/sx/boot-helpers.sxbc index 28d8294a..a9575314 100644 --- a/shared/static/wasm/sx/boot-helpers.sxbc +++ b/shared/static/wasm/sx/boot-helpers.sxbc @@ -1,3 +1,3 @@ -(sxbc 1 "01e6c0bf4543d533" +(sxbc 1 "a8ea654eb7077c41" (code - :constants ("_sx-bound-prefix" "_sxBound" "mark-processed!" {:upvalue-count 0 :arity 2 :constants ("_sx-bound-prefix" "str" "host-set!") :bytecode (16 0 20 0 0 16 1 52 1 0 2 3 52 2 0 3 50)} "is-processed?" {:upvalue-count 0 :arity 2 :constants ("_sx-bound-prefix" "str" "host-get") :bytecode (16 0 20 0 0 16 1 52 1 0 2 52 2 0 2 17 2 16 2 33 4 0 3 32 1 0 4 50)} "clear-processed!" {:upvalue-count 0 :arity 2 :constants ("_sx-bound-prefix" "str" "host-set!") :bytecode (16 0 20 0 0 16 1 52 1 0 2 2 52 2 0 3 50)} "callable?" {:upvalue-count 0 :arity 1 :constants ("type-of" "lambda" "function" "continuation") :bytecode (16 0 52 0 0 1 17 1 16 1 1 1 0 164 6 34 18 0 5 16 1 1 2 0 164 6 34 7 0 5 16 1 1 3 0 164 50)} "to-kebab" {:upvalue-count 0 :arity 1 :constants ("Convert camelCase to kebab-case." "list" 0 {:upvalue-count 3 :arity 1 :constants ("nth" "A" ">=" "Z" "<=" 0 "-" "append!" "lower" 1) :bytecode (16 0 18 0 168 165 33 96 0 18 0 16 0 52 0 0 2 17 1 16 1 1 1 0 52 2 0 2 6 33 10 0 5 16 1 1 3 0 52 4 0 2 33 38 0 16 0 1 5 0 166 33 12 0 18 1 1 6 0 52 7 0 2 32 1 0 2 5 18 1 16 1 52 8 0 1 52 7 0 2 32 8 0 18 1 16 1 52 7 0 2 5 18 2 16 0 1 9 0 160 49 1 32 1 0 2 50)} "" "join") :bytecode (1 0 0 5 52 1 0 0 17 1 1 2 0 17 2 2 17 3 51 3 0 1 0 1 1 1 3 17 3 16 3 1 2 0 48 1 5 1 4 0 16 1 52 5 0 2 50)} "sx-load-components" {:upvalue-count 0 :arity 1 :constants ("Parse and evaluate component definitions from text." 0 "sx-parse" {:upvalue-count 0 :arity 1 :constants ("cek-eval") :bytecode (20 0 0 16 0 49 1 50)} "for-each") :bytecode (1 0 0 5 16 0 6 33 8 0 5 16 0 168 1 1 0 166 33 21 0 20 2 0 16 0 48 1 17 1 51 3 0 16 1 52 4 0 2 32 1 0 2 50)} "call-expr" {:upvalue-count 0 :arity 2 :constants ("Parse and evaluate an SX expression string." "sx-parse" "empty?" "cek-eval") :bytecode (1 0 0 5 20 1 0 16 0 48 1 17 2 16 2 52 2 0 1 167 33 11 0 20 3 0 16 2 169 49 1 32 1 0 2 50)} "base-env" {:upvalue-count 0 :arity 0 :constants ("Return the current global environment." "global-env") :bytecode (1 0 0 5 20 1 0 49 0 50)} "get-render-env" {:upvalue-count 0 :arity 1 :constants ("Get the rendering environment (global env, optionally merged with extra)." "base-env" "nil?" "env-merge") :bytecode (1 0 0 5 20 1 0 48 0 17 1 16 0 6 33 8 0 5 16 0 52 2 0 1 167 33 11 0 16 1 16 0 52 3 0 2 32 2 0 16 1 50)} "merge-envs" {:upvalue-count 0 :arity 2 :constants ("Merge two environments." "env-merge" "global-env") :bytecode (1 0 0 5 16 0 6 33 3 0 5 16 1 33 11 0 16 0 16 1 52 1 0 2 32 19 0 16 0 6 34 13 0 5 16 1 6 34 6 0 5 20 2 0 49 0 50)} "sx-render-with-env" {:upvalue-count 0 :arity 2 :constants ("Parse SX source and render to DOM fragment." "document" "host-global" "createDocumentFragment" "host-call" "sx-parse" {:upvalue-count 2 :arity 1 :constants ("render-to-html" 0 "createElement" "template" "host-call" "innerHTML" "host-set!" "appendChild" "content" "host-get") :bytecode (20 0 0 16 0 48 1 17 1 16 1 6 33 8 0 5 16 1 168 1 1 0 166 33 47 0 18 0 1 2 0 1 3 0 52 4 0 3 17 2 16 2 1 5 0 16 1 52 6 0 3 5 18 1 1 7 0 16 2 1 8 0 52 9 0 2 52 4 0 3 32 1 0 2 50)} "for-each") :bytecode (1 0 0 5 1 1 0 52 2 0 1 17 2 16 2 1 3 0 52 4 0 2 17 3 20 5 0 16 0 48 1 17 4 51 6 0 1 2 1 3 16 4 52 7 0 2 5 16 3 50)} "parse-env-attr" {:upvalue-count 0 :arity 1 :constants ("Parse data-sx-env attribute (JSON key-value pairs).") :bytecode (1 0 0 5 2 50)} "store-env-attr" {:upvalue-count 0 :arity 3 :constants () :bytecode (2 50)} "resolve-mount-target" {:upvalue-count 0 :arity 1 :constants ("Resolve a CSS selector string to a DOM element." "string?" "dom-query") :bytecode (1 0 0 5 16 0 52 1 0 1 33 10 0 20 2 0 16 0 49 1 32 2 0 16 0 50)} "remove-head-element" {:upvalue-count 0 :arity 1 :constants ("Remove a element matching selector." "dom-query" "dom-remove") :bytecode (1 0 0 5 20 1 0 16 0 48 1 17 1 16 1 33 10 0 20 2 0 16 1 49 1 32 1 0 2 50)} "set-sx-comp-cookie" {:upvalue-count 0 :arity 1 :constants ("sx-components" "set-cookie") :bytecode (1 0 0 16 0 52 1 0 2 50)} "clear-sx-comp-cookie" {:upvalue-count 0 :arity 0 :constants ("sx-components" "" "set-cookie") :bytecode (1 0 0 1 1 0 52 2 0 2 50)} "log-parse-error" {:upvalue-count 0 :arity 3 :constants ("log-error" "Parse error in " ": " "str") :bytecode (20 0 0 1 1 0 16 0 1 2 0 16 2 52 3 0 4 49 1 50)} "loaded-component-names" {:upvalue-count 0 :arity 0 :constants ("dom-query-all" "dom-body" "script[data-components]" "list" {:upvalue-count 1 :arity 1 :constants ("dom-get-attr" "data-components" "" 0 {:upvalue-count 1 :arity 1 :constants ("trim" 0 "append!") :bytecode (16 0 52 0 0 1 168 1 1 0 166 33 15 0 18 0 16 0 52 0 0 1 52 2 0 2 32 1 0 2 50)} "," "split" "for-each") :bytecode (20 0 0 16 0 1 1 0 48 2 6 34 4 0 5 1 2 0 17 1 16 1 168 1 3 0 166 33 21 0 51 4 0 0 0 16 1 1 5 0 52 6 0 2 52 7 0 2 32 1 0 2 50)} "for-each") :bytecode (20 0 0 20 1 0 48 0 1 2 0 48 2 17 0 52 3 0 0 17 1 51 4 0 1 1 16 0 52 5 0 2 5 16 1 50)} "csrf-token" {:upvalue-count 0 :arity 0 :constants ("dom-query" "meta[name=\"csrf-token\"]" "dom-get-attr" "content") :bytecode (20 0 0 1 1 0 48 1 17 0 16 0 33 13 0 20 2 0 16 0 1 3 0 49 2 32 1 0 2 50)} "validate-for-request" {:upvalue-count 0 :arity 1 :constants () :bytecode (3 50)} "build-request-body" {:upvalue-count 0 :arity 3 :constants ("upper" "GET" "HEAD" "dom-tag-name" "" "FORM" "FormData" "host-new" "URLSearchParams" "toString" "host-call" "url" 0 "?" "contains?" "&" "str" "body" "content-type" "dict" "dom-get-attr" "enctype" "application/x-www-form-urlencoded" "multipart/form-data" {:upvalue-count 0 :arity 2 :constants ("dom-get-attr" "name" "" "value" "host-get" "assoc") :bytecode (20 0 0 16 1 1 1 0 48 2 17 2 16 2 6 33 8 0 5 16 2 1 2 0 164 167 33 28 0 16 0 16 2 16 1 1 3 0 52 4 0 2 6 34 4 0 5 1 2 0 52 5 0 3 32 2 0 16 0 50)} "dom-query-all" "input, textarea, select" "reduce" "serialize" "text/sx; charset=utf-8") :bytecode (16 1 52 0 0 1 17 3 16 3 1 1 0 164 6 34 7 0 5 16 3 1 2 0 164 33 155 0 16 0 6 33 24 0 5 20 3 0 16 0 48 1 6 34 4 0 5 1 4 0 52 0 0 1 1 5 0 164 33 102 0 1 6 0 16 0 52 7 0 2 17 4 1 8 0 16 4 52 7 0 2 17 5 16 5 1 9 0 52 10 0 2 17 6 1 11 0 16 6 6 33 8 0 5 16 6 168 1 12 0 166 33 32 0 16 2 16 2 1 13 0 52 14 0 2 33 6 0 1 15 0 32 3 0 1 13 0 16 6 52 16 0 3 32 2 0 16 2 1 17 0 2 1 18 0 2 52 19 0 6 32 17 0 1 11 0 16 2 1 17 0 2 1 18 0 2 52 19 0 6 32 161 0 16 0 6 33 24 0 5 20 3 0 16 0 48 1 6 34 4 0 5 1 4 0 52 0 0 1 1 5 0 164 33 111 0 20 20 0 16 0 1 21 0 48 2 6 34 4 0 5 1 22 0 17 4 16 4 1 23 0 164 33 32 0 1 6 0 16 0 52 7 0 2 17 5 1 11 0 16 2 1 17 0 16 5 1 18 0 2 52 19 0 6 32 47 0 51 24 0 52 19 0 0 20 25 0 16 0 1 26 0 48 2 52 27 0 3 17 5 1 11 0 16 2 1 17 0 16 5 52 28 0 1 1 18 0 1 29 0 52 19 0 6 32 17 0 1 11 0 16 2 1 17 0 2 1 18 0 2 52 19 0 6 50)} "abort-previous-target" {:upvalue-count 0 :arity 1 :constants () :bytecode (2 50)} "abort-previous" "track-controller" {:upvalue-count 0 :arity 2 :constants () :bytecode (2 50)} "track-controller-target" "new-abort-controller" {:upvalue-count 0 :arity 0 :constants ("AbortController" "host-new") :bytecode (1 0 0 52 1 0 1 50)} "abort-signal" {:upvalue-count 0 :arity 1 :constants ("signal" "host-get") :bytecode (16 0 1 0 0 52 1 0 2 50)} "apply-optimistic" "revert-optimistic" "dom-has-attr?" {:upvalue-count 0 :arity 2 :constants ("hasAttribute" "host-call") :bytecode (16 0 1 0 0 16 1 52 1 0 3 50)} "show-indicator" {:upvalue-count 0 :arity 1 :constants ("dom-get-attr" "sx-indicator" "dom-query" "dom-remove-class" "hidden" "dom-add-class" "sx-indicator-visible") :bytecode (20 0 0 16 0 1 1 0 48 2 17 1 16 1 33 42 0 20 2 0 16 1 48 1 17 2 16 2 33 24 0 20 3 0 16 2 1 4 0 48 2 5 20 5 0 16 2 1 6 0 48 2 32 1 0 2 32 1 0 2 5 16 1 50)} "disable-elements" {:upvalue-count 0 :arity 1 :constants ("dom-get-attr" "sx-disabled-elt" "dom-query-all" "dom-body" {:upvalue-count 0 :arity 1 :constants ("dom-set-attr" "disabled" "") :bytecode (20 0 0 16 0 1 1 0 1 2 0 49 3 50)} "for-each" "list") :bytecode (20 0 0 16 0 1 1 0 48 2 17 1 16 1 33 29 0 20 2 0 20 3 0 48 0 16 1 48 2 17 2 51 4 0 16 2 52 5 0 2 5 16 2 32 4 0 52 6 0 0 50)} "clear-loading-state" {:upvalue-count 0 :arity 3 :constants ("dom-remove-class" "sx-request" "dom-remove-attr" "aria-busy" "dom-query" "dom-add-class" "hidden" "sx-indicator-visible" {:upvalue-count 0 :arity 1 :constants ("dom-remove-attr" "disabled") :bytecode (20 0 0 16 0 1 1 0 49 2 50)} "for-each") :bytecode (20 0 0 16 0 1 1 0 48 2 5 20 2 0 16 0 1 3 0 48 2 5 16 1 33 42 0 20 4 0 16 1 48 1 17 3 16 3 33 24 0 20 5 0 16 3 1 6 0 48 2 5 20 0 0 16 3 1 7 0 48 2 32 1 0 2 32 1 0 2 5 16 2 33 12 0 51 8 0 16 2 52 9 0 2 32 1 0 2 50)} "abort-error?" {:upvalue-count 0 :arity 1 :constants ("name" "host-get" "AbortError") :bytecode (16 0 1 0 0 52 1 0 2 1 2 0 164 50)} "promise-catch" {:upvalue-count 0 :arity 2 :constants ("host-callback" "catch" "host-call") :bytecode (16 1 52 0 0 1 17 2 16 0 1 1 0 16 2 52 2 0 3 50)} "fetch-request" {:upvalue-count 0 :arity 3 :constants ("url" "get" "method" "GET" "headers" "dict" "body" "signal" "preloaded" 200 {:upvalue-count 0 :arity 1 :constants () :bytecode (2 50)} "Headers" "host-new" "Object" {:upvalue-count 2 :arity 1 :constants ("set" "get" "host-call") :bytecode (18 0 1 0 0 16 0 18 1 16 0 52 1 0 2 52 2 0 4 50)} "keys" "for-each" "host-set!" "promise-then" "dom-window" "fetch" "host-call" {:upvalue-count 2 :arity 1 :constants ("ok" "host-get" "status" {:upvalue-count 1 :arity 1 :constants ("headers" "host-get" "get" "host-call") :bytecode (18 0 1 0 0 52 1 0 2 1 2 0 16 0 52 3 0 3 50)} "promise-then" "text" "host-call" {:upvalue-count 4 :arity 1 :constants () :bytecode (18 0 18 1 18 2 18 3 16 0 49 4 50)}) :bytecode (16 0 1 0 0 52 1 0 2 17 1 16 0 1 2 0 52 1 0 2 17 2 51 3 0 1 0 17 3 20 4 0 16 0 1 5 0 52 6 0 2 51 7 0 0 0 1 1 1 2 1 3 18 1 49 3 50)}) :bytecode (16 0 1 0 0 52 1 0 2 17 3 16 0 1 2 0 52 1 0 2 6 34 4 0 5 1 3 0 17 4 16 0 1 4 0 52 1 0 2 6 34 5 0 5 52 5 0 0 17 5 16 0 1 6 0 52 1 0 2 17 6 16 0 1 7 0 52 1 0 2 17 7 16 0 1 8 0 52 1 0 2 17 8 16 8 33 16 0 16 1 3 1 9 0 51 10 0 16 8 49 4 32 132 0 1 11 0 52 12 0 1 17 9 1 13 0 52 12 0 1 17 10 51 14 0 1 9 1 5 16 5 52 15 0 1 52 16 0 2 5 16 10 1 2 0 16 4 52 17 0 3 5 16 10 1 4 0 16 9 52 17 0 3 5 16 6 33 14 0 16 10 1 6 0 16 6 52 17 0 3 32 1 0 2 5 16 7 33 14 0 16 10 1 7 0 16 7 52 17 0 3 32 1 0 2 5 20 18 0 20 19 0 48 0 1 20 0 16 3 16 10 52 21 0 4 51 22 0 1 1 1 2 16 2 49 3 50)} "fetch-location" {:upvalue-count 0 :arity 1 :constants ("dom-query" "[sx-boost]" "#main-panel" "browser-navigate") :bytecode (20 0 0 1 1 0 48 1 6 34 9 0 5 20 0 0 1 2 0 48 1 17 1 16 1 33 10 0 20 3 0 16 0 49 1 32 1 0 2 50)} "fetch-and-restore" {:upvalue-count 0 :arity 4 :constants ("fetch-request" "url" "method" "GET" "headers" "body" "signal" "dict" {:upvalue-count 2 :arity 4 :constants ("content-type" "" "text/html" "contains?" "DOMParser" "host-new" "parseFromString" "host-call" "querySelector" "#sx-content" "dom-set-inner-html" "innerHTML" "host-get" "dom-create-element" "div" "sx-render" "dom-append" "process-oob-swaps" {:upvalue-count 0 :arity 3 :constants ("dispose-islands-in" "swap-dom-nodes" "innerHTML" "children-to-fragment" "post-swap") :bytecode (20 0 0 16 0 48 1 5 20 1 0 16 0 16 2 1 2 0 164 33 10 0 20 3 0 16 1 48 1 32 2 0 16 1 16 2 48 3 5 20 4 0 16 0 49 1 50)} "select-from-container" "dispose-islands-in" "dom-get-inner-html" "post-swap" "dom-window" "scrollTo" 0) :bytecode (16 0 33 252 0 16 2 1 0 0 48 1 6 34 4 0 5 1 1 0 17 4 16 4 1 2 0 52 3 0 2 33 75 0 1 4 0 52 5 0 1 17 5 16 5 1 6 0 16 3 1 2 0 52 7 0 4 17 6 16 6 1 8 0 1 9 0 52 7 0 3 17 7 16 7 33 19 0 20 10 0 18 0 16 7 1 11 0 52 12 0 2 48 2 32 9 0 20 10 0 18 0 16 3 48 2 32 119 0 20 13 0 1 14 0 48 1 17 5 20 15 0 16 3 48 1 17 6 16 6 33 94 0 20 16 0 16 5 16 6 48 2 5 20 17 0 16 5 51 18 0 48 2 5 20 19 0 16 5 1 9 0 48 2 17 7 16 7 33 31 0 20 20 0 18 0 48 1 5 20 10 0 18 0 1 1 0 48 2 5 20 16 0 18 0 16 7 48 2 32 22 0 20 20 0 18 0 48 1 5 20 10 0 18 0 20 21 0 16 5 48 1 48 2 32 1 0 2 5 20 22 0 18 0 48 1 5 20 23 0 48 0 1 24 0 1 25 0 18 1 52 7 0 4 32 1 0 2 50)} {:upvalue-count 0 :arity 1 :constants ("log-warn" "fetch-and-restore error: " "str") :bytecode (20 0 0 1 1 0 16 0 52 2 0 2 49 1 50)}) :bytecode (20 0 0 1 1 0 16 1 1 2 0 1 3 0 1 4 0 16 2 1 5 0 2 1 6 0 2 52 7 0 10 51 8 0 1 0 1 3 51 9 0 49 3 50)} "fetch-preload" {:upvalue-count 0 :arity 3 :constants ("fetch-request" "url" "method" "GET" "headers" "body" "signal" "dict" {:upvalue-count 2 :arity 4 :constants ("preload-cache-set") :bytecode (16 0 33 14 0 20 0 0 18 0 18 1 16 3 49 3 32 1 0 2 50)} {:upvalue-count 0 :arity 1 :constants () :bytecode (2 50)}) :bytecode (20 0 0 1 1 0 16 0 1 2 0 1 3 0 1 4 0 16 1 1 5 0 2 1 6 0 2 52 7 0 10 51 8 0 1 2 1 0 51 9 0 49 3 50)} "fetch-streaming" {:upvalue-count 0 :arity 4 :constants ("fetch-and-restore" 0) :bytecode (20 0 0 16 0 16 1 16 2 1 1 0 49 4 50)} "dom-parse-html-document" {:upvalue-count 0 :arity 1 :constants ("DOMParser" "host-new" "parseFromString" "text/html" "host-call") :bytecode (1 0 0 52 1 0 1 17 1 16 1 1 2 0 16 0 1 3 0 52 4 0 4 50)} "dom-body-inner-html" {:upvalue-count 0 :arity 1 :constants ("body" "host-get" "innerHTML") :bytecode (16 0 1 0 0 52 1 0 2 1 2 0 52 1 0 2 50)} "create-script-clone" {:upvalue-count 0 :arity 1 :constants ("document" "host-global" "createElement" "script" "host-call" "attributes" "host-get" {:upvalue-count 3 :arity 1 :constants ("length" "host-get" "item" "host-call" "setAttribute" "name" "value" 1) :bytecode (16 0 18 0 1 0 0 52 1 0 2 165 33 54 0 18 0 1 2 0 16 0 52 3 0 3 17 1 18 1 1 4 0 16 1 1 5 0 52 1 0 2 16 1 1 6 0 52 1 0 2 52 3 0 4 5 18 2 16 0 1 7 0 160 49 1 32 1 0 2 50)} 0 "textContent" "host-set!") :bytecode (1 0 0 52 1 0 1 17 1 16 1 1 2 0 1 3 0 52 4 0 3 17 2 16 0 1 5 0 52 6 0 2 17 3 2 17 4 51 7 0 1 3 1 2 1 4 17 4 16 4 1 8 0 48 1 5 16 2 1 9 0 16 0 1 9 0 52 6 0 2 52 10 0 3 5 16 2 50)} "cross-origin?" {:upvalue-count 0 :arity 1 :constants ("http://" "starts-with?" "https://" "browser-location-origin") :bytecode (16 0 1 0 0 52 1 0 2 6 34 10 0 5 16 0 1 2 0 52 1 0 2 33 15 0 16 0 20 3 0 48 0 52 1 0 2 167 32 1 0 4 50)} "browser-scroll-to" {:upvalue-count 0 :arity 2 :constants ("dom-window" "scrollTo" "host-call") :bytecode (20 0 0 48 0 1 1 0 16 0 16 1 52 2 0 4 50)} "with-transition" {:upvalue-count 0 :arity 2 :constants ("document" "host-global" "startViewTransition" "host-get" "host-callback" "host-call") :bytecode (16 0 6 33 15 0 5 1 0 0 52 1 0 1 1 2 0 52 3 0 2 33 23 0 1 0 0 52 1 0 1 1 2 0 16 1 52 4 0 1 52 5 0 3 32 4 0 16 1 49 0 50)} "event-source-connect" {:upvalue-count 0 :arity 2 :constants ("EventSource" "host-new" "_sxElement" "host-set!") :bytecode (1 0 0 16 0 52 1 0 2 17 2 16 2 1 2 0 16 1 52 3 0 3 5 16 2 50)} "event-source-listen" {:upvalue-count 0 :arity 3 :constants ("addEventListener" {:upvalue-count 1 :arity 1 :constants () :bytecode (18 0 16 0 49 1 50)} "host-callback" "host-call") :bytecode (16 0 1 0 0 16 1 51 1 0 1 2 52 2 0 1 52 3 0 4 50)} "bind-boost-link" {:upvalue-count 0 :arity 2 :constants ("dom-listen" "click" {:upvalue-count 2 :arity 1 :constants ("event-modifier-key?" "prevent-default" "dom-has-attr?" "sx-get" "dom-set-attr" "sx-push-url" "true" "execute-request") :bytecode (20 0 0 16 0 48 1 167 33 83 0 20 1 0 16 0 48 1 5 20 2 0 18 0 1 3 0 48 2 167 33 15 0 20 4 0 18 0 1 3 0 18 1 48 3 32 1 0 2 5 20 2 0 18 0 1 5 0 48 2 167 33 16 0 20 4 0 18 0 1 5 0 1 6 0 48 3 32 1 0 2 5 20 7 0 18 0 2 2 49 3 32 1 0 2 50)}) :bytecode (20 0 0 16 0 1 1 0 51 2 0 1 0 1 1 49 3 50)} "bind-boost-form" {:upvalue-count 0 :arity 3 :constants ("dom-listen" "submit" {:upvalue-count 3 :arity 1 :constants ("prevent-default" "execute-request" "method" "url" "dict") :bytecode (20 0 0 16 0 48 1 5 20 1 0 18 0 1 2 0 18 1 1 3 0 18 2 52 4 0 4 2 49 3 50)}) :bytecode (20 0 0 16 0 1 1 0 51 2 0 1 0 1 1 1 2 49 3 50)} "bind-client-route-click" {:upvalue-count 0 :arity 3 :constants ("dom-listen" "click" {:upvalue-count 2 :arity 1 :constants ("event-modifier-key?" "prevent-default" "dom-query" "[sx-boost]" "dom-get-attr" "sx-boost" "true" "#sx-content" "try-client-route" "url-pathname" "save-scroll-position" "browser-push-state" "" "browser-scroll-to" 0 "log-info" "sx:route server fetch " "str" "dom-set-attr" "sx-get" "sx-target" "sx-select" "sx-push-url" "execute-request") :bytecode (20 0 0 16 0 48 1 167 33 197 0 20 1 0 16 0 48 1 5 20 2 0 1 3 0 48 1 17 1 16 1 33 40 0 20 4 0 16 1 1 5 0 48 2 17 2 16 2 6 33 8 0 5 16 2 1 6 0 164 167 33 5 0 16 2 32 3 0 1 7 0 32 3 0 1 7 0 17 2 20 8 0 20 9 0 18 0 48 1 16 2 48 2 33 32 0 20 10 0 48 0 5 20 11 0 2 1 12 0 18 0 48 3 5 20 13 0 1 14 0 1 14 0 49 2 32 77 0 20 15 0 1 16 0 18 0 52 17 0 2 48 1 5 20 18 0 18 1 1 19 0 18 0 48 3 5 20 18 0 18 1 1 20 0 16 2 48 3 5 20 18 0 18 1 1 21 0 16 2 48 3 5 20 18 0 18 1 1 22 0 1 6 0 48 3 5 20 23 0 18 1 2 2 49 3 32 1 0 2 50)}) :bytecode (20 0 0 16 0 1 1 0 51 2 0 1 1 1 0 49 3 50)} "sw-post-message" "try-parse-json" {:upvalue-count 0 :arity 1 :constants ("json-parse") :bytecode (20 0 0 16 0 49 1 50)} "strip-component-scripts" {:upvalue-count 0 :arity 1 :constants ("