jaws

package module
v0.601.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 19, 2026 License: MIT Imports: 38 Imported by: 8

README

build coverage OpenSSF Scorecard Docs

JaWS

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.

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.

There is a demo application with plenty of comments to use as a tutorial.

Installation

JaWS is distributed as a standard Go module. To add it to an existing project use the go get command:

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.

AI skill

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/.

Using curl:

mkdir -p "$HOME/.agents/skills/jaws/agents"
curl -fsSL https://raw.githubusercontent.com/linkdata/jaws/main/.agents/skills/jaws/SKILL.md \
	-o "$HOME/.agents/skills/jaws/SKILL.md"
curl -fsSL https://raw.githubusercontent.com/linkdata/jaws/main/.agents/skills/jaws/agents/openai.yaml \
	-o "$HOME/.agents/skills/jaws/agents/openai.yaml"

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.

package main

import (
	"html/template"
	"log/slog"
	"net/http"
	"sync"

	"github.com/linkdata/jaws"
	"github.com/linkdata/jaws/lib/bind"
	"github.com/linkdata/jaws/lib/ui"
)

const indexhtml = `
<html>
  <head>{{$.HeadHTML}}</head>
  <body>{{with .Dot}}
    {{$.Range .}}
  {{end}}{{$.TailHTML}}</body>
</html>
`

func main() {
	jw, err := jaws.New() // create a default JaWS instance
	if err != nil {
		panic(err)
	}
	defer jw.Close()           // ensure we clean up
	jw.Logger = slog.Default() // optionally set the logger to use

	// parse our template and inform JaWS about it
	templates := template.Must(template.New("index").Parse(indexhtml))
	_ = jw.AddTemplateLookuper(templates)

	go jw.Serve()                                 // start the JaWS processing loop
	http.DefaultServeMux.Handle("GET /jaws/", jw) // ensure the JaWS routes are handled

	var mu sync.Mutex
	var f float64

	http.DefaultServeMux.Handle("GET /", ui.Handler(jw, "index", bind.New(&mu, &f)))
	slog.Error(http.ListenAndServe("localhost:8080", nil).Error())
}

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 reused 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.

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 is done using the different methods on the Element object when the JawsUpdate() method is called.

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.

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 x<SP>y<SP>keystate<SP>name)
  • oncontextmenu invokes JawsContextMenu for non-input-origin events (val as x<SP>y<SP>keystate<SP>name)
  • oninput invokes JawsInput
  • 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.

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:

type application struct {
	clientMu sync.Mutex
	client   Client
}

// JawsMakeJsVar creates the binding for one request.
func (app *application) JawsMakeJsVar(*jaws.Request) (ui.IsJsVar, error) {
	return ui.NewJsVar(&app.clientMu, &app.client), nil
}

app := new(application)
handler := ui.Handler(jw, "index", app)
{{$.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.

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:

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 messages are line-based and field-delimited: What<TAB>Jid<TAB>Data<LF>. Keep these invariants in mind when changing client/server protocol code:

  • The browser is not trusted. Incoming frames are validated (What, Jid, framing, quoting) and invalid frames are ignored/dropped.
  • 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 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 <head> section. TailHTML() is optional; placing it before the closing </body> 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

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 is removed after its WebSocket processing exits. Maintenance or the per-IP limit can instead retire an unclaimed Request: its context is canceled, its key becomes unclaimable, and it is excluded from Pending and RequestCount. Retiring an unclaimed Request does not change its identity or Elements while an initial HTTP handler still holds them. Its key cannot be assigned to another Request while the retired Request remains reachable; no deadline is guaranteed for later reuse.

*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: when a Request that entered ServeHTTP returns, it may be placed in an internal pool, and the Element's embedded Request pointer may later represent an unrelated connection.

Dirtying is two-stage: Request.Dirty and Jaws.Dirty expand tags and record them on the Jaws instance, then the serving loop distributes those tags to matching active requests and schedules JawsUpdate calls. Broadcast helpers share that same 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.

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.

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 element/widget/value locks remaining leaf locks.
  • Request pooling clears keys, elements, tags, sessions, queues, cancellation state, and pending/request maps before a pointer can be reused.
  • Dirty dispatch expands tags once, targets only elements registered for those tags, and does not let one request's queued update reach a recycled request with a different key.
  • Session grace windows remain deliberate for unclaimed, claimed, failed-upgrade, and closed-WebSocket requests.
  • Subsecond ServeWithTimeout values are not useful in production. AI-assisted reviews should not assume subsecond timeout precision is required or treat its absence as a source of bugs.
  • 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 can periodically ping active WebSocket connections to detect peers that disappeared without a close handshake.

Set Jaws.WebSocketPingInterval to control this. The default is jaws.DefaultWebSocketPingInterval (1 minute). Set it to 0 or a negative value to disable keepalive pings.

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.<hash>.js, /jaws/.jaws.<hash>.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 that matches the resources currently configured for JaWS.

The baseline headers come from github.com/linkdata/secureheaders.

The middleware starts from secureheaders.DefaultHeaders(), replaces Content-Security-Policy with jw.ContentSecurityPolicy(), and does not trust forwarded HTTPS headers.

page := ui.Handler(jw, "index", bind.New(&mu, &f))
http.DefaultServeMux.Handle("GET /", jw.SecureHeadersMiddleware(page))
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.<hash>.css

    Serves the built-in JaWS stylesheet.

    The response should be cached indefinitely.

  • /jaws/.jaws.<hash>.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/<key>

    Serves the deferred "tail" script for a Request, emitted by TailHTML() at the end of the page body. The <key> 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:

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:

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().

Data binding

HTML input elements (e.g. ui.RequestWriter.Range()) require bi-directional data flow between the server and the browser. The first argument to these is usually a bind.Setter[T] where T is one of string, float64, bool or time.Time. It can also be a bind.Getter[T], in which case the HTML element should be made read-only.

Since all data access need to be protected with locks, you will usually use bind.New() to create a bind.Binder[T] that combines a (RW)Locker and a pointer to a value of type T. It also allows you to add chained setters, getters and on-success handlers.

Session handling

JaWS has non-persistent session handling integrated. Sessions won't be persisted across restarts and must have an expiry time.

Use one of these patterns:

  • Wrap page handlers with Jaws.SessionMiddleware(handler) to ensure a session exists.
  • Call Jaws.NewSession(w, r) explicitly to create and attach a fresh session cookie.
  • Set Jaws.AutoSession to lazily create an anonymous session during a successful WebSocket upgrade when a Request has none.

Manual session creation is still the primary pattern when the initial render depends on session data. This is especially important for authentication: authenticate the user, create or retrieve the session, and populate it before calling NewRequest() and rendering the page.

When subsequent Requests are created with NewRequest(), if the HTTP request has the cookie set and comes from the correct IP, the new Request will have access to that Session.

Session key-value pairs can be accessed using Request.Set() and Request.Get(), or directly using a Session object. It's safe to do this if there is no session; Get() will return nil, and Set() will be a no-op.

Sessions are bound to the client IP JaWS sees. Attempting to access an existing session from a different non-loopback IP will fail. Loopback addresses are treated as the same client so a reverse proxy connecting to the backend over loopback does not break binding; in deployments where every request reaches JaWS from loopback, IP binding is effectively disabled unless Jaws.TrustForwardedHeaders is enabled behind a single trusted reverse proxy.

No data is stored in the client browser except the randomly generated session cookie. You can set the cookie name in Jaws.CookieName, the default is derived from the executable name and falls back to jaws.

A note on the Context

The Request object stores a context.Context in one of its fields, contrary to recommended Go practice.

The reason is that there is no unbroken call chain from the time the Request object is created when the initial HTTP request comes in and when it is requested during the JavaScript WebSocket HTTP request.

Request.SetContext must return a non-nil context derived from the current context passed to it. If the returned context is canceled or its deadline expires, a running Request's WebSocket loop wakes promptly even while idle; no browser event or broadcast is needed.

Background work that must cancel the Request should retain its own derived context and cancellation function, not the Request pointer:

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)
	}
}()
Security of the WebSocket callback

While the Jaws instance is open, each Request gets a non-zero random 64-bit key not currently in use. This value is written to the HTML output so the JavaScript can construct the WebSocket callback URL.

While assigned to a Request, its callback key can claim that Request at most once. JaWS does not assign keys belonging to registered Requests or still-reachable retired Requests. A retired key may become eligible for reuse after its Request becomes unreachable, but reuse timing is unspecified.

In addition to this, Requests that are not claimed by a WebSocket call are retired at regular intervals. By default an unclaimed Request is retired after 10 seconds.

JaWS also limits unclaimed Requests per client IP. The default Jaws.MaxPendingRequestsPerIP value is 100; setting it to zero or a negative value disables this cap. When the cap is reached, creating a new Request evicts the oldest pending Request from the same IP, so its old WebSocket key can no longer be claimed. The cap uses the same client IP resolver as request/session binding: trusted forwarded headers when Jaws.TrustForwardedHeaders is enabled, otherwise the IP parsed from http.Request.RemoteAddr.

To guess and hijack a pending WebSocket callback, an attacker must find its key before the genuine WebSocket claims it or JaWS retires the Request. Finding a uniformly random 64-bit key takes on the order of 2^63 distinct guesses on average within that window.

Authorization and templates (Auth)

