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
10 changes: 4 additions & 6 deletions .agents/skills/jaws/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -448,12 +448,10 @@ Guideline:

## Request/session integration rules

- Ensure pages provide the configured JaWS resources and Request key metadata;
`HeadHTML` is the usual way to emit them.
- Include the `no-store` Cache-Control directive on every page response containing
`HeadHTML` or equivalent Request-key metadata; the key is a one-use capability.
`ui.Handler` sets this automatically, while custom page handlers must set it
explicitly.
- Call `jw.NewRequest(w, r)` before writing each page response. It replaces
Cache-Control with `no-store`.
- Include the configured JaWS resources and Request key metadata; `HeadHTML`
emits them but does not manage response headers.
- `TailHTML` is optional; it applies queued attr/class updates before the
WebSocket connects and can reduce initial flicker.
- Register the JaWS `/jaws/` route prefix correctly and pair request creation with `UseRequest` handling.
Expand Down
17 changes: 7 additions & 10 deletions AI.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,20 +107,17 @@ identity and multiplicity.

The normal page flow has two related HTTP requests:

1. A page handler creates a Request with `Jaws.NewRequest`. `HeadHTML` normally
emits the configured resources and request-key metadata. `TailHTML` is
optional; placing it before `</body>` applies queued initial updates before
the WebSocket connects and can reduce flicker.
1. Before writing the response, a page handler calls `Jaws.NewRequest(w, r)`,
which replaces `Cache-Control` with `no-store`. `HeadHTML` normally emits the
configured resources and request-key metadata. `TailHTML` is optional;
placing it before `</body>` applies queued initial updates before the
WebSocket connects and can reduce flicker.
2. The bundled script connects to `/jaws/<key>`. `Jaws.ServeHTTP` decodes the
key, claims the pending Request through `UseRequest`, upgrades the connection,
and begins event and DOM-update processing.

Page responses containing `HeadHTML` or equivalent Request-key metadata must
include the `no-store` Cache-Control directive. An HTTP-cached copy would repeat
the consumed one-use capability and cannot establish another WebSocket
connection. `ui.Handler` sets `Cache-Control: no-store` automatically; custom
page handlers must set it explicitly. A bfcache restoration is handled
separately by the bundled client's `pageshow` reload.
`HeadHTML` does not manage response headers. The bundled client reloads pages
restored from the bfcache.

Applications that emit equivalent resources and metadata need not call
`HeadHTML` or `TailHTML`. Custom routers may parse the trailing key with
Expand Down
2 changes: 1 addition & 1 deletion element_create_benchmark_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func BenchmarkElementCreateBatch(b *testing.B) {
b.Fatal(err)
}
defer jw.Close()
rq := jw.NewRequest(httptest.NewRequest(http.MethodGet, "/", nil))
rq := jw.newRequest(httptest.NewRequest(http.MethodGet, "/", nil))
if rq == nil {
b.Fatal("nil request")
}
Expand Down
34 changes: 17 additions & 17 deletions element_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ func TestElement_JsCallQueuesElementScopedCall(t *testing.T) {
t.Fatal(err)
}
defer jw.Close()
rq := jw.NewRequest(httptest.NewRequest(http.MethodGet, "/", nil))
rq := jw.newRequest(httptest.NewRequest(http.MethodGet, "/", nil))
if rq == nil {
t.Fatal("NewRequest returned nil")
}
Expand Down Expand Up @@ -234,7 +234,7 @@ func TestElement_Queued(t *testing.T) {
},
}

pendingRq := rq.Jaws.NewRequest(httptest.NewRequest(http.MethodGet, "/", nil))
pendingRq := rq.Jaws.newRequest(httptest.NewRequest(http.MethodGet, "/", nil))
th.NoErr(testRequestWriter{rq: pendingRq, Writer: httptest.NewRecorder()}.UI(tss))

th.NoErr(rq.UI(tss))
Expand Down Expand Up @@ -264,7 +264,7 @@ func TestElement_ChildOperations(t *testing.T) {
t.Fatal(err)
}
defer jw.Close()
rq := jw.NewRequest(nil)
rq := jw.newRequest(nil)
defer jw.recycle(rq)

