diff --git a/.agents/skills/jaws/SKILL.md b/.agents/skills/jaws/SKILL.md index 19481c4b..2ec4728d 100644 --- a/.agents/skills/jaws/SKILL.md +++ b/.agents/skills/jaws/SKILL.md @@ -12,6 +12,13 @@ Apply these rules whenever work involves any of the following: - Go templates rendered through `ui.Template` / `$.Template` - Event handling, dirtying, tag identity, or dynamic container updates +## Version-matched source guidance + +When the matching JaWS source is available, read its root `AI.md` and the +`AI.md` beside each package you change. Do not substitute guidance from another +version. This installed skill remains self-contained when source guides are not +available. + ## Primary objective Keep browser behavior thin and deterministic while preserving server-side truth, stable identity, and predictable rerenders. @@ -65,9 +72,12 @@ JaWS is an immediate-mode, server-driven UI framework, not an MVC framework. child must render one addressable direct DOM node carrying its Element's JaWS ID so removal and ordering can target it; `ui.NewTemplate` provides that node through its generated wrapper. -- Treat the package documentation shown by - `go doc github.com/linkdata/jaws/lib/ui` as the canonical standard-widget - multiplicity summary, and consult each concrete type's docs for its conditions. +- Treat the same-version `lib/ui/AI.md` as the canonical standard-widget + multiplicity summary when source guidance is available. Otherwise use + `go doc github.com/linkdata/jaws/lib/ui` for the package-wide default and + standard-widget summary, then inspect each concrete widget (for example, + `go doc github.com/linkdata/jaws/lib/ui Container`) for its documented + multiplicity and conditions. ## Constructing UI values: `ui.New` and `bind.New` diff --git a/AI.md b/AI.md new file mode 100644 index 00000000..0b9e1168 --- /dev/null +++ b/AI.md @@ -0,0 +1,458 @@ +# AI guidance for github.com/linkdata/jaws + +[Back to the human overview and quick start](./README.md). + +This is the version-matched implementation guide for the root `jaws` package. +Use exported symbol documentation as the authority for supported caller-facing +behavior. Use this file for package-wide ownership, lifecycle, security, and +maintenance invariants, and follow the package-local guide when work belongs to +a subpackage. + +## Package guide index + +The module contains 16 Go packages, each with one package-local guide: + +* [`github.com/linkdata/jaws`](./AI.md) -- core requests, sessions, serving, + routing, security, and lifecycle. +* [`github.com/linkdata/jaws/examples`](./examples/AI.md) -- canonical setup and + compile-checked examples. +* [`github.com/linkdata/jaws/examples/minesweeper`](./examples/minesweeper/AI.md) + -- example domain state and targeted dirtying. +* [`github.com/linkdata/jaws/jawsboot`](./jawsboot/AI.md) -- embedded Bootstrap + assets and version updates. +* [`github.com/linkdata/jaws/jawstest`](./jawstest/AI.md) -- isolated request + harnesses. +* [`github.com/linkdata/jaws/lib/assets`](./lib/assets/AI.md) -- bundled browser + resources and the DOM trust boundary. +* [`github.com/linkdata/jaws/lib/bind`](./lib/bind/AI.md) -- binders, adapters, + conversion, locking, and setter targets. +* [`github.com/linkdata/jaws/lib/htmlio`](./lib/htmlio/AI.md) -- low-level trusted + HTML emission. +* [`github.com/linkdata/jaws/lib/jid`](./lib/jid/AI.md) -- request-scoped element + identifiers. +* [`github.com/linkdata/jaws/lib/key`](./lib/key/AI.md) -- request-key encoding + and parsing. +* [`github.com/linkdata/jaws/lib/named`](./lib/named/AI.md) -- named inputs and + single-select collections. +* [`github.com/linkdata/jaws/lib/tag`](./lib/tag/AI.md) -- tag expansion, + registration, targeting, and rendering. +* [`github.com/linkdata/jaws/lib/templatereloader`](./lib/templatereloader/AI.md) + -- debug template reloading and last-good retention. +* [`github.com/linkdata/jaws/lib/ui`](./lib/ui/AI.md) -- templates, containers, + inputs, browser interaction, and standard widgets. +* [`github.com/linkdata/jaws/lib/what`](./lib/what/AI.md) -- command and event + meanings. +* [`github.com/linkdata/jaws/lib/wire`](./lib/wire/AI.md) -- WebSocket record + framing and transport limits. + +## Module conventions + +Throughout this module, nil is unsupported for pointer receivers and required +operational collaborators such as callbacks, handlers, providers, lockers, +writers, file systems, contexts, and pointers to mutable values unless the API +documents a meaning for nil. Unsupported nil use is caller error and may panic +immediately or when the value is used. Nil slices, maps, data values, and results +otherwise follow ordinary Go semantics and the relevant API. An interface that +contains a typed nil is non-nil; its behavior follows its concrete type and the +API receiving it. + +Preserve documented zero-value behavior. In particular, the zero `Jaws` value is +not ready for use; construct it with `jaws.New`. Do not add repetitive non-nil +preconditions to individual symbols when the module convention already applies. + +## UI and Element ownership + +Every non-nil `UI` value must be comparable at runtime and equal to itself. A +value with an interface field holding a slice, map, or function can be statically +comparable while failing at runtime; a value containing `NaN` is comparable but +not reflexive. Container widgets cancel the `Request` when a child violates this +rule, and debug builds assert it in `Request.NewElement`. Keep slice-, map-, and +function-bearing application state behind stable pointers. + +UI values and Elements follow these ownership rules: + +* Construct fresh UI values for every Request. A UI value may refer to shared + application state, binders, handlers, and tags when those values are + synchronized as required. +* Within one Request, a UI value normally backs one live Element. Reuse it for + multiple live Elements only when the concrete type documents that support and + does not retain shared Element-specific state. +* A nil `UI` interface passed to `Request.NewElement` is a render/update no-op. + A typed nil is dispatched normally, so nil-receiver behavior belongs to the + concrete type's contract. +* An Element belongs to its Request for its entire lifetime. Render-scoped + widgets may retain child Elements between render and update calls, but neither + Requests nor Elements belong in application state or background goroutines. +* A registered Element is the unit that receives events and DOM updates. Each + dynamically managed child must render one addressable direct DOM node carrying + its Jid-based HTML ID. + +Handler registration finishes before event dispatch. `Element.AddHandlers`, +`ApplyParams`, and `ApplyGetter` are render/registration operations. Once +`Element.JawsRender` returns or `Element.Freeze` runs, later handler additions are +dropped and debug builds panic. Incoming events try attached handlers in reverse +registration order and then the Element's UI value; a handler returns +`ErrEventUnhandled` to continue dispatch. + +Element update methods queue browser changes during render or update processing. +Pass an Element to `Dirty` to schedule that exact control on its owning Request. +Pass a dependency tag when every registered Element for application state must +update. Both `Request.Dirty` and `Jaws.Dirty` expand through the same global +dispatcher; ordinary tags are not restricted to the Request on which +`Request.Dirty` was called. See the [`tag` guide](./lib/tag/AI.md) for selection +and registration rules and the [`ui` guide](./lib/ui/AI.md) for widget-specific +identity and multiplicity. + +## HTTP and WebSocket flow + +The normal page flow has two related HTTP requests: + +1. A page handler creates a Request with `Jaws.NewRequest`. `HeadHTML` normally + emits the configured resources and request-key metadata. `TailHTML` is + optional; placing it before `` applies queued initial updates before + the WebSocket connects and can reduce flicker. +2. The bundled script connects to `/jaws/`. `Jaws.ServeHTTP` decodes the + key, claims the pending Request through `UseRequest`, upgrades the connection, + and begins event and DOM-update processing. + +Applications that emit equivalent resources and metadata need not call +`HeadHTML` or `TailHTML`. Custom routers may parse the trailing key with +`key.Parse`, call `UseRequest`, return 404 when it is absent, and then call the +claimed Request's `ServeHTTP` method. + +## Request lifecycle and serving + +Every `NewRequest` returns a distinct `*Request` identity. Requests are never +pooled or reused; only internal buffers are pooled. While the Jaws instance is +open, a new Request is pending and owned by that instance. `UseRequest` is the +only operation that claims it for a WebSocket, and claiming removes it from the +pending set. + +A claimed Request finishes after WebSocket processing exits. Its context is +canceled, pooled buffers are released, live registries are cleared, and its key +remains reserved while the Request is reachable. Completion leaves lock-free +Element fields and the ID counter intact. If an early callback claims and tears +down a Request while its initial renderer is still running, already-rendered +elements and tags may be forgotten, but the renderer remains race-safe and IDs +are not reused or duplicated. + +Maintenance or the per-IP cap may instead retire a non-running Request. Its +context is canceled, its key becomes unclaimable, and it leaves `Pending` and +`RequestCount`, while its identity, Elements, and buffers remain available to an +initial HTTP handler that still holds it. A Request created after `Jaws.Close` is +never registered or claimable and installs no key tombstone. + +Request timeout behavior is deliberately bounded: + +* `ServeWithTimeout` requires an exact multiple of one second from one second + through 2,147,483,646 seconds. Other values have unspecified behavior. +* Before WebSocket serving begins, retirement is periodic and approximate rather + than a hard deadline. Request creation, successful claim, and page writes mark + activity with whole-second samples; maintenance passes decide retirement. +* On an active WebSocket, the timeout bounds each keepalive ping and outbound + write. + +Event targets are fixed when an event is accepted. A target removed after +acceptance may still receive that event; a target removed before acceptance does +not. A handler can therefore receive a deleted Element, whose render, update, +and queue helpers remain no-ops for the rest of the Request. + +Dirtying is two-stage: calls expand and record selectors on the Jaws instance, +then the serving loop distributes ordinary tags across live Requests and exact +Elements only to their owners. Broadcasts, session reload/close helpers, and +dirty updates share the serving loop. Start `Serve` or `ServeWithTimeout` before +using them. + +### Calls before Serve + +The following operations are safe before the processing loop starts: + +* Construction and lifecycle: `New`, `Close`, and `Done`. +* Configuration and templates: `AddTemplateLookuper`, + `RemoveTemplateLookuper`, `LookupTemplate`, `GenerateHeadHTML`, `Setup`, and + `FaviconURL`. +* Inspection and logging: `RequestCount`, `RequestCounts`, `Pending`, + `SessionCount`, `Sessions`, `Log`, and `MustLog`. +* Static and ping endpoints through `ServeHTTP`: `/jaws/.ping`, the hashed + built-in JavaScript URL, and the hashed built-in stylesheet URL. + +`Broadcast`, `Session.Broadcast`, `Session.Reload`, and `Session.Close` may block +before the processing loop starts. + +### Keepalive pings + +JaWS pings read-idle WebSockets to detect peers that disappear without a close +handshake. Incoming data and successful pings restart the idle interval; time +spent parsing or delivering data already read does not count toward it. +`WebSocketPingInterval` defaults to `DefaultWebSocketPingInterval` and must be +positive. A non-positive value does not disable probing. + +## Context and cancellation + +The Request stores a context because page creation and the later WebSocket +callback do not share an unbroken call chain. `Request.SetContext` transforms +the current Request context. Return a context derived from the callback's parent +so deadlines and cancellation continue to propagate. If it is canceled, an idle +WebSocket loop wakes without waiting for a browser event or broadcast. + +Background work that must cancel a Request retains its own derived context and +cancellation function, not the Request pointer: + +```go +var workCtx context.Context +var cancel context.CancelCauseFunc +rq.SetContext(func(parent context.Context) context.Context { + workCtx, cancel = context.WithCancelCause(parent) + return workCtx +}) +go func() { + if err := run(workCtx); err != nil { + cancel(err) + } +}() +``` + +`Jaws.BaseContext` and a context installed by `Request.SetContext` own their +cancellation causes. If an independent cancellation or deadline wins, the +Request context exposes that cause directly; JaWS does not wrap it in +`ErrRequestCancelled`. Use an application sentinel when callers need to classify +background failure with `errors.Is`. `ErrRequestCancelled` identifies a non-nil +cause supplied when JaWS cancels a Request. + +A custom base or Request context may implement the optional +`AfterFunc(func()) func() bool` method recognized by `context.AfterFunc`. Its +registration and stop hooks must return promptly and must not synchronously call +the same Jaws or Request, or wait for work that does. Standard-library contexts +need no special handling. An interface-only `struct{ context.Context }` wrapper +can hide optional methods. + +## Sessions + +Sessions are server-side, non-persistent, expiring, and bound to the client IP +seen by JaWS. The browser stores only the random session cookie. Use one of these +creation patterns: + +* Wrap a page handler with `Jaws.SessionMiddleware`. +* Call `Jaws.NewSession` explicitly and attach the new cookie. +* Enable `Jaws.AutoSession` to create an anonymous session during a successful + WebSocket upgrade when the Request has none. + +Create or retrieve the session before `NewRequest` when initial rendering or +authentication depends on it. Later Requests with the same valid cookie and IP +can access the Session. `Request.Get` returns nil and `Request.Set` is a no-op +when no Session exists. `Jaws.Close` invalidates every Session, clears its data, +and prevents new Session creation. + +Loopback addresses are treated as the same client so a loopback reverse proxy +does not break binding. If all traffic reaches JaWS from loopback, binding is +effectively disabled unless trusted forwarding is configured behind a single +controlled proxy. `CookieName` must be a valid non-empty HTTP cookie name; its +default derives from the executable and falls back to `jaws`. + +## Configuration and logging + +Set all exported `Jaws` configuration fields immediately after `New` and before +exposing handlers, creating Requests, or starting a serve loop. They are ordinary +fields, not synchronized live settings. If `Debug` or the resource list changes, +call `GenerateHeadHTML` before rendering more pages. + +`GenerateHeadHTML` emits common JavaScript and CSS URLs, recognizes image and +font resources, and passes parsed URLs to automatic Content-Security-Policy +inference. A configured logger warns once for resources omitted from markup or +for absolute and scheme-relative URLs whose policy source cannot be represented. +Warnings redact passwords and omit queries and fragments. Resource URLs are +trusted configuration: scripts are executable and their origins affect policy. +Use an explicit `secureheaders` policy when automatic destination inference is +not appropriate. + +Configure `Jaws.Logger` for long-running applications. Initial render failures +return to the caller, but update-time paths cannot return errors to browser event +handlers and report them through `MustLog`, which panics without a logger. +`Jaws.Log` and configured `MustLog` calls enqueue `Logger.Error` callbacks for +serial asynchronous delivery. Callback panics are contained. `Close` stops +accepting new log entries and lets accepted entries drain without waiting; +`Serve` and `ServeWithTimeout` wait for the drain before returning normally. A +blocked logger callback delays later entries and the final drain. + +## Routing + +Register `Jaws.ServeHTTP` for the `/jaws/` prefix. It owns these routes: + +* `/jaws/.jaws..css` -- built-in stylesheet; cache indefinitely. +* `/jaws/.jaws..js` -- built-in client; cache indefinitely. +* `/jaws/` and `/jaws//noscript` -- single-use Request callback. The + key must parse to a nonzero value through `key.Parse`; parsing is + case-insensitive, while generated URLs use canonical lowercase base 32. A + missing Request is a 404. See the [`key` guide](./lib/key/AI.md). +* `/jaws/.tail/` -- deferred initial-update script emitted by `TailHTML`; + do not cache. +* `/jaws/.ping` -- readiness probe used before WebSocket reconnect attempts. + Return 204 while ready and 503 without a live Jaws instance; do not cache. + +The standard library setup is: + +```go +jw, err := jaws.New() +if err != nil { + panic(err) +} +defer jw.Close() +go jw.Serve() +http.DefaultServeMux.Handle("GET /jaws/", jw) +``` + +JaWS does not require a particular router; other routers must preserve the same +path prefix, status codes, caching behavior, and single-use request claim. + +## Security + +### Response headers + +`Jaws.SecureHeadersMiddleware` applies the +[`secureheaders.DefaultHeaders`](https://pkg.go.dev/github.com/linkdata/secureheaders#DefaultHeaders) +baseline and replaces its Content-Security-Policy with +`Jaws.ContentSecurityPolicy`. It does not itself trust forwarded HTTPS headers. + +```go +page := ui.Handler(jw, "index", bind.New(&mu, &value)) +http.DefaultServeMux.Handle("GET /", jw.SecureHeadersMiddleware(page)) +``` + +When a resource needs an explicit CSP destination, build a custom +`secureheaders.Middleware` policy that includes every external resource loaded +by the page. Leave a context-mismatched resource out of `GenerateHeadHTML` and +load it manually with its explicit destination. + +### WebSocket callback keys + +Each open Jaws instance assigns a pending Request a non-zero random 64-bit key +that is not currently in use. A key can claim its Request once. Keys for +registered or still-reachable retired Requests are not reassigned; reuse after a +retired Request becomes unreachable has no timing guarantee. + +Unclaimed Requests retire periodically, after 10 seconds by default. JaWS also +limits them per client IP. `MaxPendingRequestsPerIP` defaults to 100; a +non-positive value disables the cap. At the cap, a new Request evicts the oldest +idle pending Request for that IP, or the least recently written one when all are +fresh. The evicted key cannot be claimed. + +Guessing a uniformly random pending key takes about 2^63 distinct guesses on +average, and the attacker must succeed before the real browser claims the key or +the Request retires. + +WebSocket upgrades keep the single-use key, client-IP binding, and Origin host +and scheme checks together. Do not weaken one while changing another. + +### Trusted proxy headers + +Enable `TrustForwardedHeaders` only behind one controlled reverse proxy. The +proxy must remove client-supplied forwarding headers and set the client IP and +scheme itself. JaWS uses `X-Forwarded-For` and `X-Real-IP` for binding. Scheme +resolution recognizes `X-Forwarded-Proto`, `X-Forwarded-Ssl`, `Front-End-Https`, +and `Forwarded`, so the proxy must sanitize all of them. Without trusted scheme +forwarding, a TLS-terminating proxy that talks plain HTTP to JaWS causes +HTTPS-page WebSocket upgrades to fail with `ErrWebsocketOriginWrongScheme`. + +### Authorization + +Templates rendered through `ui.With` receive an `Auth` value. `Jaws.MakeAuth` +provides it. A nil `MakeAuth` installs the built-in fail-open `DefaultAuth`, whose +`IsAdmin` method returns true for every visitor. Set `MakeAuth` whenever template +output depends on authorization. + +With a logger configured, JaWS warns once when a template first evaluates +`.Auth.IsAdmin` while `MakeAuth` is nil. The warning is lazy: its absence does +not prove authorization is configured. + +## Locking and maintainer invariants + +When changing request, session, broadcast, or WebSocket code, preserve these +cross-file invariants: + +* Core lock order is `Jaws.mu -> Request.mu -> Session.mu`. + `Request.muQueue` and most Element, widget, and application-value locks remain + leaves. +* Container reconciliation's state-before-Request edge is confined to matching + and Element creation. Core-lock holders never call container render or update + methods. +* Request identities are never reused. Completion releases queued dirt, + Elements, tags, messages, and Session attachment, unregisters the identity, + and reserves the key while the Request remains reachable. +* Completion clears live lengths and relies on shrink paths to zero vacated + entries. It must not mutate render-visible Element state that a concurrent + initial renderer may read without a lock. Non-running retirement preserves + Elements and buffers for that renderer. +* Dirty inputs expand once. Ordinary keys target registered Elements and an + expanded `*Element` targets only itself on its owning Request. Neither path + may update a finished unregistered Request. +* Session grace windows remain deliberate for pending, claimed, + failed-upgrade, and closed-WebSocket Requests. +* Upgrade changes preserve single-use keys, IP binding, and fail-closed Origin + validation together. + +Review changes for correctness and performance, not just compilation. A merge +that selects individually valid files can still violate cross-file assumptions. + +## Production hardening + +Before exposing an application outside local development: + +* Configure `Jaws.Logger` so update-time failures are reported instead of + panicking through `MustLog`. +* Configure `Jaws.MakeAuth` whenever templates use authorization; the default is + fail-open. +* Treat plain strings passed to HTML-producing widgets as trusted HTML. Route + untrusted text through escaping conversion; see the [`bind`](./lib/bind/AI.md) + and [`ui`](./lib/ui/AI.md) guides. +* Define the browser-write policy for every `ui.JsVar`. Use `ui.PathSetter` for + path allow-lists or a `ClientCheck` for atomic generic-write validation, and + apply equivalent protection and the same lock to every binding that exposes + shared mutable state. See the [`ui` guide](./lib/ui/AI.md). +* Keep browser-to-server messages below the transport limit and use HTTP uploads + for large data. An oversized inbound message closes the Request connection + rather than rejecting only one value; see the [`wire` guide](./lib/wire/AI.md). +* Use `SecureHeadersMiddleware` or an equivalent explicit security-header + policy. +* Configure trusted forwarding only behind a proxy that sanitizes every + recognized forwarding header. +* Run both race/debug and production-build test legs before release. + +## Repository verification matrix + +Run commands from the module root. Start with focused tests for the changed +package, then use the complete gate for broader changes: + +```bash +go generate ./... +go vet ./... +gofmt -l . +staticcheck ./... +golangci-lint run +gosec ./... +go build ./... +JAWS_REQUIRE_NODE=1 go test -race ./... +JAWS_REQUIRE_NODE=1 go test ./... +``` + +Generation should leave the intended tracked files unchanged unless the change +deliberately updates generated assets. The race leg enables deadlock detection +and debug-gated checks. It also selects the detailed tag renderer; the plain +test leg exercises the crash-safe release renderer used in production. See the +[`tag` guide](./lib/tag/AI.md) for the build-mode details. + +`JAWS_REQUIRE_NODE=1` turns a missing Node runtime into a failure instead of +silently skipping the browser-client behavior tests. On a Linux host capable of +executing 386 binaries, also run the 32-bit numeric leg used by CI: + +```bash +GOARCH=386 CGO_ENABLED=0 go test ./lib/bind/... ./lib/ui/... +``` + +That leg exercises word-size-dependent `int` and `uint` bounds. On other hosts, +require the `build-386` CI job to pass. If the race detector is unavailable, run +`JAWS_REQUIRE_NODE=1 go test -tags "debug deadlock" ./...` plus +`JAWS_REQUIRE_NODE=1 go test ./...`. + +For performance work, commit a benchmark that exercises the changed path, use +`b.RunParallel` for contention changes or `b.ReportAllocs` for per-operation +work, and compare at least six runs of before and after results with `benchstat`. diff --git a/README.md b/README.md index f1b1211e..306f5cc0 100644 --- a/README.md +++ b/README.md @@ -7,39 +7,34 @@ JavaScript and WebSockets for creating responsive webpages. -JaWS embraces a "server holds the truth" philosophy and keeps the -complexity of modern browser applications on the backend. The -client-side script becomes a thin transport layer that faithfully -relays events and DOM updates. +JaWS embraces a "server holds the truth" philosophy and keeps the complexity +of modern browser applications on the backend. The client-side script becomes +a thin transport layer that faithfully relays events and DOM updates. ## Features * Moves web application state fully to the server. -* Keeps the browser intentionally dumb – no implicit trust in - JavaScript logic running on the client. -* Binds application data to UI elements using user-defined tags and - type-aware binders. -* Integrates with the standard library as well as third-party routers - such as Echo. -* Ships with a small standard library of UI widgets and helper types - that can be extended through interfaces. +* Keeps the browser intentionally dumb -- no implicit trust in JavaScript logic + running on the client. +* Binds application data to UI elements using user-defined tags and type-aware + binders. +* Integrates with the standard library as well as third-party routers such as + Echo. +* Ships with a small standard library of extensible UI widgets and helpers. -There is a [demo application](https://github.com/linkdata/jawsdemo) -with plenty of comments to use as a tutorial. +The [demo application](https://github.com/linkdata/jawsdemo) is a commented, +complete example. ## Installation -JaWS is distributed as a standard Go module. To add it to an existing -project use the `go get` command: +JaWS is distributed as a standard Go module: ```bash go get github.com/linkdata/jaws ``` -After the dependency is added, your Go module will be able to import -and use JaWS as demonstrated below. - -For widget authoring guidance see `lib/ui/README.md`. +For the standard widget APIs, see the +[`lib/ui` package documentation](https://pkg.go.dev/github.com/linkdata/jaws/lib/ui). ### AI skill @@ -47,6 +42,11 @@ This repository includes an AI skill under `.agents/skills/jaws/`. To install it in your local AI skills tree, copy both `SKILL.md` and `agents/openai.yaml` into `~/.agents/skills/jaws/`. +Copying from a JaWS checkout keeps the skill baseline matched to that source. +The commands below install the current development skill from `main`; when +versioned source is available, its adjacent `AI.md` guides are canonical for +version-specific behavior. + Using `curl`: ```bash @@ -59,10 +59,10 @@ curl -fsSL https://raw.githubusercontent.com/linkdata/jaws/main/.agents/skills/j ## Quick start -The following minimal program renders a single range input whose value -is kept on the server. Copy the snippet into a new module, run `go -mod tidy`, and start it with `go run .`. Visiting -http://localhost:8080/ demonstrates the full request lifecycle. +The following minimal program renders a single range input whose value stays +on the server. Copy the snippet into a new module, run `go mod tidy`, and start +it with `go run .`. Visiting demonstrates the full +request lifecycle. ```go package main @@ -112,926 +112,43 @@ func main() { } ``` -Next steps when building a real application typically include: - -1. Adding more templates and wiring them with `AddTemplateLookuper`. -2. Creating types that implement `JawsRender` and `JawsUpdate` so they - can be used as widgets. -3. Introducing sessions (see below) to keep track of user state. - -### Creating HTML entities - -When JawsRender() is called for a UI object, it can call -NewElement() to create new Elements while writing their initial -HTML code to the web page. Each Element is a unique instance -of a UI object bound to a specific Request, and will have a -unique Jid-based HTML id such as `Jid.7`. - -UI objects are request-scoped: construct fresh UI values for each Request and -never reuse one UI value across Requests. The application state, binders, -handlers and tags referenced by UI values may be shared when synchronized as -required. The `ui.RequestWriter` helpers construct fresh widgets while rendering. - -Within a Request, a UI value normally backs one live Element. It may back -multiple live Elements only when its concrete type documents that support. To -show the same application state in several places, construct distinct widgets -that share the synchronized binder, getter, handler or tag. - -If an HTML entity is not registered in a Request, JaWS will not -forward events from it, nor perform DOM manipulations for it. - -Dynamic updates of HTML entities are queued with the methods on `Element` when -`JawsUpdate` is called. To reconcile browser-local state after an event, pass the -Element itself to `Dirty`; this schedules only that Element on its owning Request. -Dirty a shared application tag instead when every dependent Element must update. - -### JavaScript events - -Supported JavaScript events are sent to the server and -are first offered to any extra objects added to the Element, in -reverse registration order (last added first). If none handle the event, the Element's UI type is -invoked. If none handle the event, it is ignored. - -The bundled client sends input, set, click, and context-menu messages only while -its WebSocket is open and does not queue them for later delivery. - -Event handlers should return `ErrEventUnhandled` if they didn't -handle the event or want to pass it to the next handler. - -* `onclick` invokes `JawsClick` for non-input-origin events - (`val` as `xykeystatename`) -* `oncontextmenu` invokes `JawsContextMenu` for non-input-origin events - (`val` as `xykeystatename`) -* `oninput` invokes `JawsInput`; editable `ui.Number` listens for `change` - instead and sends the same Input event -* `what.Set` events invoke `JawsInput` (`val` as `path=json`) - -Click and context-menu events whose target is an `input`, `select`, -`textarea` or `option` element, or inside one, are left to native input -handling and do not invoke ancestor click/context handlers. - -### JavaScript variables - -`ui.JsVar` binds a JSON-marshalable Go value to an application-owned variable -on the browser's `window`. It is intended for state that application JavaScript -must read or change and exchange with Go. Unlike most JaWS UI values, a `JsVar` -is a bidirectional channel: the binding does not by itself make either the Go -value or the browser value authoritative. - -Browser JSON numbers use JavaScript `Number` values. Signed and unsigned -integers outside `-9007199254740991` through `9007199254740991` may be rounded, -and a later browser write may commit the rounded value to Go. Represent exact -wide integers as decimal fields of Go's built-in `string` type and convert them -explicitly, for example with `strconv.FormatInt` or `strconv.FormatUint`: - -```go -type Client struct { - Counter string `json:"counter"` -} -``` - -Convert JavaScript `BigInt` values back to strings before calling `jawsVar`: - -```js -const next = BigInt(client.counter) + 1n; -client.counter = next.toString(); -jawsVar("client.counter", client.counter); -``` - -A Go `json:",string"` tag is not generic round-trip support: browser input is an -untyped string that cannot be assigned to an integer field. Use a built-in -`string` field or implement `ui.PathSetter` to parse and validate it. - -JSON-marshalable describes values JaWS can send to the browser. Generic browser -writes decode into an untyped Go value and do not invoke destination custom -unmarshaling. Consequently, `time.Time`, base64-encoded `[]byte`, and maps with -non-string Go keys are not round-trip writable by the generic setter. Use a -browser-facing DTO compatible with the generic setter, or implement -`ui.PathSetter` to parse and validate the decoded value. - -Create each binding for the Request that renders it. A `JsVarMaker` can be kept -in shared handler data because each call returns a fresh `JsVar` over the -possibly shared backing state: - -```go -type application struct { - clientMu sync.Mutex - client Client -} - -// JawsMakeJsVar creates the binding for one request. -func (app *application) JawsMakeJsVar(*jaws.Request) (ui.IsJsVar, error) { - jsv := ui.NewJsVar(&app.clientMu, &app.client) - jsv.ClientCheck = ui.JSONSizeCheck[Client](1 << 20) // 1 MiB - return jsv, nil -} - -app := new(application) -handler := ui.Handler(jw, "index", app) -``` - -```gotemplate -{{$.JsVar "client" .Dot}} -``` - -Several `JsVar` bindings may share a name. The name is a single `window` -property, and a browser-initiated write to it is delivered to every live binding -of that name; a removed binding simply stops receiving writes. This makes -re-rendering a subtree that contains a nested `JsVar` work, lets multiple requests -expose the same application-owned global, and lets one browser value fan out to -several independent Go bindings. When several bindings share the same backing -value, a browser write applies to it once per binding, so avoid exposing one -non-idempotent value through multiple simultaneously rendered bindings. - -For a value that does not implement `ui.PathSetter`, an optional -`JsVar.ClientCheck` validates each actual browser-initiated change before it -commits. The check receives the complete tentative value and the -browser-supplied jq path while the application locker is held. The path is an -inspection hint, not an authorization key: it is passed through unchanged, jq -accepts equivalent noncanonical spellings with empty components, and both `""` -and `"."` address the root. Use `ui.PathSetter` when paths must be allow-listed. -Returning nil commits the change; returning an error rolls it back without -broadcasting it. If the error matches `ui.ErrJsVarTooLarge`, -`JawsInput` returns that sentinel and, during normal framework dispatch, -cancels the associated request after releasing the application locker; any -other error is returned without cancellation. A check must only inspect the -value: it must not mutate it, re-enter the `JsVar`, call a JSON path setter on -it, acquire the same locker, or retain references into a rejected tentative -value. It must not return or wrap `jaws.ErrEventUnhandled`, which would request -handler fallthrough rather than report a rejection. - -The check validates tentative Go state, not the decoded browser value used in -the accepted peer broadcast. jq conversions and ignored map-to-struct entries -can make those values differ. Use `ui.PathSetter` when peer-visible input also -needs validation. - -`ui.JSONSizeCheck[T](maxBytes)` provides an exact serialized-size policy. It -marshals the complete tentative value. A value exceeding `maxBytes`, or one -that cannot be marshaled, makes the check match `ui.ErrJsVarTooLarge`. A -non-positive limit disables the check. Time and allocation cost depend on the -whole value and its marshaling behavior; map-key sorting and custom marshalers -can add further cost. Use it when that cost is appropriate for the value and -threat model. It bounds `encoding/json` output, not Go heap use or backing-memory -size. Use it only when JSON faithfully represents all client-growable state; -custom `MarshalJSON` or `MarshalText` methods, omitted fields, aliases, or -collection capacity require a domain-specific `ClientCheck`. A nil -`ClientCheck` accepts every type-correct generic write and imposes no -accumulated-state size limit. - -`ClientCheck` applies only to browser writes handled through the generic JSON -path setter. Server-initiated `JsVar.JawsSet` and `JsVar.JawsSetPath` calls -bypass it. A value implementing `ui.PathSetter` also bypasses it and must -allow-list paths and enforce collection or size limits in its own -`JawsSetPath`. Because the check belongs to a request-scoped binding, configure -an equivalent policy on every binding that exposes the same `Ptr` or reachable -mutable backing state to browser writes, and protect those bindings with the -same locker. One unchecked binding can otherwise modify the shared state -without validation. - -`ClientCheck` is an acceptance gate, not a state monitor. It does not inspect -the initial render, server writes, invalid or unchanged generic writes, or -`PathSetter` writes. An ordinary rejection restores Go state and sends no -broadcast, but the originating browser has already changed its local value and -can remain divergent until the application resynchronizes it. A rejection -matching `ui.ErrJsVarTooLarge` instead cancels the associated request, when -present, and is terminal for that connection. - -`jawsVar` and `JsCall` paths are application-controlled. The browser rejects -exact `__proto__` components; put user data in JSON values, not paths. - -The name may refer to an existing application global. For example, browser -code can update that object and send either the complete value or one path: - -```js -var client = {X: 0, Y: 0}; - -onmousemove = function (event) { - client.X = event.clientX; - client.Y = event.clientY; - jawsVar("client"); // send the current complete value - - // Equivalently, set and send one path: - // jawsVar("client.X", event.clientX); -}; -``` - -When the `JsVar`'s `Ptr` is non-nil, rendering serializes its current Go value -into the binding element. When the JaWS script attaches that element, the -snapshot initializes the named browser variable. A browser call to `jawsVar` -sends only while the WebSocket is open; calls made earlier are not queued for -later transmission. On the Go side, successful `JawsSet` and `JawsSetPath` -calls change the bound value. Any broadcast they produce targets matching -active requests and is not replayed to a page that has rendered but has not yet -subscribed to broadcasts. - -If either side can change the value between rendering and WebSocket setup, the -application must choose and implement a policy if it requires the two sides to -converge. Depending on the value, it may send the current browser state once -the connection is ready, resend the current Go state as part of an -application-level handshake, or merge selected paths according to application -rules. `JsVar` deliberately does not choose one of those policies. - -## Technical notes - -### WebSocket wire format notes - -JaWS WebSocket protocol records are line-based and field-delimited: -`WhatJidData`. A text message may carry multiple records. Keep -these invariants in mind when changing client/server protocol code: - -* The browser is not trusted. Incoming records are validated (`What`, `Jid`, - delimiters, quoting), and malformed records are skipped independently. -* Each inbound WebSocket message is limited to 32 KiB. The bundled client does - not chunk `Input`, `Set`, `Click`, `ContextMenu`, or `Remove`; an oversized - message closes the connection. The limit covers the complete protocol payload - after UTF-8 encoding, so there is no fixed maximum application-value length. - Keep text values, click/context-menu names, and `jawsVar` writes conservatively - sized; use HTTP uploads for large values and independently updated wrappers - for large dynamic trees. `JsVar.ClientCheck` and `JSONSizeCheck` run after - receipt and cannot enforce this transport limit. -* `what.Remove` means remove child element(s). For browser-originated `Remove` - messages, the WebSocket `Jid` identifies the parent/container in the DOM and - `Data` carries removed managed child IDs. The server only removes child IDs - that are known in the current request. -* `what.Replace` replaces the target element HTML and carries plain HTML in `Data`. -* `what.Call`/`what.Set` use `path + "=" + json` inside `Data`. Paths may not - contain tabs, newlines, carriage returns, or `=`. Embedded tabs or newlines in - JSON break message framing; `Jaws.JsCall` compacts valid JSON before sending. - A `Call` selected by a nil destination or nonzero request key uses a zero Jid - and does not require a DOM element; a zero request key is dropped. Tag - destinations remain element-scoped. Built-in strings and bare `Jid` values are - not destinations: use `tag.Tag` or a domain tag for broadcasts, and `Element` - methods for request-local operations. -* `jawsVar(name, ...)` resolves properties from `window`, so `JsVar` names share - the page's global namespace. Use an application-owned name, including an - existing global that browser code reads or changes. Do not bind a - browser-owned property such as `window.name`, or a global owned by unrelated - code: `JsVar` initialization and updates write that property. WebSocket - routing uses the top-level symbol name only. Register names as top-level - identifiers (for example, `app`), and use dotted suffixes as the JSON path - (for example, `jawsVar("app.state", value)` sends path `state`). The exact - top-level name `__proto__` is reserved; rendering it as a `JsVar` returns - `ui.ErrIllegalJsVarName`. A name may be shared by several live bindings; a - browser write is delivered to every live binding of the name. See - [JavaScript variables](#javascript-variables) for the binding and - synchronization model. - -### HTTP request flow and associating the WebSocket - -When a new HTTP request is received, create a JaWS Request using the JaWS -object's `NewRequest()` method. `HeadHTML()` is the usual way to emit the -configured resources and Request key metadata in the page's `` section. -`TailHTML()` is optional; placing it before the closing `` tag applies -updates queued during initial rendering before the WebSocket connects, which can -reduce flicker. Applications that provide equivalent resources and metadata do -not need to call either helper. - -When the client has finished loading the document and parsed the -scripts, the JaWS JavaScript will request a WebSocket connection on -`/jaws/*`, with the `*` being the encoded `Request.JawsKey` value. - -On receiving the WebSocket HTTP request, decode the key parameter from -the URL and call the JaWS object's `UseRequest()` method to retrieve the -Request created in the first step. Then call its `ServeHTTP()` method to -start up the WebSocket and begin processing JavaScript events and DOM -updates. - -### Request lifecycle invariants - -Every `NewRequest` returns a distinct `*Request` identity that is never reused -for another connection; only its internal buffers are pooled. While the `Jaws` -instance is open, `NewRequest` creates a pending request owned by it. `UseRequest` -is the only operation that claims that pending request for a WebSocket, and it -also removes the request from the pending set. A claimed Request finishes after -its WebSocket processing exits: its context is canceled, its buffers are released -to the pool, and its key is reserved until the Request is collected rather than -reassigned. Completion leaves the lock-free Element fields and the id counter intact, so if an -early `/jaws/` callback claims and tears down a Request whose initial render -is still in flight, that render may degrade — the elements and tags rendered so far -are forgotten (a later lookup finds nothing, though newly created elements are -tracked again) — but it stays race-safe: no data race, no reused identity, and no -duplicated element id. Maintenance or the per-IP limit can -instead retire a -non-running Request: its context is canceled, its key becomes unclaimable, and it is -excluded from `Pending` and `RequestCount`. Retirement preserves the Request's -identity, Elements and buffers so an initial HTTP handler still holding it can keep -rendering. For registered Requests, a finished key is not assigned to another Request -while the old one remains reachable; no deadline is guaranteed for later reuse. (A -Request created by `NewRequest` after `Jaws.Close` is never registered and installs no -tombstone, so two such post-close calls could receive the same key — harmless, since -neither is claimable.) - -`ServeWithTimeout(requestTimeout)` requires an exact multiple of `time.Second` -from `time.Second` through 2,147,483,646 seconds. Other values have unspecified -behavior. - -Before `Request.ServeHTTP` begins WebSocket processing, timeout-based Request -retirement is periodic and approximate, not a hard deadline. `NewRequest`, a -successful `UseRequest`, and `ui.RequestWriter.Write` mark activity using -whole-second samples from the epoch established by `jaws.New()`. Retirement is -checked only during maintenance passes, so it is not timed precisely from those -events. - -On an active WebSocket, `requestTimeout` bounds each keepalive ping and outbound -write. See [WebSocket keepalive ping](#websocket-keepalive-ping) for probe -scheduling. - -`*Request` values are borrowed lifecycle objects. Do not store them in -application state or pass them to background goroutines; copy the required -application data and retain the Request context instead. - -An `*Element` belongs to its owning Request and embeds a pointer to it. -Render-scoped widgets may retain child Elements they create between render and -update calls within that Request lifecycle, as container helpers do, but should -access them only from those calls. Do not let an Element escape the Request -lifecycle or pass it to background work: once the owning Request finishes it is -unregistered, so the Element receives no further broadcasts or updates, though its -fields are left intact and its methods still operate on the finished Request. -Request identities are never reused, so a stale Element can never come to -represent an unrelated connection. - -Event targets are fixed when each event is accepted. A live target remains -eligible if it is later removed, whether removal is reported by the browser or -initiated by the server; an Element removed before acceptance is excluded. A -handler may therefore receive a deleted Element, whose render, update, and queue -helpers are no-ops for the rest of the Request. - -Dirtying is two-stage: `Request.Dirty` and `Jaws.Dirty` expand and record their -selectors on the `Jaws` instance, then the serving loop distributes ordinary tags -across live Requests and each exact `*Element` only to its owner before scheduling -`JawsUpdate` calls. Broadcast helpers share the serving loop, so start `Serve` or -`ServeWithTimeout` before calling APIs that broadcast, reload, close sessions, or -rely on dirty updates. - -Cancellation flows from the request context, the initial HTTP/WebSocket request, -and `Jaws.Close`. Update paths that cannot return errors report them through -`MustLog`, so long-running applications should configure `Jaws.Logger`. -`Jaws.Log` and a configured `MustLog` enqueue `Logger.Error` calls for serial -asynchronous delivery, so those callbacks do not block JaWS processing. Panics -from those callbacks are contained by the dispatcher. `Close` stops accepting -new log entries and lets those already accepted drain without waiting for them; -`Serve` and `ServeWithTimeout` wait for the drain before returning normally after -shutdown. A blocked `Logger.Error` callback delays later entries and that final -drain. - -### Configuration lifecycle - -Set exported `Jaws` configuration fields immediately after `jaws.New()` and -before exposing handlers, creating Requests, or starting `Serve()` / -`ServeWithTimeout()`. These fields are ordinary Go fields, not synchronized -live configuration knobs. - -If you change fields that affect generated page metadata, such as `Debug` or -the resource list passed to `GenerateHeadHTML()`, call `GenerateHeadHTML()` -before rendering new pages so `Request.HeadHTML()` sees the updated data. - -`GenerateHeadHTML()` writes markup for common `.js` and `.css` URLs, including a -trailing `@version` when the final extension is otherwise unrecognized, and for -image and font resources. Every successfully parsed URL is passed to -secureheaders automatic CSP inference. A configured `Jaws.Logger` warns once -for each extra URL that is omitted from the final markup or is absolute or -scheme-relative and cannot contribute an explicit policy source. Warning URLs redact -passwords and omit queries and fragments. Applications may load omitted -resources manually. When automatic inference does not match the request -destination, use the explicit policy setup under -[Secure Response Headers](#secure-response-headers). Resource URLs must come -from trusted application configuration because matched scripts are executable -and CSP permissions apply to origins. - -### Maintainer checklist - -When changing core request, session, broadcast, or WebSocket code, re-check -these invariants before relying on a green build alone: - -* Lock order stays `Jaws.mu -> Request.mu -> Session.mu`, with `Request.muQueue` - and most element/widget/value locks remaining leaves. Container reconciliation's - state-before-Request edge stays confined to matching and Element creation; core-lock - holders never invoke container render or update methods. -* Request identities are never reused; only the internal buffers are pooled. - Completion (after WebSocket serving) releases queued dirt, elements, tags, and - messages, detaches the session, unregisters the identity, and reserves the key - with a tombstone until the Request is collected. It clears only live length, - relying on the shrink paths to zero vacated entries, and must not mutate - render-visible Element state a still-running initial renderer may read lock-free. - Non-running retirement instead preserves the Request's Elements and buffers for an - initial HTTP handler that still holds it. -* Dirty dispatch expands its inputs once. Ordinary keys target registered elements; - an expanded `*Element` targets that exact Element on its owning Request. Neither - path lets a queued update reach a finished, unregistered request (whose key stays - reserved, never reassigned to another Request, while it remains reachable). -* Session grace windows remain deliberate for unclaimed, claimed, failed-upgrade, - and closed-WebSocket requests. -* WebSocket upgrades keep the single-use key, client-IP binding, and Origin - host/scheme checks together; changes to trusted forwarded headers must preserve - the same fail-closed behavior. - -Configure `Jaws.Logger` in long-running applications. Initial render errors are -returned to the caller, but update-time paths such as template refreshes and -dynamic child appends cannot return errors to browser event handlers; they are -reported through `MustLog()`, which panics when no logger is configured. - -### WebSocket keepalive ping - -JaWS pings read-idle WebSocket connections to detect peers that disappear -without a close handshake. Incoming data and successful pings restart the idle -interval. Time spent parsing or delivering already-read data does not count -toward it. - -`Jaws.WebSocketPingInterval` defaults to -`jaws.DefaultWebSocketPingInterval` (1 minute) and must be positive; -non-positive values do not disable probing. - -### Safe to call before `Serve()` - -The following APIs are safe to call before starting the JaWS processing -loop (`Serve()` or `ServeWithTimeout()`): - -* Construction and lifecycle: `jaws.New()`, `(*Jaws).Close()`, `(*Jaws).Done()`. -* Configuration: `(*Jaws).AddTemplateLookuper()`, `(*Jaws).RemoveTemplateLookuper()`, - `(*Jaws).LookupTemplate()`, `(*Jaws).GenerateHeadHTML()`, `(*Jaws).Setup()`, - `(*Jaws).FaviconURL()`. -* Inspection and logging helpers: `(*Jaws).RequestCount()`, `(*Jaws).RequestCounts()`, - `(*Jaws).Pending()`, `(*Jaws).SessionCount()`, `(*Jaws).Sessions()`, - `(*Jaws).Log()`, `(*Jaws).MustLog()`. -* Static/ping JaWS endpoints via `(*Jaws).ServeHTTP()`: - `/jaws/.ping`, `/jaws/.jaws..js`, `/jaws/.jaws..css`. - -Broadcasting APIs are not safe before the processing loop starts. In particular, -`(*Jaws).Broadcast()` (and helpers that call it), `(*Session).Broadcast()`, -`(*Session).Reload()` and `(*Session).Close()` may block before `Serve()` or -`ServeWithTimeout()` is running. - -### Secure Response Headers - -Use `(*Jaws).SecureHeadersMiddleware(next)` to wrap page handlers with a -security-header baseline and a `Content-Security-Policy` generated from the -resource URLs currently configured for JaWS. - -The baseline headers come from -[`github.com/linkdata/secureheaders`](https://github.com/linkdata/secureheaders). - -The middleware starts from `secureheaders.DefaultHeaders()`, replaces -`Content-Security-Policy` with `jw.ContentSecurityPolicy()`, and does not trust -forwarded HTTPS headers. - -```go -page := ui.Handler(jw, "index", bind.New(&mu, &f)) -http.DefaultServeMux.Handle("GET /", jw.SecureHeadersMiddleware(page)) -``` - -When a resource needs an explicit destination, use `secureheaders.Middleware` -instead of the JaWS convenience middleware. The custom policy must include -every external resource the page loads, including resources configured through -`GenerateHeadHTML()`; a zero `Destination` keeps automatic inference. Leave a -context-mismatched resource out of `GenerateHeadHTML()` and load it manually. -Given a parsed, automatically loaded `scriptURL` and a manually fetched -`fetchURL`: - -```go -headers := secureheaders.DefaultHeaders() -headers.Set("Content-Security-Policy", secureheaders.BuildContentSecurityPolicy( - secureheaders.Resource{URL: scriptURL}, - secureheaders.Resource{URL: fetchURL, Destination: secureheaders.ResourceDestinationConnect}, -)) -http.DefaultServeMux.Handle("GET /", secureheaders.Middleware{Handler: page, Header: headers}) -``` - -### Routing - -JaWS doesn't enforce any particular router, but it does require several -endpoints to be registered in whichever router you choose to use. All of -the endpoints start with "/jaws/", and `Jaws.ServeHTTP()` will handle all -of them. - -* `/jaws/.jaws..css` - - Serves the built-in JaWS stylesheet. - - The response should be cached indefinitely. - -* `/jaws/.jaws..js` - - Serves the built-in JaWS client-side JavaScript. - - The response should be cached indefinitely. - -* `/jaws/[0-9a-v]+` (and `/jaws/[0-9a-v]+/noscript`) - - The WebSocket endpoint, where the path component is the generated lowercase - base-32 request key. When you register `Jaws.ServeHTTP()` for `GET /jaws/`, - this is handled automatically. Custom routers that dispatch the endpoint - themselves should parse the trailing string with - `key.Parse()` (`github.com/linkdata/jaws/lib/key`) and then retrieve the - matching JaWS Request with the JaWS object's `UseRequest()` method. - - If the Request is not found, return a **404 Not Found**, otherwise - call the Request `ServeHTTP()` method to start the WebSocket and begin - processing events and updates. - -* `/jaws/.tail/` - - Serves the deferred "tail" script for a Request, emitted by `TailHTML()` at the - end of the page body. The `` identifies the Request; `Jaws.ServeHTTP()` - looks it up and writes the script. Handled automatically when you register - `GET /jaws/`. - - The response should not be cached. - -* `/jaws/.ping` - - This endpoint is called by the JavaScript while waiting for the server to - come online. This is done in order to not spam the WebSocket endpoint with - connection requests, and browsers are better at handling XHR requests failing. - - If you don't have a JaWS object, or if its completion channel is closed (see - `Jaws.Done()`), return **503 Service Unavailable**. If you're ready to serve - requests, return **204 No Content**. - - The response should not be cached. - -Handling the routes with the standard library's `http.DefaultServeMux`: - -```go -jw, err := jaws.New() -if err != nil { - panic(err) -} -defer jw.Close() -go jw.Serve() -http.DefaultServeMux.Handle("GET /jaws/", jw) -``` - -Handling the routes with [Echo](https://echo.labstack.com/): - -```go -jw, err := jaws.New() -if err != nil { - panic(err) -} -defer jw.Close() -go jw.Serve() -router := echo.New() -router.GET("/jaws/*", func(c echo.Context) error { - jw.ServeHTTP(c.Response().Writer, c.Request()) - return nil -}) -``` - -### HTML rendering - -HTML output elements (e.g. `ui.NewDiv()` and `ui.RequestWriter.Div()`) accept values that can -be made into a `bind.HTMLGetter` using `bind.MakeHTMLGetter()`. - -In order of precedence, this can be: -* `bind.HTMLGetter`: `JawsGetHTML(*Element) template.HTML` to be used as-is. -* `bind.Binder[string]` or `bind.Getter[string]`: `JawsGet(*Element) string` that will be escaped using `html.EscapeString`. -* `fmt.Stringer`: `String() string` that will be escaped using `html.EscapeString`. -* a static `template.HTML` or `string` to be used as-is with no HTML escaping. -* everything else is rendered using `fmt.Sprint()` and escaped using `html.EscapeString`. - -You can use `bind.New(...).GetHTML(...)`, `bind.HTMLGetterFunc()` or `bind.StringGetterFunc()` to build a custom renderer -for trivial rendering tasks, or define a custom type implementing `HTMLGetter`. -Plain strings are treated as trusted HTML. Escape untrusted string input yourself, or pass it through -a `bind.Getter[string]`, `bind.StringGetterFunc()` or `fmt.Stringer` so JaWS escapes it before rendering. - -Initial HTML rendering returns errors directly. Later updates run from the -request processing loop, so custom `JawsUpdate` implementations should keep -their work deterministic and report unrecoverable failures through -`Element.Request.MustLog()` or `Element.Jaws.MustLog()`. - -`ui.RequestWriter.Register` is an advanced escape hatch primarily for attaching a -custom, render-independent updater to otherwise static template-authored HTML. -The registered HTML is intended to contain no JaWS widgets. Render standard -widgets through their normal helpers; `Register` makes no compatibility guarantees -for using them as its updater or inside its HTML. - -### Data binding - -HTML input elements bind browser state to Go values. Text inputs use `string`, -checkable inputs use `bool`, and date inputs use `time.Time`. `ui.Number` and -`ui.Range` accept a `bind.Getter[T]` for every `ui.Numeric` type and become -editable when the source also implements `bind.Setter[T]`. Numeric types include -signed and unsigned integers, `uintptr`, `float32`, `float64`, and named types -with one of those underlying types. - -When interaction before the initial WebSocket opens must be prevented, render -native controls disabled or make the interactive region inert. A request -`ConnectFn` can update request-local readiness and dirty the request-specific -readiness tag used by the Template, or the exact Element whose custom updater -removes the gate. - -Managed inputs and selects do not support native HTML form reset. A -`