Templates rendered through ui.With receive an Auth value exposing Data(), Email() and IsAdmin(), letting templates gate UI such as {{if .Auth.IsAdmin}}.

You provide the implementation by setting Jaws.MakeAuth. If you leave Jaws.MakeAuth nil, templates receive the built-in DefaultAuth, which is fail-open: DefaultAuth.IsAdmin() returns true for every visitor (Data() and Email() are fail-safe — nil and empty). A page that gates privileged UI on {{if .Auth.IsAdmin}} will therefore show it to everyone on an instance that forgot to set MakeAuth.

Always set Jaws.MakeAuth in production and treat a nil MakeAuth as "no authorization configured", not "deny". As a safety net, when MakeAuth is nil and a Jaws.Logger is configured, JaWS logs a one-time warning the first time a template evaluates .Auth.IsAdmin. Note this is lazy: a page that never gates on .Auth.IsAdmin never logs it, so absence of the warning does not prove MakeAuth is set.

Production hardening checklist

Before serving a JaWS application outside a local development loop, check these items explicitly:

  • Configure Jaws.Logger. Update-time errors and API misuse are reported through MustLog(), which panics when no logger is configured.
  • Configure Jaws.MakeAuth whenever templates read .Auth, especially .Auth.IsAdmin. Leaving it nil installs the fail-open DefaultAuth described above.
  • Treat plain string values passed to HTML-inner widgets as trusted HTML. Route user-controlled text through bind.Getter[string], bind.StringGetterFunc(), fmt.Stringer or template escaping before rendering.
  • Implement ui.PathSetter on any ui.JsVar value whose browser-writable paths must be allow-listed or size-bounded beyond the default serialized-size cap.
  • Enable Jaws.TrustForwardedHeaders only behind a single trusted reverse proxy that overwrites X-Forwarded-For, X-Real-IP and X-Forwarded-Proto.
  • Run the test suite with -race before release so the deadlock lock-order detector and JaWS debug-gated checks are exercised. If the race detector is unavailable, use -tags "debug deadlock".
Testing

Always run the test suite with the -race flag:

go test -race ./...

Race detection sets deadlock.Debug = true and deadlock.Enabled = true (see Dependencies), which exercises the deadlock lock-order detector and JaWS debug-gated checks such as the late-handler panic. The tag comparability checks in lib/tag run in normal code paths too. Plain go test does not exercise the debug-only branches or the deadlock detector. CI builds with -race. If the race detector is unavailable, use go test -tags "debug deadlock" ./...; the debug tag alone sets deadlock.Debug, but does not enable the deadlock detector.

Dependencies

We try to minimize dependencies outside of the standard library.

Learn more

  • Browse the Go package documentation for an API-by-API overview.
  • Inspect the example_test.go file for compile-checked example programs to copy and adapt. They start a blocking HTTP server, so they are illustrations rather than examples executed by go test.
  • Explore the demo application to see a more complete, heavily commented project structure.

Documentation

Overview

Package jaws provides a mechanism to create dynamic webpages using JavaScript and WebSockets.

It integrates well with Go's html/template package, but can be used without it. It can be used with any router that supports the standard http.Handler interface.

This package holds the core engine and the UI interfaces. The standard widgets (Span, Button, Select, Text, and so on) and the RequestWriter helper methods live in github.com/linkdata/jaws/lib/ui, and value binding lives in github.com/linkdata/jaws/lib/bind.

Locking

The package uses a single, acyclic lock hierarchy. When more than one of these locks is held at once they must be acquired in this order, outermost first:

Jaws.mu  ->  Request.mu  ->  Session.mu

Request.muQueue and per-Element state are leaf locks taken below all of the above. Blocking work (channel sends, user callbacks) is always performed after snapshotting the needed state and releasing the relevant lock; see Session.Broadcast and Session.Close for the canonical pattern. The Request.SetContext transform is the deliberate exception: it runs while holding Request.mu so the read-modify-write is atomic, and therefore must not call back into the same Request or block.

UI value and widget types in the subpackages carry their own leaf locks that guard the bound value: the binders in github.com/linkdata/jaws/lib/bind, the JsVar in github.com/linkdata/jaws/lib/ui and the named values in github.com/linkdata/jaws/lib/named. These are leaves with respect to each other, acquired containing-before-contained (for example a named BoolArray's mutex is taken before a member Bool's). They sit strictly below the three core locks: every value type mutates the bound value under its value lock, releases it, and only then marks the Element dirty or broadcasts the change (which ultimately takes the outermost Jaws.mu), so a value lock is never held while a core lock is acquired. lib/bind, lib/ui and lib/named all follow this mutate-release-then-dirty pattern, and new value types must too. The safety of this rests on an invariant the deadlock detector cannot enforce (value locks are leaves distinct from Jaws.mu): no code path holding Jaws.mu, Request.mu or Session.mu ever calls into a UI value's Get/Set/Dirty methods, which are the only callers that take a value lock — were it otherwise, the later dirty step's Jaws.mu acquisition would invert the core lock order. Code holding any of the three core locks must therefore never invoke a UI value method.

A deliberate reverse edge lives in github.com/linkdata/jaws/lib/ui: ContainerHelper.reconcile holds its own widget mutex while calling Request.NewElement, which takes Request.mu — again leaf-before-core. It is safe for the same reason: no code path holding any of the three core locks ever invokes a widget's render or update method (the Serve loop calls JawsRender and JawsUpdate only after releasing Request.mu). Code holding Jaws.mu, Request.mu or Session.mu must therefore never call a container's render/update entry points. Note the widget mutex is a plain sync.Mutex, so deadlock.Debug cannot observe this inversion; the invariant is maintained by convention.

Element handlers are an intentional exception to the locking rules: they are populated only while an Element is rendered and are then read without a lock on the event goroutine. Element.JawsRender and Element.Freeze publish the final handler slice through the Element's atomic frozen flag; request event dispatch reads handlers only after observing that flag. This also covers child Elements rendered after a WebSocket connects: a preemptive event for a still-rendering Element is ignored. Handlers must not be added after JawsRender returns or Freeze is called. All builds enforce this through an internal chokepoint that drops late additions; debug builds panic instead.

Testing

Always run the tests with the -race flag. Race builds set deadlock.Debug and deadlock.Enabled, exercising the deadlock lock-order detector described above and JaWS debug-gated runtime checks such as the late-handler panic. If the race detector is unavailable, use -tags "debug deadlock" so both categories stay active: the debug tag sets deadlock.Debug, while the deadlock tag enables the detector. Those JaWS debug branches are compile-time dead in normal builds, so a plain "go test" neither exercises them nor reports their statement coverage. Runtime tag-comparability checks in github.com/linkdata/jaws/lib/tag run in every build. CI builds with -race.

Index

Constants

View Source
const (
	// DefaultUpdateInterval is the default browser update interval.
	DefaultUpdateInterval = time.Millisecond * 100

	// DefaultWebSocketPingInterval is the default WebSocket keepalive ping interval.
	DefaultWebSocketPingInterval = time.Minute

	// DefaultWebSocketTimeout is the default time allowed for WebSocket connect and ping responses.
	DefaultWebSocketTimeout = time.Second * 10

	// DefaultMaxPendingRequestsPerIP is the default maximum number of unclaimed
	// Requests allowed for each client IP.
	DefaultMaxPendingRequestsPerIP = 100
)

Variables

View Source
var ErrEventHandlerPanic errEventHandlerPanic

ErrEventHandlerPanic is returned by CallEventHandlers when a user event handler panics.

Match it with errors.Is. When the recovered panic value is itself an error it is available via Unwrap (and thus errors.As / errors.Is); a non-error panic value appears only in the formatted message.

View Source
var ErrEventUnhandled = errEventUnhandled{}

ErrEventUnhandled returned by InputHandler.JawsInput, ClickHandler.JawsClick or ContextMenuHandler.JawsContextMenu causes the next available handler to be invoked.

View Source
var ErrInvalidChildElement = errors.New("invalid child element")

ErrInvalidChildElement indicates an invalid child Element.

Child operations report this error when the child is nil, deleted, unregistered, the receiver itself, or belongs to another Request.

View Source
var ErrInvalidChildIndex = errors.New("invalid child index")

ErrInvalidChildIndex indicates an invalid child index.

Jaws.Insert reports this error for a negative index. Use Jaws.Append to insert at the end.

View Source
var ErrJavascriptDisabled = errors.New("javascript is disabled")

ErrJavascriptDisabled is returned when the noscript probe indicates JavaScript is disabled.

View Source
var ErrNoWebSocketRequest errNoWebSocketRequest

ErrNoWebSocketRequest is returned when the WebSocket callback was not received within the timeout period. The most common reason is that the client is not using JavaScript.

View Source
var ErrRequestAlreadyClaimed = errors.New("request already claimed")

ErrRequestAlreadyClaimed is returned when Jaws.UseRequest is called more than once for a Request.

View Source
var ErrRequestCancelled errRequestCancelled

ErrRequestCancelled indicates a Request was cancelled.

The concrete error reachable via context.Cause on Request.Context wraps the underlying cancellation cause, so it can be matched with errors.Is and its cause retrieved with Unwrap. The exported sentinel itself carries no cause.

View Source
var ErrRequestOverloaded = errors.New("request overloaded")

ErrRequestOverloaded indicates a Request was torn down because it could not keep up with the messages addressed to it.