parent := rq.NewElement(&testUi{})
Expand Down Expand Up @@ -338,9 +338,9 @@ func TestElement_ChildOperationsRejectInvalidElement(t *testing.T) {
defer jw.Close()
logger := &captureErrorLogger{}
jw.Logger = logger
rq := jw.NewRequest(nil)
rq := jw.newRequest(nil)
defer jw.recycle(rq)
other := jw.NewRequest(nil)
other := jw.newRequest(nil)
defer jw.recycle(other)
parent := rq.NewElement(&testUi{})
child := tt.child(parent, other)
Expand Down Expand Up @@ -376,7 +376,7 @@ func TestElement_ChildOperationsOnDeletedParentAreInert(t *testing.T) {
defer jw.Close()
logger := &captureErrorLogger{}
jw.Logger = logger
rq := jw.NewRequest(nil)
rq := jw.newRequest(nil)
defer jw.recycle(rq)
parent := rq.NewElement(&testUi{})
child := rq.NewElement(&testUi{})
Expand Down Expand Up @@ -408,7 +408,7 @@ func TestElement_ReplaceRejectsMissingId(t *testing.T) {
defer jw.Close()
logger := &captureErrorLogger{}
jw.Logger = logger
rq := jw.NewRequest(httptest.NewRequest(http.MethodGet, "/", nil))
rq := jw.newRequest(httptest.NewRequest(http.MethodGet, "/", nil))
e := rq.NewElement(&testUi{s: "foo"})

if deadlock.Debug {
Expand Down Expand Up @@ -459,9 +459,9 @@ func TestElement_AttrHelpersRejectReservedId(t *testing.T) {
defer jw.Close()
logger := &captureErrorLogger{}
jw.Logger = logger
// A plain NewRequest has no running process loop, so nothing drains
// A plain Request has no running process loop, so nothing drains
// wsQueue underneath the assertion (unlike newTestRequest).
rq := jw.NewRequest(nil)
rq := jw.newRequest(nil)
defer jw.recycle(rq)
e := rq.NewElement(&testUi{})

Expand Down Expand Up @@ -503,9 +503,9 @@ func TestElement_AttrHelpersAllowNormalAttr(t *testing.T) {
t.Fatal(err)
}
defer jw.Close()
// A plain NewRequest has no running process loop, so nothing drains wsQueue
// A plain Request has no running process loop, so nothing drains wsQueue
// underneath the assertion (unlike newTestRequest).
rq := jw.NewRequest(nil)
rq := jw.newRequest(nil)
defer jw.recycle(rq)
e := rq.NewElement(&testUi{})
e.SetAttr("hidden", "yes")
Expand Down Expand Up @@ -666,7 +666,7 @@ func TestElement_RenderDebugAndDeletedBranches(t *testing.T) {
t.Fatal(err)
}
defer jw.Close()
rq := jw.NewRequest(httptest.NewRequest(http.MethodGet, "/", nil))
rq := jw.newRequest(httptest.NewRequest(http.MethodGet, "/", nil))

tu := &testUi{renderFn: func(*Element, io.Writer, []any) error { return nil }}
elem := rq.NewElement(tu)
Expand Down Expand Up @@ -720,7 +720,7 @@ func TestElement_JawsRenderDebugTagCanReenterRequest(t *testing.T) {
t.Fatal(err)
}
jw.Debug = true
rq := jw.NewRequest(httptest.NewRequest(http.MethodGet, "/", nil))
rq := jw.newRequest(httptest.NewRequest(http.MethodGet, "/", nil))

tu := &testUi{renderFn: func(elem *Element, _ io.Writer, _ []any) error {
elem.Tag(testReentrantDebugTag{rq: rq})
Expand Down Expand Up @@ -759,7 +759,7 @@ func TestElement_JawsRenderReturnsDebugWriteError(t *testing.T) {
}
defer jw.Close()
jw.Debug = true
rq := jw.NewRequest(httptest.NewRequest(http.MethodGet, "/", nil))
rq := jw.newRequest(httptest.NewRequest(http.MethodGet, "/", nil))
elem := rq.NewElement(&testUi{})
wantErr := errors.New("debug write failed")

Expand All @@ -775,7 +775,7 @@ func TestElement_RenderDebugSanitizesHTML5CommentClose(t *testing.T) {
t.Fatal(err)
}
defer jw.Close()
rq := jw.NewRequest(httptest.NewRequest(http.MethodGet, "/", nil))
rq := jw.newRequest(httptest.NewRequest(http.MethodGet, "/", nil))
rq.Jaws.Debug = true

tu := &testUi{renderFn: func(*Element, io.Writer, []any) error { return nil }}
Expand Down Expand Up @@ -976,7 +976,7 @@ func TestElement_ApplyGetter_NonComparableHandler_NilLogger(t *testing.T) {
t.Fatal("expected nil Logger by default")
}

rq := jw.NewRequest(httptest.NewRequest(http.MethodGet, "/", nil))
rq := jw.newRequest(httptest.NewRequest(http.MethodGet, "/", nil))
e := rq.NewElement(&testUi{s: "x"})
tch := testNonComparableClickHandler{names: []string{"name"}}
gotTag := e.ApplyGetter(tch)
Expand All @@ -1003,7 +1003,7 @@ func TestElement_ApplyGetter_NonComparableHandler_NoLog(t *testing.T) {
var buf bytes.Buffer
jw.Logger = slog.New(slog.NewTextHandler(&buf, nil))

rq := jw.NewRequest(httptest.NewRequest(http.MethodGet, "/", nil))
rq := jw.newRequest(httptest.NewRequest(http.MethodGet, "/", nil))
e := rq.NewElement(&testUi{s: "x"})
tch := testNonComparableClickHandler{names: []string{"name"}}
e.ApplyGetter(tch)
Expand Down
2 changes: 1 addition & 1 deletion examples/minesweeper/main_benchmark_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func BenchmarkSingleCellDirtyFanout(b *testing.B) {
}
defer jw.Close()

rq := jw.NewRequest(httptest.NewRequest(http.MethodGet, "/", nil))
rq := jw.NewRequest(httptest.NewRecorder(), httptest.NewRequest(http.MethodGet, "/", nil))
if rq == nil {
b.Fatal("expected a request")
}
Expand Down
Loading
Loading