Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions .agents/skills/jaws/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ Apply these rules whenever work involves any of the following:
- Go templates rendered through `ui.Template` / `$.Template`
- Event handling, dirtying, tag identity, or dynamic container updates

## Version-matched source guidance

When the matching JaWS source is available, read its root `AI.md` and the
`AI.md` beside each package you change. Do not substitute guidance from another
version. This installed skill remains self-contained when source guides are not
available.

## Primary objective

Keep browser behavior thin and deterministic while preserving server-side truth, stable identity, and predictable rerenders.
Expand Down Expand Up @@ -65,9 +72,12 @@ JaWS is an immediate-mode, server-driven UI framework, not an MVC framework.
child must render one addressable direct DOM node carrying its Element's JaWS ID so
removal and ordering can target it; `ui.NewTemplate` provides that node through its
generated wrapper.
- Treat the package documentation shown by
`go doc github.com/linkdata/jaws/lib/ui` as the canonical standard-widget
multiplicity summary, and consult each concrete type's docs for its conditions.
- Treat the same-version `lib/ui/AI.md` as the canonical standard-widget
multiplicity summary when source guidance is available. Otherwise use
`go doc github.com/linkdata/jaws/lib/ui` for the package-wide default and
standard-widget summary, then inspect each concrete widget (for example,
`go doc github.com/linkdata/jaws/lib/ui Container`) for its documented
multiplicity and conditions.

## Constructing UI values: `ui.New` and `bind.New`

Expand Down
458 changes: 458 additions & 0 deletions AI.md

Large diffs are not rendered by default.

987 changes: 52 additions & 935 deletions README.md

Large diffs are not rendered by default.