A Request is overloaded when its buffered broadcast channel or its internal event-call channel fills before it can drain them. Rather than silently dropping messages, which could leave the browser and backend in inconsistent and nonreproducible states, the Request is cancelled. The cancellation cause reachable via context.Cause on Request.Context wraps this sentinel, so it can be matched with errors.Is; the wrapped text identifies which channel overflowed.

View Source
var ErrServeAlreadyRunning = errors.New("serve loop already running")

ErrServeAlreadyRunning indicates the JaWS processing loop is already running.

View Source
var ErrTooManyPendingRequests errTooManyPendingRequests

ErrTooManyPendingRequests indicates an older pending Request was evicted because its client IP had reached Jaws.MaxPendingRequestsPerIP.

View Source
var ErrValueUnchanged = errors.New("value unchanged")

ErrValueUnchanged reports a successful no-op set: there was no error, but the underlying value already equaled the desired value.

Setter-style implementations (the JawsSet / JawsSetPath methods in github.com/linkdata/jaws/lib/ui and github.com/linkdata/jawstree) return it, and callers test for it with errors.Is. It lives in this package so all implementations share one error identity.

View Source
var ErrWebSocketIPMismatch errWebSocketIPMismatch

ErrWebSocketIPMismatch is returned when the WebSocket callback for a Request arrives from a different client IP than the initial HTTP request.

View Source
var ErrWebsocketOriginMissing = errors.New("websocket request missing Origin header")

ErrWebsocketOriginMissing is returned when a WebSocket request has no Origin header.

View Source
var ErrWebsocketOriginNoInitial = errors.New("websocket Origin cannot be validated: no initial request")

ErrWebsocketOriginNoInitial is returned when origin validation cannot run because the Request has no initial HTTP request to compare against. The check fails closed rather than accepting an unverified Origin.

View Source
var ErrWebsocketOriginWrongHost = errors.New("websocket Origin host mismatch")

ErrWebsocketOriginWrongHost is returned when a WebSocket Origin host does not match the initial request host.

View Source
var ErrWebsocketOriginWrongScheme = errors.New("websocket Origin not http or https")

ErrWebsocketOriginWrongScheme is returned when a WebSocket Origin is not HTTP or HTTPS.

Functions

func CallEventHandlers added in v0.300.0

func CallEventHandlers(ui any, elem *Element, wht what.What, value string) (err error)

CallEventHandlers calls the event handlers for the given Element.

Recovers from panics in user-provided handlers, returning them as errors.

Request event dispatch calls this only after the Element is frozen, publishing the completed handler slice before its lock-free read. A direct caller must not run it concurrently with rendering or handler registration.

func ParseParams added in v0.60.0

func ParseParams(params []any) (tags []any, handlers []any, attrs []string)

ParseParams parses the parameters passed to UI helpers when creating a new Element, returning UI tags, event handlers and HTML attributes.

Unlike Element.ApplyGetter, which is given the primary getter, ParseParams only recognizes InputFn, InputHandler, ClickHandler and ContextMenuHandler. A param implementing InitHandler or InitialHTMLAttrHandler is treated only as a tag here; its JawsInit / JawsInitialHTMLAttr are intentionally invoked only for the primary getter.

A param recognized as an event handler is appended to handlers, and if it is also usable as a tag (comparable, per usableAsTag) it is additionally appended to tags, so a comparable handler is returned in both slices.

Types

type Auth added in v0.85.0

type Auth interface {
	// Data returns authenticated user data, or nil.
	Data() map[string]any
	// Email returns the authenticated user email, or an empty string.
	Email() string
	// IsAdmin reports whether the authenticated user has administrator access.
	IsAdmin() bool
}

Auth describes authentication data available to templates through ui.With.

type Click added in v0.400.0

type Click struct {
	// Name is the event target name. Parsing off the wire normalizes it: leading
	// and trailing whitespace is trimmed and internal whitespace runs collapse to a
	// single space, so it does not round-trip losslessly through [Click.String].
	Name    string
	X       float64 // X is the browser clientX coordinate in CSS pixels.
	Y       float64 // Y is the browser clientY coordinate in CSS pixels.
	Shift   bool    // Shift reports whether the Shift key was held during the event.
	Control bool    // Control reports whether the Control key was held during the event.
	Alt     bool    // Alt reports whether the Alt key was held during the event.
}

Click identifies a browser click-like event, pointer location and modifier state.

func (Click) String added in v0.400.0

func (clk Click) String() string

String formats clk for the JaWS wire protocol.

It is not a lossless inverse of parsing: a Click.Name with leading, trailing or repeated internal whitespace is normalized when parsed back (see the Name field). The production wire direction is browser-to-server (parse only).

type ClickHandler added in v0.31.0

type ClickHandler interface {
	// JawsClick is called for non-input-origin browser clicks.
	//
	// The client sends clicks from an [Element]'s HTML element and from
	// non-form-control descendants. Clicks whose event target is an input,
	// select, textarea or option element, or inside one, are left to native
	// input handling and do not invoke JawsClick on an ancestor.
	//
	// [Click.Name] is the first name HTML attribute or 'button' textContent
	// found while walking from the event target up through its ancestors. If none
	// is found it falls back to the event target's HTML id, so it is empty only
	// when the target has no id either.
	JawsClick(elem *Element, click Click) (err error)
}

ClickHandler handles click events sent from the browser.

type ConnectFn

type ConnectFn = func(rq *Request) error

ConnectFn can be used to interact with a Request before message processing starts. Returning an error causes the Request to abort, and the WebSocket connection to close.

type Container added in v0.31.0

type Container interface {
	// JawsContains returns the current child [UI] values contained by elem.
	//
	// The returned [UI] values must be comparable, since they are used as map keys
	// (see [UI] for the comparability requirement), and the slice contents must not
	// be modified after returning it. A child UI may be returned repeatedly for
	// the same Request, but must not be shared with a different Request.
	JawsContains(elem *Element) (contents []UI)
}

Container is implemented by UI values that render a dynamic list of child UI values.

type ContextMenuHandler added in v0.400.0

type ContextMenuHandler interface {
	// JawsContextMenu is called for non-input-origin browser context menus.
	//
	// The client sends context-menu events from an [Element]'s HTML element and
	// from non-form-control descendants. Events whose target is an input, select,
	// textarea or option element, or inside one, are left to native browser
	// handling and do not invoke JawsContextMenu on an ancestor.
	JawsContextMenu(elem *Element, click Click) (err error)
}

ContextMenuHandler handles context-menu events sent from the browser.

type DefaultAuth added in v0.300.0

type DefaultAuth struct {
	// contains filtered or unexported fields
}

DefaultAuth is the permissive default Auth implementation used for templates when Jaws.MakeAuth is nil.

SECURITY: DefaultAuth.IsAdmin always returns true. Because it is substituted whenever Jaws.MakeAuth is unset, a template that gates privileged UI on {{if .Auth.IsAdmin}} will render that UI to EVERY visitor on any instance that forgot to set Jaws.MakeAuth. Data and Email are fail-safe (nil / empty); only IsAdmin is fail-open. Always set Jaws.MakeAuth in production, and treat a nil MakeAuth as "no authorization configured", not "deny".

func (*DefaultAuth) Data added in v0.300.0

func (*DefaultAuth) Data() map[string]any

Data returns no authenticated user data.

func (*DefaultAuth) Email added in v0.300.0

func (*DefaultAuth) Email() string

Email returns an empty authenticated user email.

func (*DefaultAuth) IsAdmin added in v0.300.0

func (da *DefaultAuth) IsAdmin() bool

IsAdmin returns true for every caller.

If a logger was supplied at construction, it logs a one-time warning that Jaws.MakeAuth is unset and authorization is fail-open.

type Element added in v0.31.0

type Element struct {
	*Request // (read-only) the Request the Element belongs to
	// contains filtered or unexported fields
}

Element is an instance of a Request, a UI object and a Jid.

An Element pointer supplied to a render, update or event handler is borrowed for that call. A request-scoped widget may retain child Elements it creates between its render and update calls within the same Request lifecycle, but should access them only from those calls. Do not retain an Element in longer-lived application state or pass it to background work: the embedded Request may later be pooled and reused for another connection.

func (*Element) AddHandlers added in v0.300.0

func (elem *Element) AddHandlers(h ...any)

AddHandlers adds the given handlers to the Element.

It must be called while the Element is being rendered, before any event can be processed for it; see the package "Locking" documentation. Handlers added after Element.JawsRender has returned (or Element.Freeze has been called) are dropped; debug builds panic.

func (*Element) Append added in v0.31.0

func (elem *Element) Append(htmlCode template.HTML)

Append appends a new HTML element as a child to the current one.

Call this while the Element is rendering or updating, when a send pass is imminent. To change the Element in response to a browser event, mark it dirty with Request.Dirty instead: a change queued directly from an event handler is flushed only when the processing loop is next woken, which on an otherwise-idle request is not guaranteed to be prompt (see [Element.queue]).

func (*Element) ApplyGetter added in v0.75.0

func (elem *Element) ApplyGetter(getter any) (tagValue any, attrs []template.HTMLAttr, err error)

ApplyGetter examines getter and resolves its tag candidate.

If getter implements tag.TagGetter, the candidate is its returned value; otherwise the candidate is getter itself. TagGetter values, supported tag slices and runtime-comparable candidates are passed to Element.Tag for normal validation. Other non-comparable candidates are not automatically tagged, matching ParseParams.

If getter is an InputHandler, ClickHandler, ContextMenuHandler or InitialHTMLAttrHandler, relevant values are added to the Element.

Finally, if getter is an InitHandler, its JawsInit function is called.

Returns the tag that was added (nil if none was added, whether because getter was nil or its candidate was not usable as a tag), any initial HTML attrs provided by InitialHTMLAttrHandler, and any error returned from JawsInit() if it was called.

If the Element is already frozen and getter is an event handler, the handler is not added: in production with a Jaws.Logger configured this is logged and tag and init processing still occur, while debug builds and servers without a Logger panic via reportMisuse, aborting before tag and init processing. A non-event-handler getter never calls reportMisuse, so its tag and init processing always occur.

func (*Element) ApplyParams added in v0.60.0

func (elem *Element) ApplyParams(params []any) (attrs []template.HTMLAttr)

ApplyParams parses the parameters passed to UI() when creating a new Element, adding UI tags, adding any additional event handlers found.

Returns the list of HTML attributes found, if any.

Handlers found in params are added only while the Element is mutable; after it is frozen (Element.JawsRender returning or Element.Freeze) they are dropped (debug builds panic), though tags and HTML attributes are still processed.

func (*Element) Deleted added in v0.600.0

func (elem *Element) Deleted() bool

Deleted reports whether the Element has been removed from its Request.

Element.JawsRender, Element.JawsUpdate and the queue helpers are no-ops on a deleted Element. A request-scoped widget that retains child Elements it creates between render and update calls within one Request lifecycle can use Deleted to detect and discard children removed out-of-band before reuse. Deleted is not a lifetime check: it does not report whether the embedded Request still represents the owning connection or make that Request safe to use after its lifecycle.

func (*Element) Freeze added in v0.500.0

func (elem *Element) Freeze()

Freeze marks the Element's handlers as final, as Element.JawsRender does on return. After Freeze, the handler-mutating methods (AddHandlers, ApplyParams, ApplyGetter) drop handlers; debug builds panic. Use this for elements registered for updates without being rendered.

func (*Element) HasTag added in v0.31.0

func (elem *Element) HasTag(tagValue any) bool

HasTag returns true if this Element has the given tag.

func (*Element) InsertBefore added in v0.601.0

func (elem *Element) InsertBefore(child *Element, htmlCode template.HTML)

InsertBefore inserts new HTML immediately before child.

child must be a live, distinct Element belonging to the same Request as elem. Violations are reported as ErrInvalidChildElement, and no browser command is queued. The browser also verifies that child is a direct DOM child of elem before applying the insertion.

Call this while elem is rendering or updating, when a send pass is imminent. To insert HTML at the same child index in every element matching a tag, use Jaws.Insert.

func (*Element) JawsRender added in v0.55.0

func (elem *Element) JawsRender(w io.Writer, params []any) (err error)

JawsRender calls Renderer.JawsRender for this Element.

Do not call this yourself unless it is from within another JawsRender implementation.

func (*Element) JawsUpdate added in v0.55.0

func (elem *Element) JawsUpdate()

JawsUpdate calls Updater.JawsUpdate for this Element.

Do not call this yourself unless it is from within another JawsUpdate implementation.

func (*Element) Jid added in v0.31.0

func (elem *Element) Jid() jid.Jid

Jid returns the JaWS ID for this Element, unique within its Request.

func (*Element) JsCall added in v0.75.0

func (elem *Element) JsCall(jsfunc, jsonstr string)

JsCall queues a browser JavaScript function path call for the Element.

In the receiving browser, jsfunc is resolved as a path from window and called with JSON.parse(jsonstr); the Element is not passed as this or as an argument.

Call this while the Element is rendering or updating, when a send pass is imminent; a call queued directly from an event handler is only flushed when the processing loop is next woken (see [Element.queue]). To call JavaScript for every element matching a tag, use Jaws.JsCall.

func (*Element) Order added in v0.31.0

func (elem *Element) Order(jidList []jid.Jid)

Order reorders the HTML elements.

Call this while the Element is rendering or updating, when a send pass is imminent. To change the Element in response to a browser event, mark it dirty with Request.Dirty instead: a change queued directly from an event handler is flushed only when the processing loop is next woken, which on an otherwise-idle request is not guaranteed to be prompt (see [Element.queue]).

func (*Element) Remove added in v0.31.0

func (elem *Element) Remove(child *Element)

Remove removes child from the browser and its Request registry.

child must be a live, distinct Element belonging to the same Request as elem. Violations are reported as ErrInvalidChildElement, and neither the DOM nor the registry is changed. The caller is responsible for ensuring child is a direct DOM child of elem; the browser verifies that relationship before applying the removal.

Call this while the Element is rendering or updating, when a send pass is imminent. To change the Element in response to a browser event, mark it dirty with Request.Dirty instead: a change queued directly from an event handler is flushed only when the processing loop is next woken, which on an otherwise-idle request is not guaranteed to be prompt (see [Element.queue]).

func (*Element) RemoveAttr added in v0.31.0

func (elem *Element) RemoveAttr(attr string)

RemoveAttr queues sending a request to remove an attribute to the browser for the Element.

Call this while the Element is rendering or updating, when a send pass is imminent. To change the Element in response to a browser event, mark it dirty with Request.Dirty instead: a change queued directly from an event handler is flushed only when the processing loop is next woken, which on an otherwise-idle request is not guaranteed to be prompt (see [Element.queue]).

func (*Element) RemoveClass added in v0.31.0

func (elem *Element) RemoveClass(cls string)

RemoveClass queues sending a request to remove a class to the browser for the Element.

Call this while the Element is rendering or updating, when a send pass is imminent. To change the Element in response to a browser event, mark it dirty with Request.Dirty instead: a change queued directly from an event handler is flushed only when the processing loop is next woken, which on an otherwise-idle request is not guaranteed to be prompt (see [Element.queue]).

func (*Element) Replace added in v0.31.0

func (elem *Element) Replace(htmlCode template.HTML)

Replace replaces the Element's entire HTML DOM node with new HTML code.

The trusted HTML should preserve the element identity by putting the element's own JaWS id on the replacement root element, normally as id="Jid.N". Replace is not an HTML validator: it performs only a lightweight textual guard for that expected id attribute. If the guard does not find it, the call is a programming error: debug builds panic and production builds report it via Jaws.MustLog and skip the replacement.

Call this while the Element is rendering or updating, when a send pass is imminent. To change the Element in response to a browser event, mark it dirty with Request.Dirty instead: a change queued directly from an event handler is flushed only when the processing loop is next woken, which on an otherwise-idle request is not guaranteed to be prompt (see [Element.queue]).

func (*Element) SetAttr added in v0.31.0

func (elem *Element) SetAttr(attr, value string)

SetAttr queues sending a new attribute value to the browser for the Element.

The value parameter must be the unescaped logical attribute value. It is sent to the browser DOM and used as the value argument to setAttribute().

Call this while the Element is rendering or updating, when a send pass is imminent. To change the Element in response to a browser event, mark it dirty with Request.Dirty instead: a change queued directly from an event handler is flushed only when the processing loop is next woken, which on an otherwise-idle request is not guaranteed to be prompt (see [Element.queue]).

func (*Element) SetClass added in v0.31.0

func (elem *Element) SetClass(cls string)

SetClass queues sending a class to the browser for the Element.

Call this while the Element is rendering or updating, when a send pass is imminent. To change the Element in response to a browser event, mark it dirty with Request.Dirty instead: a change queued directly from an event handler is flushed only when the processing loop is next woken, which on an otherwise-idle request is not guaranteed to be prompt (see [Element.queue]).

func (*Element) SetInner added in v0.31.0

func (elem *Element) SetInner(innerHTML template.HTML)

SetInner queues sending new inner HTML content to the browser for the Element.

Call this while the Element is rendering or updating, when a send pass is imminent. To change the Element in response to a browser event, mark it dirty with Request.Dirty instead: a change queued directly from an event handler is flushed only when the processing loop is next woken, which on an otherwise-idle request is not guaranteed to be prompt (see [Element.queue]).

func (*Element) SetValue added in v0.31.0

func (elem *Element) SetValue(value string)

SetValue queues sending a new current input value in textual form to the browser for the Element.

Call this while the Element is rendering or updating, when a send pass is imminent. To change the Element in response to a browser event, mark it dirty with Request.Dirty instead: a change queued directly from an event handler is flushed only when the processing loop is next woken, which on an otherwise-idle request is not guaranteed to be prompt (see [Element.queue]).

func (*Element) String added in v0.31.0

func (elem *Element) String() string

func (*Element) Tag added in v0.31.0

func (elem *Element) Tag(tags ...any)

Tag adds the given tags to the Element.

func (*Element) UI added in v0.112.1

func (elem *Element) UI() UI

UI returns the UI object.

type HandleFunc added in v0.111.6

type HandleFunc = func(pattern string, handler http.Handler)

HandleFunc matches the signature of http.ServeMux.Handle.

type InitHandler added in v0.110.0

type InitHandler interface {
	JawsInit(elem *Element) (err error)
}

InitHandler allows initializing UI getters and setters before their use.

You can of course initialize them in the call from the template engine, but at that point you don't have access to the Element, Request.Context or Request.Session.

type InitialHTMLAttrHandler added in v0.400.0