35 changes: 15 additions & 20 deletions doc.go
Original file line number Diff line number Diff line change
@@ -1,27 +1,22 @@
// Package jaws creates dynamic server-driven webpages over WebSockets.
//
// It integrates with [html/template] and any router that supports [http.Handler].
// It provides the core engine, requests, sessions, and [UI] interfaces and
// integrates with [html/template] and routers that support [http.Handler].
// Standard widgets live in [github.com/linkdata/jaws/lib/ui], value binding in
// [github.com/linkdata/jaws/lib/bind], and dirty-target selection in
// [github.com/linkdata/jaws/lib/tag].
//
// 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].
// Applications keep authoritative state on the server. Tags associate [Element]
// values with application data or logical signals for targeted dirtying,
// broadcasts, and lookup; see [github.com/linkdata/jaws/lib/tag].
//
// # Nil values
//
// Throughout this module, including its subpackages, nil is unsupported for pointer
// receivers and for values used as required operational collaborators, such as
// callbacks, handlers, providers, lockers, writers, file systems, contexts, and
// pointers to mutable values, unless the relevant API documents a meaning for nil.
// Passing an unsupported nil is a caller error and may panic immediately or when
// used later. Nil slices, maps, data values, and results otherwise follow ordinary
// Go semantics and the relevant API. An interface containing a typed nil is non-nil;
// its behavior follows the relevant interface, API, and concrete type.
//
// # Tags
//
// Tags associate [Element] values with application data or logical signals for
// targeted dirtying, broadcasts, and lookup. See
// [github.com/linkdata/jaws/lib/tag] for tag selection, expansion, registration,
// and lifetime.
// Throughout this module, nil is unsupported for pointer receivers and values
// used as required operational collaborators, such as callbacks, handlers,
// providers, lockers, writers, file systems, contexts, and pointers to mutable
// values, unless an API documents a meaning for nil. Unsupported nil use is
// caller error and may panic. Nil slices, maps, data values, and results otherwise
// follow ordinary Go semantics and the relevant API. An interface containing a
// typed nil is non-nil; its behavior follows the receiving API and concrete type.
package jaws
5 changes: 2 additions & 3 deletions element.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,8 @@ func (elem *Element) Freeze() {
// 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.
// be processed for it. Handlers added after [Element.JawsRender] has returned
// (or [Element.Freeze] has been called) are dropped; debug builds panic.
//
// Input callback functions used directly by signature are recognized according
// to the dynamic-type rules documented by [InputFn].
Expand Down
46 changes: 46 additions & 0 deletions examples/AI.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# AI guidance for github.com/linkdata/jaws/examples

See the [module-wide AI guidance](../AI.md) before changing these examples.

## Purpose

This package contains compile-checked examples that show supported JaWS setup
patterns. Keep the minimal `Example` synchronized with the root README quick
start so a reader can move between them without encountering a different
lifecycle or wiring order.

The canonical setup sequence is:

1. Create a `jaws.Jaws` and arrange to close it.
2. Configure instance fields such as `Logger` before exposing handlers.
3. Parse templates and add their `TemplateLookuper` to the JaWS instance.
4. Start `Serve` before relying on dirtying or broadcasts.
5. Mount the `GET /jaws/` route on the selected mux.
6. Construct request-scoped UI values and mount the page handler.
7. Start the HTTP server.

Examples that add sessions or secure headers should build on that sequence,
not replace it with a second basic setup pattern. Keep detailed lifecycle,
security, and widget contracts in the package that owns them; this package
demonstrates how those contracts fit together.

## Example conventions

- Examples must compile as part of `go test`. Use real public APIs and handle
returned errors unless the API explicitly makes an error impossible for the
demonstrated input.
- The server examples intentionally block in `http.ListenAndServe`. They have no
`Output:` comment, so `go test` compile-checks but does not execute them.
- Keep examples short enough to copy. A production concern belongs here only
when omitting it would teach an unsafe default; link to the owning package for
the full contract.
- If the README quick start changes, update `Example` in the same change and
compare imports, template helpers, initialization order, routes, and cleanup.
`Example_secureSession` may contain the additional middleware needed for its
specific subject.

## Verification

Run `go test ./examples` from the module root. Also inspect the rendered example
with `go doc github.com/linkdata/jaws/examples` and compare the minimal example
against the root README quick start.
6 changes: 0 additions & 6 deletions examples/doc.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,2 @@
// Package examples contains compile-checked examples for JaWS applications.
//
// The examples mirror the README quick-start wiring: create a Jaws instance,
// register templates, start the processing loop, mount the /jaws/ routes, and
// serve a page through package ui. Keep these examples synchronized with the
// README so new users can move between the two without learning different
// setup patterns.
package examples
66 changes: 66 additions & 0 deletions examples/minesweeper/AI.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# AI guidance for github.com/linkdata/jaws/examples/minesweeper

See the [module-wide AI guidance](../../AI.md) before changing this example.

## Application model

The demo is a server-driven JaWS application with no custom client-side state.
One `game` is created in `run` and shared by all visitors, making the running
demo intentionally collaborative. Create request- or session-owned games in
the page handler if that product behavior changes; do not silently turn the
existing shared game into per-user state.

The board shape and cell pointers are fixed after construction. Mutable game
and cell fields are protected by `game.mu`. Rendering takes an immutable
`cellView` snapshot while holding the lock, releases the lock, and then derives
trusted cell markup and queues Element presentation updates from that snapshot.
Keep state mutations out of getter/render paths.

`run` deliberately constructs the application inline and injects only
`listenAndServe`. Preserve that copyable layout unless a production behavior
requires another seam.

## Dirty targeting

- A `Cell` is its own precise tag. `Cell.JawsGetTag` must return only the cell.
- Every cell button separately registers `Cell.BoardTag`, which is `&g.cells`.
This lets `Dirty(cell)` update one cell and `Dirty(&g.cells)` refresh the
complete board.
- Do not return the shared board tag from `Cell.JawsGetTag`. Tag expansion would
turn every single-cell action into a full-board update.
- Scalar status dependencies use the addresses of the exact `game` fields.
Mutations snapshot scalar state before changing it, then `changedTags` emits
only fields whose values differ afterward. HTML-inner widgets do not perform
application-level diffing, so broad scalar dirtying causes needless DOM work.
- A loss, win, or reset changes many cells and uses the shared board tag. Normal
reveals return the individual cells reached by flood fill, and flag toggles
return only the affected cell plus changed scalar fields.

The committed `BenchmarkSingleCellDirtyFanout` guards the targeted-update
design. Keep it when changing cell identity or tag registration, and verify it
still resolves a single-cell action to one cell Element.

## Domain behavior

The first reveal places mines while excluding the selected cell. Empty-cell
reveal uses an iterative stack and does not reveal flagged cells or mines.
Construction clamps the board to at least two rows and columns and clamps the
mine count to at least one and below the cell count. The game ends when a mine
is revealed or every safe cell has been revealed; both terminal states reveal
the mines and refresh the board.

The static `template.HTML` fragments in `cellView.HTML` contain only fixed
markup plus an integer adjacency count. Do not interpolate user-controlled
content into those trusted fragments.

## Testing responsibilities

- Keep pure domain tests for construction bounds, first-click safety, mine
placement, adjacency, flood fill, flags, win/loss, reset, and no-op guards.
- Keep UI integration tests on real JaWS Elements for tag registration, event
dispatch, queued class/attribute updates, and exact dirty fanout.
- Keep the HTTP wiring test for templates, static assets, middleware, and route
setup without binding a real port.
- Run `go test -race ./examples/minesweeper` and a plain
`go test ./examples/minesweeper` from the module root. Run the benchmark with
`-bench=SingleCellDirtyFanout -benchmem` when changing dirty-target behavior.
7 changes: 3 additions & 4 deletions examples/minesweeper/doc.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// Package main implements the JaWS Minesweeper demo.
//
// The demo keeps all game state on the server and uses JaWS tags to refresh only
// the cells and status fields affected by each move. The implementation is kept
// in one file so it remains copyable as an application-wiring example; tests
// cover the game rules, dirty-targeting behavior, and HTTP handler wiring.
// The running demo keeps one server-side game shared by all visitors. It uses
// cell-level and board-level tags to target updates without client-side
// application state.
package main
55 changes: 55 additions & 0 deletions jawsboot/AI.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# AI guidance for github.com/linkdata/jaws/jawsboot

See the [module-wide AI guidance](../AI.md) before changing this package.

## Package role

`jawsboot` vendors Bootstrap v5.3.8 as the gzip-compressed upstream artifacts
`bootstrap.bundle.min.js` and `bootstrap.min.css` under `assets/static`. The
human-facing provenance table and integration example remain in
[README.md](./README.md).

`Setup` is intended for `jaws.Jaws.Setup`. It walks the embedded assets, exposes
their content-hashed `staticserve` names, and returns the same rooted URL paths
that it registers. Absolute, relative, parent-relative, and empty prefixes are
cleaned through the same path construction. Handler patterns use serialized
URLs so braces and other `http.ServeMux` pattern syntax in a logical prefix are
treated as literal path data.

The predictable un-hashed Bootstrap sourcemap paths are registered with exact
404 handlers. Sourcemaps are not bundled, and devtools probes must not fall
through to an application wildcard route.

## Embedded asset layout

- Keep the two upstream artifacts gzip-compressed directly under
`assets/static`; `//go:embed assets/static` and `staticserve.WalkDir` depend on
that tree.
- Keep the JavaScript bundle variant: Bootstrap components used by JaWS alerts
require the bundled runtime, not only the core Bootstrap script.
- Do not add generated hash manifests or tests that pin repository-tracked blob
hashes. Git history records the blobs; tests should exercise serving and
integration behavior.

## Bootstrap version update checklist

1. Obtain the new minified bundle JavaScript and minified CSS artifacts from
the official Bootstrap distribution and replace their gzip-compressed files
without renaming them.
2. Update the version and provenance in `README.md`, the public version in
`doc.go`, this guide, and the `assetsFS` source comment in `jawsboot.go` in
the same change.
3. Confirm decompression yields the intended upstream filenames/content and
that both plain and gzip HTTP responses remain valid.
4. Review the sourcemap names registered by `Setup`; update the exact 404 list
if upstream artifact names change, without bundling maps implicitly.
5. Run the package tests and inspect generated JaWS head markup to ensure both
returned asset URLs resolve to their registered handlers for every supported
prefix form.

## Verification

Run `go test -race ./jawsboot` and `go test ./jawsboot` from the module root.
The tests cover plain/gzip serving headers and bodies, URL/handler parity,
literal brace prefixes, nil registration through `Jaws.Setup`, and sourcemap
404 behavior. Also compile-check the README-shaped package example.
2 changes: 1 addition & 1 deletion jawsboot/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ https://getbootstrap.com/) and stored gzip-compressed under `assets/static`:
| `assets/static/bootstrap.bundle.min.js.gz` | `bootstrap.bundle.min.js` |
| `assets/static/bootstrap.min.css.gz` | `bootstrap.min.css` |

When bumping Bootstrap, update this section and `doc.go` in the same change.
Maintainers should follow the [Bootstrap version update checklist](./AI.md#bootstrap-version-update-checklist).

Example usage that loads your templates, favicon and Bootstrap. Also uses a `templatereloader`
so that when running with `-tags debug` or `-race` templates are reloaded from disk as needed.
Expand Down
9 changes: 1 addition & 8 deletions jawsboot/doc.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,4 @@
// Package jawsboot provides embedded Bootstrap assets for JaWS applications.
//
// The embedded assets are Bootstrap v5.3.8, downloaded from
// https://getbootstrap.com/ (bootstrap.bundle.min.js and bootstrap.min.css,
// stored gzip-compressed under assets/static). When bumping the vendored files,
// update this version note and README.md's provenance table so the shipped
// release stays auditable against upstream security advisories.
//
// Nil values follow the module-wide convention documented by
// [github.com/linkdata/jaws].
// The embedded assets are Bootstrap v5.3.8.
package jawsboot
4 changes: 2 additions & 2 deletions jawsboot/jawsboot.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import (
"github.com/linkdata/staticserve"
)

// assetsFS holds Bootstrap v5.3.8 from https://getbootstrap.com/ (see the package
// doc); keep this version note in sync with doc.go when updating the files.
// assetsFS holds Bootstrap v5.3.8 from https://getbootstrap.com/. Follow the
// version-update checklist in AI.md when updating the files.
//
//go:embed assets/static
var assetsFS embed.FS
Expand Down
50 changes: 50 additions & 0 deletions jawstest/AI.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# AI guidance for github.com/linkdata/jaws/jawstest

See the [module-wide AI guidance](../AI.md) before changing this package.

## Package boundary

`jawstest` is an importable, low-level harness for a real `jaws.Request`
message-processing loop. It lives outside package `jaws` so consumers of the
production package do not acquire its `net/http/httptest` dependency. The
harness reaches the loop only through the exported `jaws.Jaws.TestServe` hook;
it is not a second implementation of request processing.

Keep higher-level rendering assertions in the package under test. `Recorder` is
only a sink for HTML the test explicitly renders; the harness never writes to
it. `BodyString` trims that recorded body, and `BodyHTML` trusts it because the
test itself supplied the content.

## Harness lifecycle

1. Create a `jaws.Jaws` and start `Serve`; `TestServe` requires the processing
loop to be running.
2. Construct the harness with `NewTestRequest`. A nil HTTP request means a
bodyless `GET /` request. Construction creates and claims one real Request.
3. Wait for `ReadyCh` before depending on the loop.
4. Send browser-originated frames on `InCh`, read server frames from `OutCh`,
and inject broadcasts on `BcastCh`.
5. Drain `OutCh` whenever the test can produce output. Its buffer is finite; a
full output channel stalls the request loop and can prevent shutdown.
6. Call `Close` to close the input side, then wait for `DoneCh`. `Close` is
idempotent but does not itself wait.

If a test continuously drains output in a goroutine, terminate that goroutine
from `DoneCh` and wait for it before the test returns. Do not close output or
broadcast channels from test code; `Close` owns only the inbound channel.

Use `NewTestRequestWithPanic` only when the test needs to observe an expected
request-loop panic. Its callback runs on the loop goroutine and receives either
the recovered value or nil; `DoneCh` closes only after the callback returns or
unwinds. The ordinary constructor re-panics non-nil values so unexpected loop
panics remain visible.

## Maintenance and tests

Preserve construction failure as an immediate panic when a Request cannot be
created or claimed. The `newRequest` package seam exists only to exercise that
failure path; production `Jaws.NewRequest` does not return nil while open.

Run `go test -race ./jawstest` and `go test ./jawstest` from the module root.
Keep coverage for channel directions, readiness, close idempotence, output
draining, explicit requests, panic delivery, failed claims, and Request cleanup.
20 changes: 9 additions & 11 deletions jawstest/jawstest.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
// Package jawstest provides an importable harness for driving a [jaws.Request]'s
// WebSocket message-processing loop in tests.
// Package jawstest provides a harness for driving a [jaws.Request]'s WebSocket
// message-processing loop in tests.
//
// It lives in its own package, rather than in package jaws, so that
// net/http/httptest stays out of the production build of consumers that import
// github.com/linkdata/jaws. It reaches the request loop through the exported
// [jaws.Jaws.TestServe] hook.
//
// Harness channels are intentionally low-level. Tests that drive output must
// drain [TestRequest.OutCh], and after [TestRequest.Close] should wait for
// [TestRequest.DoneCh] before returning. Close closes the inbound channel and is
// safe to call more than once.
// The harness uses the real JaWS request-processing loop and exposes its channels
// directly. Start [jaws.Jaws.Serve] or [jaws.Jaws.ServeWithTimeout] before
// constructing a [TestRequest], wait for [TestRequest.ReadyCh] before driving it,
// and drain [TestRequest.OutCh] while output can be produced. [TestRequest.Close]
// closes only the inbound channel; callers should then wait for
// [TestRequest.DoneCh] and must not close [TestRequest.OutCh] or
// [TestRequest.BcastCh].
package jawstest

import (
Expand Down
Loading
Loading