type InitialHTMLAttrHandler interface {
	// JawsInitialHTMLAttr is called when an [Element] is initially rendered,
	// and may return an initial HTML attribute string to write out.
	JawsInitialHTMLAttr(elem *Element) (s template.HTMLAttr)
}

InitialHTMLAttrHandler can add attributes during initial Element rendering.

type InputFn added in v0.401.0

type InputFn = func(elem *Element, value string) (err error)

InputFn is the signature of an input handling function. JaWS calls it for an input or set message received from JavaScript over the WebSocket connection, and for a hook message, which tests use to invoke the handler synchronously (see what.Hook).

type InputHandler added in v0.401.0

type InputHandler interface {
	// JawsInput is called when an [Element] receives a browser input event.
	JawsInput(elem *Element, value string) (err error)
}

InputHandler handles input events sent from the browser.

type Jaws

type Jaws struct {
	CookieName              string          // Name for session cookies; defaults to a name derived from the executable ([assets.DefaultCookieName]), falling back to "jaws"
	AutoSession             bool            // Create a session during a successful WebSocket upgrade when a Request has none. Defaults to false.
	TrustForwardedHeaders   bool            // Trust X-Forwarded-* headers: governs the session cookie Secure flag (X-Forwarded-Proto) and the client IP used for session/request binding (X-Forwarded-For/X-Real-IP). Defaults to false; only enable behind a single reverse proxy you control that sets these headers.
	Logger                  Logger          // Optional logger to use
	Debug                   bool            // Set to true to enable debug info in generated HTML code. Call GenerateHeadHTML after changing it.
	MakeAuth                MakeAuthFn      // Function to create ui.With.Auth for Templates. If nil, templates get the fail-open DefaultAuth (IsAdmin()==true for everyone); set it to enforce authorization. See DefaultAuth.
	BaseContext             context.Context // Non-nil base context for Requests, set to context.Background() in New()
	WebSocketPingInterval   time.Duration   // Interval between keepalive pings on active WebSocket connections. Defaults to DefaultWebSocketPingInterval. Set <=0 to disable keepalive pings.
	MaxPendingRequestsPerIP int             // Maximum number of unclaimed Requests per client IP. Defaults to DefaultMaxPendingRequestsPerIP. Set <=0 to disable the cap.
	// contains filtered or unexported fields
}

Jaws holds the server-side state and configuration for a JaWS instance.

A single Jaws value coordinates template lookup, session handling and the request lifecycle that keeps the browser and backend synchronized via WebSockets. The zero value is not ready for use; construct instances with New to ensure the helper goroutines and static assets are prepared.

The exported configuration fields are ordinary fields, not live synchronized settings. Several are consulted on each connection or request (for example MaxPendingRequestsPerIP and WebSocketPingInterval), so set them all before exposing handlers, creating Requests, or starting Jaws.Serve / Jaws.ServeWithTimeout; mutating one after serving has begun is an unsynchronized write and is not supported. Methods document their own concurrency behavior and may be called concurrently when stated.

func New

func New() (jw *Jaws, err error)

New allocates a JaWS instance with the default configuration.

The returned Jaws value is ready for use: static assets are embedded, the broadcast channels and update ticker are allocated and the request pool is primed. You must still start the processing loop with Jaws.Serve or Jaws.ServeWithTimeout on its own goroutine before broadcasting. Call Jaws.Close when finished with the instance to free associated resources.

func (*Jaws) AddTemplateLookuper added in v0.45.0

func (jw *Jaws) AddTemplateLookuper(tl TemplateLookuper) (err error)

AddTemplateLookuper adds a TemplateLookuper.

The lookuper must be comparable so it can be removed with Jaws.RemoveTemplateLookuper.

func (*Jaws) Alert

func (jw *Jaws) Alert(level, msg string)

Alert sends an alert to all active Request values.

The level argument should be one of Bootstrap's alert levels: primary, secondary, success, danger, warning, info, light or dark.

The level and msg are HTML-escaped before being sent, so it is safe to pass untrusted text; do not pre-escape it.

func (*Jaws) Append

func (jw *Jaws) Append(target any, html template.HTML)

Append calls the JavaScript appendChild method on all HTML elements matching target.

func (*Jaws) Broadcast

func (jw *Jaws) Broadcast(msg wire.Message)

Broadcast sends msg to the active Request and Element values selected by wire.Message.Dest.

It must not be called before the JaWS processing loop (Jaws.Serve or Jaws.ServeWithTimeout) is running. Otherwise this call may block.

All convenience helpers on Jaws that call Broadcast inherit this requirement.

A nil wire.Message.Dest targets every active Request; a key.Key Dest targets the active Request with that identity key, and a zero key is dropped. Any other Dest is expanded into tags. Plain strings and Jid values are illegal tag types; use tag.Tag, a domain tag, or an Element method instead.

A wire.Message.Dest that cannot be expanded into tags (an illegal tag type) is reported through Jaws.MustLog, which panics when no Jaws.Logger is set; with a Logger the error is logged and the message is sent to the destinations that did expand.

func (*Jaws) Close

func (jw *Jaws) Close()

Close initiates shutdown of the Jaws instance.

Jaws.Done is closed as shutdown begins. Before Close returns, the context returned by Request.Context for every current Request is canceled, including pending Requests whose WebSocket never connected. Non-running Requests become unclaimable but retain their identity while callers hold them. Active WebSocket handlers observe cancellation and finish asynchronously.

Calls to Jaws.NewRequest after shutdown begins return Requests with already-canceled contexts that Jaws.UseRequest cannot claim. Broadcasts and sends may be discarded after Done closes. Subsequent calls to Close have no effect.

func (*Jaws) ContentSecurityPolicy added in v0.300.0

func (jw *Jaws) ContentSecurityPolicy() (s string)

ContentSecurityPolicy returns the generated Content-Security-Policy header value.

func (*Jaws) DefaultAuth added in v0.600.0

func (jw *Jaws) DefaultAuth() *DefaultAuth

DefaultAuth returns the shared fail-open DefaultAuth used for templates when Jaws.MakeAuth is nil.

It is created on first use and reused, so the sync.Once warning in DefaultAuth.IsAdmin fires at most once per Jaws rather than once per template render. The value of Jaws.Logger in effect at first use is captured.

func (*Jaws) Delete added in v0.31.0

func (jw *Jaws) Delete(target any)

Delete removes the HTML element(s) matching target.

func (*Jaws) Dirty added in v0.31.0

func (jw *Jaws) Dirty(dirtyTags ...any)

Dirty marks all Element values that have one or more of the given tags as dirty.

If any tag implements tag.TagGetter it is called with a nil Request; prefer Request.Dirty, which avoids this. A tag that is not hashable panics the calling goroutine, but the panic is contained there and the Jaws.Serve loop is unaffected. Request.Dirty behaves the same here.

func (*Jaws) Done

func (jw *Jaws) Done() <-chan struct{}

Done returns a channel closed when Jaws.Close begins shutdown.

func (*Jaws) FaviconURL added in v0.111.6

func (jw *Jaws) FaviconURL() (s string)

FaviconURL returns the favicon URL discovered by Jaws.GenerateHeadHTML.

func (*Jaws) GenerateHeadHTML added in v0.5.0

func (jw *Jaws) GenerateHeadHTML(extra ...string) (err error)

GenerateHeadHTML regenerates the HTML code that goes in the HEAD section, ensuring that the provided URL resources in extra are loaded, along with the JaWS JavaScript.

If one of the resources is named "favicon", its URL will be stored and can be retrieved using Jaws.FaviconURL.

If one or more URLs in extra fail to parse, GenerateHeadHTML still installs the regenerated head HTML and Content-Security-Policy with the failing resources omitted, and returns the joined parse errors.

You only need to call this if you add your own images, scripts and stylesheets.

func (*Jaws) GetSession added in v0.11.0

func (jw *Jaws) GetSession(r *http.Request) (sess *Session)

GetSession returns the Session associated with the given http.Request, or nil.

Sessions are bound to the client IP (see the clientIP method). Behind a reverse proxy that connects over loopback, every request appears to come from loopback and IP binding is effectively disabled unless Jaws.TrustForwardedHeaders is enabled so the forwarded client IP is used instead.

func (*Jaws) Insert

func (jw *Jaws) Insert(target any, childIndex int, html template.HTML)

Insert inserts html before the child at childIndex in every element matching target.

target follows Jaws.Broadcast's tag rules. For request-local insertion before a known child, use Element.InsertBefore.

A negative childIndex is reported as ErrInvalidChildIndex and no message is sent. Use Jaws.Append to insert at the end. html is trusted HTML, matching Jaws.SetInner and Jaws.Append.

func (*Jaws) JsCall added in v0.114.0

func (jw *Jaws) JsCall(target any, jsfunc, jsonstr string)

JsCall calls a browser JavaScript function path for matching targets.

target selects which requests or elements receive the Call message. In each receiving browser, jsfunc is resolved as a path from window and called with JSON.parse(jsonstr); the matched element is not passed as this or as an argument. A nil target calls each active Request once. A nonzero key.Key target calls the matching active Request once without requiring a matching DOM element; a zero key is ignored. Other targets follow Jaws.Broadcast's tag rules.

func (*Jaws) Log

func (jw *Jaws) Log(err error) error

Log sends an error to the Jaws.Logger if set. Has no effect if err is nil or the Logger is nil. Returns err.

func (*Jaws) LookupTemplate added in v0.66.0

func (jw *Jaws) LookupTemplate(name string) *template.Template

LookupTemplate queries the known TemplateLookuper values in the order they were added and returns the first found.

func (*Jaws) MustLog added in v0.1.1

func (jw *Jaws) MustLog(err error)

MustLog sends an error to the Jaws.Logger if set, or panics with the given error if the Logger is nil. Has no effect if err is nil.

Some update-time paths cannot return errors to their caller and report them through MustLog. Set Jaws.Logger when those errors should be logged instead of treated as fatal programming errors.

func (*Jaws) NewRequest

func (jw *Jaws) NewRequest(r *http.Request) (rq *Request)

NewRequest returns a new JaWS Request.

While the Jaws instance is open, the returned Request is pending until it is claimed or retired.

Call this as soon as you start processing an HTML request, and store the returned Request pointer so it can be used while constructing the HTML response in order to register the JaWS IDs you use in the response, and use its Request.JawsKey when sending the JavaScript portion of the reply. Do not retain the pointer beyond the initial HTTP handling and rendering; see Request.

Automatic timeout handling is performed by Jaws.ServeWithTimeout. The default Jaws.Serve helper uses a 10-second timeout.

A Request created after Jaws.Close has an already-canceled context and cannot be claimed by Jaws.UseRequest.

When timeout maintenance or the per-IP pending limit retires an unclaimed Request, its key becomes unclaimable. The key also remains unavailable for assignment to another Request while the retired Request is reachable; no deadline is guaranteed for later reuse.

NewRequest panics if the system CSPRNG (crypto/rand) fails while generating the request key, which does not happen on supported platforms.

func (*Jaws) NewSession added in v0.26.0

func (jw *Jaws) NewSession(w http.ResponseWriter, r *http.Request) (sess *Session)

NewSession creates a new Session.

Any pre-existing Session will be cleared and closed. This may call Session.Close on an existing session and therefore requires the JaWS processing loop (Jaws.Serve or Jaws.ServeWithTimeout) to be running.

Subsequent Request values created with Jaws.NewRequest that have the cookie set and originate from the same IP will be able to access the Session. The IP comparison is the same loopback-aware, optionally forwarded-header-based match used everywhere else; see Jaws.GetSession and Jaws.TrustForwardedHeaders for the reverse-proxy caveat.

As a side effect, the session cookie is also added to r itself, so the new Session is visible to Jaws.GetSession and Jaws.NewRequest for the remainder of the same HTTP request.

It panics if the system CSPRNG (crypto/rand) fails while generating the session ID, which does not happen on supported platforms.

func (*Jaws) Pending

func (jw *Jaws) Pending() (n int)

Pending returns the number of requests waiting for their WebSocket callbacks.

func (*Jaws) Redirect

func (jw *Jaws) Redirect(url string)

Redirect requests all active Request values to navigate to the given URL.

The URL is validated to be a relative path or an http/https URL; script-bearing schemes such as javascript: and protocol-relative ("//host") URLs are refused and logged rather than sent to the browser.

func (*Jaws) Reload

func (jw *Jaws) Reload()

Reload requests all active Request values to reload their current page.

func (*Jaws) RemoveAttr

func (jw *Jaws) RemoveAttr(target any, attr string)

RemoveAttr sends a request to remove the given attribute from all HTML elements matching target.

func (*Jaws) RemoveClass added in v0.31.0

func (jw *Jaws) RemoveClass(target any, cls string)

RemoveClass sends a request to remove the given class from all HTML elements matching target.

func (*Jaws) RemoveTemplateLookuper added in v0.45.0

func (jw *Jaws) RemoveTemplateLookuper(tl TemplateLookuper) (err error)

RemoveTemplateLookuper removes the given TemplateLookuper.

func (*Jaws) Replace

func (jw *Jaws) Replace(target any, html template.HTML)

Replace replaces HTML on all HTML elements matching target.

html is trusted HTML, matching Jaws.SetInner and Jaws.Append.

func (*Jaws) RequestCount added in v0.25.0

func (jw *Jaws) RequestCount() (n int)

RequestCount returns the total Request count.

It equals the total returned by Jaws.RequestCounts.

func (*Jaws) RequestCounts added in v0.407.0

func (jw *Jaws) RequestCounts() (total, active int)

RequestCounts returns the total and active Request counts.

The total includes pending, claimed, and active Request values. It excludes retired Requests, even if an initial HTTP handler still holds them. The active count includes Requests whose Request.ServeHTTP loop is running.

func (*Jaws) SecureHeadersMiddleware added in v0.300.0

func (jw *Jaws) SecureHeadersMiddleware(next http.Handler) http.Handler

SecureHeadersMiddleware wraps next with security headers that match the current JaWS configuration.

It clones secureheaders.DefaultHeaders(), replacing the Content-Security-Policy value with Jaws.ContentSecurityPolicy for each request so responses allow the resources configured by Jaws.GenerateHeadHTML.

The returned middleware does not trust forwarded HTTPS headers. Note that the session cookie Secure flag is governed separately by Jaws.TrustForwardedHeaders (also false by default), so the two stay consistent unless you opt in. The next handler must be non-nil.

func (*Jaws) Serve

func (jw *Jaws) Serve()

Serve calls ServeWithTimeout(DefaultWebSocketTimeout). It is intended to run on its own goroutine. It returns when Jaws.Close is called.

func (*Jaws) ServeHTTP added in v0.19.0

func (jw *Jaws) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP can handle the required JaWS endpoints, which all start with "/jaws/".

The method is checked per matched endpoint, not up front: the static asset and .ping endpoints answer GET and HEAD (any other method gets 405 with an Allow header), while the per-Request key and tail-script endpoints are GET-only capability URLs that fall through to 404 on any other method. An unknown path or a wrong method on a capability URL therefore 404s rather than 405s, and never reveals whether a key is valid.

func (*Jaws) ServeWithTimeout

func (jw *Jaws) ServeWithTimeout(requestTimeout time.Duration)

ServeWithTimeout begins processing requests with the given timeout. It is intended to run on its own goroutine. It returns when Jaws.Close is called.

func (*Jaws) SessionCount added in v0.11.0

func (jw *Jaws) SessionCount() (n int)

SessionCount returns the number of active sessions.

func (*Jaws) SessionMiddleware added in v0.600.0

func (jw *Jaws) SessionMiddleware(h http.Handler) http.Handler

SessionMiddleware returns an http.Handler that ensures a JaWS Session exists before invoking h, creating one if the request has none.

It is the session-ensuring middleware, distinct from the session accessors: Jaws.GetSession and Request.Session look up an existing Session, while this wraps a handler. It composes with Jaws.SecureHeadersMiddleware.

func (*Jaws) Sessions added in v0.11.0

func (jw *Jaws) Sessions() (sessions []*Session)

Sessions returns a list of all active sessions, which may be nil.

func (*Jaws) SetAttr

func (jw *Jaws) SetAttr(target any, attr, value string)

SetAttr sends a request to replace the given attribute value in all HTML elements matching target.

The value parameter must be the unescaped logical attribute value. It is sent to the browser DOM and used as the value argument to setAttribute().

func (*Jaws) SetClass added in v0.31.0

func (jw *Jaws) SetClass(target any, cls string)

SetClass sends a request to set the given class in all HTML elements matching target.

func (*Jaws) SetInner

func (jw *Jaws) SetInner(target any, innerHTML template.HTML)

SetInner sends a request to replace the inner HTML of all HTML elements matching target.

func (*Jaws) SetValue

func (jw *Jaws) SetValue(target any, value string)

SetValue sends a request to set the current input value (in textual form) of all HTML elements matching target. It sets the live DOM value/state, not the HTML "value" attribute.

func (*Jaws) Setup added in v0.111.6

func (jw *Jaws) Setup(handleFn HandleFunc, prefix string, extras ...any) (err error)

Setup configures Jaws with extra functionality and resources.

The list of extras can be strings, *url.URL, *staticserve.StaticServe or []*staticserve.StaticServe URL resources, or a setup function matching SetupFunc such as jawsboot.Setup.

It calls Jaws.GenerateHeadHTML with the final list of URLs, with any relative URL paths prefixed with prefix.

If handleFn is nil, Setup generates head HTML from the configured resources without registering any handlers.

func (*Jaws) TestServe added in v0.500.0

func (jw *Jaws) TestServe(rq *Request, onPanic func(recovered any)) (inCh chan wire.WsMsg, outCh chan wire.WsMsg, bcastCh chan wire.Message, readyCh, doneCh chan struct{})

TestServe runs rq's WebSocket message-processing loop for test harnesses, including the out-of-package harness in github.com/linkdata/jaws/jawstest.

It subscribes rq to broadcasts, waits for the running Serve loop to process the subscription, then runs rq.process in a new goroutine using freshly created inbound/outbound channels, recycling rq when the loop stops. It panics if the Jaws processing loop (Jaws.Serve or Jaws.ServeWithTimeout) is not running.

TestServe is exported solely to let test harnesses outside package jaws drive a request loop without access to unexported internals. It is not intended for production use; it does not import any testing-only packages, so it does not pull net/http/httptest into the production build.

onPanic must be non-nil; it is called with the recovered value (nil if the loop exited normally) when the loop goroutine stops, before doneCh is closed, so a harness can publish captured panic state before any <-doneCh waiter observes it. A harness that does not expect panics should re-panic when the value is non-nil so unexpected loop panics still surface.

func (*Jaws) UseRequest

func (jw *Jaws) UseRequest(jawsKey key.Key, r *http.Request) (rq *Request)

UseRequest extracts the JaWS Request with the given key from the request map if it exists and the HTTP request remote IP matches.

Call it when receiving the WebSocket connection on "/jaws/:key" to get the associated Request, and then call its Request.ServeHTTP method to process the WebSocket messages.

Returns nil if the key was not found, the request was already claimed by an earlier WebSocket callback, or the IP doesn't match, in which case you should return an HTTP "404 Not Found" status.

The returned pointer is borrowed for WebSocket handling. Do not retain it after Request.ServeHTTP returns; see Request.

type Jid added in v0.31.0

type Jid = jid.Jid // convenience alias

Jid is the identifier type used for HTML elements managed by JaWS.

It is provided as a convenience alias to the value defined in the jid subpackage so applications do not have to import that package directly when working with element IDs.

type Logger added in v0.110.1

type Logger interface {
	Info(msg string, args ...any)
	Warn(msg string, args ...any)
	Error(msg string, args ...any)
}

Logger is satisfied by a *log/slog.Logger via its Info, Warn and Error methods.

type MakeAuthFn added in v0.85.0

type MakeAuthFn = func(rq *Request) Auth

MakeAuthFn constructs an Auth value for a Request.

Set Jaws.MakeAuth to your implementation to enforce real authorization. If Jaws.MakeAuth is left nil, templates receive DefaultAuth, which is fail-open: see its documentation.

It is a type alias so a bare func value can be assigned without conversion, matching the sibling callback types ConnectFn, InputFn and HandleFunc.

type Renderer added in v0.60.0

type Renderer interface {
	// JawsRender is called once per [Element] when rendering the initial webpage.
	// Do not call this yourself unless it is from within another JawsRender implementation.
	// The engine does not invoke this once the [Element] is deleted (see [Element.Deleted]).
	JawsRender(elem *Element, w io.Writer, params []any) error
}

Renderer renders the initial HTML for a UI object.

type Request

type Request struct {
	Jaws    *Jaws   // (read-only) the JaWS instance the Request belongs to
	JawsKey key.Key // (read-only) random key assigned to this Request; routes JaWS URLs and request-targeted broadcasts only while registered
	// contains filtered or unexported fields
}

Request maintains the state for a JaWS WebSocket connection, and handles processing of events and broadcasts.

A Request pointer is borrowed for the HTTP or WebSocket lifecycle that supplied it. Do not retain it in application state or use it from a background goroutine: a Request that enters Request.ServeHTTP may be returned to an internal pool when ServeHTTP returns, and the same pointer may later represent another connection. Background work should retain Request.Context and, when it must terminate the connection, the cancel function returned while deriving a replacement context through Request.SetContext.

Unlike Session, whose methods are nil-safe, Request methods are not safe to call on a nil *Request: a Request is always obtained from Jaws.NewRequest or Jaws.UseRequest and is never legitimately nil. The nil-receiver guard on Request.JawsKeyString (and thus Request.String) lets a nil Request render into error text, while those on Request.Log and Request.MustLog let it forward to the logger; both exist only for that diagnostic use, not as a public nil-safe contract.

func (*Request) Alert

func (rq *Request) Alert(level, msg string)

Alert attempts to show an alert message on the current request webpage if it has an HTML element with the data-jaws-alerts attribute.

The level argument should be one of Bootstrap's alert levels: primary, secondary, success, danger, warning, info, light or dark.

The level and msg are HTML-escaped before being sent, so it is safe to pass untrusted text; do not pre-escape it.

The default JaWS JavaScript only supports Bootstrap dismissible alerts.

See Request for pointer lifetime and Jaws.Broadcast for processing-loop requirements.

func (*Request) AlertError

func (rq *Request) AlertError(err error)

AlertError logs err via Jaws.Log and, if it is non-nil, also shows it to the current request as a danger-level Request.Alert.

func (*Request) Cancel added in v0.500.0

func (rq *Request) Cancel(err error)

Cancel aborts the Request.

It cancels the Request's context with the given cause (logged via Jaws.Logger); the WebSocket processing loop and its goroutines observe the cancelled context and shut down asynchronously. Cancel returns immediately and does not wait for teardown. It is safe to call synchronously from UI code, for example to terminate a connection that violates a server-side limit. A nil err cancels without a specific cause.

Do not retain the Request for asynchronous cancellation; use Request.SetContext and retain the derived context's cancellation function instead.

func (*Request) Context

func (rq *Request) Context() (ctx context.Context)

Context returns the Request's context.

The context is derived from Jaws.BaseContext by default. Unlike the Request pointer, it may be retained by background work.

func (*Request) DeleteElement added in v0.300.0

func (rq *Request) DeleteElement(elem *Element)

DeleteElement removes elem from the Request element registry without queueing a browser operation.

Use Element.Remove to remove a managed DOM child and unregister it together. DeleteElement is intended for elements that were never successfully rendered, or whose DOM lifecycle is managed separately.

A nil elem is a no-op, matching Request.Tag, Request.TagExpanded and Request.TagsOf; passing the nil that Request.GetElementByJid returns for an unknown Jid is therefore safe.

func (*Request) Dirty added in v0.31.0

func (rq *Request) Dirty(dirtyTags ...any)

Dirty marks all Element values that have one or more of the given tags as dirty.

func (*Request) Get added in v0.11.0

func (rq *Request) Get(key string) any

Get is shorthand for Session.Get.

It returns the session value associated with key, or nil if no session is associated with the Request.

func (*Request) GetConnectFn added in v0.7.0

func (rq *Request) GetConnectFn() (fn ConnectFn)

GetConnectFn returns the currently set ConnectFn. That function will be called before starting the WebSocket tunnel if not nil.

func (*Request) GetElementByJid added in v0.300.0

func (rq *Request) GetElementByJid(jid Jid) (elem *Element)

GetElementByJid returns the element with jid, or nil if it is not known.

func (*Request) GetElements added in v0.31.0

func (rq *Request) GetElements(tagValue any) (elems []*Element)

GetElements returns a list of the UI elements in the Request that have the given tags.

func (*Request) HasTag added in v0.31.0

func (rq *Request) HasTag(elem *Element, tagValue any) (yes bool)

HasTag reports whether elem has tagValue in rq.

func (*Request) HeadHTML

func (rq *Request) HeadHTML(w io.Writer) (err error)

HeadHTML writes the configured resources and Request key metadata for the page head.

func (*Request) Initial added in v0.8.0

func (rq *Request) Initial() (r *http.Request)

Initial returns the Request's initial HTTP request, or nil.

func (*Request) JawsKeyString

func (rq *Request) JawsKeyString() string

JawsKeyString returns the request key in the text form used by JaWS URLs.

func (*Request) Log added in v0.300.0

func (rq *Request) Log(err error) error

Log sends an error to the Jaws.Logger if set. Has no effect if err is nil or the Logger is nil. Returns err.

func (*Request) MarkWritten added in v0.600.0

func (rq *Request) MarkWritten()

MarkWritten records that the Request's initial HTML is being written, so the pending-eviction logic spares it while a render is in flight.

[RequestWriter.Write] calls it on every write. It is lock-free and safe to call concurrently. Concurrent calls never move the recorded second backward.

func (*Request) MustLog added in v0.300.0

func (rq *Request) MustLog(err error)

MustLog sends an error to the Jaws.Logger if set, or panics with the given error if the Logger is nil. Has no effect if err is nil.

Some update-time paths cannot return errors to their caller and report them through MustLog. Set Jaws.Logger when those errors should be logged instead of treated as fatal programming errors.

func (*Request) NewElement added in v0.31.0

func (rq *Request) NewElement(ui UI) *Element

NewElement creates a new Element using the given UI object.

The UI value becomes scoped to rq and must not be used with another Request. See UI for the ownership contract.

Panics if the build tag "debug" is set and the UI object doesn't satisfy all requirements.

func (*Request) Redirect

func (rq *Request) Redirect(url string)

Redirect requests the current Request to navigate to the given URL.

The URL is validated to be a relative path or an http/https URL; script-bearing schemes such as javascript: and protocol-relative ("//host") URLs are refused and logged rather than sent to the browser.

See Request for pointer lifetime and Jaws.Broadcast for processing-loop requirements.

func (*Request) ServeHTTP

func (rq *Request) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP implements http.Handler.

Requires Jaws.UseRequest to have been successfully called for the Request. The JaWS processing loop (Jaws.Serve or Jaws.ServeWithTimeout) must also be running so the request can subscribe to broadcasts and unsubscribe on exit.

func (*Request) Session added in v0.14.0

func (rq *Request) Session() (sess *Session)

Session returns the Request's Session, or nil.

func (*Request) Set added in v0.11.0

func (rq *Request) Set(key string, value any)

Set is shorthand for Session.Set.

It associates value with key in the session; a nil value removes the key. It does nothing if no session is associated with the Request.

func (*Request) SetConnectFn added in v0.7.0

func (rq *Request) SetConnectFn(fn ConnectFn)

SetConnectFn sets the ConnectFn. That function will be called before starting the WebSocket tunnel if not nil.

func (*Request) SetContext added in v0.110.0

func (rq *Request) SetContext(fn func(oldCtx context.Context) (newCtx context.Context))

SetContext atomically transforms the Request's context.

fn receives the current context and must return a non-nil context derived from it so cancellation and deadlines continue to propagate. Cancellation or deadline expiration of the returned context wakes a running Request.ServeHTTP loop promptly, even while it is idle; no WebSocket event or broadcast is required.

fn runs while the Request lock is held. It must not call methods on the same Request, call code that may do so, or block on work that needs the same Request. SetContext panics if fn is nil. If fn panics, SetContext releases the lock and propagates the panic.

Background work that must cancel the Request should create a derived context in fn and retain that context's cancellation function, not the Request pointer.

Returning a nil context is a programming error: debug builds panic and production builds report it through Jaws.MustLog and retain the current context.

func (*Request) String

func (rq *Request) String() string

String returns the Request in the form "Request<key>", using Request.JawsKeyString to encode the key. Like JawsKeyString it tolerates a nil receiver for diagnostics only; see the Request type documentation.

func (*Request) Tag added in v0.31.0

func (rq *Request) Tag(elem *Element, tagItems ...any)

Tag adds the given tags to the given Element.

func (*Request) TagExpanded added in v0.300.0

func (rq *Request) TagExpanded(elem *Element, expandedTags []any)

TagExpanded adds already-expanded tags to the given Element.

func (*Request) TagsOf added in v0.31.0

func (rq *Request) TagsOf(elem *Element) (tags []any)

TagsOf returns the tags currently associated with elem in this Request, or nil if elem is nil. The returned slice is a newly allocated snapshot and may be retained and modified by the caller.

func (*Request) TailHTML added in v0.79.0

func (rq *Request) TailHTML(w io.Writer) (err error)

TailHTML writes optional HTML code at the end of the page's BODY section that will immediately apply HTML attribute and class updates made during initial rendering, which minimizes flicker without having to write the correct value in templates or during Renderer.JawsRender.

It also adds a <noscript> tag that warns of reduced functionality.

type Session added in v0.11.0

type Session struct {
	// contains filtered or unexported fields
}

Session stores server-side per-user state shared by one or more requests.

A Session is bound to the remote IP that created it. Its exported methods are safe to call on a nil *Session; those calls return the documented zero value or do nothing.

func (*Session) Broadcast added in v0.26.0

func (sess *Session) Broadcast(msg wire.Message)

Broadcast attempts to send a message to all active Request values using this session.

It must not be called before the JaWS processing loop (Jaws.Serve or Jaws.ServeWithTimeout) is running. Otherwise this call may block. It is safe to call on a nil Session.

func (*Session) Clear added in v0.16.0

func (sess *Session) Clear()

Clear removes all key/value pairs from the session. It is safe to call on a nil Session.

func (*Session) Close added in v0.17.0

func (sess *Session) Close() (cookie *http.Cookie)

Close invalidates and expires the Session. Future Request values won't be able to associate with it, and Session.Cookie will return a deletion cookie.

Existing Request values already associated with the Session will ask the browser to reload the pages. Key/value pairs in the Session are left unmodified; use Session.Clear to remove all of them.

It must not be called before the JaWS processing loop (Jaws.Serve or Jaws.ServeWithTimeout) is running, because reload broadcasts may block.

Returns a cookie to be sent to the client browser that will delete the browser cookie. It is safe to call on a nil Session, in which case it returns nil; for any non-nil Session it returns a non-nil deletion cookie.

func (*Session) Cookie added in v0.11.0

func (sess *Session) Cookie() (cookie *http.Cookie)

Cookie returns a cookie for the Session. Returns a delete cookie if the Session is expired. It is safe to call on a nil Session, in which case it returns nil.

func (*Session) CookieValue added in v0.11.0

func (sess *Session) CookieValue() (s string)

CookieValue returns the session cookie value. It is safe to call on a nil Session, in which case it returns an empty string.

func (*Session) Get added in v0.11.0

func (sess *Session) Get(key string) (value any)

Get returns the value associated with the key, or nil. It is safe to call on a nil Session.

func (*Session) ID added in v0.11.0

func (sess *Session) ID() (id uint64)

ID returns the session ID, a 64-bit random value. It is safe to call on a nil Session, in which case it returns zero.

func (*Session) IP added in v0.11.0

func (sess *Session) IP() (ip netip.Addr)

IP returns the remote IP the session is bound to, or the zero netip.Addr if unset. It is safe to call on a nil Session, in which case it returns the zero netip.Addr.

func (*Session) Jaws added in v0.81.0

func (sess *Session) Jaws() (jw *Jaws)

Jaws returns the Jaws instance of the Session, or nil. It is safe to call on a nil Session.

func (*Session) Reload added in v0.17.0

func (sess *Session) Reload()

Reload calls Session.Broadcast with a message asking browsers to reload the page. See Session.Broadcast for the processing-loop requirement. It is safe to call on a nil Session.

func (*Session) Requests added in v0.37.0

func (sess *Session) Requests() (requests []*Request)

Requests returns a list of the Request values using this Session.

The returned slice is a snapshot. Its Request pointers are not pinned and may become stale immediately; see Request. It is safe to call on a nil Session.

func (*Session) Set added in v0.11.0

func (sess *Session) Set(key string, value any)

Set sets a value to be associated with the key. If value is nil, the key is removed from the session. It is safe to call on a nil Session.

type SetupFunc added in v0.111.6

type SetupFunc = func(jw *Jaws, handleFn HandleFunc, prefix string) (urls []*url.URL, err error)

SetupFunc is called by Jaws.Setup and allows setting up addons for JaWS.

When Jaws.Setup is called with a nil HandleFunc, setup functions receive a no-op handler registration function.

The URLs returned will be used in a call to Jaws.GenerateHeadHTML.

type TemplateLookuper added in v0.45.0

type TemplateLookuper interface {
	Lookup(name string) *template.Template
}

TemplateLookuper resolves a name to a *template.Template.

type UI added in v0.31.0

type UI interface {
	Renderer
	Updater
}

UI defines the required methods on JaWS UI objects.

A UI value is request-scoped. Once it has been used to create an Element for one Request, it must not be used to create an Element for another Request. Construct a fresh UI value for each Request. The application state, getters, setters, handlers and tags referenced by those UI values may be shared across Requests when synchronized as required.

In addition, all UI objects must be comparable so they can be used as map keys. The compile-time type must be comparable; debug builds additionally perform a runtime value-level check in Request.NewElement and panic on a value that is statically comparable but not comparable at runtime (for example a comparable struct holding a func in an interface field). Production builds rely on the static check alone, so such a value is accepted and instead panics when first used as a map key; callers must therefore ensure UI values are genuinely comparable.

type Updater added in v0.60.0

type Updater interface {
	// JawsUpdate is called for an [Element] that has been marked dirty to update its HTML.
	// Do not call this yourself unless it is from within another JawsUpdate implementation.
	// The engine does not invoke this once the [Element] is deleted (see [Element.Deleted]).
	JawsUpdate(elem *Element)
}

Updater updates browser-side DOM for a dirty Element.

Directories

Path Synopsis
Package examples contains compile-checked examples for JaWS applications.
Package examples contains compile-checked examples for JaWS applications.
minesweeper command
Package main implements the JaWS Minesweeper demo.
Package main implements the JaWS Minesweeper demo.
Package jawsboot provides embedded Bootstrap assets for JaWS applications.
Package jawsboot provides embedded Bootstrap assets for JaWS applications.
Package jawstest provides an importable harness for driving a jaws.Request's WebSocket message-processing loop in tests.
Package jawstest provides an importable harness for driving a jaws.Request's WebSocket message-processing loop in tests.
lib
assets
Package assets contains the embedded client assets and helpers used by JaWS setup code.
Package assets contains the embedded client assets and helpers used by JaWS setup code.
bind
Package bind adapts Go values to JaWS getter, setter, HTML and tag interfaces.
Package bind adapts Go values to JaWS getter, setter, HTML and tag interfaces.
htmlio
Package htmlio writes the small HTML fragments used by standard JaWS widgets.
Package htmlio writes the small HTML fragments used by standard JaWS widgets.
jid
Package jid provides JaWS element identifiers and helpers for writing them into HTML.
Package jid provides JaWS element identifiers and helpers for writing them into HTML.
key
Package key implements JaWS key encoding.
Package key implements JaWS key encoding.
named
Package named provides named boolean values and collections used by select, option and radio widgets.
Package named provides named boolean values and collections used by select, option and radio widgets.
tag
Package tag expands JaWS tag values into comparable keys that identify elements during dirtying, broadcasts and event routing.
Package tag expands JaWS tag values into comparable keys that identify elements during dirtying, broadcasts and event routing.
templatereloader
Package templatereloader provides a jaws.TemplateLookuper that reparses templates from disk while running in debug or race builds.
Package templatereloader provides a jaws.TemplateLookuper that reparses templates from disk while running in debug or race builds.
ui
Package ui contains the standard JaWS widget implementations.
Package ui contains the standard JaWS widget implementations.
what
Package what defines the commands and events used by the JaWS wire protocol.
Package what defines the commands and events used by the JaWS wire protocol.
wire
Package wire formats and parses the line-based JaWS WebSocket protocol.
Package wire formats and parses the line-based JaWS WebSocket protocol.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL