host API

host

package

API reference for the host package.

F
function

TestSpan

Parameters

host/html_test.go:8-22
func TestSpan(t *testing.T)

{
	got := Span("Visit", 1)
	if !strings.Contains(got, `data-host-var="Visit"`) {
		t.Fatalf("missing data-host-var: %s", got)
	}
	if !strings.Contains(got, `data-host-expected="1"`) {
		t.Fatalf("missing data-host-expected: %s", got)
	}
	if !strings.Contains(got, ">1</span>") {
		t.Fatalf("missing value: %s", got)
	}
	if !strings.HasPrefix(got, "<span ") {
		t.Fatalf("wrong tag: %s", got)
	}
}
F
function

TestDiv

Parameters

host/html_test.go:24-32
func TestDiv(t *testing.T)

{
	got := Div("message", "hello")
	if !strings.Contains(got, `data-host-var="message"`) {
		t.Fatalf("missing data-host-var: %s", got)
	}
	if !strings.Contains(got, ">hello</div>") {
		t.Fatalf("missing value: %s", got)
	}
}
F
function

TestP

Parameters

host/html_test.go:34-42
func TestP(t *testing.T)

{
	got := P("desc", "some text")
	if !strings.Contains(got, `data-host-var="desc"`) {
		t.Fatalf("missing data-host-var: %s", got)
	}
	if !strings.Contains(got, ">some text</p>") {
		t.Fatalf("missing value: %s", got)
	}
}
F
function

TestTag

Parameters

host/html_test.go:44-52
func TestTag(t *testing.T)

{
	got := Tag("em", "count", 42)
	if !strings.Contains(got, `data-host-var="count"`) {
		t.Fatalf("missing data-host-var: %s", got)
	}
	if !strings.Contains(got, ">42</em>") {
		t.Fatalf("missing value: %s", got)
	}
}
F
function

TestHelpersEscapeValues

Helper values are HTML-escaped by default so user-derived data cannot
inject markup through the initial snapshot.

Parameters

host/html_test.go:56-67
func TestHelpersEscapeValues(t *testing.T)

{
	got := Span("msg", `<img src=x onerror=alert(1)>`)
	if !strings.Contains(got, "&lt;img src=x onerror=alert(1)&gt;") {
		t.Fatalf("value not escaped: %s", got)
	}
	if strings.Contains(got, "><img") {
		t.Fatalf("markup injected: %s", got)
	}
	if !strings.Contains(got, `data-host-expected="&lt;img src=x onerror=alert(1)&gt;"`) {
		t.Fatalf("expected escaped migration value: %s", got)
	}
}
F
function

TestRawTag

RawTag is the explicit trust API: the value passes through unescaped.

Parameters

host/html_test.go:70-78
func TestRawTag(t *testing.T)

{
	got := RawTag("div", "content", `<b>ok</b>`)
	if !strings.Contains(got, "><b>ok</b></div>") {
		t.Fatalf("raw value escaped: %s", got)
	}
	if !strings.Contains(got, `data-host-expected=""`) {
		t.Fatalf("raw markup must skip text expectation: %s", got)
	}
}
F
function

TestRaw

Parameters

host/html_test.go:80-85
func TestRaw(t *testing.T)

{
	html := `<div class="custom">foo</div>`
	if got := Raw(html); got != html {
		t.Fatalf("Raw should passthrough: got %q", got)
	}
}
F
function

TestJoin

Parameters

host/html_test.go:87-95
func TestJoin(t *testing.T)

{
	got := Join(Span("a", 1), Div("b", 2))
	if !strings.Contains(got, `data-host-var="a"`) {
		t.Fatalf("missing first var: %s", got)
	}
	if !strings.Contains(got, `data-host-var="b"`) {
		t.Fatalf("missing second var: %s", got)
	}
}
F
function

TestHostVariableAttributesAreEscaped

Parameters

host/html_test.go:97-105
func TestHostVariableAttributesAreEscaped(t *testing.T)

{
	got := Span(`x" onmouseover="alert(1)`, "test")
	if strings.Contains(got, `data-host-var="x" onmouseover=`) {
		t.Fatalf("host variable name injected an attribute: %s", got)
	}
	if !strings.Contains(got, `data-host-var="x&#34; onmouseover=&#34;alert(1)"`) {
		t.Fatalf("host variable name was not escaped: %s", got)
	}
}
F
function

TestTagRejectsInvalidName

Parameters

host/html_test.go:107-111
func TestTagRejectsInvalidName(t *testing.T)

{
	if got := Tag(`div onmouseover="alert(1)"`, "x", "test"); got != "" {
		t.Fatalf("invalid tag name was accepted: %s", got)
	}
}
F
function

resumeRequest

Parameters

user
string

Returns

host/resume_authorization_test.go:12-20
func resumeRequest(t *testing.T, user string) *http.Request

{
	t.Helper()
	request, err := http.NewRequest(http.MethodGet, "/ws", nil)
	if err != nil {
		t.Fatalf("build request: %v", err)
	}
	request.Header.Set(resumeIdentityHeader, user)
	return request
}
F
function

bindResumeIdentity

Parameters

request
session

Returns

error
host/resume_authorization_test.go:22-25
func bindResumeIdentity(request *http.Request, session *Session) error

{
	session.ContextSet("user", request.Header.Get(resumeIdentityHeader))
	return nil
}
F
function

authorizeSameUserResume

Parameters

request
session

Returns

error
host/resume_authorization_test.go:27-33
func authorizeSameUserResume(request *http.Request, session *Session) error

{
	user, ok := session.ContextGet("user")
	if !ok || user != request.Header.Get(resumeIdentityHeader) {
		return errors.New("resume denied")
	}
	return nil
}
F
function

resumableRuntime

Parameters

opts
...MuxOption

Returns

host/resume_authorization_test.go:35-38
func resumableRuntime(opts ...MuxOption) *WSRuntime

{
	base := []MuxOption{WithSSCLimits(SSCLimits{ReplayMessages: 4, ResumeTTL: time.Minute})}
	return NewWSRuntime(append(base, opts...)...)
}
F
function

detachedSession

Parameters

runtime
user
string

Returns

host/resume_authorization_test.go:40-51
func detachedSession(t *testing.T, runtime *WSRuntime, user string) *Session

{
	t.Helper()
	session, resumed, err := runtime.OpenSession(resumeRequest(t, user), "")
	if err != nil {
		t.Fatalf("open session: %v", err)
	}
	if resumed || session.ResumeToken() == "" {
		t.Fatalf("expected a new resumable session, resumed=%v token=%q", resumed, session.ResumeToken())
	}
	SuspendSession(session, runtime.ResumeTTL())
	return session
}
F
function

assertDetached

Parameters

session
host/resume_authorization_test.go:53-65
func assertDetached(t *testing.T, session *Session)

{
	t.Helper()
	session.deliveryMu.Lock()
	attached := session.attached
	expires := session.expires
	session.deliveryMu.Unlock()
	if attached {
		t.Fatal("rejected attempt attached the retained session")
	}
	if expires.IsZero() {
		t.Fatal("rejected attempt cleared the retention deadline")
	}
}
F
function

TestResumeAuthorizerRejectsAnotherUser

A request authenticated as another user must not reattach a retained
session, and the refusal must leave that session resumable by its owner.

Parameters

host/resume_authorization_test.go:69-109
func TestResumeAuthorizerRejectsAnotherUser(t *testing.T)

{
	initializations := 0
	runtime := resumableRuntime(
		WithSSCSessionInitializer(func(request *http.Request, session *Session) error {
			initializations++
			return bindResumeIdentity(request, session)
		}),
		WithSSCResumeAuthorizer(authorizeSameUserResume),
	)
	owner := detachedSession(t, runtime, "alice")
	defer ReleaseSession(owner)
	token := owner.ResumeToken()

	intruder, resumed, err := runtime.OpenSession(resumeRequest(t, "mallory"), token)
	if err != nil {
		t.Fatalf("open session for rejected resume: %v", err)
	}
	defer ReleaseSession(intruder)
	if resumed || intruder == owner {
		t.Fatalf("foreign request resumed the retained session: resumed=%v", resumed)
	}
	if user, _ := owner.ContextGet("user"); user != "alice" {
		t.Fatalf("retained session identity changed to %v", user)
	}
	assertDetached(t, owner)

	restored, resumed, err := runtime.OpenSession(resumeRequest(t, "alice"), token)
	if err != nil {
		t.Fatalf("open session for owner resume: %v", err)
	}
	if !resumed || restored != owner {
		t.Fatalf("owner could not resume after a rejected attempt: resumed=%v", resumed)
	}
	if user, _ := restored.ContextGet("user"); user != "alice" {
		t.Fatalf("resume rebound the session identity to %v", user)
	}
	// The two new sessions were initialized; the resume was not.
	if initializations != 2 {
		t.Fatalf("session initializations = %d, want 2", initializations)
	}
}
F
function

TestResumeAuthorizerRejectsRevokedIdentity

Identity alone is not enough: an authorizer that consults revocation state
keeps a revoked credential from reattaching its own session.

Parameters

host/resume_authorization_test.go:113-149
func TestResumeAuthorizerRejectsRevokedIdentity(t *testing.T)

{
	revoked := map[string]bool{"alice": true}
	runtime := resumableRuntime(
		WithSSCSessionInitializer(bindResumeIdentity),
		WithSSCResumeAuthorizer(func(request *http.Request, session *Session) error {
			if err := authorizeSameUserResume(request, session); err != nil {
				return err
			}
			if revoked[request.Header.Get(resumeIdentityHeader)] {
				return errors.New("credential revoked")
			}
			return nil
		}),
	)
	owner := detachedSession(t, runtime, "alice")
	defer ReleaseSession(owner)
	token := owner.ResumeToken()

	replacement, resumed, err := runtime.OpenSession(resumeRequest(t, "alice"), token)
	if err != nil {
		t.Fatalf("open session for revoked resume: %v", err)
	}
	defer ReleaseSession(replacement)
	if resumed || replacement == owner {
		t.Fatalf("revoked identity resumed the session: resumed=%v", resumed)
	}
	assertDetached(t, owner)

	revoked["alice"] = false
	restored, resumed, err := runtime.OpenSession(resumeRequest(t, "alice"), token)
	if err != nil {
		t.Fatalf("open session after revocation cleared: %v", err)
	}
	if !resumed || restored != owner {
		t.Fatalf("cleared credential could not resume: resumed=%v", resumed)
	}
}
F
function

TestConcurrentResumeAttachesOneAuthorizedSession

Two upgrades presenting the same token are authorized against the same
candidate. Only one may attach it, and the loser must not be handed the
retained session under the other request’s authorization.

Parameters

host/resume_authorization_test.go:154-211
func TestConcurrentResumeAttachesOneAuthorizedSession(t *testing.T)

{
	authorizing := make(chan struct{}, 2)
	release := make(chan struct{})
	runtime := resumableRuntime(
		WithSSCSessionInitializer(bindResumeIdentity),
		WithSSCResumeAuthorizer(func(request *http.Request, session *Session) error {
			if err := authorizeSameUserResume(request, session); err != nil {
				return err
			}
			authorizing <- struct{}{}
			<-release
			return nil
		}),
	)
	owner := detachedSession(t, runtime, "alice")
	defer ReleaseSession(owner)
	token := owner.ResumeToken()

	type outcome struct {
		session *Session
		resumed bool
		err     error
	}
	requests := []*http.Request{resumeRequest(t, "alice"), resumeRequest(t, "alice")}
	results := make(chan outcome, len(requests))
	for _, request := range requests {
		go func(request *http.Request) {
			session, resumed, err := runtime.OpenSession(request, token)
			results <- outcome{session: session, resumed: resumed, err: err}
		}(request)
	}
	// Both attempts hold an authorized candidate before either commits.
	<-authorizing
	<-authorizing
	close(release)

	attached := 0
	for range requests {
		result := <-results
		if result.err != nil {
			t.Fatalf("concurrent open session: %v", result.err)
		}
		if result.resumed {
			attached++
			if result.session != owner {
				t.Fatal("resume attached a session the request was not authorized for")
			}
			continue
		}
		if result.session == owner {
			t.Fatal("losing attempt received the retained session")
		}
		ReleaseSession(result.session)
	}
	if attached != 1 {
		t.Fatalf("resumed sessions = %d, want 1", attached)
	}
}
F
function

TestResumeWithoutAuthorizerKeepsTokenBehavior

Deployments that configure no resume authorizer keep the historical
behavior, where the token alone reattaches the session.

Parameters

host/resume_authorization_test.go:215-227
func TestResumeWithoutAuthorizerKeepsTokenBehavior(t *testing.T)

{
	runtime := resumableRuntime()
	owner := detachedSession(t, runtime, "alice")
	defer ReleaseSession(owner)

	restored, resumed, err := runtime.OpenSession(resumeRequest(t, "mallory"), owner.ResumeToken())
	if err != nil {
		t.Fatalf("open session: %v", err)
	}
	if !resumed || restored != owner {
		t.Fatalf("token resume changed without an authorizer: resumed=%v", resumed)
	}
}
F
function

TestReleasedSessionTokenStaysUnresumable

Releasing a retained session is final. Its token stops naming a candidate,
so a later upgrade presenting it never reaches the authorizer, the token-only
path cannot reattach it either, and the caller is served a new session with a
token of its own.

Parameters

host/resume_authorization_test.go:233-272
func TestReleasedSessionTokenStaysUnresumable(t *testing.T)

{
	handed := 0
	runtime := resumableRuntime(
		WithSSCSessionInitializer(bindResumeIdentity),
		WithSSCResumeAuthorizer(func(request *http.Request, session *Session) error {
			handed++
			return authorizeSameUserResume(request, session)
		}),
	)
	owner := detachedSession(t, runtime, "alice")
	token := owner.ResumeToken()

	ReleaseSession(owner)
	ReleaseSession(owner)

	for attempt := 0; attempt < 2; attempt++ {
		session, resumed, err := runtime.OpenSession(resumeRequest(t, "alice"), token)
		if err != nil {
			t.Fatalf("attempt %d: open session: %v", attempt, err)
		}
		t.Cleanup(func() { ReleaseSession(session) })
		if resumed || session == owner {
			t.Fatalf("attempt %d: the released session was resumed: resumed=%v", attempt, resumed)
		}
		if session.ResumeToken() == "" || session.ResumeToken() == token {
			t.Fatalf("attempt %d: the fresh session carries token %q", attempt, session.ResumeToken())
		}
	}
	if handed != 0 {
		t.Fatalf("the authorizer was handed a released session %d times", handed)
	}
	if _, registered := SessionByID(owner.ID()); registered {
		t.Fatal("the released session is still registered")
	}
	// Detaching it again must not put its token back into circulation.
	SuspendSession(owner, runtime.ResumeTTL())
	if resumed, ok := ResumeSession(token); ok {
		t.Fatalf("the released token reattached session %q", resumed.ID())
	}
}
F
function

TestResumeAuthorizerSkippedForUnknownToken

An unknown token is refused before the authorizer runs: there is no
candidate to authorize, and the caller gets a fresh session.

Parameters

host/resume_authorization_test.go:276-293
func TestResumeAuthorizerSkippedForUnknownToken(t *testing.T)

{
	calls := 0
	runtime := resumableRuntime(
		WithSSCSessionInitializer(bindResumeIdentity),
		WithSSCResumeAuthorizer(func(*http.Request, *Session) error {
			calls++
			return nil
		}),
	)
	session, resumed, err := runtime.OpenSession(resumeRequest(t, "alice"), "unknown-token")
	if err != nil {
		t.Fatalf("open session: %v", err)
	}
	defer ReleaseSession(session)
	if resumed || calls != 0 {
		t.Fatalf("unknown token reached the authorizer: resumed=%v calls=%d", resumed, calls)
	}
}
F
function

TestNewMuxServesBrotliWasmWithEncoding

Parameters

host/server_wasm_test.go:14-83
func TestNewMuxServesBrotliWasmWithEncoding(t *testing.T)

{
	t.Setenv("RFW_DEVTOOLS", "")
	root := t.TempDir()
	clientDir := filepath.Join(root, "client")
	if err := os.MkdirAll(clientDir, 0o755); err != nil {
		t.Fatalf("failed to create client dir: %v", err)
	}
	wasmPath := filepath.Join(clientDir, "app.wasm.br")
	if err := os.WriteFile(wasmPath, []byte("compressed"), 0o644); err != nil {
		t.Fatalf("failed to write wasm: %v", err)
	}
	if err := os.WriteFile(filepath.Join(clientDir, "index.html"), []byte("<html></html>"), 0o644); err != nil {
		t.Fatalf("failed to write index: %v", err)
	}
	if err := os.WriteFile(filepath.Join(clientDir, "rfw_config.js"), []byte("//cfg"), 0o644); err != nil {
		t.Fatalf("failed to write config: %v", err)
	}

	mux := NewMux(clientDir)
	srv := httptest.NewServer(mux)
	defer srv.Close()

	resp, err := http.Get(srv.URL + "/app.wasm.br?v=abc123")
	if err != nil {
		t.Fatalf("failed to get wasm: %v", err)
	}
	defer closeTestResource(t, resp.Body)

	if resp.StatusCode != http.StatusOK {
		t.Fatalf("unexpected status: %d", resp.StatusCode)
	}
	if enc := resp.Header.Get("Content-Encoding"); enc != "br" {
		t.Fatalf("expected Content-Encoding br, got %q", enc)
	}
	if ct := resp.Header.Get("Content-Type"); ct != "application/wasm" {
		t.Fatalf("expected Content-Type application/wasm, got %q", ct)
	}
	if cache := resp.Header.Get("Cache-Control"); cache != "public, max-age=31536000, immutable" {
		t.Fatalf("unexpected Cache-Control header: %q", cache)
	}
	if vary := resp.Header.Get("Vary"); vary != "Accept-Encoding" && vary != "Accept-Encoding, Accept-Encoding" {
		// Allow duplicated value as Go's header may append values depending on environment.
		t.Fatalf("unexpected Vary header: %q", vary)
	}
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		t.Fatalf("failed to read body: %v", err)
	}
	if string(body) != "compressed" {
		t.Fatalf("unexpected body: %q", string(body))
	}

	resp, err = http.Get(srv.URL + "/app.wasm.br?v=")
	if err != nil {
		t.Fatalf("failed to get unversioned wasm: %v", err)
	}
	defer closeTestResource(t, resp.Body)
	if cache := resp.Header.Get("Cache-Control"); cache != "no-cache" {
		t.Fatalf("unexpected unversioned Cache-Control header: %q", cache)
	}

	resp, err = http.Get(srv.URL + "/rfw_config.js")
	if err != nil {
		t.Fatalf("failed to get runtime config: %v", err)
	}
	defer closeTestResource(t, resp.Body)
	if cache := resp.Header.Get("Cache-Control"); cache != "no-cache" {
		t.Fatalf("unexpected runtime config Cache-Control header: %q", cache)
	}
}
F
function

TestNewMuxDevModeNoStore

TestNewMuxDevModeNoStore verifies that under rfw dev (RFW_DEV_BUILD=1) every
asset, including a versioned wasm and rfw_config.js, is served no-store so a
rebuild is never masked by an immutable cache entry.

Parameters

host/server_wasm_test.go:88-121
func TestNewMuxDevModeNoStore(t *testing.T)

{
	t.Setenv("RFW_DEVTOOLS", "")
	t.Setenv("RFW_DEV_BUILD", "1")
	root := t.TempDir()
	clientDir := filepath.Join(root, "client")
	if err := os.MkdirAll(clientDir, 0o755); err != nil {
		t.Fatalf("failed to create client dir: %v", err)
	}
	if err := os.WriteFile(filepath.Join(clientDir, "app.wasm"), []byte("wasm"), 0o644); err != nil {
		t.Fatalf("failed to write wasm: %v", err)
	}
	if err := os.WriteFile(filepath.Join(clientDir, "rfw_config.js"), []byte("//cfg"), 0o644); err != nil {
		t.Fatalf("failed to write config: %v", err)
	}
	if err := os.WriteFile(filepath.Join(clientDir, "index.html"), []byte("<html></html>"), 0o644); err != nil {
		t.Fatalf("failed to write index: %v", err)
	}

	mux := NewMux(clientDir)
	srv := httptest.NewServer(mux)
	defer srv.Close()

	for _, path := range []string{"/app.wasm?v=abc123", "/rfw_config.js", "/"} {
		resp, err := http.Get(srv.URL + path)
		if err != nil {
			t.Fatalf("get %s: %v", path, err)
		}
		cache := resp.Header.Get("Cache-Control")
		closeTestResource(t, resp.Body)
		if cache != "no-store" {
			t.Fatalf("dev %s: expected no-store, got %q", path, cache)
		}
	}
}
F
function

TestSessionResumeAndReplay

Parameters

host/session_delivery_test.go:9-31
func TestSessionResumeAndReplay(t *testing.T)

{
	session := AllocateResumableSession(4)
	token := session.ResumeToken()
	first := session.PrepareOutbound(Outbound{Component: "Counter", Payload: map[string]any{"value": 1}})
	second := session.PrepareOutbound(Outbound{Component: "Counter", Payload: map[string]any{"value": 2}})
	if first.Sequence != 1 || second.Sequence != 2 || token == "" {
		t.Fatalf("missing delivery metadata: first=%#v second=%#v", first, second)
	}

	SuspendSession(session, time.Second)
	resumed, ok := ResumeSession(token)
	if !ok || resumed != session {
		t.Fatal("session did not resume")
	}
	replay, err := resumed.ReplayAfter(first.Sequence)
	if err != nil {
		t.Fatalf("replay: %v", err)
	}
	if len(replay) != 1 || replay[0].Sequence != second.Sequence {
		t.Fatalf("unexpected replay: %#v", replay)
	}
	ReleaseSession(session)
}
F
function

TestSessionRejectsDuplicatesAndGaps

Parameters

host/session_delivery_test.go:33-47
func TestSessionRejectsDuplicatesAndGaps(t *testing.T)

{
	session := newSession("ordered")
	if err := session.AcceptInbound(1); err != nil {
		t.Fatalf("first message: %v", err)
	}
	if err := session.AcceptInbound(1); !errors.Is(err, ErrDuplicateMessage) {
		t.Fatalf("duplicate result: %v", err)
	}
	if err := session.AcceptInbound(3); !errors.Is(err, ErrSequenceGap) {
		t.Fatalf("gap result: %v", err)
	}
	if err := session.AcceptInbound(2); err != nil {
		t.Fatalf("next message: %v", err)
	}
}
F
function

TestSessionReplayReportsEvictedHistory

Parameters

host/session_delivery_test.go:49-57
func TestSessionReplayReportsEvictedHistory(t *testing.T)

{
	session := AllocateResumableSession(1)
	defer ReleaseSession(session)
	session.PrepareOutbound(Outbound{Payload: "one"})
	session.PrepareOutbound(Outbound{Payload: "two"})
	if _, err := session.ReplayAfter(0); !errors.Is(err, ErrReplayUnavailable) {
		t.Fatalf("expected replay error, got %v", err)
	}
}
F
function

TestSessionAllocationLimit

Parameters

host/session_delivery_test.go:59-72
func TestSessionAllocationLimit(t *testing.T)

{
	sessionMu.RLock()
	current := len(sessions)
	sessionMu.RUnlock()

	session, err := allocateSession(1, current+1)
	if err != nil {
		t.Fatalf("allocate within limit: %v", err)
	}
	defer ReleaseSession(session)
	if _, err := allocateSession(1, current+1); !errors.Is(err, ErrSessionLimit) {
		t.Fatalf("expected session limit, got %v", err)
	}
}
F
function

TestWithoutSSCResumeCreatesEphemeralSession

Parameters

host/session_delivery_test.go:74-84
func TestWithoutSSCResumeCreatesEphemeralSession(t *testing.T)

{
	runtime := NewWSRuntime(WithoutSSCResume())
	session, err := runtime.NewSession(nil)
	if err != nil {
		t.Fatalf("new session: %v", err)
	}
	if session.ResumeToken() != "" || runtime.ResumeTTL() != 0 {
		t.Fatalf("resume was not disabled: token=%q ttl=%s", session.ResumeToken(), runtime.ResumeTTL())
	}
	ReleaseSession(session)
}
F
function

TestExpiredTimerDoesNotReleaseResumedSession

Parameters

host/session_delivery_test.go:86-103
func TestExpiredTimerDoesNotReleaseResumedSession(t *testing.T)

{
	session := AllocateResumableSession(1)
	defer ReleaseSession(session)
	token := session.ResumeToken()
	SuspendSession(session, time.Second)
	session.deliveryMu.Lock()
	expectedExpiry := session.expires
	session.deliveryMu.Unlock()
	if _, ok := ResumeSession(token); !ok {
		t.Fatal("session did not resume")
	}

	releaseSession(session, expectedExpiry)

	if current, ok := SessionByID(session.ID()); !ok || current != session {
		t.Fatal("stale expiry released the resumed session")
	}
}
F
function

readPort

Returns

int
host/start.go:11-31
func readPort() int

{
	if override := strings.TrimSpace(os.Getenv("RFW_HOST_PORT")); override != "" {
		if p, err := strconv.Atoi(override); err == nil && p > 0 {
			return p
		}
	}
	var manifest struct {
		Port int `json:"port"`
	}
	data, err := os.ReadFile("rfw.json")
	if err != nil {
		return 8080
	}
	if err := json.Unmarshal(data, &manifest); err != nil {
		return 8080
	}
	if manifest.Port == 0 {
		return 8080
	}
	return manifest.Port
}
F
function

StartAuto

StartAuto launches HTTP and HTTPS servers serving files from the default
client build directory. It resolves the root path from rfw.json or falls
back to “build/client”. This is the recommended way to start the host server.

Returns

error
host/start.go:36-39
func StartAuto() error

{
	root := resolveRoot()
	return Start(root)
}
F
function

resolveRoot

Returns

string
host/start.go:41-55
func resolveRoot() string

{
	// Check rfw.json for build configuration.
	var manifest struct {
		Build struct {
			Dir string `json:"dir"`
		} `json:"build"`
	}
	if data, err := os.ReadFile("rfw.json"); err == nil {
		_ = json.Unmarshal(data, &manifest)
		if manifest.Build.Dir != "" {
			return manifest.Build.Dir
		}
	}
	return "build/client"
}
F
function

Start

Start launches HTTP and HTTPS servers serving files from root.
The HTTPS port is the HTTP port + 1.

Parameters

root
string

Returns

error
host/start.go:59-72
func Start(root string) error

{
	port := readPort()
	httpsPort := port + 1

	go func() {
		addr := fmt.Sprintf(":%d", port)
		if err := ListenAndServe(addr, root); err != nil {
			logger.Error("HTTP server error", "err", err)
		}
	}()

	httpsAddr := fmt.Sprintf(":%d", httpsPort)
	return ListenAndServeTLS(httpsAddr, root)
}
F
function

TestResolveTransportPrecedence

Parameters

host/transport_test.go:10-37
func TestResolveTransportPrecedence(t *testing.T)

{
	if runtime.GOOS == "js" {
		t.Skip("os.Chdir is not implemented on js")
	}
	dir := t.TempDir()
	previous, err := os.Getwd()
	if err != nil {
		t.Fatal(err)
	}
	if err := os.Chdir(dir); err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { _ = os.Chdir(previous) })

	if got := ResolveTransport(); got != TransportWebSocket {
		t.Fatalf("default = %q", got)
	}
	if err := os.WriteFile(filepath.Join(dir, "rfw.json"), []byte(`{"transport":"streambus"}`), 0o600); err != nil {
		t.Fatal(err)
	}
	if got := ResolveTransport(); got != TransportStreamBus {
		t.Fatalf("manifest = %q", got)
	}
	t.Setenv("RFW_TRANSPORT", "ws")
	if got := ResolveTransport(); got != TransportWebSocket {
		t.Fatalf("environment = %q", got)
	}
}
F
function

TestResolveTransportAliases

Parameters

host/transport_test.go:39-52
func TestResolveTransportAliases(t *testing.T)

{
	for value, want := range map[string]Transport{
		"websocket":      TransportWebSocket,
		"WS":             TransportWebSocket,
		"streambus":      TransportStreamBus,
		"webtransport":   TransportStreamBus,
		"warp-streambus": TransportStreamBus,
		"auto":           TransportAuto,
	} {
		if got := normalizeTransport(value); got != want {
			t.Errorf("normalizeTransport(%q) = %q, want %q", value, got, want)
		}
	}
}
T
type

ActionHandler

ActionHandler handles a typed client action.

host/actions.go:15-15
type ActionHandler func(context.Context, *Session, Request) (Response, error)
T
type

ActionAuthorizer

ActionAuthorizer can reject an action after the request is decoded.

host/actions.go:18-18
type ActionAuthorizer func(context.Context, *Session, Request) error
S
struct

actionConfig

host/actions.go:20-22
type actionConfig struct

Fields

Name Type Description
authorize ActionAuthorizer[Request]
T
type

ActionOption

ActionOption configures a typed action.

host/actions.go:25-25
type ActionOption func(*actionConfig[Request])
F
function

WithActionAuthorizer

WithActionAuthorizer adds action-specific authorization.

Parameters

authorize
ActionAuthorizer[Request]

Returns

ActionOption[Request]
host/actions.go:28-32
func WithActionAuthorizer[Request any](authorize ActionAuthorizer[Request]) ActionOption[Request]

{
	return func(config *actionConfig[Request]) {
		config.authorize = authorize
	}
}
I
interface

registeredAction

host/actions.go:34-36
type registeredAction interface

Methods

dispatch
Method

Parameters

map[string]any

Returns

func dispatch(...)
S
struct

typedAction

host/actions.go:38-41
type typedAction struct

Fields

Name Type Description
handler ActionHandler[Request, Response]
authorize ActionAuthorizer[Request]
F
function

RegisterAction

RegisterAction registers a strict, typed SSC action.

Parameters

name
string
handler
ActionHandler[Request, Response]
opts
...ActionOption[Request]

Returns

error
host/actions.go:75-96
func RegisterAction[Request, Response any](name string, handler ActionHandler[Request, Response], opts ...ActionOption[Request]) error

{
	if name == "" {
		return errors.New("host: empty action name")
	}
	if handler == nil {
		return errors.New("host: nil action handler")
	}
	var config actionConfig[Request]
	for _, opt := range opts {
		opt(&config)
	}
	actionRegistry.Lock()
	defer actionRegistry.Unlock()
	if _, exists := actionRegistry.actions[name]; exists {
		return fmt.Errorf("host: action %q already registered", name)
	}
	actionRegistry.actions[name] = typedAction[Request, Response]{
		handler:   handler,
		authorize: config.authorize,
	}
	return nil
}
F
function

DispatchAction

DispatchAction decodes and executes a registered action.

Parameters

session
name
string
payload
map[string]any

Returns

host/actions.go:99-107
func DispatchAction(ctx context.Context, session *Session, name string, payload map[string]any) (any, *ActionError)

{
	actionRegistry.RLock()
	action := actionRegistry.actions[name]
	actionRegistry.RUnlock()
	if action == nil {
		return nil, NewActionError("action_not_found", "action not found")
	}
	return action.dispatch(ctx, session, payload)
}
F
function

decodeActionPayload

Parameters

payload
map[string]any
target
any

Returns

error
host/actions.go:109-126
func decodeActionPayload(payload map[string]any, target any) error

{
	if payload == nil {
		payload = map[string]any{}
	}
	data, err := json.Marshal(payload)
	if err != nil {
		return errors.New("request payload is not valid JSON")
	}
	decoder := json.NewDecoder(bytes.NewReader(data))
	decoder.DisallowUnknownFields()
	if err := decoder.Decode(target); err != nil {
		return fmt.Errorf("invalid request: %w", err)
	}
	if err := decoder.Decode(&struct{}{}); !errors.Is(err, io.EOF) {
		return errors.New("invalid request: multiple JSON values")
	}
	return nil
}
F
function

publicActionError

Parameters

err
error
fallbackCode
string
fallbackMessage
string

Returns

host/actions.go:128-134
func publicActionError(err error, fallbackCode, fallbackMessage string) *ActionError

{
	var actionErr *ActionError
	if errors.As(err, &actionErr) {
		return actionErr
	}
	return NewActionError(fallbackCode, fallbackMessage)
}
T
type

FieldErrors

FieldErrors maps form field names to validation messages.

host/actions.go:137-137
type FieldErrors map[string]string
S
struct

FormResponse

FormResponse is returned by typed form actions.

host/actions.go:140-144
type FormResponse struct

Fields

Name Type Description
Data Response json:"data,omitempty"
Fields FieldErrors json:"fields,omitempty"
Valid bool json:"valid"
F
function

RegisterForm

RegisterForm registers a typed action with field validation.

Parameters

name
string
validate
func(Values) FieldErrors
submit
ActionHandler[Values, Response]
opts
...ActionOption[Values]

Returns

error
host/actions.go:147-168
func RegisterForm[Values, Response any](name string, validate func(Values) FieldErrors, submit ActionHandler[Values, Response], opts ...ActionOption[Values]) error

{
	if submit == nil {
		return errors.New("host: nil form handler")
	}
	return RegisterAction(name, func(ctx context.Context, session *Session, values Values) (FormResponse[Response], error) {
		if validate != nil {
			if fields := validate(values); len(fields) > 0 {
				return FormResponse[Response]{Fields: fields}, nil
			}
		}
		data, err := submit(ctx, session, values)
		if err != nil {
			return FormResponse[Response]{}, err
		}
		return FormResponse[Response]{Data: data, Valid: true}, nil
	}, opts...)
}
F
function

TestFrameRoundTrip

Parameters

host/framing_test.go:10-23
func TestFrameRoundTrip(t *testing.T)

{
	var stream bytes.Buffer
	want := []byte(`{"component":"Counter","payload":{"value":42}}`)
	if err := writeFrame(&stream, want); err != nil {
		t.Fatal(err)
	}
	got, err := readFrame(bufio.NewReader(&stream), 1024)
	if err != nil {
		t.Fatal(err)
	}
	if !bytes.Equal(got, want) {
		t.Fatalf("frame = %q, want %q", got, want)
	}
}
F
function

TestFrameRejectsPlatformOverflow

Parameters

host/framing_test.go:25-31
func TestFrameRejectsPlatformOverflow(t *testing.T)

{
	var prefix [binary.MaxVarintLen64]byte
	n := binary.PutUvarint(prefix[:], uint64(^uint(0)>>1)+1)
	if _, err := readFrame(bufio.NewReader(bytes.NewReader(prefix[:n])), 0); err == nil {
		t.Fatal("readFrame accepted a length larger than int")
	}
}
F
function

TestReadPortOverride

Parameters

host/start_test.go:5-10
func TestReadPortOverride(t *testing.T)

{
	t.Setenv("RFW_HOST_PORT", "9095")
	if got := readPort(); got != 9095 {
		t.Fatalf("expected override port 9095, got %d", got)
	}
}
F
function

buildDir

buildDir writes a client build with the artifacts the test names and returns
the mux serving it.

Parameters

artifacts
map[string]string

Returns

map[string]int
host/wasm_negotiation_test.go:14-32
func buildDir(t *testing.T, artifacts map[string]string) (*http.ServeMux, map[string]int)

{
	t.Helper()
	dir := t.TempDir()
	client := filepath.Join(dir, "client")
	if err := os.MkdirAll(client, 0o750); err != nil {
		t.Fatalf("mkdir client: %v", err)
	}
	sizes := map[string]int{}
	for name, body := range artifacts {
		if err := os.WriteFile(filepath.Join(client, name), []byte(body), 0o600); err != nil {
			t.Fatalf("write %s: %v", name, err)
		}
		sizes[name] = len(body)
	}
	if err := os.WriteFile(filepath.Join(client, "index.html"), []byte("<html></html>"), 0o600); err != nil {
		t.Fatalf("write index: %v", err)
	}
	return NewMux(client), sizes
}
F
function

request

Parameters

target
string
acceptEncoding
string

Returns

host/wasm_negotiation_test.go:34-43
func request(t *testing.T, mux *http.ServeMux, target, acceptEncoding string) *http.Response

{
	t.Helper()
	req := httptest.NewRequest(http.MethodGet, target, nil)
	if acceptEncoding != "" {
		req.Header.Set("Accept-Encoding", acceptEncoding)
	}
	recorder := httptest.NewRecorder()
	mux.ServeHTTP(recorder, req)
	return recorder.Result()
}
F
function

TestNegotiationPrefersBrotli

A client that accepts brotli gets brotli from the raw URL, with the headers
that let the browser decode it and a cache entry a shared cache cannot
mis-serve.

Parameters

host/wasm_negotiation_test.go:54-74
func TestNegotiationPrefersBrotli(t *testing.T)

{
	mux, sizes := buildDir(t, allArtifacts)
	resp := request(t, mux, "/app.wasm?v=abc123", "gzip, deflate, br")
	defer func() { _ = resp.Body.Close() }()

	if got := resp.Header.Get("Content-Encoding"); got != "br" {
		t.Fatalf("Content-Encoding = %q, want br", got)
	}
	if got := resp.Header.Get("Content-Type"); got != "application/wasm" {
		t.Fatalf("Content-Type = %q", got)
	}
	if got := resp.Header.Get("Content-Length"); got != strconv.Itoa(sizes["app.wasm.br"]) {
		t.Fatalf("Content-Length = %q, want the compressed size %d", got, sizes["app.wasm.br"])
	}
	if got := resp.Header.Get("Vary"); got != "Accept-Encoding" {
		t.Fatalf("Vary = %q", got)
	}
	if got := resp.Header.Get("Cache-Control"); got != "public, max-age=31536000, immutable" {
		t.Fatalf("Cache-Control on a versioned URL = %q", got)
	}
}
F
function

TestNegotiationFallsBackToGzip

A plain HTTP browser advertises gzip but not brotli, and must get gzip
rather than the raw bundle.

Parameters

host/wasm_negotiation_test.go:78-93
func TestNegotiationFallsBackToGzip(t *testing.T)

{
	mux, sizes := buildDir(t, allArtifacts)
	resp := request(t, mux, "/app.wasm", "gzip, deflate")
	defer func() { _ = resp.Body.Close() }()

	if got := resp.Header.Get("Content-Encoding"); got != "gzip" {
		t.Fatalf("Content-Encoding = %q, want gzip", got)
	}
	if got := resp.Header.Get("Content-Length"); got != strconv.Itoa(sizes["app.wasm.gz"]) {
		t.Fatalf("Content-Length = %q, want %d", got, sizes["app.wasm.gz"])
	}
	// An unversioned URL must revalidate or a release cannot replace it.
	if got := resp.Header.Get("Cache-Control"); got != "no-cache" {
		t.Fatalf("Cache-Control on an unversioned URL = %q", got)
	}
}
F
function

TestNegotiationNeverSendsUnrequestedBrotli

Brotli is never sent to a client that did not advertise it, whatever the
build produced.

Parameters

host/wasm_negotiation_test.go:97-107
func TestNegotiationNeverSendsUnrequestedBrotli(t *testing.T)

{
	mux, _ := buildDir(t, allArtifacts)
	for _, accept := range []string{"gzip", "gzip, deflate", "identity", "br;q=0, gzip"} {
		resp := request(t, mux, "/app.wasm", accept)
		encoding := resp.Header.Get("Content-Encoding")
		_ = resp.Body.Close()
		if encoding == "br" {
			t.Fatalf("Accept-Encoding %q got brotli", accept)
		}
	}
}
F
function

TestNegotiationHonoursAnExplicitRefusal

q=0 is an explicit refusal, not a low preference.

Parameters

host/wasm_negotiation_test.go:110-117
func TestNegotiationHonoursAnExplicitRefusal(t *testing.T)

{
	mux, _ := buildDir(t, allArtifacts)
	resp := request(t, mux, "/app.wasm", "br;q=0, gzip;q=0")
	defer func() { _ = resp.Body.Close() }()
	if got := resp.Header.Get("Content-Encoding"); got != "" {
		t.Fatalf("Content-Encoding = %q, want none", got)
	}
}
F
function

TestNegotiationServesRawWithoutAnAcceptEncoding

A client that sends no Accept-Encoding gets the raw bundle, which is the
only body it is guaranteed to understand.

Parameters

host/wasm_negotiation_test.go:121-131
func TestNegotiationServesRawWithoutAnAcceptEncoding(t *testing.T)

{
	mux, sizes := buildDir(t, allArtifacts)
	resp := request(t, mux, "/app.wasm", "")
	defer func() { _ = resp.Body.Close() }()
	if got := resp.Header.Get("Content-Encoding"); got != "" {
		t.Fatalf("Content-Encoding = %q, want none", got)
	}
	if got := resp.Header.Get("Content-Length"); got != strconv.Itoa(sizes["app.wasm"]) {
		t.Fatalf("Content-Length = %q, want the raw size", got)
	}
}
F
function

TestNegotiationFallsThroughWhenNoArtifactExists

A build with no compressed artifact still serves, so a development build
keeps working.

Parameters

host/wasm_negotiation_test.go:135-145
func TestNegotiationFallsThroughWhenNoArtifactExists(t *testing.T)

{
	mux, _ := buildDir(t, map[string]string{"app.wasm": "raw only"})
	resp := request(t, mux, "/app.wasm", "gzip, br")
	defer func() { _ = resp.Body.Close() }()
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("status = %d", resp.StatusCode)
	}
	if got := resp.Header.Get("Content-Encoding"); got != "" {
		t.Fatalf("Content-Encoding = %q with no artifact on disk", got)
	}
}
F
function

TestNegotiationSkipsAMissingBrotliArtifact

Only gzip exists, and a brotli-capable client takes it.

Parameters

host/wasm_negotiation_test.go:148-155
func TestNegotiationSkipsAMissingBrotliArtifact(t *testing.T)

{
	mux, _ := buildDir(t, map[string]string{"app.wasm": "raw", "app.wasm.gz": "gzipped"})
	resp := request(t, mux, "/app.wasm", "br, gzip")
	defer func() { _ = resp.Body.Close() }()
	if got := resp.Header.Get("Content-Encoding"); got != "gzip" {
		t.Fatalf("Content-Encoding = %q, want gzip", got)
	}
}
F
function

TestDirectArtifactURLsAreLabelled

A directly addressed artifact, which is how static hosting reaches it, is
still labelled correctly.

Parameters

host/wasm_negotiation_test.go:159-184
func TestDirectArtifactURLsAreLabelled(t *testing.T)

{
	mux, _ := buildDir(t, allArtifacts)
	for path, want := range map[string]string{
		"/app.wasm.br": "br",
		"/app.wasm.gz": "gzip",
	} {
		resp := request(t, mux, path+"?v=abc123", "gzip, br")
		encoding := resp.Header.Get("Content-Encoding")
		contentType := resp.Header.Get("Content-Type")
		cache := resp.Header.Get("Cache-Control")
		vary := resp.Header.Get("Vary")
		_ = resp.Body.Close()
		if encoding != want {
			t.Errorf("%s Content-Encoding = %q, want %q", path, encoding, want)
		}
		if contentType != "application/wasm" {
			t.Errorf("%s Content-Type = %q", path, contentType)
		}
		if cache != "public, max-age=31536000, immutable" {
			t.Errorf("%s Cache-Control = %q", path, cache)
		}
		if vary != "Accept-Encoding" {
			t.Errorf("%s Vary = %q", path, vary)
		}
	}
}
F
function

TestClientConfigRevalidates

The pointer file has to revalidate, or a browser keeps an old version and
therefore an old bundle URL.

Parameters

host/wasm_negotiation_test.go:188-195
func TestClientConfigRevalidates(t *testing.T)

{
	mux, _ := buildDir(t, map[string]string{"app.wasm": "raw", "rfw_config.js": "window.RFW_WASM_VERSION = \"abc\";"})
	resp := request(t, mux, "/rfw_config.js?v=abc123", "gzip")
	defer func() { _ = resp.Body.Close() }()
	if got := resp.Header.Get("Cache-Control"); got != "no-cache" {
		t.Fatalf("rfw_config.js Cache-Control = %q, want no-cache", got)
	}
}
F
function

TestStampedPageRevalidates

index.html carries the stamped bootstrap script tags, so a browser that
caches it keeps loading the previous release’s loader. It has to revalidate.

Parameters

host/wasm_negotiation_test.go:199-209
func TestStampedPageRevalidates(t *testing.T)

{
	mux, _ := buildDir(t, map[string]string{"app.wasm": "raw"})
	for _, target := range []string{"/", "/index.html"} {
		resp := request(t, mux, target, "gzip")
		cache := resp.Header.Get("Cache-Control")
		_ = resp.Body.Close()
		if cache != "no-cache" {
			t.Errorf("%s Cache-Control = %q, want no-cache", target, cache)
		}
	}
}
F
function

TestAcceptedEncodings

Parameters

host/wasm_negotiation_test.go:211-237
func TestAcceptedEncodings(t *testing.T)

{
	cases := map[string]map[string]float64{
		"":                     nil,
		"gzip":                 {"gzip": 1},
		"gzip, deflate, br":    {"gzip": 1, "deflate": 1, "br": 1},
		"BR":                   {"br": 1},
		" gzip ;q=0.5 , br ":   {"gzip": 0.5, "br": 1},
		"br;q=0, gzip":         {"br": 0, "gzip": 1},
		"*":                    {"*": 1},
		"*, br;q=0":            {"*": 1, "br": 0},
		"identity;q=1, br;q=0": {"identity": 1, "br": 0},
		"br;q=bogus, gzip":     {"br": 0, "gzip": 1},
		"br;q=2, gzip":         {"br": 0, "gzip": 1},
	}
	for header, want := range cases {
		got := acceptedEncodings(header)
		if len(got) != len(want) {
			t.Errorf("acceptedEncodings(%q) = %v, want %v", header, got, want)
			continue
		}
		for name, quality := range want {
			if got[name] != quality {
				t.Errorf("acceptedEncodings(%q)[%q] = %v, want %v", header, name, got[name], quality)
			}
		}
	}
}
F
function

TestNegotiationHonorsClientEncodingPreference

Parameters

host/wasm_negotiation_test.go:239-250
func TestNegotiationHonorsClientEncodingPreference(t *testing.T)

{
	mux, _ := buildDir(t, map[string]string{
		"app.wasm":    "raw",
		"app.wasm.br": "brotli",
		"app.wasm.gz": "gzip",
	})
	resp := request(t, mux, "/app.wasm", "gzip;q=1, br;q=0.1")
	defer func() { _ = resp.Body.Close() }()
	if got := resp.Header.Get("Content-Encoding"); got != "gzip" {
		t.Fatalf("Content-Encoding = %q, want gzip", got)
	}
}
F
function

TestNegotiationUsesServerPreferenceForEqualQualities

Parameters

host/wasm_negotiation_test.go:252-263
func TestNegotiationUsesServerPreferenceForEqualQualities(t *testing.T)

{
	mux, _ := buildDir(t, map[string]string{
		"app.wasm":    "raw",
		"app.wasm.br": "brotli",
		"app.wasm.gz": "gzip",
	})
	resp := request(t, mux, "/app.wasm", "gzip, br")
	defer func() { _ = resp.Body.Close() }()
	if got := resp.Header.Get("Content-Encoding"); got != "br" {
		t.Fatalf("Content-Encoding = %q, want br", got)
	}
}
S
struct

blockingJSONPayload

host/websocket_order_test.go:15-19
type blockingJSONPayload struct

Methods

MarshalJSON
Method

Returns

[]byte
error
func (blockingJSONPayload) MarshalJSON() ([]byte, error)
{
	payload.entered <- struct{}{}
	<-payload.release
	return json.Marshal(payload.value)
}

Fields

Name Type Description
entered chan<- struct{}
release <-chan struct{}
value string
S
struct

signalingJSONPayload

host/websocket_order_test.go:27-30
type signalingJSONPayload struct

Methods

MarshalJSON
Method

Returns

[]byte
error
func (signalingJSONPayload) MarshalJSON() ([]byte, error)
{
	payload.entered <- struct{}{}
	return json.Marshal(payload.value)
}

Fields

Name Type Description
entered chan<- struct{}
value string
F
function

openWriteTestSocket

Parameters

Returns

host/websocket_order_test.go:37-58
func openWriteTestSocket(t *testing.T) (*websocket.Conn, *websocket.Conn, func())

{
	t.Helper()
	accepted := make(chan *websocket.Conn, 1)
	done := make(chan struct{})
	server := httptest.NewServer(websocket.Handler(func(ws *websocket.Conn) {
		accepted <- ws
		<-done
	}))
	client, err := websocket.Dial("ws"+strings.TrimPrefix(server.URL, "http"), "", server.URL)
	if err != nil {
		server.Close()
		t.Fatalf("dial websocket: %v", err)
	}
	serverSocket := <-accepted
	return client, serverSocket, func() {
		ForgetConnection(serverSocket)
		closeTestResource(t, client)
		closeTestResource(t, serverSocket)
		close(done)
		server.Close()
	}
}
F
function

receiveOrderedMessage

Parameters

Returns

host/websocket_order_test.go:60-71
func receiveOrderedMessage(t *testing.T, socket *websocket.Conn) Outbound

{
	t.Helper()
	var raw []byte
	if err := websocket.Message.Receive(socket, &raw); err != nil {
		t.Fatalf("receive websocket message: %v", err)
	}
	var message Outbound
	if err := json.Unmarshal(raw, &message); err != nil {
		t.Fatalf("decode websocket message: %v", err)
	}
	return message
}
F
function

TestSendSessionOutboundSerializesSequenceAndWrite

Parameters

host/websocket_order_test.go:73-118
func TestSendSessionOutboundSerializesSequenceAndWrite(t *testing.T)

{
	client, server, closeSockets := openWriteTestSocket(t)
	defer closeSockets()
	session := newSession("ordered-write")
	BindSessionConnection(server, session)
	firstEntered := make(chan struct{}, 1)
	firstRelease := make(chan struct{})
	secondEntered := make(chan struct{}, 1)
	firstDone := make(chan struct{})
	secondDone := make(chan struct{})

	go func() {
		SendSessionOutbound(server, session, Outbound{Payload: blockingJSONPayload{
			entered: firstEntered,
			release: firstRelease,
			value:   "first",
		}})
		close(firstDone)
	}()
	<-firstEntered
	go func() {
		SendSessionOutbound(server, session, Outbound{Payload: signalingJSONPayload{
			entered: secondEntered,
			value:   "second",
		}})
		close(secondDone)
	}()

	select {
	case <-secondEntered:
		close(firstRelease)
		<-firstDone
		<-secondDone
		t.Fatal("second message reached the writer before the first completed")
	case <-time.After(50 * time.Millisecond):
	}
	close(firstRelease)
	<-firstDone
	<-secondDone

	first := receiveOrderedMessage(t, client)
	second := receiveOrderedMessage(t, client)
	if first.Sequence != 1 || second.Sequence != 2 {
		t.Fatalf("messages arrived out of order: first=%d second=%d", first.Sequence, second.Sequence)
	}
}
F
function

TestReplaySessionDoesNotInterleaveNewMessages

Parameters

host/websocket_order_test.go:120-167
func TestReplaySessionDoesNotInterleaveNewMessages(t *testing.T)

{
	client, server, closeSockets := openWriteTestSocket(t)
	defer closeSockets()
	session := newSession("ordered-replay", sessionOptions{replayLimit: 4})
	BindSessionConnection(server, session)
	firstEntered := make(chan struct{}, 1)
	firstRelease := make(chan struct{})
	newEntered := make(chan struct{}, 1)
	session.PrepareOutbound(Outbound{Payload: blockingJSONPayload{
		entered: firstEntered,
		release: firstRelease,
		value:   "first",
	}})
	session.PrepareOutbound(Outbound{Payload: "second"})
	replayDone := make(chan struct{})
	sendDone := make(chan struct{})

	go func() {
		ReplaySession(server, session, 0)
		close(replayDone)
	}()
	<-firstEntered
	go func() {
		SendSessionOutbound(server, session, Outbound{Payload: signalingJSONPayload{
			entered: newEntered,
			value:   "third",
		}})
		close(sendDone)
	}()

	select {
	case <-newEntered:
		close(firstRelease)
		<-replayDone
		<-sendDone
		t.Fatal("new message reached the writer during replay")
	case <-time.After(50 * time.Millisecond):
	}
	close(firstRelease)
	<-replayDone
	<-sendDone

	for sequence := uint64(1); sequence <= 3; sequence++ {
		if message := receiveOrderedMessage(t, client); message.Sequence != sequence {
			t.Fatalf("unexpected replay order: got %d want %d", message.Sequence, sequence)
		}
	}
}
F
function

TestStaleConnectionDoesNotConsumeSequenceAfterResume

Parameters

host/websocket_order_test.go:169-202
func TestStaleConnectionDoesNotConsumeSequenceAfterResume(t *testing.T)

{
	_, oldServer, closeOld := openWriteTestSocket(t)
	defer closeOld()
	newClient, newServer, closeNew := openWriteTestSocket(t)
	defer closeNew()
	session := AllocateResumableSession(4)
	defer ReleaseSession(session)
	BindSessionConnection(oldServer, session)
	token := session.ResumeToken()

	SuspendSession(session, time.Second)
	resumed, ok := ResumeSession(token)
	if !ok {
		t.Fatal("session did not resume")
	}
	BindSessionConnection(newServer, resumed)

	staleEntered := make(chan struct{}, 1)
	SendSessionOutbound(oldServer, resumed, Outbound{Payload: signalingJSONPayload{
		entered: staleEntered,
		value:   "stale",
	}})
	select {
	case <-staleEntered:
		t.Fatal("stale connection reached the writer")
	default:
	}

	SendSessionOutbound(newServer, resumed, Outbound{Payload: "current"})
	message := receiveOrderedMessage(t, newClient)
	if message.Sequence != 1 {
		t.Fatalf("stale connection consumed a sequence: got %d want 1", message.Sequence)
	}
}
F
function

TestCustomHandlerDeliveryBindsAcrossResume

Parameters

host/websocket_order_test.go:204-256
func TestCustomHandlerDeliveryBindsAcrossResume(t *testing.T)

{
	oldClient, oldServer, closeOld := openWriteTestSocket(t)
	defer closeOld()
	newClient, newServer, closeNew := openWriteTestSocket(t)
	defer closeNew()
	session := AllocateResumableSession(4)
	defer ReleaseSession(session)
	token := session.ResumeToken()

	SendSessionOutbound(oldServer, session, Outbound{Payload: "first"})
	first := receiveOrderedMessage(t, oldClient)
	if first.Sequence != 1 {
		t.Fatalf("first sequence = %d, want 1", first.Sequence)
	}
	SuspendSession(session, time.Second)
	resumed, ok := ResumeSession(token)
	if !ok {
		t.Fatal("session did not resume")
	}

	staleEntered := make(chan struct{}, 1)
	SendSessionOutbound(oldServer, resumed, Outbound{Payload: signalingJSONPayload{
		entered: staleEntered,
		value:   "stale",
	}})
	select {
	case <-staleEntered:
		t.Fatal("stale custom handler connection reached the writer")
	default:
	}

	ReplaySession(newServer, resumed, 0)
	replayed := receiveOrderedMessage(t, newClient)
	if replayed.Sequence != first.Sequence {
		t.Fatalf("replayed sequence = %d, want %d", replayed.Sequence, first.Sequence)
	}

	SendSessionOutbound(oldServer, resumed, Outbound{Payload: signalingJSONPayload{
		entered: staleEntered,
		value:   "stale",
	}})
	select {
	case <-staleEntered:
		t.Fatal("stale custom handler connection reached the writer")
	default:
	}

	SendSessionOutbound(newServer, resumed, Outbound{Payload: "second"})
	second := receiveOrderedMessage(t, newClient)
	if second.Sequence != 2 {
		t.Fatalf("second sequence = %d, want 2", second.Sequence)
	}
}
F
function

TestManagedSessionRejectsAllPriorConnections

Parameters

host/websocket_order_test.go:258-306
func TestManagedSessionRejectsAllPriorConnections(t *testing.T)

{
	_, firstServer, closeFirst := openWriteTestSocket(t)
	defer closeFirst()
	_, secondServer, closeSecond := openWriteTestSocket(t)
	defer closeSecond()
	thirdClient, thirdServer, closeThird := openWriteTestSocket(t)
	defer closeThird()
	session := AllocateResumableSession(4)
	defer ReleaseSession(session)
	token := session.ResumeToken()
	BindSessionConnection(firstServer, session)

	SuspendSession(session, time.Second)
	resumed, ok := ResumeSession(token)
	if !ok {
		t.Fatal("first resume failed")
	}
	BindSessionConnection(secondServer, resumed)
	SuspendSession(resumed, time.Second)
	resumed, ok = ResumeSession(token)
	if !ok {
		t.Fatal("second resume failed")
	}
	BindSessionConnection(thirdServer, resumed)

	firstEntered := make(chan struct{}, 1)
	SendSessionOutbound(firstServer, resumed, Outbound{Payload: signalingJSONPayload{
		entered: firstEntered,
		value:   "first-stale",
	}})
	secondEntered := make(chan struct{}, 1)
	SendSessionOutbound(secondServer, resumed, Outbound{Payload: signalingJSONPayload{
		entered: secondEntered,
		value:   "second-stale",
	}})
	select {
	case <-firstEntered:
		t.Fatal("first stale connection reached the writer")
	case <-secondEntered:
		t.Fatal("second stale connection reached the writer")
	default:
	}

	SendSessionOutbound(thirdServer, resumed, Outbound{Payload: "current"})
	message := receiveOrderedMessage(t, thirdClient)
	if message.Sequence != 1 {
		t.Fatalf("stale connection consumed a sequence: got %d want 1", message.Sequence)
	}
}
S
struct

SSCLimits

SSCLimits bounds WebSocket resource use and action execution.

host/ws_runtime.go:13-23
type SSCLimits struct

Fields

Name Type Description
MaxMessageBytes int
MaxConnections int64
MaxSessions int
MessagesPerMinute int
HandlerTimeout time.Duration
WriteTimeout time.Duration
OutboundQueueSize int
ResumeTTL time.Duration
ReplayMessages int
F
function

DefaultSSCLimits

DefaultSSCLimits returns the production defaults used by NewMux.

Returns

host/ws_runtime.go:26-38
func DefaultSSCLimits() SSCLimits

{
	return SSCLimits{
		MaxMessageBytes:   1 << 20,
		MaxConnections:    4096,
		MaxSessions:       8192,
		MessagesPerMinute: 600,
		HandlerTimeout:    15 * time.Second,
		WriteTimeout:      10 * time.Second,
		OutboundQueueSize: 64,
		ResumeTTL:         2 * time.Minute,
		ReplayMessages:    256,
	}
}
T
type

MessageAuthorizer

MessageAuthorizer can reject any decoded SSC message.

host/ws_runtime.go:41-41
type MessageAuthorizer func(context.Context, *Session, Inbound) error
T
type

SessionInitializer

SessionInitializer copies authenticated request state into a new session.

host/ws_runtime.go:44-44
type SessionInitializer func(*http.Request, *Session) error
T
type

ResumeAuthorizer

ResumeAuthorizer decides whether an upgrade request may reattach a retained
session. It receives the new request and the detached session the presented
token identifies, and returns a non-nil error to refuse the reattachment.

host/ws_runtime.go:49-49
type ResumeAuthorizer func(*http.Request, *Session) error
T
type

MuxOption

MuxOption configures the WebSocket endpoint created by NewMux.

host/ws_runtime.go:52-52
type MuxOption func(*WSRuntime)
S
struct

WSRuntime

WSRuntime holds the guards, limits, and connection count for one endpoint.

host/ws_runtime.go:55-63
type WSRuntime struct

Methods

Guard
Method

Guard applies origin and upgrade authentication checks.

Parameters

Returns

func (*WSRuntime) Guard(next http.Handler) http.Handler
{
	if runtime == nil {
		return next
	}
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if len(runtime.origins) > 0 {
			origin := r.Header.Get("Origin")
			allowed := false
			for _, candidate := range runtime.origins {
				if origin == candidate {
					allowed = true
					break
				}
			}
			if !allowed {
				http.Error(w, "origin not allowed", http.StatusForbidden)
				return
			}
		}
		if runtime.authFunc != nil && !runtime.authFunc(r) {
			http.Error(w, "unauthorized", http.StatusUnauthorized)
			return
		}
		next.ServeHTTP(w, r)
	})
}

AcquireConnection reserves a connection slot.

Returns

bool
func (*WSRuntime) AcquireConnection() bool
{
	if runtime == nil {
		return true
	}
	active := runtime.connections.Add(1)
	if runtime.limits.MaxConnections > 0 && active > runtime.limits.MaxConnections {
		runtime.connections.Add(-1)
		return false
	}
	return true
}

ReleaseConnection frees a reserved connection slot.

func (*WSRuntime) ReleaseConnection()
{
	if runtime != nil {
		runtime.connections.Add(-1)
	}
}

ConfigureConnection applies frame and outbound delivery limits.

Parameters

func (*WSRuntime) ConfigureConnection(ws *websocket.Conn)
{
	if runtime == nil {
		return
	}
	if runtime.limits.MaxMessageBytes > 0 {
		ws.MaxPayloadBytes = runtime.limits.MaxMessageBytes
	}
	configureConnectionWriter(ws, runtime.limits.WriteTimeout, runtime.limits.OutboundQueueSize)
}
NewSession
Method

NewSession allocates and initializes a resumable session.

Parameters

request *http.Request

Returns

error
func (*WSRuntime) NewSession(request *http.Request) (*Session, error)
{
	replayLimit := 0
	if runtime != nil {
		replayLimit = runtime.limits.ReplayMessages
	}
	maxSessions := 0
	if runtime != nil {
		maxSessions = runtime.limits.MaxSessions
	}
	session, err := allocateSession(replayLimit, maxSessions)
	if err != nil {
		return nil, err
	}
	if runtime != nil && runtime.initialize != nil {
		if err := runtime.initialize(request, session); err != nil {
			ReleaseSession(session)
			return nil, err
		}
	}
	return session, nil
}
OpenSession
Method

OpenSession resumes a retained session before allocating a new one. A token the request is not allowed to reattach is treated like an unresumable one: the caller gets a new session and the retained one stays available to its owner.

Parameters

request *http.Request
resumeToken string

Returns

bool
error
func (*WSRuntime) OpenSession(request *http.Request, resumeToken string) (*Session, bool, error)
{
	if resumed, ok := runtime.resumeSession(request, resumeToken); ok {
		return resumed, true, nil
	}
	session, err := runtime.NewSession(request)
	return session, false, err
}
resumeSession
Method

resumeSession authorizes a reattachment before any session state changes.

Parameters

request *http.Request
resumeToken string

Returns

bool
func (*WSRuntime) resumeSession(request *http.Request, resumeToken string) (*Session, bool)
{
	if runtime == nil || runtime.authorizeResume == nil {
		return ResumeSession(resumeToken)
	}
	candidate, ok := resumeCandidate(resumeToken)
	if !ok {
		return nil, false
	}
	if err := runtime.authorizeResume(request, candidate); err != nil {
		return nil, false
	}
	if !commitResume(resumeToken, candidate) {
		return nil, false
	}
	return candidate, true
}
Authorize
Method

Authorize validates a decoded message.

Parameters

session *Session
message Inbound

Returns

error
func (*WSRuntime) Authorize(ctx context.Context, session *Session, message Inbound) error
{
	if runtime == nil || runtime.authorize == nil {
		return nil
	}
	return runtime.authorize(ctx, session, message)
}

HandlerContext returns a context bounded by HandlerTimeout.

func (*WSRuntime) HandlerContext(parent context.Context) (context.Context, context.CancelFunc)
{
	if runtime == nil || runtime.limits.HandlerTimeout <= 0 {
		return context.WithCancel(parent)
	}
	return context.WithTimeout(parent, runtime.limits.HandlerTimeout)
}
ResumeTTL
Method

ResumeTTL returns the configured detached-session lifetime.

Returns

func (*WSRuntime) ResumeTTL() time.Duration
{
	if runtime == nil {
		return 0
	}
	return runtime.limits.ResumeTTL
}

MessagesPerMinute returns the configured per-session message limit.

Returns

int
func (*WSRuntime) MessagesPerMinute() int
{
	if runtime == nil {
		return 0
	}
	return runtime.limits.MessagesPerMinute
}

DispatchAction executes a typed action within the configured handler deadline.

Parameters

session *Session
message Inbound

Returns

func (*WSRuntime) DispatchAction(parent context.Context, session *Session, message Inbound) (any, *ActionError)
{
	ctx, cancel := runtime.HandlerContext(parent)
	defer cancel()
	type result struct {
		payload any
		err     *ActionError
	}
	resultChannel := make(chan result, 1)
	go func() {
		defer func() {
			if recover() != nil {
				resultChannel <- result{err: NewActionError("action_failed", "action failed")}
			}
		}()
		payload, actionErr := DispatchAction(ctx, session, message.Action, message.Payload)
		resultChannel <- result{payload: payload, err: actionErr}
	}()
	select {
	case response := <-resultChannel:
		return response.payload, response.err
	case <-ctx.Done():
		return nil, NewActionError("action_timeout", "action timed out")
	}
}

Fields

Name Type Description
authFunc func(*http.Request) bool
origins []string
authorize MessageAuthorizer
initialize SessionInitializer
authorizeResume ResumeAuthorizer
limits SSCLimits
connections atomic.Int64
F
function

NewWSRuntime

NewWSRuntime resolves MuxOptions into an endpoint runtime.

Parameters

opts
...MuxOption

Returns

host/ws_runtime.go:66-72
func NewWSRuntime(opts ...MuxOption) *WSRuntime

{
	runtime := &WSRuntime{limits: DefaultSSCLimits()}
	for _, opt := range opts {
		opt(runtime)
	}
	return runtime
}
F
function

WithAuthFunc

WithAuthFunc registers a callback invoked before the WebSocket upgrade.

Parameters

fn
func(*http.Request) bool

Returns

host/ws_runtime.go:75-77
func WithAuthFunc(fn func(*http.Request) bool) MuxOption

{
	return func(runtime *WSRuntime) { runtime.authFunc = fn }
}
F
function

WithOriginAllowlist

WithOriginAllowlist restricts upgrades to exact Origin matches.

Parameters

origins
...string

Returns

host/ws_runtime.go:80-84
func WithOriginAllowlist(origins ...string) MuxOption

{
	return func(runtime *WSRuntime) {
		runtime.origins = append(runtime.origins, origins...)
	}
}
F
function

WithSSCAuthorizer

WithSSCAuthorizer adds authorization after a message is decoded.

Parameters

authorize

Returns

host/ws_runtime.go:87-89
func WithSSCAuthorizer(authorize MessageAuthorizer) MuxOption

{
	return func(runtime *WSRuntime) { runtime.authorize = authorize }
}
F
function

WithSSCSessionInitializer

WithSSCSessionInitializer initializes session identity from the upgrade request.
It runs for new sessions only; a resumed session keeps the identity bound
when it was created, so use WithSSCResumeAuthorizer to decide who may
reattach it.

Parameters

initialize

Returns

host/ws_runtime.go:95-97
func WithSSCSessionInitializer(initialize SessionInitializer) MuxOption

{
	return func(runtime *WSRuntime) { runtime.initialize = initialize }
}
F
function

WithSSCResumeAuthorizer

WithSSCResumeAuthorizer authorizes reattachment of a retained session.
The callback compares the authenticated upgrade request with the session it
would resume (identity bound by the session initializer, revocation state,
anything else the deployment tracks) and returns an error to refuse.

A refusal leaves the retained session untouched and detached, so its owner
can still resume it while its TTL lasts, and the caller is served a fresh
session exactly like an unknown token.

Without this option any client presenting a valid token resumes the session,
which is the historical behavior. An authenticated multi-user deployment must
configure a resume authorizer or disable resume with WithoutSSCResume.

Parameters

authorize

Returns

host/ws_runtime.go:111-113
func WithSSCResumeAuthorizer(authorize ResumeAuthorizer) MuxOption

{
	return func(runtime *WSRuntime) { runtime.authorizeResume = authorize }
}
F
function

WithSSCLimits

WithSSCLimits overrides non-zero SSC resource limits.

Parameters

limits

Returns

host/ws_runtime.go:116-146
func WithSSCLimits(limits SSCLimits) MuxOption

{
	return func(runtime *WSRuntime) {
		if limits.MaxMessageBytes > 0 {
			runtime.limits.MaxMessageBytes = limits.MaxMessageBytes
		}
		if limits.MaxConnections > 0 {
			runtime.limits.MaxConnections = limits.MaxConnections
		}
		if limits.MaxSessions > 0 {
			runtime.limits.MaxSessions = limits.MaxSessions
		}
		if limits.MessagesPerMinute > 0 {
			runtime.limits.MessagesPerMinute = limits.MessagesPerMinute
		}
		if limits.HandlerTimeout > 0 {
			runtime.limits.HandlerTimeout = limits.HandlerTimeout
		}
		if limits.WriteTimeout > 0 {
			runtime.limits.WriteTimeout = limits.WriteTimeout
		}
		if limits.OutboundQueueSize > 0 {
			runtime.limits.OutboundQueueSize = limits.OutboundQueueSize
		}
		if limits.ResumeTTL > 0 {
			runtime.limits.ResumeTTL = limits.ResumeTTL
		}
		if limits.ReplayMessages > 0 {
			runtime.limits.ReplayMessages = limits.ReplayMessages
		}
	}
}
F
function

WithoutSSCResume

WithoutSSCResume releases sessions as soon as their connection closes.

Returns

host/ws_runtime.go:149-154
func WithoutSSCResume() MuxOption

{
	return func(runtime *WSRuntime) {
		runtime.limits.ResumeTTL = 0
		runtime.limits.ReplayMessages = 0
	}
}
F
function

GuardWS

GuardWS wraps a WebSocket handler using MuxOptions.

Parameters

opts
...MuxOption

Returns

host/ws_runtime.go:185-187
func GuardWS(next http.Handler, opts ...MuxOption) http.Handler

{
	return NewWSRuntime(opts...).Guard(next)
}
F
function

TestTypedActionRejectsUnknownFields

Parameters

host/actions_test.go:9-38
func TestTypedActionRejectsUnknownFields(t *testing.T)

{
	type request struct {
		Name string `json:"name"`
	}
	type response struct {
		Greeting string `json:"greeting"`
	}
	const name = "test.typed.strict"
	if err := RegisterAction(name, func(_ context.Context, _ *Session, request request) (response, error) {
		return response{Greeting: "hello " + request.Name}, nil
	}); err != nil {
		t.Fatalf("register action: %v", err)
	}

	result, actionErr := DispatchAction(context.Background(), newSession("typed"), name, map[string]any{"name": "Ada"})
	if actionErr != nil {
		t.Fatalf("dispatch action: %v", actionErr)
	}
	if result.(response).Greeting != "hello Ada" {
		t.Fatalf("unexpected response: %#v", result)
	}

	_, actionErr = DispatchAction(context.Background(), newSession("strict"), name, map[string]any{
		"name":  "Ada",
		"admin": true,
	})
	if actionErr == nil || actionErr.Code != "invalid_request" {
		t.Fatalf("unknown field was accepted: %#v", actionErr)
	}
}
F
function

TestTypedActionAuthorizationHidesInternalError

Parameters

host/actions_test.go:40-63
func TestTypedActionAuthorizationHidesInternalError(t *testing.T)

{
	type request struct {
		Owner string `json:"owner"`
	}
	const name = "test.typed.authorized"
	if err := RegisterAction(name,
		func(_ context.Context, _ *Session, request request) (request, error) {
			return request, nil
		},
		WithActionAuthorizer(func(_ context.Context, _ *Session, request request) error {
			if request.Owner != "allowed" {
				return errors.New("database policy detail")
			}
			return nil
		}),
	); err != nil {
		t.Fatalf("register action: %v", err)
	}

	_, actionErr := DispatchAction(context.Background(), newSession("denied"), name, map[string]any{"owner": "denied"})
	if actionErr == nil || actionErr.Code != "forbidden" || actionErr.Message != "action forbidden" {
		t.Fatalf("unexpected authorization response: %#v", actionErr)
	}
}
F
function

TestTypedFormReturnsFieldErrors

Parameters

host/actions_test.go:65-95
func TestTypedFormReturnsFieldErrors(t *testing.T)

{
	type values struct {
		Email string `json:"email"`
	}
	type result struct {
		ID int `json:"id"`
	}
	const name = "test.form.validation"
	if err := RegisterForm(name,
		func(values values) FieldErrors {
			if values.Email == "" {
				return FieldErrors{"email": "required"}
			}
			return nil
		},
		func(_ context.Context, _ *Session, _ values) (result, error) {
			return result{ID: 7}, nil
		},
	); err != nil {
		t.Fatalf("register form: %v", err)
	}

	raw, actionErr := DispatchAction(context.Background(), newSession("form"), name, map[string]any{})
	if actionErr != nil {
		t.Fatalf("dispatch form: %v", actionErr)
	}
	response := raw.(FormResponse[result])
	if response.Valid || response.Fields["email"] != "required" {
		t.Fatalf("unexpected form response: %#v", response)
	}
}
S
struct

streamBusEndpoint

host/streambus_server.go:37-42
type streamBusEndpoint struct

Methods

ServeHTTP
Method
func (*streamBusEndpoint) ServeHTTP(w http.ResponseWriter, r *http.Request)
{
	e.mu.RLock()
	server := e.server
	e.mu.RUnlock()
	if server == nil {
		http.Error(w, "streambus requires HTTP/3", http.StatusUpgradeRequired)
		return
	}
	session, err := server.Upgrade(w, r)
	if err != nil {
		return
	}
	if err := e.serveSession(r, session); err != nil && !errors.Is(err, context.Canceled) && !errors.Is(err, io.EOF) {
		log.Printf("streambus: %v", err)
	}
}
serveSession
Method

Parameters

request *http.Request
transport *wt.Session

Returns

error
func (*streamBusEndpoint) serveSession(request *http.Request, transport *wt.Session) error
{
	if !e.runtime.AcquireConnection() {
		_ = transport.CloseWithError(1, "connection limit reached")
		return errors.New("streambus: connection limit reached")
	}
	defer e.runtime.ReleaseConnection()
	stream, err := transport.AcceptStream(transport.Context())
	if err != nil {
		return err
	}
	connection, err := newStreamBusConnection(transport.Context(), request, transport, stream, e.bus, e.runtime.limits.MaxMessageBytes)
	if err != nil {
		return err
	}
	defer connection.Close()
	return streamBusProtocolLoop(connection, e.runtime)
}

Fields

Name Type Description
runtime *WSRuntime
bus *streambus.InMemory
mu sync.RWMutex
server *wt.Server
F
function

newStreamBusEndpoint

Parameters

runtime
host/streambus_server.go:44-56
func newStreamBusEndpoint(runtime *WSRuntime) *streamBusEndpoint

{
	maximum := DefaultSSCLimits().MaxMessageBytes
	if runtime != nil && runtime.limits.MaxMessageBytes > 0 {
		maximum = runtime.limits.MaxMessageBytes
	}
	return &streamBusEndpoint{
		runtime: runtime,
		bus: streambus.NewInMemory(streambus.Config{
			DefaultBuffer: 256, MaxBuffer: 4096, ReplayCapacity: 256,
			MaxPayloadBytes: maximum,
		}),
	}
}
F
function

registerStreamBus

Parameters

runtime
host/streambus_server.go:58-75
func registerStreamBus(mux *http.ServeMux, runtime *WSRuntime)

{
	if !streamBusEnabled() {
		return
	}
	endpoint := newStreamBusEndpoint(runtime)
	mux.Handle(streamBusPath, runtime.Guard(endpoint))
	mux.HandleFunc("/__rfw/streambus-config", func(w http.ResponseWriter, _ *http.Request) {
		hash, _ := streamCertHash.Load().(string)
		port, _ := streamHTTP3Port.Load().(string)
		if hash == "" || port == "" {
			http.Error(w, "streambus certificate is not ready", http.StatusServiceUnavailable)
			return
		}
		w.Header().Set("Content-Type", "application/json")
		_ = json.NewEncoder(w).Encode(map[string]string{"certificateHash": hash, "port": port})
	})
	streamEndpoints.Store(mux, endpoint)
}
S
struct

streamBusConnection

host/streambus_server.go:112-124
type streamBusConnection struct

Methods

Receive
Method

Parameters

message *Inbound

Returns

error
func (*streamBusConnection) Receive(message *Inbound) error
{
	payload, err := readFrame(c.reader, c.maximum)
	if err != nil {
		return err
	}
	return json.Unmarshal(payload, message)
}
Send
Method

Parameters

out Outbound

Returns

error
func (*streamBusConnection) Send(out Outbound) error
{
	payload, err := json.Marshal(out)
	if err != nil {
		return err
	}
	ctx, cancel := context.WithTimeout(c.transport.Context(), 5*time.Second)
	defer cancel()
	_, err = c.bus.Publish(ctx, streambus.Frame{
		Topic: c.topic, Payload: payload, Reliability: streambus.Reliable,
		Priority: streambus.PriorityInteractive,
	})
	return err
}
writeLoop
Method
func (*streamBusConnection) writeLoop()
{
	defer close(c.done)
	for frame := range c.subscription.Frames() {
		c.writeMu.Lock()
		err := writeFrame(c.stream, frame.Payload)
		c.writeMu.Unlock()
		if err != nil {
			_ = c.transport.CloseWithError(2, "write failed")
			return
		}
	}
}
Close
Method
func (*streamBusConnection) Close()
{
	c.closeOnce.Do(func() {
		_ = c.transport.CloseWithError(0, "")
		_ = c.subscription.Close()
		c.writeMu.Lock()
		_ = c.stream.Close()
		c.writeMu.Unlock()
	})
}

Fields

Name Type Description
request *http.Request
transport *wt.Session
stream *wt.Stream
reader *bufio.Reader
bus *streambus.InMemory
topic string
subscription *streambus.Subscription
maximum int
writeMu sync.Mutex
closeOnce sync.Once
done chan struct{}
F
function

newStreamBusConnection

Parameters

request
transport
stream
maximum
int

Returns

host/streambus_server.go:126-138
func newStreamBusConnection(ctx context.Context, request *http.Request, transport *wt.Session, stream *wt.Stream, bus *streambus.InMemory, maximum int) (*streamBusConnection, error)

{
	topic := fmt.Sprintf("rfw/connection/%d", streamID.Add(1))
	subscription, err := bus.Subscribe(ctx, streambus.SubscribeOptions{Topic: topic, Buffer: 256, Overflow: streambus.Block})
	if err != nil {
		return nil, err
	}
	connection := &streamBusConnection{
		request: request, transport: transport, stream: stream, reader: bufio.NewReader(stream),
		bus: bus, topic: topic, subscription: subscription, maximum: maximum, done: make(chan struct{}),
	}
	go connection.writeLoop()
	return connection, nil
}
F
function

streamBusProtocolLoop

Parameters

connection
runtime

Returns

error
host/streambus_server.go:185-302
func streamBusProtocolLoop(connection *streamBusConnection, runtime *WSRuntime) error

{
	var session *Session
	var subscribed []string
	defer func() {
		streamClientsMu.Lock()
		for _, name := range subscribed {
			if set := streamClients[name]; set != nil {
				delete(set, connection)
				if len(set) == 0 {
					delete(streamClients, name)
				}
			}
		}
		streamClientsMu.Unlock()
		SuspendSession(session, runtime.ResumeTTL())
	}()

	for {
		var message Inbound
		if err := connection.Receive(&message); err != nil {
			return err
		}
		if session == nil {
			var resumed bool
			var err error
			session, resumed, err = runtime.OpenSession(connection.request, message.ResumeToken)
			if err != nil {
				_ = connection.Send(Outbound{Error: NewActionError("session_rejected", "session rejected")})
				return err
			}
			if !bindStreamBusConnection(connection, session, true) {
				_ = connection.Send(Outbound{Error: NewActionError("session_rejected", "session already has an active connection")})
				return errors.New("streambus: session already has an active connection")
			}
			if resumed {
				replayStreamBus(connection, session, message.Ack)
			} else if message.ResumeToken != "" {
				sendStreamBusSession(connection, session, Outbound{Control: "resume_rejected", Error: NewActionError("resume_rejected", "session could not be resumed")})
			}
		}
		session.Acknowledge(message.Ack)
		if message.Control == "ping" {
			sendStreamBusSession(connection, session, Outbound{Control: "pong"})
			continue
		}
		if err := session.AcceptInbound(message.Sequence); err != nil {
			if errors.Is(err, ErrDuplicateMessage) {
				continue
			}
			sendStreamBusSession(connection, session, Outbound{ID: message.ID, Action: message.Action, Error: NewActionError("sequence_gap", "client message sequence gap")})
			continue
		}
		if !session.AllowMessage(runtime.MessagesPerMinute()) {
			sendStreamBusSession(connection, session, Outbound{ID: message.ID, Action: message.Action, Error: NewActionError("rate_limited", "message rate limit exceeded")})
			continue
		}
		authorizeCtx, cancel := runtime.HandlerContext(context.Background())
		authorizeErr := runtime.Authorize(authorizeCtx, session, message)
		cancel()
		if authorizeErr != nil {
			sendStreamBusSession(connection, session, Outbound{Component: message.Component, Action: message.Action, ID: message.ID, Error: NewActionError("forbidden", "message forbidden")})
			continue
		}
		if message.Action != "" {
			payload, actionErr := runtime.DispatchAction(context.Background(), session, message)
			sendStreamBusSession(connection, session, Outbound{Action: message.Action, ID: message.ID, Payload: payload, Error: actionErr})
			continue
		}
		if message.Component != "" && message.Payload != nil && message.Payload["unsubscribe"] == true {
			streamClientsMu.Lock()
			if set := streamClients[message.Component]; set != nil {
				delete(set, connection)
				if len(set) == 0 {
					delete(streamClients, message.Component)
				}
			}
			for index, name := range subscribed {
				if name == message.Component {
					subscribed = append(subscribed[:index], subscribed[index+1:]...)
					break
				}
			}
			streamClientsMu.Unlock()
			sendStreamBusSession(connection, session, Outbound{Component: message.Component, Control: "unsubscribed"})
			continue
		}
		if component, ok := Get(message.Component); ok {
			streamClientsMu.Lock()
			if streamClients[message.Component] == nil {
				streamClients[message.Component] = make(map[*streamBusConnection]*Session)
			}
			if _, tracked := streamClients[message.Component][connection]; !tracked {
				streamClients[message.Component][connection] = session
				subscribed = append(subscribed, message.Component)
			}
			streamClientsMu.Unlock()
			response := component.HandleWithSession(session, message.Payload)
			if response != nil {
				switch value := response.(type) {
				case *InitSnapshot:
					if value != nil {
						sendStreamBusSession(connection, session, Outbound{Component: message.Component, ID: message.ID, Payload: map[string]any{"initSnapshot": value}})
					}
				case InitSnapshot:
					sendStreamBusSession(connection, session, Outbound{Component: message.Component, ID: message.ID, Payload: map[string]any{"initSnapshot": value}})
				default:
					sendStreamBusSession(connection, session, Outbound{Component: message.Component, ID: message.ID, Payload: response})
				}
				continue
			}
			if message.Payload != nil && message.Payload["init"] == true {
				sendStreamBusSession(connection, session, Outbound{Component: message.Component, Payload: map[string]any{"session": session.ID()}})
				continue
			}
		}
		sendStreamBusSession(connection, session, Outbound{Control: "ack"})
	}
}
F
function

bindStreamBusConnection

Parameters

connection
session
handoff
bool

Returns

bool
host/streambus_server.go:304-330
func bindStreamBusConnection(connection *streamBusConnection, session *Session, handoff bool) bool

{
	if session == nil || connection == nil {
		return false
	}
	session.outboundMu.Lock()
	defer session.outboundMu.Unlock()
	session.deliveryMu.Lock()
	active := session.attached && !session.released
	resumePending := session.resumePending
	if active && resumePending && handoff {
		session.resumePending = false
	}
	session.deliveryMu.Unlock()
	if !active || (resumePending && !handoff) {
		return false
	}
	if session.streamConnection == connection {
		return true
	}
	if session.streamConnection != nil || session.connection != nil || (session.connectionManaged && !handoff) {
		return false
	}
	session.streamConnection = connection
	session.connection = nil
	session.connectionManaged = true
	return true
}
F
function

sendStreamBusSession

Parameters

connection
session
out
host/streambus_server.go:332-342
func sendStreamBusSession(connection *streamBusConnection, session *Session, out Outbound)

{
	if session == nil {
		return
	}
	session.outboundMu.Lock()
	defer session.outboundMu.Unlock()
	if session.streamConnection != connection {
		return
	}
	_ = connection.Send(session.PrepareOutbound(out))
}
F
function

replayStreamBus

Parameters

connection
session
acknowledged
uint64
host/streambus_server.go:344-358
func replayStreamBus(connection *streamBusConnection, session *Session, acknowledged uint64)

{
	session.outboundMu.Lock()
	defer session.outboundMu.Unlock()
	if session.streamConnection != connection {
		return
	}
	messages, err := session.ReplayAfter(acknowledged)
	if err != nil {
		_ = connection.Send(session.PrepareOutbound(Outbound{Error: NewActionError("resync_required", "message history is no longer available")}))
		return
	}
	for _, message := range messages {
		_ = connection.Send(message)
	}
}
F
function

startStreamBusHTTP3

Parameters

addr
string
tlsConfig
host/streambus_server.go:360-389
func startStreamBusHTTP3(addr string, mux *http.ServeMux, tlsConfig *tls.Config)

{
	value, ok := streamEndpoints.Load(mux)
	if !ok {
		return
	}
	endpoint := value.(*streamBusEndpoint)
	port := strings.TrimPrefix(addr, ":")
	if _, parsedPort, err := net.SplitHostPort(addr); err == nil {
		port = parsedPort
	}
	streamHTTP3Port.Store(port)
	if len(tlsConfig.Certificates) > 0 && len(tlsConfig.Certificates[0].Certificate) > 0 {
		hash := sha256.Sum256(tlsConfig.Certificates[0].Certificate[0])
		streamCertHash.Store(fmt.Sprintf("%x", hash[:]))
	}
	h3 := &http3.Server{
		Addr: addr, Handler: mux, TLSConfig: tlsConfig.Clone(), EnableDatagrams: true,
		QUICConfig: &quic.Config{EnableDatagrams: true, EnableStreamResetPartialDelivery: true},
	}
	server := &wt.Server{H3: h3}
	wt.ConfigureHTTP3Server(h3)
	endpoint.mu.Lock()
	endpoint.server = server
	endpoint.mu.Unlock()
	go func() {
		if err := server.ListenAndServe(); err != nil {
			log.Printf("streambus HTTP/3 server: %v", err)
		}
	}()
}
T
type

BroadcastOption

BroadcastOption configures a broadcast call.

host/websocket.go:16-16
type BroadcastOption func(*BroadcastOptions)
S
struct

BroadcastOptions

BroadcastOptions holds optional parameters for Broadcast.

host/websocket.go:19-21
type BroadcastOptions struct

Fields

Name Type Description
Session string
F
function

WithSessionTarget

WithSessionTarget limits a broadcast to a specific session ID.

Parameters

sessionID
string

Returns

host/websocket.go:24-28
func WithSessionTarget(sessionID string) BroadcastOption

{
	return func(opts *BroadcastOptions) {
		opts.Session = sessionID
	}
}
S
struct

connectionWriter

host/websocket.go:37-47
type connectionWriter struct

Methods

run
Method
func (*connectionWriter) run()
{
writerLoop:
	for {
		select {
		case <-writer.resync:
			writer.sendResyncRequired()
			return
		case <-writer.done:
			return
		default:
		}

		select {
		case <-writer.resync:
			writer.sendResyncRequired()
			return
		case <-writer.done:
			return
		case batch := <-writer.queue:
			for _, out := range batch {
				if !writer.beginWrite() {
					continue writerLoop
				}
				if err := sendOutboundUnlocked(writer.ws, out, writer.writeTimeout); err != nil {
					writer.endWrite()
					log.Printf("send: %v", err)
					writer.stopAndClose()
					return
				}
				writer.endWrite()
			}
		}
	}
}
beginWrite
Method

Returns

bool
func (*connectionWriter) beginWrite() bool
{
	writer.mu.Lock()
	defer writer.mu.Unlock()
	if writer.stopped {
		return false
	}
	writer.writing = true
	return true
}
endWrite
Method
func (*connectionWriter) endWrite()
{
	writer.mu.Lock()
	writer.writing = false
	writer.mu.Unlock()
}
enqueue
Method

Parameters

batch func() ([]Outbound, error)

Returns

bool
func (*connectionWriter) enqueue(batch func() ([]Outbound, error)) bool
{
	writer.mu.Lock()
	defer writer.mu.Unlock()
	if writer.stopped {
		return false
	}

	messages, err := batch()
	if err != nil {
		log.Printf("marshal outbound payload: %v", err)
		return false
	}
	select {
	case writer.queue <- messages:
		return true
	default:
		writer.stopped = true
		writer.resync <- struct{}{}
		return false
	}
}
func (*connectionWriter) sendResyncRequired()
{
	writer.mu.Lock()
	writer.writing = true
	writer.mu.Unlock()
	err := sendOutboundUnlocked(writer.ws, Outbound{
		Control: "resync_required",
		Error:   NewActionError("resync_required", "outbound delivery could not keep up"),
	}, writer.writeTimeout)
	if err != nil {
		log.Printf("send resync_required: %v", err)
	}
	writer.endWrite()
	writer.close(true)
}
stopAndClose
Method
func (*connectionWriter) stopAndClose()
{
	writer.mu.Lock()
	writer.stopped = true
	writer.mu.Unlock()
	writer.close(false)
}
close
Method

Parameters

resetDeadline bool
func (*connectionWriter) close(resetDeadline bool)
{
	if resetDeadline && writer.writeTimeout > 0 {
		_ = writer.ws.SetWriteDeadline(time.Now().Add(writer.writeTimeout))
	}
	if err := writer.ws.Close(); err != nil {
		logger.Debug("close outbound websocket", "error", err)
	}
}
forget
Method
func (*connectionWriter) forget()
{
	writer.mu.Lock()
	writer.stopped = true
	writing := writer.writing
	close(writer.done)
	writer.mu.Unlock()
	if writing {
		_ = writer.ws.SetWriteDeadline(time.Now())
	}
}

Fields

Name Type Description
ws *websocket.Conn
writeTimeout time.Duration
queue chan []Outbound
resync chan struct{}
done chan struct{}
mu sync.Mutex
stopped bool
writing bool
F
function

configureConnectionWriter

Parameters

writeTimeout
queueSize
int
host/websocket.go:49-64
func configureConnectionWriter(ws *websocket.Conn, writeTimeout time.Duration, queueSize int)

{
	if ws == nil || queueSize <= 0 {
		return
	}
	writer := &connectionWriter{
		ws:           ws,
		writeTimeout: writeTimeout,
		queue:        make(chan []Outbound, queueSize),
		resync:       make(chan struct{}, 1),
		done:         make(chan struct{}),
	}
	if _, loaded := connWriters.LoadOrStore(ws, writer); loaded {
		return
	}
	go writer.run()
}
F
function

AnswerControl

AnswerControl replies to an out-of-band control frame and reports whether it
consumed the message.

A browser client cannot send protocol ping frames, so liveness rides on a
control message instead. It carries no sequence, so its answer stays out of
the replay history and out of the message budget, matching the frame level
pong it replaces.

Both this package’s handler and the one in ssc route inbound frames through
here. They are separate loops over the same protocol, and when only one of
them answered a ping an idle connection to an SSC server dropped and
reconnected on every heartbeat.

Parameters

Returns

bool
host/websocket.go:193-199
func AnswerControl(ws *websocket.Conn, msg Inbound) bool

{
	if msg.Control != "ping" {
		return false
	}
	SendOutbound(ws, Outbound{Control: "pong"})
	return true
}
F
function

wsHandler

Parameters

runtime
host/websocket.go:201-362
func wsHandler(ws *websocket.Conn, runtime *WSRuntime)

{
	if !runtime.AcquireConnection() {
		SendOutbound(ws, Outbound{Error: NewActionError("connection_limit", "connection limit reached")})
		if err := ws.Close(); err != nil {
			log.Printf("close rejected websocket: %v", err)
		}
		return
	}
	defer runtime.ReleaseConnection()
	runtime.ConfigureConnection(ws)

	var session *Session
	var subscribed []string
	defer func() {
		connMu.Lock()
		for _, name := range subscribed {
			if set, ok := connections[name]; ok {
				delete(set, ws)
				if len(set) == 0 {
					delete(connections, name)
				}
			}
		}
		connMu.Unlock()
		SuspendSession(session, runtime.ResumeTTL())
		ForgetConnection(ws)
		if err := ws.Close(); err != nil {
			log.Printf("close websocket: %v", err)
		}
	}()
	for {
		var raw []byte
		if err := websocket.Message.Receive(ws, &raw); err != nil {
			if err == io.EOF {
				break
			}
			log.Printf("recv: %v", err)
			return
		}
		var msg Inbound
		if err := json.Unmarshal(raw, &msg); err != nil {
			log.Printf("unmarshal: %v", err)
			continue
		}
		if session == nil {
			var resumed bool
			var err error
			session, resumed, err = runtime.OpenSession(ws.Request(), msg.ResumeToken)
			if err != nil {
				SendOutbound(ws, Outbound{Error: NewActionError("session_rejected", "session rejected")})
				return
			}
			BindSessionConnection(ws, session)
			if resumed {
				ReplaySession(ws, session, msg.Ack)
			} else if msg.ResumeToken != "" {
				SendSessionOutbound(ws, session, Outbound{
					Control: "resume_rejected",
					Error:   NewActionError("resume_rejected", "session could not be resumed"),
				})
			}
		}
		session.Acknowledge(msg.Ack)
		if AnswerControl(ws, msg) {
			continue
		}
		if err := session.AcceptInbound(msg.Sequence); err != nil {
			if errors.Is(err, ErrDuplicateMessage) {
				continue
			}
			SendSessionOutbound(ws, session, Outbound{
				ID:     msg.ID,
				Action: msg.Action,
				Error:  NewActionError("sequence_gap", "client message sequence gap"),
			})
			continue
		}
		if !session.AllowMessage(runtime.MessagesPerMinute()) {
			SendSessionOutbound(ws, session, Outbound{
				ID:     msg.ID,
				Action: msg.Action,
				Error:  NewActionError("rate_limited", "message rate limit exceeded"),
			})
			continue
		}
		authorizeCtx, cancelAuthorize := runtime.HandlerContext(context.Background())
		authorizeErr := runtime.Authorize(authorizeCtx, session, msg)
		cancelAuthorize()
		if authorizeErr != nil {
			SendSessionOutbound(ws, session, Outbound{
				Component: msg.Component,
				Action:    msg.Action,
				ID:        msg.ID,
				Error:     NewActionError("forbidden", "message forbidden"),
			})
			continue
		}
		if msg.Action != "" {
			payload, actionErr := runtime.DispatchAction(context.Background(), session, msg)
			SendSessionOutbound(ws, session, Outbound{
				Action:  msg.Action,
				ID:      msg.ID,
				Payload: payload,
				Error:   actionErr,
			})
			continue
		}
		if msg.Component != "" && msg.Payload != nil && msg.Payload["unsubscribe"] == true {
			connMu.Lock()
			if set, ok := connections[msg.Component]; ok {
				delete(set, ws)
				if len(set) == 0 {
					delete(connections, msg.Component)
				}
			}
			for index, name := range subscribed {
				if name == msg.Component {
					subscribed = append(subscribed[:index], subscribed[index+1:]...)
					break
				}
			}
			connMu.Unlock()
			SendSessionOutbound(ws, session, Outbound{Component: msg.Component, Control: "unsubscribed"})
			continue
		}
		if hc, ok := Get(msg.Component); ok {
			connMu.Lock()
			if _, ok := connections[msg.Component]; !ok {
				connections[msg.Component] = make(map[*websocket.Conn]*Session)
			}
			if _, tracked := connections[msg.Component][ws]; !tracked {
				connections[msg.Component][ws] = session
				subscribed = append(subscribed, msg.Component)
			}
			connMu.Unlock()
			resp := hc.HandleWithSession(session, msg.Payload)
			if resp != nil {
				switch v := resp.(type) {
				case *InitSnapshot:
					if v != nil {
						SendSessionOutbound(ws, session, Outbound{Component: msg.Component, ID: msg.ID, Payload: map[string]any{"initSnapshot": v}})
					}
					continue
				case InitSnapshot:
					SendSessionOutbound(ws, session, Outbound{Component: msg.Component, ID: msg.ID, Payload: map[string]any{"initSnapshot": v}})
					continue
				default:
					SendSessionOutbound(ws, session, Outbound{Component: msg.Component, ID: msg.ID, Payload: resp})
					continue
				}
			}
			if msg.Payload != nil && msg.Payload["init"] == true {
				SendSessionOutbound(ws, session, Outbound{
					Component: msg.Component,
					Payload:   map[string]any{"session": session.ID()},
				})
				continue
			}
		}
		SendSessionOutbound(ws, session, Outbound{Control: "ack"})
	}
}
F
function

Broadcast

Broadcast sends the given payload to all connections subscribed to the component name.

Parameters

name
string
payload
any
opts
...BroadcastOption
host/websocket.go:365-410
func Broadcast(name string, payload any, opts ...BroadcastOption)

{
	var options BroadcastOptions
	for _, opt := range opts {
		opt(&options)
	}

	// Snapshot the (conn, session) pairs under the lock: wsHandler mutates the
	// connection map on subscribe/disconnect, so iterating it after releasing
	// connMu races with those writes. Sends happen outside the lock.
	type target struct {
		ws      *websocket.Conn
		session *Session
	}
	connMu.RLock()
	targets := make([]target, 0, len(connections[name]))
	for ws, session := range connections[name] {
		targets = append(targets, target{ws: ws, session: session})
	}
	connMu.RUnlock()

	for _, t := range targets {
		if options.Session != "" && t.session.ID() != options.Session {
			continue
		}
		SendSessionOutbound(t.ws, t.session, Outbound{Component: name, Payload: payload})
	}

	streamClientsMu.RLock()
	streamTargets := make([]struct {
		connection *streamBusConnection
		session    *Session
	}, 0, len(streamClients[name]))
	for connection, session := range streamClients[name] {
		streamTargets = append(streamTargets, struct {
			connection *streamBusConnection
			session    *Session
		}{connection: connection, session: session})
	}
	streamClientsMu.RUnlock()
	for _, target := range streamTargets {
		if options.Session != "" && target.session.ID() != options.Session {
			continue
		}
		sendStreamBusSession(target.connection, target.session, Outbound{Component: name, Payload: payload})
	}
}
F
function

ReplaySession

ReplaySession sends retained messages after the client’s acknowledgement.

Parameters

session
acknowledged
uint64
host/websocket.go:439-478
func ReplaySession(ws *websocket.Conn, session *Session, acknowledged uint64)

{
	if session == nil {
		return
	}
	session.outboundMu.Lock()
	defer session.outboundMu.Unlock()
	accepted, _ := sessionAcceptsConnection(session, ws, true)
	if !accepted {
		return
	}
	if writerValue, ok := connWriters.Load(ws); ok {
		writerValue.(*connectionWriter).enqueue(func() ([]Outbound, error) {
			messages, err := session.ReplayAfter(acknowledged)
			if err != nil {
				return []Outbound{session.PrepareOutbound(Outbound{
					Error: NewActionError("resync_required", "message history is no longer available"),
				})}, nil
			}
			return messages, nil
		})
		return
	}
	lock := connectionWriteLock(ws)
	lock.Lock()
	defer lock.Unlock()
	messages, err := session.ReplayAfter(acknowledged)
	if err != nil {
		_ = sendOutboundUnlocked(ws, session.PrepareOutbound(Outbound{
			Error: NewActionError("resync_required", "message history is no longer available"),
		}), DefaultSSCLimits().WriteTimeout)
		return
	}
	for _, message := range messages {
		if err := sendOutboundUnlocked(ws, message, DefaultSSCLimits().WriteTimeout); err != nil {
			log.Printf("send replay: %v", err)
			_ = ws.Close()
			return
		}
	}
}
F
function

SendSessionOutbound

SendSessionOutbound assigns delivery metadata and sends a message. It binds
ws when the session has no active connection. After ResumeSession, callers
must complete the handoff with ReplaySession or BindSessionConnection before
sending.

Parameters

session
out
host/websocket.go:484-515
func SendSessionOutbound(ws *websocket.Conn, session *Session, out Outbound)

{
	if session == nil {
		return
	}
	session.outboundMu.Lock()
	defer session.outboundMu.Unlock()
	accepted, handoffPending := sessionAcceptsConnection(session, ws, false)
	if !accepted {
		if handoffPending {
			logger.Debug("session outbound dropped before connection handoff", "session", session.ID())
		}
		return
	}
	out, err := prepareOutboundPayload(out)
	if err != nil {
		log.Printf("marshal outbound payload: %v", err)
		return
	}
	if writerValue, ok := connWriters.Load(ws); ok {
		writerValue.(*connectionWriter).enqueue(func() ([]Outbound, error) {
			return []Outbound{session.PrepareOutbound(out)}, nil
		})
		return
	}
	lock := connectionWriteLock(ws)
	lock.Lock()
	defer lock.Unlock()
	if err := sendOutboundUnlocked(ws, session.PrepareOutbound(out), DefaultSSCLimits().WriteTimeout); err != nil {
		log.Printf("send: %v", err)
		_ = ws.Close()
	}
}
F
function

BindSessionConnection

BindSessionConnection marks ws as the active connection for session delivery.
It binds a session without an active connection and completes the connection
handoff after ResumeSession. It does not replace an active connection.

Parameters

session
host/websocket.go:520-530
func BindSessionConnection(ws *websocket.Conn, session *Session)

{
	if session == nil {
		return
	}
	session.outboundMu.Lock()
	accepted, _ := sessionAcceptsConnection(session, ws, true)
	if accepted {
		session.connectionManaged = true
	}
	session.outboundMu.Unlock()
}
F
function

sessionAcceptsConnection

Parameters

session
handoff
bool

Returns

bool
bool
host/websocket.go:532-558
func sessionAcceptsConnection(session *Session, ws *websocket.Conn, handoff bool) (bool, bool)

{
	if ws == nil {
		return false, false
	}
	session.deliveryMu.Lock()
	active := session.attached && !session.released
	resumePending := session.resumePending
	if active && resumePending && handoff {
		session.resumePending = false
	}
	session.deliveryMu.Unlock()
	if !active {
		return false, false
	}
	if resumePending && !handoff {
		return false, true
	}
	if session.connection == ws {
		return true, false
	}
	if session.connection != nil || session.streamConnection != nil || (session.connectionManaged && !handoff) {
		return false, false
	}
	session.connection = ws
	session.streamConnection = nil
	return true, false
}
F
function

SendOutbound

SendOutbound queues a write on configured connections and otherwise writes
synchronously with the default deadline.

Parameters

host/websocket.go:562-586
func SendOutbound(ws *websocket.Conn, out Outbound)

{
	if writerValue, ok := connWriters.Load(ws); ok {
		writerValue.(*connectionWriter).enqueue(func() ([]Outbound, error) {
			prepared, err := prepareOutboundPayload(out)
			if err != nil {
				return nil, err
			}
			return []Outbound{prepared}, nil
		})
		return
	}
	lock := connectionWriteLock(ws)
	lock.Lock()
	defer lock.Unlock()
	var err error
	out, err = prepareOutboundPayload(out)
	if err != nil {
		log.Printf("marshal outbound payload: %v", err)
		return
	}
	if err := sendOutboundUnlocked(ws, out, DefaultSSCLimits().WriteTimeout); err != nil {
		log.Printf("send: %v", err)
		_ = ws.Close()
	}
}
F
function

connectionWriteLock

Parameters

Returns

host/websocket.go:588-591
func connectionWriteLock(ws *websocket.Conn) *sync.Mutex

{
	lockValue, _ := connWrites.LoadOrStore(ws, &sync.Mutex{})
	return lockValue.(*sync.Mutex)
}
F
function

sendOutboundUnlocked

Parameters

out
writeTimeout

Returns

error
host/websocket.go:593-610
func sendOutboundUnlocked(ws *websocket.Conn, out Outbound, writeTimeout time.Duration) error

{
	b, err := marshalOutbound(out)
	if err != nil {
		return err
	}
	if writeTimeout > 0 {
		if err := ws.SetWriteDeadline(time.Now().Add(writeTimeout)); err != nil {
			return err
		}
	}
	if err := websocket.Message.Send(ws, b); err != nil {
		return err
	}
	if writeTimeout > 0 {
		return ws.SetWriteDeadline(time.Time{})
	}
	return nil
}
F
function

prepareOutboundPayload

Parameters

out

Returns

error
host/websocket.go:612-622
func prepareOutboundPayload(out Outbound) (Outbound, error)

{
	if out.Payload == nil || out.encodedPayload != nil {
		return out, nil
	}
	payload, err := json.Marshal(out.Payload)
	if err != nil {
		return out, err
	}
	out.encodedPayload = payload
	return out, nil
}
F
function

marshalOutbound

Parameters

out

Returns

[]byte
error
host/websocket.go:624-631
func marshalOutbound(out Outbound) ([]byte, error)

{
	if out.encodedPayload == nil {
		return json.Marshal(out)
	}
	wire := out
	wire.Payload = json.RawMessage(out.encodedPayload)
	return json.Marshal(wire)
}
F
function

ForgetConnection

ForgetConnection releases the connection’s outbound delivery resources.

Parameters

host/websocket.go:634-639
func ForgetConnection(ws *websocket.Conn)

{
	if writerValue, ok := connWriters.LoadAndDelete(ws); ok {
		writerValue.(*connectionWriter).forget()
	}
	connWrites.Delete(ws)
}
S
struct

singleConnListener

host/websocket_queue_test.go:20-25
type singleConnListener struct

Methods

Accept
Method

Returns

error
func (*singleConnListener) Accept() (net.Conn, error)
{
	listener.mu.Lock()
	if listener.conn != nil {
		conn := listener.conn
		listener.conn = nil
		listener.mu.Unlock()
		return conn, nil
	}
	listener.mu.Unlock()
	<-listener.closed
	return nil, net.ErrClosed
}
Close
Method

Returns

error
func (*singleConnListener) Close() error
{
	listener.closeOnce.Do(func() { close(listener.closed) })
	return nil
}
Addr
Method

Returns

func (*singleConnListener) Addr() net.Addr
{ return pipeAddr{} }

Fields

Name Type Description
mu sync.Mutex
conn net.Conn
closed chan struct{}
closeOnce sync.Once
S
struct

pipeAddr

host/websocket_queue_test.go:47-47
type pipeAddr struct

Methods

Network
Method

Returns

string
func (pipeAddr) Network() string
{ return "pipe" }
String
Method

Returns

string
func (pipeAddr) String() string
{ return "pipe" }
F
function

openBlockedProtocolSocket

Parameters

runtime

Returns

func()
host/websocket_queue_test.go:52-90
func openBlockedProtocolSocket(t *testing.T, runtime *WSRuntime) (*websocket.Conn, func())

{
	t.Helper()
	serverConn, clientConn := net.Pipe()
	listener := &singleConnListener{conn: serverConn, closed: make(chan struct{})}
	server := &http.Server{
		Handler: runtime.Guard(websocket.Handler(func(ws *websocket.Conn) {
			wsHandler(ws, runtime)
		})),
		ReadHeaderTimeout: time.Second,
	}
	serveDone := make(chan error, 1)
	go func() { serveDone <- server.Serve(listener) }()

	config, err := websocket.NewConfig("ws://pipe.invalid/ws", "http://pipe.invalid")
	if err != nil {
		t.Fatalf("configure pipe websocket: %v", err)
	}
	client, err := websocket.NewClient(config, clientConn)
	if err != nil {
		_ = server.Close()
		_ = listener.Close()
		t.Fatalf("open pipe websocket: %v", err)
	}
	cleanup := func() {
		_ = client.SetDeadline(time.Now())
		_ = client.Close()
		_ = server.Close()
		_ = listener.Close()
		select {
		case err := <-serveDone:
			if err != nil && !errors.Is(err, http.ErrServerClosed) && !errors.Is(err, net.ErrClosed) {
				t.Errorf("serve pipe websocket: %v", err)
			}
		case <-time.After(time.Second):
			t.Error("pipe websocket server did not stop")
		}
	}
	return client, cleanup
}
F
function

openRuntimeServer

Parameters

runtime

Returns

string
host/websocket_queue_test.go:92-100
func openRuntimeServer(t *testing.T, runtime *WSRuntime) (*httptest.Server, string)

{
	t.Helper()
	mux := http.NewServeMux()
	mux.Handle("/ws", runtime.Guard(websocket.Handler(func(ws *websocket.Conn) {
		wsHandler(ws, runtime)
	})))
	server := httptest.NewServer(mux)
	return server, "ws" + strings.TrimPrefix(server.URL, "http") + "/ws"
}
F
function

sendTestInbound

Parameters

host/websocket_queue_test.go:102-111
func sendTestInbound(t *testing.T, ws *websocket.Conn, message Inbound)

{
	t.Helper()
	data, err := json.Marshal(message)
	if err != nil {
		t.Fatalf("marshal inbound: %v", err)
	}
	if err := websocket.Message.Send(ws, data); err != nil {
		t.Fatalf("send inbound: %v", err)
	}
}
F
function

waitForConnectionCount

Parameters

component
string
count
int

Returns

map[*websocket.Conn]*Session
host/websocket_queue_test.go:113-130
func waitForConnectionCount(t *testing.T, component string, count int) map[*websocket.Conn]*Session

{
	t.Helper()
	deadline := time.Now().Add(2 * time.Second)
	for time.Now().Before(deadline) {
		connMu.RLock()
		current := make(map[*websocket.Conn]*Session, len(connections[component]))
		for ws, session := range connections[component] {
			current[ws] = session
		}
		connMu.RUnlock()
		if len(current) == count {
			return current
		}
		time.Sleep(time.Millisecond)
	}
	t.Fatalf("component %q connection count did not reach %d", component, count)
	return nil
}
F
function

waitForSessionDetached

Parameters

session
host/websocket_queue_test.go:132-145
func waitForSessionDetached(t *testing.T, session *Session)

{
	t.Helper()
	deadline := time.Now().Add(2 * time.Second)
	for time.Now().Before(deadline) {
		session.deliveryMu.Lock()
		attached := session.attached
		session.deliveryMu.Unlock()
		if !attached {
			return
		}
		time.Sleep(time.Millisecond)
	}
	t.Fatal("session did not detach after its connection closed")
}
F
function

TestWriteDeadlineClosesAClientThatDoesNotRead

Parameters

host/websocket_queue_test.go:147-176
func TestWriteDeadlineClosesAClientThatDoesNotRead(t *testing.T)

{
	const componentName = "write-deadline-client"
	Register(NewHostComponent(componentName, func(map[string]any) any {
		return map[string]any{"ready": true}
	}))
	runtime := NewWSRuntime(WithSSCLimits(SSCLimits{
		WriteTimeout:      25 * time.Millisecond,
		OutboundQueueSize: 4,
	}))
	client, cleanup := openBlockedProtocolSocket(t, runtime)
	defer cleanup()

	sendTestInbound(t, client, Inbound{Component: componentName, Payload: map[string]any{"init": true}})
	targets := waitForConnectionCount(t, componentName, 1)
	var session *Session
	for _, candidate := range targets {
		session = candidate
	}
	waitForSessionDetached(t, session)

	if err := client.SetReadDeadline(time.Now().Add(time.Second)); err == nil {
		var raw []byte
		if err := websocket.Message.Receive(client, &raw); err == nil {
			t.Fatalf("slow client remained open after the write deadline: %s", raw)
		}
	} else if !errors.Is(err, io.ErrClosedPipe) && !errors.Is(err, net.ErrClosed) {
		t.Fatalf("set client read deadline: %v", err)
	}
	ReleaseSession(session)
}
F
function

TestInvalidPayloadDoesNotCloseConnectionOrConsumeSequence

Parameters

host/websocket_queue_test.go:178-191
func TestInvalidPayloadDoesNotCloseConnectionOrConsumeSequence(t *testing.T)

{
	client, server, closeSockets := openWriteTestSocket(t)
	defer closeSockets()
	configureConnectionWriter(server, time.Second, 4)
	session := newSession("invalid-payload")
	BindSessionConnection(server, session)

	SendSessionOutbound(server, session, Outbound{Payload: make(chan int)})
	SendSessionOutbound(server, session, Outbound{Payload: "valid"})
	message := receiveOrderedMessage(t, client)
	if message.Sequence != 1 || message.Payload != "valid" {
		t.Fatalf("valid message after marshal failure = %#v", message)
	}
}
F
function

TestBroadcastDoesNotWaitForSlowClientAndOverflowResumes

Parameters

host/websocket_queue_test.go:193-316
func TestBroadcastDoesNotWaitForSlowClientAndOverflowResumes(t *testing.T)

{
	const componentName = "bounded-broadcast-client"
	Register(NewHostComponent(componentName, func(map[string]any) any {
		return map[string]any{"ready": true}
	}))
	slowRuntime := NewWSRuntime(WithSSCLimits(SSCLimits{
		WriteTimeout:      2 * time.Second,
		OutboundQueueSize: 2,
		ResumeTTL:         time.Second,
		ReplayMessages:    16,
	}))
	fastRuntime := NewWSRuntime(WithSSCLimits(SSCLimits{
		WriteTimeout:      2 * time.Second,
		OutboundQueueSize: 16,
		ResumeTTL:         time.Second,
		ReplayMessages:    16,
	}))

	slowClient, closeSlow := openBlockedProtocolSocket(t, slowRuntime)
	defer closeSlow()
	sendTestInbound(t, slowClient, Inbound{Component: componentName, Payload: map[string]any{"init": true}})
	slowTargets := waitForConnectionCount(t, componentName, 1)
	var (
		slowServer  *websocket.Conn
		slowSession *Session
	)
	for ws, session := range slowTargets {
		slowServer = ws
		slowSession = session
	}
	token := slowSession.ResumeToken()
	sessionID := slowSession.ID()
	if token == "" {
		t.Fatal("slow session was not resumable")
	}

	deadline := time.Now().Add(time.Second)
	for {
		slowSession.deliveryMu.Lock()
		sequence := slowSession.outboundSeq
		slowSession.deliveryMu.Unlock()
		writerValue, ok := connWriters.Load(slowServer)
		if ok && sequence == 1 && len(writerValue.(*connectionWriter).queue) == 0 {
			break
		}
		if time.Now().After(deadline) {
			t.Fatal("slow client writer did not enter its blocked write")
		}
		time.Sleep(time.Millisecond)
	}

	server, wsURL := openRuntimeServer(t, fastRuntime)
	defer server.Close()
	fastClient, err := websocket.Dial(wsURL, "", server.URL)
	if err != nil {
		t.Fatalf("dial fast websocket: %v", err)
	}
	defer closeTestResource(t, fastClient)
	sendTestInbound(t, fastClient, Inbound{Component: componentName, Payload: map[string]any{"init": true}})
	_ = receiveOrderedMessage(t, fastClient)
	waitForConnectionCount(t, componentName, 2)

	started := time.Now()
	for tick := 1; tick <= 3; tick++ {
		Broadcast(componentName, map[string]any{"tick": tick})
	}
	if elapsed := time.Since(started); elapsed > 100*time.Millisecond {
		t.Fatalf("broadcast waited for the slow client: %s", elapsed)
	}
	for tick := 1; tick <= 3; tick++ {
		message := receiveOrderedMessage(t, fastClient)
		payload, ok := message.Payload.(map[string]any)
		if !ok || payload["tick"] != float64(tick) {
			t.Fatalf("fast client broadcast %d = %#v", tick, message)
		}
	}

	if err := slowClient.SetReadDeadline(time.Now().Add(time.Second)); err != nil {
		t.Fatalf("set slow client read deadline: %v", err)
	}
	resyncRequired := false
	for {
		var raw []byte
		if err := websocket.Message.Receive(slowClient, &raw); err != nil {
			break
		}
		var message Outbound
		if err := json.Unmarshal(raw, &message); err != nil {
			t.Fatalf("decode slow client message: %v", err)
		}
		if message.Control == "resync_required" && message.Error != nil && message.Error.Code == "resync_required" {
			resyncRequired = true
		}
	}
	if !resyncRequired {
		t.Fatal("queue overflow closed the slow client without resync_required")
	}
	waitForSessionDetached(t, slowSession)

	resumedClient, err := websocket.Dial(wsURL, "", server.URL)
	if err != nil {
		t.Fatalf("dial resumed websocket: %v", err)
	}
	sendTestInbound(t, resumedClient, Inbound{Control: "ping", ResumeToken: token})
	for sequence := uint64(1); sequence <= 4; sequence++ {
		message := receiveOrderedMessage(t, resumedClient)
		if message.Session != sessionID || message.Sequence != sequence {
			t.Fatalf("resumed message %d = %#v", sequence, message)
		}
		if sequence >= 2 {
			payload, ok := message.Payload.(map[string]any)
			if !ok || payload["tick"] != float64(sequence-1) {
				t.Fatalf("replayed broadcast %d = %#v", sequence-1, message)
			}
		}
	}
	pong := receiveOrderedMessage(t, resumedClient)
	if pong.Control != "pong" || pong.Sequence != 0 {
		t.Fatalf("resume did not continue on the new connection: %#v", pong)
	}
	closeTestResource(t, resumedClient)
	waitForSessionDetached(t, slowSession)
	ReleaseSession(slowSession)
}
F
function

TestClientCanUnsubscribeFromBroadcasts

Parameters

host/broadcast_test.go:16-61
func TestClientCanUnsubscribeFromBroadcasts(t *testing.T)

{
	const componentName = "broadcast-unsubscribe"
	Register(NewHostComponent(componentName, func(map[string]any) any {
		return map[string]any{"ready": true}
	}))

	root := t.TempDir()
	if err := os.WriteFile(filepath.Join(root, "index.html"), []byte("ok"), 0o600); err != nil {
		t.Fatalf("write index: %v", err)
	}
	srv := httptest.NewServer(NewMux(root))
	defer srv.Close()
	ws, err := websocket.Dial("ws"+strings.TrimPrefix(srv.URL, "http")+"/ws", "", srv.URL)
	if err != nil {
		t.Fatalf("dial: %v", err)
	}
	defer closeTestResource(t, ws)

	init, _ := json.Marshal(map[string]any{"component": componentName, "payload": map[string]any{"init": true}})
	if err := websocket.Message.Send(ws, init); err != nil {
		t.Fatalf("subscribe: %v", err)
	}
	var raw []byte
	if err := websocket.Message.Receive(ws, &raw); err != nil {
		t.Fatalf("receive init: %v", err)
	}

	unsubscribe, _ := json.Marshal(map[string]any{"component": componentName, "payload": map[string]any{"unsubscribe": true}})
	if err := websocket.Message.Send(ws, unsubscribe); err != nil {
		t.Fatalf("unsubscribe: %v", err)
	}
	if err := websocket.Message.Receive(ws, &raw); err != nil {
		t.Fatalf("receive unsubscribe acknowledgement: %v", err)
	}

	Broadcast(componentName, map[string]any{"unexpected": true})
	if err := ws.SetDeadline(time.Now().Add(50 * time.Millisecond)); err != nil {
		t.Fatalf("set deadline: %v", err)
	}
	if err := websocket.Message.Receive(ws, &raw); err == nil {
		t.Fatalf("received broadcast after unsubscribe: %s", raw)
	}
	if err := ws.SetDeadline(time.Time{}); err != nil {
		t.Fatalf("reset deadline: %v", err)
	}
}
F
function

TestBroadcastConcurrentWithConnectionChurn

Broadcast must snapshot the (conn, session) pairs under connMu: clients
subscribing and disconnecting concurrently with broadcasts used to race with
the map iteration. Run with -race to exercise the invariant.

Parameters

host/broadcast_test.go:66-118
func TestBroadcastConcurrentWithConnectionChurn(t *testing.T)

{
	const componentName = "broadcast-churn"
	Register(NewHostComponentWithSession(componentName, func(_ *Session, _ map[string]any) any {
		return map[string]any{"ok": true}
	}))

	root := t.TempDir()
	if err := os.WriteFile(filepath.Join(root, "index.html"), []byte("ok"), 0o600); err != nil {
		t.Fatalf("write index: %v", err)
	}
	srv := httptest.NewServer(NewMux(root))
	defer srv.Close()
	wsURL := "ws" + strings.TrimPrefix(srv.URL, "http") + "/ws"

	init, err := json.Marshal(map[string]any{
		"component": componentName,
		"payload":   map[string]any{"init": true},
	})
	if err != nil {
		t.Fatalf("marshal init: %v", err)
	}

	var wg sync.WaitGroup
	// Churn: connections subscribe, receive the handler response and close.
	for i := 0; i < 8; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for j := 0; j < 5; j++ {
				ws, err := websocket.Dial(wsURL, "", srv.URL)
				if err != nil {
					continue
				}
				if err := websocket.Message.Send(ws, init); err == nil {
					var raw []byte
					_ = websocket.Message.Receive(ws, &raw)
				}
				closeTestResource(t, ws)
			}
		}()
	}
	// Broadcasters run against the same component while the map churns.
	for i := 0; i < 4; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for j := 0; j < 50; j++ {
				Broadcast(componentName, map[string]any{"tick": j})
			}
		}()
	}
	wg.Wait()
}
F
function

NewMuxFS

NewMuxFS is the fs.FS counterpart of NewMux: it serves the client build from
an fs.FS (for example an embed.FS sub-tree) instead of a directory on disk,
and registers the WebSocket handler at /ws. This lets an application ship as
a single self-contained binary with the build embedded via go:embed, or mount
the rfw endpoints on assets it already holds in memory.

fsys is treated as the complete served tree: index.html, app.wasm and any
static assets must live inside it. Unlike NewMux there is no on-disk sibling
static directory. Options gate the WebSocket endpoint exactly as in NewMux.

Parameters

fsys
opts
...MuxOption

Returns

host/server_fs.go:23-54
func NewMuxFS(fsys fs.FS, opts ...MuxOption) *http.ServeMux

{
	runtime := NewWSRuntime(opts...)
	mux := http.NewServeMux()
	if os.Getenv("RFW_DEVTOOLS") != "" {
		mux.Handle("/debug/vars", expvar.Handler())
		mux.HandleFunc("/debug/pprof/", pprof.Index)
		mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
		mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
		mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
		mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
	}
	fileServer := http.FileServerFS(fsys)
	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		if regularFileFS(fsys, r.URL.Path) {
			setWasmEncodingHeaders(w, r.URL.Path, r.URL.Query().Get("v") != "")
			fileServer.ServeHTTP(w, r)
			return
		}
		// Serve index.html only for HTML requests or bare paths, so a missing
		// asset (CSS, JS, image) still returns 404 instead of HTML.
		accept := r.Header.Get("Accept")
		if strings.Contains(accept, "text/html") || r.URL.Path == "/" || r.URL.Path == "" {
			serveIndexFS(w, r, fsys)
			return
		}
		http.NotFound(w, r)
	})
	mux.Handle("/ws", runtime.Guard(websocket.Handler(func(ws *websocket.Conn) {
		wsHandler(ws, runtime)
	})))
	return mux
}
F
function

serveIndexFS

serveIndexFS writes fsys’s index.html, the SPA entry point.

Parameters

host/server_fs.go:57-59
func serveIndexFS(w http.ResponseWriter, r *http.Request, fsys fs.FS)

{
	http.ServeFileFS(w, r, fsys, "index.html")
}
F
function

regularFileFS

regularFileFS reports whether name (a URL path) maps to a regular file in
fsys, the fs.FS analogue of regularFile.

Parameters

fsys
name
string

Returns

bool
host/server_fs.go:63-70
func regularFileFS(fsys fs.FS, name string) bool

{
	name = strings.TrimPrefix(name, "/")
	if name == "" || !fs.ValidPath(name) {
		return false
	}
	info, err := fs.Stat(fsys, name)
	return err == nil && !info.IsDir()
}
S
struct

Session

Session represents per-connection state for a WebSocket client.
It exposes an isolated StoreManager and a context bag for arbitrary data.

host/session.go:16-40
type Session struct

Methods

ID
Method

ID returns the session ID.

Returns

string
func (*Session) ID() string
{ return s.id }
ResumeToken
Method

ResumeToken returns the opaque token used to resume this session.

Returns

string
func (*Session) ResumeToken() string
{ return s.resumeToken }
StoreManager
Method

StoreManager returns the session-local store registry.

func (*Session) StoreManager() *state.StoreManager
{ return s.stores }
ContextGet
Method

ContextGet retrieves a value from the session context.

Parameters

key string

Returns

any
bool
func (*Session) ContextGet(key string) (any, bool)
{
	s.ctxMu.RLock()
	defer s.ctxMu.RUnlock()
	v, ok := s.ctx[key]
	return v, ok
}
ContextSet
Method

ContextSet stores a value in the session context.

Parameters

key string
value any
func (*Session) ContextSet(key string, value any)
{
	s.ctxMu.Lock()
	s.ctx[key] = value
	s.ctxMu.Unlock()
}
ContextDelete
Method

ContextDelete removes a value from the session context.

Parameters

key string
func (*Session) ContextDelete(key string)
{
	s.ctxMu.Lock()
	delete(s.ctx, key)
	s.ctxMu.Unlock()
}
Snapshot
Method

Snapshot returns a copy of all stores registered in this session.

Returns

map[string]map[string]map[string]any
func (*Session) Snapshot() map[string]map[string]map[string]any
{
	return s.stores.Snapshot()
}

resumableLocked reports whether a detached session can still be reattached.

Returns

bool
func (*Session) resumableLocked() bool
{
	return !s.released && !s.attached &&
		(s.expires.IsZero() || !time.Now().After(s.expires))
}

markResumedLocked attaches the session and cancels its retention timer.

func (*Session) markResumedLocked()
{
	s.attached = true
	s.resumePending = true
	s.expires = time.Time{}
	if s.expiryTimer != nil {
		s.expiryTimer.Stop()
		s.expiryTimer = nil
	}
}
AcceptInbound
Method

AcceptInbound validates and records an inbound sequence.

Parameters

sequence uint64

Returns

error
func (*Session) AcceptInbound(sequence uint64) error
{
	if s == nil || sequence == 0 {
		return nil
	}
	s.deliveryMu.Lock()
	defer s.deliveryMu.Unlock()
	if sequence <= s.inboundSeq {
		return ErrDuplicateMessage
	}
	if s.inboundSeq != 0 && sequence != s.inboundSeq+1 {
		return ErrSequenceGap
	}
	s.inboundSeq = sequence
	return nil
}
AllowMessage
Method

AllowMessage enforces a fixed per-session message window.

Parameters

limit int

Returns

bool
func (*Session) AllowMessage(limit int) bool
{
	if s == nil || limit <= 0 {
		return true
	}
	s.deliveryMu.Lock()
	defer s.deliveryMu.Unlock()
	now := time.Now()
	if s.rateStart.IsZero() || now.Sub(s.rateStart) >= time.Minute {
		s.rateStart = now
		s.rateCount = 0
	}
	s.rateCount++
	return s.rateCount <= limit
}

PrepareOutbound assigns delivery metadata and stores replay history.

Parameters

out Outbound

Returns

func (*Session) PrepareOutbound(out Outbound) Outbound
{
	if s == nil {
		return out
	}
	s.deliveryMu.Lock()
	defer s.deliveryMu.Unlock()
	s.outboundSeq++
	out.Session = s.id
	out.Sequence = s.outboundSeq
	out.Ack = s.inboundSeq
	out.ResumeToken = s.resumeToken
	if s.replayLimit > 0 {
		s.replay = append(s.replay, out)
		if extra := len(s.replay) - s.replayLimit; extra > 0 {
			copy(s.replay, s.replay[extra:])
			s.replay = s.replay[:s.replayLimit]
		}
	}
	return out
}
Acknowledge
Method

Acknowledge removes outbound messages confirmed by the client.

Parameters

sequence uint64
func (*Session) Acknowledge(sequence uint64)
{
	if s == nil || sequence == 0 {
		return
	}
	s.deliveryMu.Lock()
	defer s.deliveryMu.Unlock()
	remove := 0
	for remove < len(s.replay) && s.replay[remove].Sequence <= sequence {
		remove++
	}
	if remove > 0 {
		s.replay = append([]Outbound(nil), s.replay[remove:]...)
	}
}
ReplayAfter
Method

ReplayAfter returns retained outbound messages after sequence.

Parameters

sequence uint64

Returns

error
func (*Session) ReplayAfter(sequence uint64) ([]Outbound, error)
{
	if s == nil {
		return nil, nil
	}
	s.deliveryMu.Lock()
	defer s.deliveryMu.Unlock()
	if len(s.replay) == 0 {
		return nil, nil
	}
	if sequence+1 < s.replay[0].Sequence {
		return nil, ErrReplayUnavailable
	}
	index := 0
	for index < len(s.replay) && s.replay[index].Sequence <= sequence {
		index++
	}
	return append([]Outbound(nil), s.replay[index:]...), nil
}

Fields

Name Type Description
id string
resumeToken string
stores *state.StoreManager
ctxMu sync.RWMutex
ctx map[string]any
deliveryMu sync.Mutex
outboundMu sync.Mutex
connection *websocket.Conn
streamConnection *streamBusConnection
connectionManaged bool
resumePending bool
attached bool
released bool
expires time.Time
expiryTimer *time.Timer
inboundSeq uint64
outboundSeq uint64
rateStart time.Time
rateCount int
replayLimit int
replay []Outbound
S
struct

sessionOptions

host/session.go:42-45
type sessionOptions struct

Fields

Name Type Description
resumeToken string
replayLimit int
F
function

newSession

Parameters

id
string
options
...sessionOptions

Returns

host/session.go:47-60
func newSession(id string, options ...sessionOptions) *Session

{
	var config sessionOptions
	if len(options) > 0 {
		config = options[0]
	}
	return &Session{
		id:          id,
		resumeToken: config.resumeToken,
		stores:      state.NewStoreManager(),
		ctx:         make(map[string]any),
		attached:    true,
		replayLimit: config.replayLimit,
	}
}
F
function

AllocateSession

AllocateSession creates and registers a session.

Returns

host/session.go:105-108
func AllocateSession() *Session

{
	session, _ := allocateSession(0, 0)
	return session
}
F
function

AllocateResumableSession

AllocateResumableSession creates a session with ordered delivery history.

Parameters

replayLimit
int

Returns

host/session.go:111-114
func AllocateResumableSession(replayLimit int) *Session

{
	session, _ := allocateSession(replayLimit, 0)
	return session
}
F
function

allocateSession

Parameters

replayLimit
int
maxSessions
int

Returns

error
host/session.go:116-134
func allocateSession(replayLimit, maxSessions int) (*Session, error)

{
	id := generateSessionID()
	token := ""
	if replayLimit > 0 {
		token = generateSessionID() + generateSessionID()
	}
	session := newSession(id, sessionOptions{resumeToken: token, replayLimit: replayLimit})
	sessionMu.Lock()
	if maxSessions > 0 && len(sessions) >= maxSessions {
		sessionMu.Unlock()
		return nil, ErrSessionLimit
	}
	sessions[id] = session
	if token != "" {
		sessionByToken[token] = session
	}
	sessionMu.Unlock()
	return session, nil
}
F
function

SuspendSession

SuspendSession detaches a connection and retains resumable state for ttl.

Parameters

session
host/session.go:137-164
func SuspendSession(session *Session, ttl time.Duration)

{
	if session == nil {
		return
	}
	session.outboundMu.Lock()
	session.deliveryMu.Lock()
	if !session.attached {
		session.deliveryMu.Unlock()
		session.outboundMu.Unlock()
		return
	}
	session.connection = nil
	session.streamConnection = nil
	session.attached = false
	if ttl <= 0 || session.resumeToken == "" {
		session.deliveryMu.Unlock()
		session.outboundMu.Unlock()
		ReleaseSession(session)
		return
	}
	expires := time.Now().Add(ttl)
	session.expires = expires
	session.expiryTimer = time.AfterFunc(ttl, func() {
		releaseSession(session, expires)
	})
	session.deliveryMu.Unlock()
	session.outboundMu.Unlock()
}
F
function

ResumeSession

ResumeSession attaches a disconnected session by opaque token.
The new socket must call ReplaySession or BindSessionConnection before sends.

Parameters

token
string

Returns

bool
host/session.go:168-180
func ResumeSession(token string) (*Session, bool)

{
	session := sessionForToken(token)
	if session == nil {
		return nil, false
	}
	session.deliveryMu.Lock()
	defer session.deliveryMu.Unlock()
	if !session.resumableLocked() {
		return nil, false
	}
	session.markResumedLocked()
	return session, true
}
F
function

resumeCandidate

resumeCandidate reports the session a token can currently resume without
mutating it, so an authorization decision can be taken before the session is
attached, its expiry cleared or its connection replaced.

Parameters

token
string

Returns

bool
host/session.go:185-196
func resumeCandidate(token string) (*Session, bool)

{
	session := sessionForToken(token)
	if session == nil {
		return nil, false
	}
	session.deliveryMu.Lock()
	defer session.deliveryMu.Unlock()
	if !session.resumableLocked() {
		return nil, false
	}
	return session, true
}
F
function

commitResume

commitResume attaches a candidate that authorization approved. It fails
closed when the token stopped mapping to that exact session or the session is
no longer resumable, so a concurrent attempt cannot reattach a session other
than the one that was authorized.

Parameters

token
string
candidate

Returns

bool
host/session.go:202-213
func commitResume(token string, candidate *Session) bool

{
	if candidate == nil || sessionForToken(token) != candidate {
		return false
	}
	candidate.deliveryMu.Lock()
	defer candidate.deliveryMu.Unlock()
	if !candidate.resumableLocked() {
		return false
	}
	candidate.markResumedLocked()
	return true
}
F
function

sessionForToken

Parameters

token
string

Returns

host/session.go:215-222
func sessionForToken(token string) *Session

{
	if token == "" {
		return nil
	}
	sessionMu.RLock()
	defer sessionMu.RUnlock()
	return sessionByToken[token]
}
F
function

ReleaseSession

ReleaseSession removes a session from the registry.

Parameters

session
host/session.go:242-244
func ReleaseSession(session *Session)

{
	releaseSession(session, time.Time{})
}
F
function

releaseSession

Parameters

session
expectedExpiry
host/session.go:246-278
func releaseSession(session *Session, expectedExpiry time.Time)

{
	if session == nil {
		return
	}
	session.outboundMu.Lock()
	session.deliveryMu.Lock()
	if session.released || (!expectedExpiry.IsZero() &&
		(session.attached || !session.expires.Equal(expectedExpiry))) {
		session.deliveryMu.Unlock()
		session.outboundMu.Unlock()
		return
	}
	session.released = true
	if session.expiryTimer != nil {
		session.expiryTimer.Stop()
		session.expiryTimer = nil
	}
	session.connection = nil
	session.streamConnection = nil
	session.connectionManaged = false
	session.resumePending = false
	session.attached = false
	session.deliveryMu.Unlock()
	session.outboundMu.Unlock()
	sessionMu.Lock()
	if sessions[session.id] == session {
		delete(sessions, session.id)
	}
	if session.resumeToken != "" && sessionByToken[session.resumeToken] == session {
		delete(sessionByToken, session.resumeToken)
	}
	sessionMu.Unlock()
}
F
function

SessionByID

SessionByID retrieves a session for the given ID.

Parameters

id
string

Returns

bool
host/session.go:281-286
func SessionByID(id string) (*Session, bool)

{
	sessionMu.RLock()
	defer sessionMu.RUnlock()
	s, ok := sessions[id]
	return s, ok
}
F
function

generateSessionID

Returns

string
host/session.go:390-396
func generateSessionID() string

{
	buf := make([]byte, 16)
	if _, err := rand.Read(buf); err != nil {
		panic(err)
	}
	return hex.EncodeToString(buf)
}
F
function

TestSessionIsolation

Parameters

host/session_test.go:20-191
func TestSessionIsolation(t *testing.T)

{
	t.Helper()

	registry = make(map[string]*ServerComponent)

	const componentName = "SessionHost"
	Register(NewHostComponentWithSession(componentName, func(session *Session, payload map[string]any) any {
		const storeKey = "counter"
		storeVal, ok := session.ContextGet(storeKey)
		var store *state.Store
		if ok {
			store = storeVal.(*state.Store)
		} else {
			store = session.StoreManager().NewStore("counter")
			store.Set("value", 0)
			session.ContextSet(storeKey, store)
		}
		if inc, ok := payload["increment"].(bool); ok && inc {
			current, _ := store.Get("value").(int)
			store.Set("value", current+1)
		}
		return map[string]any{"value": store.Get("value")}
	}))

	root := t.TempDir()
	// Ensure an index exists so NewMux can serve fallback responses without error.
	if err := os.WriteFile(filepath.Join(root, "index.html"), []byte("ok"), 0o600); err != nil {
		t.Fatalf("write index: %v", err)
	}

	srv := httptest.NewServer(loggingMiddleware(NewMux(root)))
	defer srv.Close()

	wsURL := "ws" + strings.TrimPrefix(srv.URL, "http") + "/ws"

	type sessionConn struct {
		ws  *websocket.Conn
		id  string
		idx int
	}

	dial := func(idx int) sessionConn {
		ws, err := websocket.Dial(wsURL, "", srv.URL)
		if err != nil {
			t.Fatalf("dial %d: %v", idx, err)
		}
		init := map[string]any{
			"component": componentName,
			"payload":   map[string]any{"init": true},
		}
		raw, err := json.Marshal(init)
		if err != nil {
			t.Fatalf("marshal init %d: %v", idx, err)
		}
		if err := websocket.Message.Send(ws, raw); err != nil {
			t.Fatalf("send init %d: %v", idx, err)
		}
		var respRaw []byte
		if err := websocket.Message.Receive(ws, &respRaw); err != nil {
			t.Fatalf("recv init %d: %v", idx, err)
		}
		var resp struct {
			Component string         `json:"component"`
			Payload   map[string]any `json:"payload"`
			Session   string         `json:"session"`
		}
		if err := json.Unmarshal(respRaw, &resp); err != nil {
			t.Fatalf("unmarshal init %d: %v", idx, err)
		}
		if resp.Session == "" {
			t.Fatalf("session id missing for conn %d", idx)
		}
		if resp.Component != componentName {
			t.Fatalf("unexpected component %s", resp.Component)
		}
		if val, ok := resp.Payload["value"].(float64); !ok || val != 0 {
			t.Fatalf("unexpected init value for conn %d: %v", idx, resp.Payload)
		}
		return sessionConn{ws: ws, id: resp.Session, idx: idx}
	}

	sessions := []sessionConn{dial(0), dial(1)}
	defer func() {
		for _, sc := range sessions {
			closeTestResource(t, sc.ws)
		}
	}()

	counts := []int{5, 2}
	if len(counts) != len(sessions) {
		t.Fatalf("mismatched counts")
	}

	errCh := make(chan error, len(sessions))
	var wg sync.WaitGroup
	for i, sc := range sessions {
		wg.Add(1)
		count := counts[i]
		go func(sc sessionConn, target int) {
			defer wg.Done()
			for j := 0; j < target; j++ {
				payload := map[string]any{
					"component": componentName,
					"payload":   map[string]any{"increment": true},
				}
				raw, err := json.Marshal(payload)
				if err != nil {
					errCh <- fmt.Errorf("marshal increment idx=%d: %w", sc.idx, err)
					return
				}
				if err := websocket.Message.Send(sc.ws, raw); err != nil {
					errCh <- fmt.Errorf("send increment idx=%d: %w", sc.idx, err)
					return
				}
				var respRaw []byte
				if err := websocket.Message.Receive(sc.ws, &respRaw); err != nil {
					errCh <- fmt.Errorf("recv increment idx=%d: %w", sc.idx, err)
					return
				}
				var resp struct {
					Component string         `json:"component"`
					Payload   map[string]any `json:"payload"`
					Session   string         `json:"session"`
				}
				if err := json.Unmarshal(respRaw, &resp); err != nil {
					errCh <- fmt.Errorf("unmarshal increment idx=%d: %w", sc.idx, err)
					return
				}
				if resp.Session != sc.id {
					errCh <- fmt.Errorf("response session mismatch idx=%d: got %s want %s", sc.idx, resp.Session, sc.id)
					return
				}
			}
			errCh <- nil
		}(sc, count)
	}

	wg.Wait()
	close(errCh)
	for err := range errCh {
		if err != nil {
			t.Fatal(err)
		}
	}

	for i, sc := range sessions {
		sess, ok := SessionByID(sc.id)
		if !ok {
			t.Fatalf("session %d not found", i)
		}
		snap := sess.Snapshot()
		module := snap["default"]
		if module == nil {
			t.Fatalf("session %d missing default module snapshot", i)
		}
		counter := module["counter"]
		if counter == nil {
			t.Fatalf("session %d missing counter store", i)
		}
		val, ok := counter["value"].(int)
		if !ok {
			t.Fatalf("session %d missing value entry: %v", i, counter)
		}
		if val != counts[i] {
			t.Fatalf("session %d got value %d want %d", i, val, counts[i])
		}
	}

	if sessions[0].id == sessions[1].id {
		t.Fatal("session ids should differ")
	}
}
F
function

TestStreamBusWebTransportProtocol

Parameters

host/streambus_integration_test.go:22-115
func TestStreamBusWebTransportProtocol(t *testing.T)

{
	t.Setenv("RFW_TRANSPORT", "streambus")
	root := t.TempDir()
	if err := os.WriteFile(filepath.Join(root, "index.html"), []byte("ok"), 0o600); err != nil {
		t.Fatal(err)
	}
	mux := NewMux(root)
	value, ok := streamEndpoints.Load(mux)
	if !ok {
		t.Fatal("StreamBus endpoint was not registered")
	}
	endpoint := value.(*streamBusEndpoint)

	certificate, err := generateSelfSignedCert()
	if err != nil {
		t.Fatal(err)
	}
	leaf, err := x509.ParseCertificate(certificate.Certificate[0])
	if err != nil {
		t.Fatal(err)
	}
	roots := x509.NewCertPool()
	roots.AddCert(leaf)
	serverTLS := &tls.Config{Certificates: []tls.Certificate{certificate}, NextProtos: []string{http3.NextProtoH3}}
	h3 := &http3.Server{
		TLSConfig: serverTLS, Handler: mux, EnableDatagrams: true,
		QUICConfig: &quic.Config{EnableDatagrams: true, EnableStreamResetPartialDelivery: true},
	}
	server := &wt.Server{H3: h3}
	wt.ConfigureHTTP3Server(h3)
	endpoint.mu.Lock()
	endpoint.server = server
	endpoint.mu.Unlock()

	address, err := net.ResolveUDPAddr("udp", "127.0.0.1:0")
	if err != nil {
		t.Fatal(err)
	}
	packet, err := net.ListenUDP("udp", address)
	if err != nil {
		t.Fatal(err)
	}
	done := make(chan error, 1)
	go func() { done <- server.Serve(packet) }()
	t.Cleanup(func() {
		_ = server.Close()
		_ = packet.Close()
		select {
		case <-done:
		case <-time.After(time.Second):
			t.Error("WebTransport server did not stop")
		}
	})

	dialer := &wt.Dialer{
		TLSClientConfig: &tls.Config{RootCAs: roots, ServerName: "localhost"},
		QUICConfig:      &quic.Config{EnableDatagrams: true, EnableStreamResetPartialDelivery: true},
	}
	t.Cleanup(func() { _ = dialer.Close() })
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	url := fmt.Sprintf("https://localhost:%d%s", packet.LocalAddr().(*net.UDPAddr).Port, streamBusPath)
	response, session, err := dialer.Dial(ctx, url, nil)
	if err != nil {
		t.Fatal(err)
	}
	defer func() { _ = response.Body.Close() }()
	if response.StatusCode != http.StatusOK {
		t.Fatalf("status = %d", response.StatusCode)
	}
	t.Cleanup(func() { _ = session.CloseWithError(0, "") })
	stream, err := session.OpenStreamSync(ctx)
	if err != nil {
		t.Fatal(err)
	}
	inbound, err := json.Marshal(Inbound{Component: "missing", Sequence: 1})
	if err != nil {
		t.Fatal(err)
	}
	if err := writeFrame(stream, inbound); err != nil {
		t.Fatal(err)
	}
	payload, err := readFrame(bufio.NewReader(stream), 1<<20)
	if err != nil {
		t.Fatal(err)
	}
	var outbound Outbound
	if err := json.Unmarshal(payload, &outbound); err != nil {
		t.Fatal(err)
	}
	if outbound.Control != "ack" || outbound.Session == "" || outbound.Sequence == 0 {
		t.Fatalf("unexpected response: %#v", outbound)
	}
}
S
struct

statusRecorder

host/middleware.go:11-14
type statusRecorder struct

Methods

WriteHeader
Method

Parameters

code int
func (*statusRecorder) WriteHeader(code int)
{
	r.status = code
	r.ResponseWriter.WriteHeader(code)
}
Hijack
Method

Returns

func (*statusRecorder) Hijack() (net.Conn, *bufio.ReadWriter, error)
{
	if h, ok := r.ResponseWriter.(http.Hijacker); ok {
		return h.Hijack()
	}
	return nil, nil, errors.New("http.Hijacker not supported")
}

Fields

Name Type Description
status int
F
function

loggingMiddleware

Parameters

Returns

host/middleware.go:28-35
func loggingMiddleware(next http.Handler) http.Handler

{
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		rec := &statusRecorder{ResponseWriter: w, status: http.StatusOK}
		start := time.Now()
		next.ServeHTTP(rec, r)
		logger.Info("request", "method", r.Method, "path", r.URL.Path, "status", rec.status, "duration", time.Since(start))
	})
}
F
function

TestNewMuxDebugEndpoints

Parameters

host/server_debug_test.go:11-38
func TestNewMuxDebugEndpoints(t *testing.T)

{
	t.Setenv("RFW_DEVTOOLS", "1")
	mux := NewMux(t.TempDir())
	ts := httptest.NewServer(mux)
	defer ts.Close()

	resp, err := http.Get(ts.URL + "/debug/vars")
	if err != nil {
		t.Fatalf("vars request failed: %v", err)
	}
	if err := resp.Body.Close(); err != nil {
		t.Fatalf("close debug response: %v", err)
	}
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("expected 200, got %d", resp.StatusCode)
	}

	resp, err = http.Get(ts.URL + "/debug/pprof/")
	if err != nil {
		t.Fatalf("pprof request failed: %v", err)
	}
	if err := resp.Body.Close(); err != nil {
		t.Fatalf("close pprof response: %v", err)
	}
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("expected 200, got %d", resp.StatusCode)
	}
}
F
function

TestHostComponent

TestHostComponent verifies registration and handler execution.

Parameters

host/host_test.go:8-25
func TestHostComponent(t *testing.T)

{
	called := false
	hc := NewHostComponent("cmp", func(payload map[string]any) any {
		called = true
		if payload["x"] != 1 {
			t.Fatalf("unexpected payload: %v", payload)
		}
		return "ok"
	})
	Register(hc)
	got, ok := Get("cmp")
	if !ok || got != hc {
		t.Fatalf("component not registered")
	}
	if resp := hc.Handle(map[string]any{"x": 1}); resp != "ok" || !called {
		t.Fatalf("handler not executed or wrong response: %v", resp)
	}
}
F
function

TestHostComponentWithSession

Parameters

host/host_test.go:27-52
func TestHostComponentWithSession(t *testing.T)

{
	hc := NewHostComponentWithSession("withSession", func(session *Session, payload map[string]any) any {
		if session == nil {
			t.Fatalf("session should not be nil")
		}
		store := session.StoreManager().NewStore("test")
		store.Set("value", payload["v"])
		return store.Snapshot()
	})

	sess := newSession("abc")
	resp := hc.HandleWithSession(sess, map[string]any{"v": 42})
	snap, ok := resp.(map[string]any)
	if !ok {
		t.Fatalf("unexpected response type %T", resp)
	}
	if snap["value"] != 42 {
		t.Fatalf("unexpected store snapshot: %v", snap)
	}
	if !hc.SessionAware() {
		t.Fatalf("expected session aware component")
	}
	if hc.StoreManager(sess) != sess.StoreManager() {
		t.Fatalf("StoreManager helper mismatch")
	}
}
F
function

TestLogLevel

TestLogLevel checks environment variable parsing.

Parameters

host/host_test.go:55-68
func TestLogLevel(t *testing.T)

{
	t.Setenv("RFW_LOG_LEVEL", "debug")
	if lvl := logLevel(); lvl.String() != "DEBUG" {
		t.Fatalf("expected DEBUG level, got %s", lvl)
	}
	t.Setenv("RFW_LOG_LEVEL", "warn")
	if lvl := logLevel(); lvl.String() != "WARN" {
		t.Fatalf("expected WARN level, got %s", lvl)
	}
	t.Setenv("RFW_LOG_LEVEL", "")
	if lvl := logLevel(); lvl.String() != "INFO" {
		t.Fatalf("expected INFO level, got %s", lvl)
	}
}
F
function

TestGenerateSelfSignedCert

TestGenerateSelfSignedCert ensures a certificate is generated.

Parameters

host/host_test.go:71-79
func TestGenerateSelfSignedCert(t *testing.T)

{
	cert, err := generateSelfSignedCert()
	if err != nil {
		t.Fatalf("generateSelfSignedCert returned error: %v", err)
	}
	if len(cert.Certificate) == 0 {
		t.Fatalf("expected certificate data")
	}
}
F
function

ResolveRoot

ResolveRoot resolves a content root relative to the executable when needed.

Parameters

root
string

Returns

string
host/server.go:22-33
func ResolveRoot(root string) string

{
	if _, err := os.Stat(root); err == nil {
		return root
	}
	if exe, err := os.Executable(); err == nil {
		candidate := filepath.Join(filepath.Dir(exe), "..", root)
		if _, err := os.Stat(candidate); err == nil {
			return candidate
		}
	}
	return root
}
F
function

NewMux

NewMux returns an HTTP mux that serves static files from root and the
WebSocket handler at /ws. Options gate the WebSocket endpoint; by default it
accepts any origin and identity.

Parameters

root
string
opts
...MuxOption

Returns

host/server.go:38-103
func NewMux(root string, opts ...MuxOption) *http.ServeMux

{
	root = ResolveRoot(root)
	runtime := NewWSRuntime(opts...)
	staticRoot := filepath.Join(root, "..", "static")
	mux := http.NewServeMux()
	if os.Getenv("RFW_DEVTOOLS") != "" {
		mux.Handle("/debug/vars", expvar.Handler())
		mux.HandleFunc("/debug/pprof/", pprof.Index)
		mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
		mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
		mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
		mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
	}
	fs := http.FileServer(http.Dir(root))
	rootDir := http.Dir(root)
	var sfs http.Handler
	var staticDir http.Dir
	if _, err := os.Stat(staticRoot); err == nil {
		staticDir = http.Dir(staticRoot)
		sfs = http.FileServer(staticDir)
	}
	if sfs != nil {
		mux.Handle("/static/", http.StripPrefix("/static", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			setWasmEncodingHeaders(w, r.URL.Path, r.URL.Query().Get("v") != "")
			sfs.ServeHTTP(w, r)
		})))
	}
	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		// A compressed artifact answers the raw URL whenever the client says
		// it can decode one. The raw bundle only reaches a client that asked
		// for no encoding, or a build that produced no artifact.
		if negotiateWasm(w, r, rootDir, r.URL.Path) {
			return
		}
		if sfs != nil {
			if regularFile(staticDir, r.URL.Path) {
				setWasmEncodingHeaders(w, r.URL.Path, r.URL.Query().Get("v") != "")
				sfs.ServeHTTP(w, r)
				return
			}
		}
		if regularFile(rootDir, r.URL.Path) {
			setWasmEncodingHeaders(w, r.URL.Path, r.URL.Query().Get("v") != "")
			fs.ServeHTTP(w, r)
			return
		}
		// Serve index.html only for HTML requests or bare paths to avoid
		// returning HTML for CSS, JS, image, etc. requests.
		accept := r.Header.Get("Accept")
		if strings.Contains(accept, "text/html") || r.URL.Path == "/" || r.URL.Path == "" {
			if devMode() {
				w.Header().Set("Cache-Control", "no-store")
			} else {
				w.Header().Set("Cache-Control", "no-cache")
			}
			http.ServeFile(w, r, filepath.Join(root, "index.html"))
			return
		}
		http.NotFound(w, r)
	})
	mux.Handle("/ws", runtime.Guard(websocket.Handler(func(ws *websocket.Conn) {
		wsHandler(ws, runtime)
	})))
	registerStreamBus(mux, runtime)
	return mux
}
F
function

ListenAndServe

ListenAndServe starts an HTTP server using NewMux to serve files and the
WebSocket endpoint.

Parameters

addr
string
root
string

Returns

error
host/server.go:107-110
func ListenAndServe(addr, root string) error

{
	logger.Info("serving HTTP", "addr", addr)
	return newHTTPServer(addr, loggingMiddleware(NewMux(root))).ListenAndServe()
}
F
function

ListenAndServeWithMux

ListenAndServeWithMux starts an HTTP server using the provided mux.

Parameters

addr
string

Returns

error
host/server.go:113-116
func ListenAndServeWithMux(addr string, mux *http.ServeMux) error

{
	logger.Info("serving HTTP", "addr", addr)
	return newHTTPServer(addr, loggingMiddleware(mux)).ListenAndServe()
}
F
function

ListenAndServeTLS

ListenAndServeTLS starts an HTTPS server using a self-signed certificate
and NewMux to serve files and the WebSocket endpoint.

Parameters

addr
string
root
string

Returns

error
host/server.go:120-132
func ListenAndServeTLS(addr, root string) error

{
	cert, err := generateSelfSignedCert()
	if err != nil {
		return err
	}
	mux := NewMux(root)
	tlsConfig := &tls.Config{Certificates: []tls.Certificate{cert}}
	startStreamBusHTTP3(addr, mux, tlsConfig)
	srv := newHTTPServer(addr, loggingMiddleware(mux))
	srv.TLSConfig = tlsConfig
	logger.Info("serving HTTPS", "addr", addr)
	return srv.ListenAndServeTLS("", "")
}
F
function

ListenAndServeTLSWithMux

ListenAndServeTLSWithMux starts an HTTPS server using a self-signed certificate
and the provided mux, preserving any additional routes registered by callers.

Parameters

addr
string

Returns

error
host/server.go:136-147
func ListenAndServeTLSWithMux(addr string, mux *http.ServeMux) error

{
	cert, err := generateSelfSignedCert()
	if err != nil {
		return err
	}
	tlsConfig := &tls.Config{Certificates: []tls.Certificate{cert}}
	startStreamBusHTTP3(addr, mux, tlsConfig)
	srv := newHTTPServer(addr, loggingMiddleware(mux))
	srv.TLSConfig = tlsConfig
	logger.Info("serving HTTPS", "addr", addr)
	return srv.ListenAndServeTLS("", "")
}
F
function

newHTTPServer

Parameters

addr
string
handler

Returns

host/server.go:149-155
func newHTTPServer(addr string, handler http.Handler) *http.Server

{
	return &http.Server{
		Addr:              addr,
		Handler:           handler,
		ReadHeaderTimeout: 5 * time.Second,
	}
}
F
function

regularFile

Parameters

root
name
string

Returns

bool
host/server.go:157-165
func regularFile(root http.Dir, name string) bool

{
	f, err := root.Open(name)
	if err != nil {
		return false
	}
	info, statErr := f.Stat()
	closeErr := f.Close()
	return statErr == nil && closeErr == nil && !info.IsDir()
}
F
function

devMode

devMode reports whether the server is running under rfw dev. The dev command
exports RFW_DEV_BUILD=1 and propagates it to the SSC host child via os.Environ,
so both the static and host-proxied serving paths observe it.

Returns

bool
host/server.go:170-170
func devMode() bool

{ return os.Getenv("RFW_DEV_BUILD") == "1" }
F
function

setWasmEncodingHeaders

Parameters

path
string
versioned
bool
host/server.go:172-197
func setWasmEncodingHeaders(w http.ResponseWriter, path string, versioned bool)

{
	// In dev, nothing may be cached: the wasm version pointer lives in
	// rfw_config.js and the binary is fetched as app.wasm?v=<hash>. Caching
	// either one leaves the browser re-requesting a stale ?v= against an
	// immutable entry, so rebuilds are never picked up. no-store on every asset
	// forces a fresh fetch each load. Production keeps the immutable policy.
	if devMode() {
		w.Header().Set("Cache-Control", "no-store")
	} else if revalidates(path) {
		w.Header().Set("Cache-Control", "no-cache")
	}
	if !wasmRequest(path) {
		return
	}
	header := w.Header()
	setWasmCacheControl(header, versioned)
	encoding, compressed := artifactEncoding(path)
	if !compressed {
		return
	}
	// A directly addressed artifact still has to be labelled, or the browser
	// hands compressed bytes to WebAssembly instead of decoding them.
	header.Set("Content-Encoding", encoding)
	header.Set("Content-Type", "application/wasm")
	addVaryAcceptEncoding(header)
}
F
function

generateSelfSignedCert

Returns

host/server.go:199-221
func generateSelfSignedCert() (tls.Certificate, error)

{
	priv, err := rsa.GenerateKey(rand.Reader, 2048)
	if err != nil {
		return tls.Certificate{}, err
	}
	tmpl := x509.Certificate{
		SerialNumber: big.NewInt(1),
		NotBefore:    time.Now(),
		// WebTransport accepts certificate hashes for development certificates
		// only when their validity is at most two weeks.
		NotAfter:    time.Now().Add(13 * 24 * time.Hour),
		KeyUsage:    x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
		ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
		DNSNames:    []string{"localhost"},
	}
	der, err := x509.CreateCertificate(rand.Reader, &tmpl, &tmpl, &priv.PublicKey, priv)
	if err != nil {
		return tls.Certificate{}, err
	}
	certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})
	keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)})
	return tls.X509KeyPair(certPEM, keyPEM)
}
F
function

testClientFS

Returns

host/server_fs_test.go:12-20
func testClientFS() fstest.MapFS

{
	return fstest.MapFS{
		"index.html":     {Data: []byte("<!doctype html><div id=app></div>")},
		"app.wasm":       {Data: []byte("\x00asm")},
		"app.wasm.br":    {Data: []byte("brotli-bytes")},
		"rfw_config.js":  {Data: []byte("//cfg")},
		"assets/app.css": {Data: []byte(".a{}")},
	}
}
F
function

TestNewMuxFSServesEmbeddedBuild

Parameters

host/server_fs_test.go:22-78
func TestNewMuxFSServesEmbeddedBuild(t *testing.T)

{
	srv := httptest.NewServer(NewMuxFS(testClientFS()))
	defer srv.Close()

	type result struct {
		status int
		header http.Header
	}
	// get issues the request and closes the body before returning, so the
	// assertions never hold an open response.
	get := func(path, accept string) result {
		req, err := http.NewRequest(http.MethodGet, srv.URL+path, nil)
		if err != nil {
			t.Fatalf("new request %s: %v", path, err)
		}
		if accept != "" {
			req.Header.Set("Accept", accept)
		}
		resp, err := http.DefaultClient.Do(req)
		if err != nil {
			t.Fatalf("get %s: %v", path, err)
		}
		if cerr := resp.Body.Close(); cerr != nil {
			t.Fatalf("close body %s: %v", path, cerr)
		}
		return result{status: resp.StatusCode, header: resp.Header}
	}

	if got := get("/", "text/html").status; got != http.StatusOK {
		t.Fatalf("root status = %d, want 200", got)
	}
	if got := get("/assets/app.css", "").status; got != http.StatusOK {
		t.Fatalf("nested asset status = %d, want 200", got)
	}
	if got := get("/app.wasm?v=abc", "").header.Get("Cache-Control"); got != "public, max-age=31536000, immutable" {
		t.Fatalf("versioned wasm Cache-Control = %q", got)
	}
	if got := get("/app.wasm.br", "").header.Get("Content-Encoding"); got != "br" {
		t.Fatalf("wasm.br Content-Encoding = %q, want br", got)
	}
	if got := get("/rfw_config.js", "").header.Get("Cache-Control"); got != "no-cache" {
		t.Fatalf("rfw_config.js Cache-Control = %q, want no-cache", got)
	}
	// An unknown HTML route falls back to index.html (single-page app).
	if got := get("/dashboard/live", "text/html").status; got != http.StatusOK {
		t.Fatalf("unknown html route status = %d, want 200 (index fallback)", got)
	}
	// A missing non-HTML asset is a 404, not the index.
	if got := get("/missing.css", "").status; got != http.StatusNotFound {
		t.Fatalf("missing asset status = %d, want 404", got)
	}
	// A plain GET is not a WebSocket handshake, so /ws rejects it, but it must be
	// routed rather than falling through to the catch-all 404.
	if got := get("/ws", "").status; got == http.StatusNotFound {
		t.Fatalf("/ws returned 404, endpoint not registered")
	}
}
F
function

wsProbe

Parameters

origin
string

Returns

int
host/server_guard_test.go:12-31
func wsProbe(t *testing.T, mux *http.ServeMux, origin string) int

{
	t.Helper()
	srv := httptest.NewServer(mux)
	defer srv.Close()
	req, err := http.NewRequest(http.MethodGet, srv.URL+"/ws", nil)
	if err != nil {
		t.Fatalf("request: %v", err)
	}
	if origin != "" {
		req.Header.Set("Origin", origin)
	}
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		t.Fatalf("do: %v", err)
	}
	if err := resp.Body.Close(); err != nil {
		t.Fatalf("close response: %v", err)
	}
	return resp.StatusCode
}
F
function

TestWSOpenByDefault

Without options the endpoint stays open: a plain GET reaches the WebSocket
handler (which rejects the missing upgrade with 400, not a guard status).

Parameters

host/server_guard_test.go:35-40
func TestWSOpenByDefault(t *testing.T)

{
	mux := NewMux(t.TempDir())
	if code := wsProbe(t, mux, "http://evil.example"); code == http.StatusForbidden || code == http.StatusUnauthorized {
		t.Fatalf("default mux rejected connection: %d", code)
	}
}
F
function

TestWSOriginAllowlist

Parameters

host/server_guard_test.go:42-53
func TestWSOriginAllowlist(t *testing.T)

{
	mux := NewMux(t.TempDir(), WithOriginAllowlist("https://app.example.com"))
	if code := wsProbe(t, mux, "http://evil.example"); code != http.StatusForbidden {
		t.Fatalf("expected 403 for unlisted origin, got %d", code)
	}
	if code := wsProbe(t, mux, ""); code != http.StatusForbidden {
		t.Fatalf("expected 403 for missing origin, got %d", code)
	}
	if code := wsProbe(t, mux, "https://app.example.com"); code == http.StatusForbidden {
		t.Fatalf("allowed origin rejected: %d", code)
	}
}
F
function

TestWSAuthFunc

Parameters

host/server_guard_test.go:55-65
func TestWSAuthFunc(t *testing.T)

{
	mux := NewMux(t.TempDir(), WithAuthFunc(func(r *http.Request) bool {
		return r.Header.Get("Origin") == "https://trusted.example.com"
	}))
	if code := wsProbe(t, mux, "http://evil.example"); code != http.StatusUnauthorized {
		t.Fatalf("expected 401 for rejected auth, got %d", code)
	}
	if code := wsProbe(t, mux, "https://trusted.example.com"); code == http.StatusUnauthorized {
		t.Fatalf("accepted auth rejected: %d", code)
	}
}
F
function

TestWSConnectionLimit

Parameters

host/server_guard_test.go:67-80
func TestWSConnectionLimit(t *testing.T)

{
	runtime := NewWSRuntime(WithSSCLimits(SSCLimits{MaxConnections: 1}))
	if !runtime.AcquireConnection() {
		t.Fatal("first connection was rejected")
	}
	if runtime.AcquireConnection() {
		t.Fatal("second connection exceeded the limit")
	}
	runtime.ReleaseConnection()
	if !runtime.AcquireConnection() {
		t.Fatal("released connection slot was not reusable")
	}
	runtime.ReleaseConnection()
}
F
function

TestWSOutboundLimits

Parameters

host/server_guard_test.go:82-95
func TestWSOutboundLimits(t *testing.T)

{
	defaults := DefaultSSCLimits()
	if defaults.WriteTimeout != 10*time.Second || defaults.OutboundQueueSize != 64 {
		t.Fatalf("unexpected outbound defaults: timeout=%s queue=%d", defaults.WriteTimeout, defaults.OutboundQueueSize)
	}

	runtime := NewWSRuntime(WithSSCLimits(SSCLimits{
		WriteTimeout:      250 * time.Millisecond,
		OutboundQueueSize: 8,
	}))
	if runtime.limits.WriteTimeout != 250*time.Millisecond || runtime.limits.OutboundQueueSize != 8 {
		t.Fatalf("outbound overrides were not applied: %#v", runtime.limits)
	}
}
F
function

BenchmarkStreamBusFanout

Parameters

host/streambus_benchmark_test.go:15-55
func BenchmarkStreamBusFanout(b *testing.B)

{
	for _, subscribers := range []int{1, 10, 100} {
		b.Run(fmt.Sprintf("subscribers-%d", subscribers), func(b *testing.B) {
			bus := streambus.NewInMemory(streambus.Config{DefaultBuffer: 4096, MaxBuffer: 4096})
			defer func() { _ = bus.Close() }()
			topics := make([]string, subscribers)
			for i := 0; i < subscribers; i++ {
				topics[i] = fmt.Sprintf("rfw/connection/%d", i)
				subscription, err := bus.Subscribe(context.Background(), streambus.SubscribeOptions{
					Topic: topics[i], Buffer: 4096, Overflow: streambus.Block,
				})
				if err != nil {
					b.Fatal(err)
				}
				defer func() { _ = subscription.Close() }()
				go func() {
					for range subscription.Frames() {
						continue
					}
				}()
			}
			outbound := Outbound{Component: "Chart", Payload: map[string]any{"value": 42}}
			b.ReportAllocs()
			b.ResetTimer()
			for i := 0; i < b.N; i++ {
				for _, topic := range topics {
					payload, err := json.Marshal(outbound)
					if err != nil {
						b.Fatal(err)
					}
					if _, err := bus.Publish(context.Background(), streambus.Frame{
						Topic: topic, Payload: payload, Reliability: streambus.Reliable,
						Priority: streambus.PriorityInteractive,
					}); err != nil {
						b.Fatal(err)
					}
				}
			}
		})
	}
}
F
function

BenchmarkWebSocketFanout

Parameters

host/streambus_benchmark_test.go:57-104
func BenchmarkWebSocketFanout(b *testing.B)

{
	for _, subscribers := range []int{1, 10, 100} {
		b.Run(fmt.Sprintf("subscribers-%d", subscribers), func(b *testing.B) {
			accepted := make(chan *websocket.Conn, subscribers)
			stop := make(chan struct{})
			server := httptest.NewServer(websocket.Handler(func(connection *websocket.Conn) {
				accepted <- connection
				<-stop
			}))
			defer server.Close()
			serverConnections := make([]*websocket.Conn, 0, subscribers)
			clientConnections := make([]*websocket.Conn, 0, subscribers)
			for i := 0; i < subscribers; i++ {
				client, err := websocket.Dial("ws"+strings.TrimPrefix(server.URL, "http"), "", server.URL)
				if err != nil {
					b.Fatal(err)
				}
				clientConnections = append(clientConnections, client)
				serverConnections = append(serverConnections, <-accepted)
				go func() {
					for {
						var payload []byte
						if websocket.Message.Receive(client, &payload) != nil {
							return
						}
					}
				}()
			}
			defer func() {
				for _, connection := range clientConnections {
					_ = connection.Close()
				}
				for _, connection := range serverConnections {
					ForgetConnection(connection)
				}
				close(stop)
			}()
			outbound := Outbound{Component: "Chart", Payload: map[string]any{"value": 42}}
			b.ReportAllocs()
			b.ResetTimer()
			for i := 0; i < b.N; i++ {
				for _, connection := range serverConnections {
					SendOutbound(connection, outbound)
				}
			}
		})
	}
}
T
type

Handler

Handler processes inbound payloads for a HostComponent and returns a
response payload to send back to the wasm runtime. Returning nil results in
no message being sent.

host/host_component.go:12-12
type Handler func(payload map[string]any) any
T
type

HandlerWithSession

HandlerWithSession processes inbound payloads with the associated Session.

host/host_component.go:15-15
type HandlerWithSession func(*Session, map[string]any) any
S
struct

HostComponent

HostComponent represents server-side logic backing an HTML component.

host/host_component.go:18-23
type HostComponent struct

Methods

WithInitSnapshot registers a callback that produces an InitSnapshot when a resync is requested.

Parameters

fn func(*Session, map[string]any) *InitSnapshot

Returns

func (*HostComponent) WithInitSnapshot(fn func(*Session, map[string]any) *InitSnapshot) *HostComponent
{
	hc.initSnapshot = fn
	return hc
}
Name
Method

Name returns the registered component name.

Returns

string
func (*HostComponent) Name() string
{ return hc.name }
Handle
Method

Handle executes the component's handler.

Parameters

payload map[string]any

Returns

any
func (*HostComponent) Handle(payload map[string]any) any
{
	if hc.handler != nil {
		return hc.handler(payload)
	}
	return nil
}

HandleWithSession executes the session-aware handler when available.

Parameters

session *Session
payload map[string]any

Returns

any
func (*HostComponent) HandleWithSession(session *Session, payload map[string]any) any
{
	if payload != nil {
		if _, ok := payload["resync"]; ok && hc.initSnapshot != nil {
			if snap := hc.initSnapshot(session, payload); snap != nil {
				return snap
			}
		}
	}
	if hc.sessionHandler != nil {
		return hc.sessionHandler(session, payload)
	}
	if hc.handler != nil {
		return hc.handler(payload)
	}
	return nil
}
SessionAware
Method

SessionAware reports whether the component registered a session handler.

Returns

bool
func (*HostComponent) SessionAware() bool
{ return hc.sessionHandler != nil }
StoreManager
Method

StoreManager returns the session-specific store manager when available. If session is nil a reference to the global manager is returned for backward compatibility with legacy handlers.

Parameters

session *Session
func (*HostComponent) StoreManager(session *Session) *state.StoreManager
{
	if session != nil {
		return session.StoreManager()
	}
	return state.GlobalStoreManager
}

Fields

Name Type Description
name string
handler Handler
sessionHandler HandlerWithSession
initSnapshot func(*Session, map[string]any) *InitSnapshot
T
type

ServerComponent

ServerComponent is the concise name for HostComponent.

host/host_component.go:26-26
type ServerComponent HostComponent
S
struct

InitSnapshot

InitSnapshot represents markup the host can send to force the client to
repaint a fragment. HTML is injected into the component root as raw HTML on
the client, so build it from the escaping helpers (Span, Div, P, Tag); any
unescaped user-derived data in it is an XSS vector. Use Raw/RawTag only for
markup you generated or sanitized yourself.

host/host_component.go:33-36
type InitSnapshot struct

Fields

Name Type Description
HTML string json:"html"
Vars []string json:"vars,omitempty"
F
function

NewHostComponent

NewHostComponent registers a handler for the given component name.

Parameters

name
string
handler

Returns

host/host_component.go:39-47
func NewHostComponent(name string, handler Handler) *HostComponent

{
	hc := &HostComponent{name: name, handler: handler}
	if handler != nil {
		hc.sessionHandler = func(_ *Session, payload map[string]any) any {
			return handler(payload)
		}
	}
	return hc
}
F
function

NewHostComponentWithSession

NewHostComponentWithSession registers a session-aware handler.

Parameters

name
string

Returns

host/host_component.go:67-69
func NewHostComponentWithSession(name string, handler HandlerWithSession) *HostComponent

{
	return &HostComponent{name: name, sessionHandler: handler}
}
I
interface

Component

Component is the interface for struct-based host components.
Register a struct implementing Component via RegisterComponent.

host/host_component.go:104-107
type Component interface

Methods

Name
Method

Returns

string
func Name(...)
Serve
Method

Parameters

map[string]any

Returns

any
func Serve(...)
F
function

RegisterComponent

RegisterComponent creates a HostComponent from a struct implementing Component
and registers it in the global registry. This is the recommended way to
define host components — it provides type safety and clean separation.

Parameters

host/host_component.go:112-120
func RegisterComponent(c Component)

{
	hc := &HostComponent{
		name:           c.Name(),
		sessionHandler: func(s *Session, p map[string]any) any { return c.Serve(s, p) },
	}
	registryMu.Lock()
	registry[c.Name()] = hc
	registryMu.Unlock()
}
F
function

Register

Register adds a HostComponent to the global registry so incoming messages
can be routed to it.

Parameters

host/host_component.go:129-133
func Register(hc *HostComponent)

{
	registryMu.Lock()
	registry[hc.name] = hc
	registryMu.Unlock()
}
F
function

Get

Get returns a registered HostComponent by name.

Parameters

name
string

Returns

host/host_component.go:136-141
func Get(name string) (*HostComponent, bool)

{
	registryMu.RLock()
	hc, ok := registry[name]
	registryMu.RUnlock()
	return hc, ok
}
S
struct

Inbound

Inbound is a client-to-host SSC protocol message.

host/protocol.go:6-15
type Inbound struct

Fields

Name Type Description
Component string json:"component,omitempty"
Action string json:"action,omitempty"
Control string json:"control,omitempty"
ID string json:"id,omitempty"
Payload map[string]any json:"payload,omitempty"
Sequence uint64 json:"sequence,omitempty"
Ack uint64 json:"ack,omitempty"
ResumeToken string json:"resumeToken,omitempty"
S
struct

Outbound

Outbound is a host-to-client SSC protocol message.

host/protocol.go:18-31
type Outbound struct

Fields

Name Type Description
Component string json:"component,omitempty"
Action string json:"action,omitempty"
Control string json:"control,omitempty"
ID string json:"id,omitempty"
Payload any json:"payload,omitempty"
Error *ActionError json:"error,omitempty"
Session string json:"session,omitempty"
Sequence uint64 json:"sequence,omitempty"
Ack uint64 json:"ack,omitempty"
ResumeToken string json:"resumeToken,omitempty"
encodedPayload []byte
S
struct

ActionError

ActionError is a public, machine-readable action failure.

host/protocol.go:34-38
type ActionError struct

Methods

Error
Method

Returns

string
func (*ActionError) Error() string
{
	if e == nil {
		return ""
	}
	return fmt.Sprintf("%s: %s", e.Code, e.Message)
}

Fields

Name Type Description
Code string json:"code"
Message string json:"message"
Fields map[string]string json:"fields,omitempty"
F
function

NewActionError

NewActionError creates a public action error safe to return to the client.

Parameters

code
string
message
string

Returns

host/protocol.go:48-50
func NewActionError(code, message string) *ActionError

{
	return &ActionError{Code: code, Message: message}
}
T
type

Transport

Transport identifies the browser-to-host transport used by SSC.

host/transport.go:10-10
type Transport string
F
function

ResolveTransport

ResolveTransport reads RFW_TRANSPORT first, then the top-level transport
key in rfw.json. Unknown and empty values preserve the WebSocket default.

Returns

host/transport.go:23-38
func ResolveTransport() Transport

{
	if mode := normalizeTransport(os.Getenv("RFW_TRANSPORT")); mode != "" {
		return mode
	}
	var manifest struct {
		Transport string `json:"transport"`
	}
	if data, err := os.ReadFile("rfw.json"); err == nil {
		if json.Unmarshal(data, &manifest) == nil {
			if mode := normalizeTransport(manifest.Transport); mode != "" {
				return mode
			}
		}
	}
	return TransportWebSocket
}
F
function

normalizeTransport

Parameters

value
string

Returns

host/transport.go:40-51
func normalizeTransport(value string) Transport

{
	switch strings.ToLower(strings.TrimSpace(value)) {
	case string(TransportWebSocket), "ws":
		return TransportWebSocket
	case string(TransportStreamBus), "webtransport", "warp-streambus":
		return TransportStreamBus
	case string(TransportAuto):
		return TransportAuto
	default:
		return ""
	}
}
F
function

streamBusEnabled

Returns

bool
host/transport.go:53-56
func streamBusEnabled() bool

{
	mode := ResolveTransport()
	return mode == TransportStreamBus || mode == TransportAuto
}
F
function

writeFrame

Parameters

payload
[]byte

Returns

error
host/framing.go:10-17
func writeFrame(w io.Writer, payload []byte) error

{
	var prefix [binary.MaxVarintLen64]byte
	n := binary.PutUvarint(prefix[:], uint64(len(payload)))
	if err := writeAll(w, prefix[:n]); err != nil {
		return err
	}
	return writeAll(w, payload)
}
F
function

readFrame

Parameters

maximum
int

Returns

[]byte
error
host/framing.go:19-35
func readFrame(r *bufio.Reader, maximum int) ([]byte, error)

{
	size, err := binary.ReadUvarint(r)
	if err != nil {
		return nil, err
	}
	if maximum > 0 && size > uint64(maximum) {
		return nil, fmt.Errorf("host: frame length %d exceeds %d", size, maximum)
	}
	if size > uint64(^uint(0)>>1) {
		return nil, fmt.Errorf("host: frame length %d exceeds platform limit", size)
	}
	payload := make([]byte, size)
	if _, err := io.ReadFull(r, payload); err != nil {
		return nil, err
	}
	return payload, nil
}
F
function

writeAll

Parameters

payload
[]byte

Returns

error
host/framing.go:37-49
func writeAll(w io.Writer, payload []byte) error

{
	for len(payload) > 0 {
		n, err := w.Write(payload)
		if err != nil {
			return err
		}
		if n <= 0 {
			return io.ErrShortWrite
		}
		payload = payload[n:]
	}
	return nil
}
F
function

logLevel

Returns

host/logging.go:11-22
func logLevel() slog.Level

{
	switch strings.ToLower(os.Getenv("RFW_LOG_LEVEL")) {
	case "debug":
		return slog.LevelDebug
	case "warn":
		return slog.LevelWarn
	case "error":
		return slog.LevelError
	default:
		return slog.LevelInfo
	}
}
F
function

openProtocolSocket

Parameters

opts
...MuxOption

Returns

func()
host/ssc_protocol_test.go:17-30
func openProtocolSocket(t *testing.T, opts ...MuxOption) (*websocket.Conn, func())

{
	t.Helper()
	server := httptest.NewServer(NewMux(t.TempDir(), opts...))
	wsURL := "ws" + strings.TrimPrefix(server.URL, "http") + "/ws"
	socket, err := websocket.Dial(wsURL, "", server.URL)
	if err != nil {
		server.Close()
		t.Fatalf("dial websocket: %v", err)
	}
	return socket, func() {
		closeTestResource(t, socket)
		server.Close()
	}
}
F
function

sendProtocolMessage

Parameters

socket
message
host/ssc_protocol_test.go:32-41
func sendProtocolMessage(t *testing.T, socket *websocket.Conn, message Inbound)

{
	t.Helper()
	data, err := json.Marshal(message)
	if err != nil {
		t.Fatalf("marshal message: %v", err)
	}
	if err := websocket.Message.Send(socket, data); err != nil {
		t.Fatalf("send message: %v", err)
	}
}
F
function

receiveProtocolMessage

Parameters

Returns

host/ssc_protocol_test.go:43-54
func receiveProtocolMessage(t *testing.T, socket *websocket.Conn) Outbound

{
	t.Helper()
	var data []byte
	if err := websocket.Message.Receive(socket, &data); err != nil {
		t.Fatalf("receive message: %v", err)
	}
	var message Outbound
	if err := json.Unmarshal(data, &message); err != nil {
		t.Fatalf("decode message: %v", err)
	}
	return message
}
F
function

TestWSTypedActionRejectsUnknownFields

Parameters

host/ssc_protocol_test.go:56-79
func TestWSTypedActionRejectsUnknownFields(t *testing.T)

{
	type request struct {
		Value int `json:"value"`
	}
	const action = "test.ws.strict"
	if err := RegisterAction(action, func(_ context.Context, _ *Session, request request) (request, error) {
		return request, nil
	}); err != nil {
		t.Fatalf("register action: %v", err)
	}
	socket, closeSocket := openProtocolSocket(t)
	defer closeSocket()

	sendProtocolMessage(t, socket, Inbound{
		Action:   action,
		ID:       "strict",
		Sequence: 1,
		Payload:  map[string]any{"value": 1, "admin": true},
	})
	response := receiveProtocolMessage(t, socket)
	if response.ID != "strict" || response.Error == nil || response.Error.Code != "invalid_request" {
		t.Fatalf("unexpected strict response: %#v", response)
	}
}
F
function

TestWSMessageAuthorizationRunsBeforeAction

Parameters

host/ssc_protocol_test.go:81-106
func TestWSMessageAuthorizationRunsBeforeAction(t *testing.T)

{
	type request struct {
		Value int `json:"value"`
	}
	const action = "test.ws.authorized"
	called := false
	if err := RegisterAction(action, func(_ context.Context, _ *Session, request request) (request, error) {
		called = true
		return request, nil
	}); err != nil {
		t.Fatalf("register action: %v", err)
	}
	socket, closeSocket := openProtocolSocket(t, WithSSCAuthorizer(func(_ context.Context, _ *Session, message Inbound) error {
		if message.Action == action {
			return errors.New("denied")
		}
		return nil
	}))
	defer closeSocket()

	sendProtocolMessage(t, socket, Inbound{Action: action, ID: "denied", Sequence: 1})
	response := receiveProtocolMessage(t, socket)
	if response.Error == nil || response.Error.Code != "forbidden" || called {
		t.Fatalf("authorization failed closed incorrectly: response=%#v called=%v", response, called)
	}
}
F
function

TestWSRateLimitRejectsExcessMessages

Parameters

host/ssc_protocol_test.go:108-131
func TestWSRateLimitRejectsExcessMessages(t *testing.T)

{
	type request struct{}
	const action = "test.ws.rate"
	if err := RegisterAction(action, func(_ context.Context, _ *Session, _ request) (string, error) {
		return "ok", nil
	}); err != nil {
		t.Fatalf("register action: %v", err)
	}
	socket, closeSocket := openProtocolSocket(t, WithSSCLimits(SSCLimits{MessagesPerMinute: 1}))
	defer closeSocket()

	sendProtocolMessage(t, socket, Inbound{Action: action, ID: "first", Sequence: 1})
	if response := receiveProtocolMessage(t, socket); response.Error != nil {
		t.Fatalf("first message rejected: %#v", response)
	}
	sendProtocolMessage(t, socket, Inbound{Action: action, ID: "second", Sequence: 2})
	response := receiveProtocolMessage(t, socket)
	if response.Error == nil || response.Error.Code != "rate_limited" {
		t.Fatalf("rate limit did not reject second message: %#v", response)
	}
	if response.Ack != 2 {
		t.Fatalf("rate-limited message was not acknowledged: %#v", response)
	}
}
F
function

TestWSActionTimeout

Parameters

host/ssc_protocol_test.go:133-150
func TestWSActionTimeout(t *testing.T)

{
	type request struct{}
	const action = "test.ws.timeout"
	if err := RegisterAction(action, func(_ context.Context, _ *Session, _ request) (string, error) {
		time.Sleep(50 * time.Millisecond)
		return "late", nil
	}); err != nil {
		t.Fatalf("register action: %v", err)
	}
	socket, closeSocket := openProtocolSocket(t, WithSSCLimits(SSCLimits{HandlerTimeout: 5 * time.Millisecond}))
	defer closeSocket()

	sendProtocolMessage(t, socket, Inbound{Action: action, ID: "timeout", Sequence: 1})
	response := receiveProtocolMessage(t, socket)
	if response.Error == nil || response.Error.Code != "action_timeout" {
		t.Fatalf("slow action did not time out: %#v", response)
	}
}
F
function

TestWSActionPanicReturnsPublicError

Parameters

host/ssc_protocol_test.go:152-168
func TestWSActionPanicReturnsPublicError(t *testing.T)

{
	type request struct{}
	const action = "test.ws.panic"
	if err := RegisterAction(action, func(_ context.Context, _ *Session, _ request) (string, error) {
		panic("private handler detail")
	}); err != nil {
		t.Fatalf("register action: %v", err)
	}
	socket, closeSocket := openProtocolSocket(t)
	defer closeSocket()

	sendProtocolMessage(t, socket, Inbound{Action: action, ID: "panic", Sequence: 1})
	response := receiveProtocolMessage(t, socket)
	if response.Error == nil || response.Error.Code != "action_failed" || response.Error.Message != "action failed" {
		t.Fatalf("panic details crossed the protocol: %#v", response)
	}
}
F
function

TestWSRejectsOversizedFrame

Parameters

host/ssc_protocol_test.go:170-189
func TestWSRejectsOversizedFrame(t *testing.T)

{
	socket, closeSocket := openProtocolSocket(t, WithSSCLimits(SSCLimits{MaxMessageBytes: 128}))
	defer closeSocket()

	data, err := json.Marshal(Inbound{
		Component: "oversized",
		Sequence:  1,
		Payload:   map[string]any{"value": strings.Repeat("x", 512)},
	})
	if err != nil {
		t.Fatalf("marshal oversized message: %v", err)
	}
	if err := websocket.Message.Send(socket, data); err != nil {
		t.Fatalf("send oversized message: %v", err)
	}
	var response []byte
	if err := websocket.Message.Receive(socket, &response); err == nil {
		t.Fatalf("oversized frame was accepted: %s", response)
	}
}
F
function

TestWSSessionResumeReplaysUnacknowledgedResponse

Parameters

host/ssc_protocol_test.go:191-276
func TestWSSessionResumeReplaysUnacknowledgedResponse(t *testing.T)

{
	type request struct{}
	type response struct {
		Count int `json:"count"`
	}
	const action = "test.ws.resume"
	if err := RegisterAction(action, func(_ context.Context, session *Session, _ request) (response, error) {
		count := 0
		if stored, ok := session.ContextGet("count"); ok {
			count = stored.(int)
		}
		count++
		session.ContextSet("count", count)
		return response{Count: count}, nil
	}); err != nil {
		t.Fatalf("register action: %v", err)
	}

	sessionMu.RLock()
	maxSessions := len(sessions) + 1
	sessionMu.RUnlock()
	server := httptest.NewServer(NewMux(t.TempDir(), WithSSCLimits(SSCLimits{
		ResumeTTL:      time.Second,
		ReplayMessages: 8,
		MaxSessions:    maxSessions,
	})))
	defer server.Close()
	wsURL := "ws" + strings.TrimPrefix(server.URL, "http") + "/ws"
	dial := func() *websocket.Conn {
		socket, err := websocket.Dial(wsURL, "", server.URL)
		if err != nil {
			t.Fatalf("dial websocket: %v", err)
		}
		return socket
	}

	firstSocket := dial()
	sendProtocolMessage(t, firstSocket, Inbound{Action: action, ID: "one", Sequence: 1})
	first := receiveProtocolMessage(t, firstSocket)
	sendProtocolMessage(t, firstSocket, Inbound{Action: action, ID: "two", Sequence: 2, Ack: first.Sequence})
	second := receiveProtocolMessage(t, firstSocket)
	token := second.ResumeToken
	sessionID := second.Session
	closeTestResource(t, firstSocket)

	var (
		secondSocket *websocket.Conn
		replayed     Outbound
		current      Outbound
	)
	deadline := time.Now().Add(time.Second)
	for {
		secondSocket = dial()
		sendProtocolMessage(t, secondSocket, Inbound{
			Action:      action,
			ID:          "three",
			Sequence:    3,
			Ack:         first.Sequence,
			ResumeToken: token,
		})
		replayed = receiveProtocolMessage(t, secondSocket)
		if replayed.Session == sessionID {
			current = receiveProtocolMessage(t, secondSocket)
			break
		}
		closeTestResource(t, secondSocket)
		if time.Now().After(deadline) {
			t.Fatalf("session did not become resumable: %#v", replayed)
		}
		time.Sleep(10 * time.Millisecond)
	}
	defer closeTestResource(t, secondSocket)
	if replayed.Sequence != second.Sequence || replayed.ID != "two" {
		t.Fatalf("unexpected replay: %#v", replayed)
	}
	if current.Session != sessionID || current.ID != "three" {
		t.Fatalf("session did not resume: %#v", current)
	}
	payload, ok := current.Payload.(map[string]any)
	if !ok || payload["count"] != float64(3) {
		t.Fatalf("session state was not retained: %#v", current.Payload)
	}
	if session, ok := SessionByID(sessionID); ok {
		ReleaseSession(session)
	}
}
F
function

TestWSSessionResumeExpires

Parameters

host/ssc_protocol_test.go:278-315
func TestWSSessionResumeExpires(t *testing.T)

{
	type request struct{}
	const action = "test.ws.expiry"
	if err := RegisterAction(action, func(_ context.Context, _ *Session, _ request) (string, error) {
		return "ok", nil
	}); err != nil {
		t.Fatalf("register action: %v", err)
	}
	server := httptest.NewServer(NewMux(t.TempDir(), WithSSCLimits(SSCLimits{ResumeTTL: 10 * time.Millisecond})))
	defer server.Close()
	wsURL := "ws" + strings.TrimPrefix(server.URL, "http") + "/ws"

	first, err := websocket.Dial(wsURL, "", server.URL)
	if err != nil {
		t.Fatalf("dial first socket: %v", err)
	}
	sendProtocolMessage(t, first, Inbound{Action: action, ID: "first", Sequence: 1})
	response := receiveProtocolMessage(t, first)
	token := response.ResumeToken
	closeTestResource(t, first)
	time.Sleep(40 * time.Millisecond)

	second, err := websocket.Dial(wsURL, "", server.URL)
	if err != nil {
		t.Fatalf("dial second socket: %v", err)
	}
	defer closeTestResource(t, second)
	sendProtocolMessage(t, second, Inbound{
		Action:      action,
		ID:          "second",
		Sequence:    2,
		ResumeToken: token,
	})
	rejected := receiveProtocolMessage(t, second)
	if rejected.Control != "resume_rejected" || rejected.Error == nil {
		t.Fatalf("expired session resumed: %#v", rejected)
	}
}
F
function

TestWSResumeAuthorizationRejectsForeignToken

A detached session is reattached only for a request the resume authorizer
approves. The refused client is served a new session and the retained one
stays available to the identity that created it.

Parameters

host/ssc_protocol_test.go:320-411
func TestWSResumeAuthorizationRejectsForeignToken(t *testing.T)

{
	type request struct{}
	const action = "test.ws.resume.identity"
	if err := RegisterAction(action, func(_ context.Context, session *Session, _ request) (string, error) {
		user, _ := session.ContextGet("user")
		text, _ := user.(string)
		return text, nil
	}); err != nil {
		t.Fatalf("register action: %v", err)
	}

	server := httptest.NewServer(NewMux(t.TempDir(),
		WithSSCLimits(SSCLimits{ResumeTTL: 5 * time.Second, ReplayMessages: 8}),
		WithSSCSessionInitializer(bindResumeIdentity),
		WithSSCResumeAuthorizer(authorizeSameUserResume),
	))
	defer server.Close()
	wsURL := "ws" + strings.TrimPrefix(server.URL, "http") + "/ws"
	dial := func(user string) *websocket.Conn {
		config, err := websocket.NewConfig(wsURL, server.URL)
		if err != nil {
			t.Fatalf("websocket config: %v", err)
		}
		config.Header.Set(resumeIdentityHeader, user)
		socket, err := websocket.DialConfig(config)
		if err != nil {
			t.Fatalf("dial websocket: %v", err)
		}
		return socket
	}

	ownerSocket := dial("alice")
	sendProtocolMessage(t, ownerSocket, Inbound{Action: action, ID: "alice", Sequence: 1})
	owner := receiveProtocolMessage(t, ownerSocket)
	if owner.Payload != "alice" || owner.ResumeToken == "" {
		t.Fatalf("session identity was not bound: %#v", owner)
	}
	closeTestResource(t, ownerSocket)

	intruderSocket := dial("mallory")
	defer closeTestResource(t, intruderSocket)
	sendProtocolMessage(t, intruderSocket, Inbound{
		Action:      action,
		ID:          "mallory",
		Sequence:    1,
		Ack:         owner.Sequence,
		ResumeToken: owner.ResumeToken,
	})
	rejected := receiveProtocolMessage(t, intruderSocket)
	if rejected.Control != "resume_rejected" || rejected.Error == nil {
		t.Fatalf("foreign token was not refused: %#v", rejected)
	}
	if rejected.Session == owner.Session {
		t.Fatalf("refused client attached the retained session: %#v", rejected)
	}
	response := receiveProtocolMessage(t, intruderSocket)
	if response.Payload != "mallory" || response.Session == owner.Session {
		t.Fatalf("refused client did not get its own session: %#v", response)
	}

	var (
		resumedSocket *websocket.Conn
		restored      Outbound
	)
	deadline := time.Now().Add(2 * time.Second)
	for {
		resumedSocket = dial("alice")
		sendProtocolMessage(t, resumedSocket, Inbound{
			Action:      action,
			ID:          "resume",
			Sequence:    2,
			Ack:         owner.Sequence,
			ResumeToken: owner.ResumeToken,
		})
		restored = receiveProtocolMessage(t, resumedSocket)
		if restored.Session == owner.Session {
			break
		}
		closeTestResource(t, resumedSocket)
		if time.Now().After(deadline) {
			t.Fatalf("owner could not resume after the refused attempt: %#v", restored)
		}
		time.Sleep(10 * time.Millisecond)
	}
	defer closeTestResource(t, resumedSocket)
	if restored.ID != "resume" || restored.Payload != "alice" {
		t.Fatalf("resumed session lost its identity: %#v", restored)
	}
	if session, ok := SessionByID(owner.Session); ok {
		ReleaseSession(session)
	}
}
F
function

TestWSControlPingIsAnsweredOutOfBand

Parameters

host/ssc_protocol_test.go:413-431
func TestWSControlPingIsAnsweredOutOfBand(t *testing.T)

{
	socket, closeSocket := openProtocolSocket(t)
	defer closeSocket()

	sendProtocolMessage(t, socket, Inbound{Control: "ping"})
	pong := receiveProtocolMessage(t, socket)
	if pong.Control != "pong" {
		t.Fatalf("unexpected control response: %#v", pong)
	}
	if pong.Sequence != 0 || pong.Session != "" {
		t.Fatalf("pong carried delivery metadata: %#v", pong)
	}

	sendProtocolMessage(t, socket, Inbound{Component: "unregistered", Sequence: 1})
	next := receiveProtocolMessage(t, socket)
	if next.Sequence != 1 {
		t.Fatalf("pong consumed an outbound sequence: %#v", next)
	}
}
S
struct

wasmEncoding

wasmEncoding pairs a content coding with the suffix of the artifact that
carries it. The order is the server’s preference, best first.

host/wasm_negotiation.go:14-17
type wasmEncoding struct

Fields

Name Type Description
name string
ext string
F
function

negotiateWasm

negotiateWasm answers a request for the raw bundle with the best encoding
the client accepts and the build actually produced. Serving the compressed
artifact under the raw URL is what lets one URL work for every client: the
browser advertises what it can decode, and no application configuration has
to guess.

It reports whether it wrote the response.

Parameters

dir
urlPath
string

Returns

bool
host/wasm_negotiation.go:31-73
func negotiateWasm(w http.ResponseWriter, r *http.Request, dir http.Dir, urlPath string) bool

{
	if !strings.HasSuffix(urlPath, ".wasm") {
		return false
	}
	accepted := acceptedEncodings(r.Header.Get("Accept-Encoding"))
	if len(accepted) == 0 {
		return false
	}
	encodings := append([]wasmEncoding(nil), wasmEncodings...)
	sort.SliceStable(encodings, func(i, j int) bool {
		return encodingQuality(accepted, encodings[i].name) > encodingQuality(accepted, encodings[j].name)
	})
	for _, encoding := range encodings {
		if encodingQuality(accepted, encoding.name) <= 0 {
			continue
		}
		file, size, ok := openArtifact(dir, urlPath+encoding.ext)
		if !ok {
			continue
		}
		defer func() {
			_ = file.Close()
		}()
		header := w.Header()
		header.Set("Content-Type", "application/wasm")
		header.Set("Content-Encoding", encoding.name)
		// The length of what actually goes on the wire. A client that measures
		// progress against it is measuring compressed bytes, which is why the
		// loader treats an encoded response as indeterminate.
		header.Set("Content-Length", strconv.FormatInt(size, 10))
		addVaryAcceptEncoding(header)
		setWasmCacheControl(header, r.URL.Query().Get("v") != "")
		if r.Method == http.MethodHead {
			w.WriteHeader(http.StatusOK)
			return true
		}
		if _, err := io.Copy(w, file); err != nil {
			logger.Warn("wasm negotiation write failed", "path", urlPath, "error", err)
		}
		return true
	}
	return false
}
F
function

openArtifact

openArtifact opens a compressed bundle and reports its size on disk.

Parameters

dir
name
string

Returns

int64
bool
host/wasm_negotiation.go:76-87
func openArtifact(dir http.Dir, name string) (http.File, int64, bool)

{
	file, err := dir.Open(filepath.ToSlash(name))
	if err != nil {
		return nil, 0, false
	}
	info, err := file.Stat()
	if err != nil || info.IsDir() {
		_ = file.Close()
		return nil, 0, false
	}
	return file, info.Size(), true
}
F
function

acceptedEncodings

acceptedEncodings parses an Accept-Encoding header into coding qualities.
Invalid q values reject that coding rather than accidentally preferring it.

Parameters

header
string

Returns

map[string]float64
host/wasm_negotiation.go:91-113
func acceptedEncodings(header string) map[string]float64

{
	if strings.TrimSpace(header) == "" {
		return nil
	}
	accepted := map[string]float64{}
	for _, part := range strings.Split(header, ",") {
		name, params, _ := strings.Cut(strings.TrimSpace(part), ";")
		name = strings.ToLower(strings.TrimSpace(name))
		if name == "" {
			continue
		}
		quality := 1.0
		if parsed, present, valid := parseQuality(params); present {
			if !valid {
				quality = 0
			} else {
				quality = parsed
			}
		}
		accepted[name] = quality
	}
	return accepted
}
F
function

encodingQuality

Parameters

accepted
map[string]float64
name
string

Returns

float64
host/wasm_negotiation.go:115-120
func encodingQuality(accepted map[string]float64, name string) float64

{
	if quality, ok := accepted[name]; ok {
		return quality
	}
	return accepted["*"]
}
F
function

parseQuality

parseQuality reads the q value out of an Accept-Encoding parameter list.

Parameters

params
string

Returns

quality
float64
present
bool
valid
bool
host/wasm_negotiation.go:123-136
func parseQuality(params string) (quality float64, present bool, valid bool)

{
	for _, param := range strings.Split(params, ";") {
		key, value, found := strings.Cut(strings.TrimSpace(param), "=")
		if !found || !strings.EqualFold(strings.TrimSpace(key), "q") {
			continue
		}
		quality, err := strconv.ParseFloat(strings.TrimSpace(value), 64)
		if err != nil || quality < 0 || quality > 1 {
			return 0, true, false
		}
		return quality, true, true
	}
	return 0, false, false
}
F
function

addVaryAcceptEncoding

addVaryAcceptEncoding records that the response body depends on the request
encoding, so a shared cache cannot hand a brotli body to a client that
cannot decode it.

Parameters

header
host/wasm_negotiation.go:141-149
func addVaryAcceptEncoding(header http.Header)

{
	vary := header.Get("Vary")
	switch {
	case vary == "":
		header.Set("Vary", "Accept-Encoding")
	case !strings.Contains(vary, "Accept-Encoding"):
		header.Set("Vary", vary+", Accept-Encoding")
	}
}
F
function

revalidates

revalidates reports whether a document has to be checked with the server on
every load. Both entries here carry pointers to versioned assets:
index.html holds the stamped bootstrap script tags and rfw_config.js holds
the wasm version. Caching either one lets a browser keep following an old
pointer, so a release could never replace the loader or the bundle.

Parameters

path
string

Returns

bool
host/wasm_negotiation.go:156-163
func revalidates(path string) bool

{
	switch strings.Trim(path, "/") {
	case "index.html", "rfw_config.js":
		return true
	default:
		return false
	}
}
F
function

setWasmCacheControl

setWasmCacheControl applies the caching policy for a bundle. A URL that
carries a content version is immutable; one that does not has to be
revalidated or a release cannot replace it.

Parameters

header
versioned
bool
host/wasm_negotiation.go:168-178
func setWasmCacheControl(header http.Header, versioned bool)

{
	if devMode() {
		header.Set("Cache-Control", "no-store")
		return
	}
	if versioned {
		header.Set("Cache-Control", "public, max-age=31536000, immutable")
		return
	}
	header.Set("Cache-Control", "no-cache")
}
F
function

artifactEncoding

artifactEncoding reports the coding a directly requested artifact carries.

Parameters

path
string

Returns

string
bool
host/wasm_negotiation.go:181-191
func artifactEncoding(path string) (string, bool)

{
	if !strings.Contains(path, ".wasm") {
		return "", false
	}
	for _, encoding := range wasmEncodings {
		if strings.HasSuffix(path, ".wasm"+encoding.ext) {
			return encoding.name, true
		}
	}
	return "", false
}
F
function

wasmRequest

wasmRequest reports whether path addresses a bundle in any encoding.

Parameters

path
string

Returns

bool
host/wasm_negotiation.go:194-200
func wasmRequest(path string) bool

{
	if strings.HasSuffix(path, ".wasm") {
		return true
	}
	_, ok := artifactEncoding(path)
	return ok
}
F
function

closeTestResource

Parameters

resource
host/close_test.go:8-13
func closeTestResource(t *testing.T, resource io.Closer)

{
	t.Helper()
	if err := resource.Close(); err != nil {
		t.Errorf("close resource: %v", err)
	}
}
F
function

validTagName

Parameters

tag
string

Returns

bool
host/html.go:12-23
func validTagName(tag string) bool

{
	if tag == "" || !isASCIILetter(rune(tag[0])) {
		return false
	}
	for _, char := range tag {
		if isASCIILetter(char) || char >= '0' && char <= '9' || char == '-' {
			continue
		}
		return false
	}
	return true
}
F
function

isASCIILetter

Parameters

char
rune

Returns

bool
host/html.go:25-27
func isASCIILetter(char rune) bool

{
	return char >= 'a' && char <= 'z' || char >= 'A' && char <= 'Z'
}
F
function

hostVarTag

hostVarTag builds a host variable element. The value is HTML-escaped so
user-derived data cannot inject markup through the initial snapshot.

Parameters

tag
string
name
string
value
any
escape
bool

Returns

string
host/html.go:31-44
func hostVarTag(tag, name string, value any, escape bool) string

{
	if !validTagName(tag) {
		return ""
	}
	v := fmt.Sprintf("%v", value)
	body := v
	expected := ""
	if escape {
		body = html.EscapeString(v)
		expected = html.EscapeString(v)
	}
	return fmt.Sprintf(`<%s %s="%s" %s="%s">%s</%s>`,
		tag, hostVarAttr, html.EscapeString(name), hostExpectedAttr, expected, body, tag)
}
F
function

Span

Span renders an escaped host variable in a span.

Parameters

name
string
value
any

Returns

string
host/html.go:47-49
func Span(name string, value any) string

{
	return hostVarTag("span", name, value, true)
}
F
function

Div

Div renders an escaped host variable in a div.

Parameters

name
string
value
any

Returns

string
host/html.go:52-54
func Div(name string, value any) string

{
	return hostVarTag("div", name, value, true)
}
F
function

P

P renders an escaped host variable in a paragraph.

Parameters

name
string
value
any

Returns

string
host/html.go:57-59
func P(name string, value any) string

{
	return hostVarTag("p", name, value, true)
}
F
function

Tag

Tag renders an escaped host variable with tag.

Parameters

tag
string
name
string
value
any

Returns

string
host/html.go:62-64
func Tag(tag, name string, value any) string

{
	return hostVarTag(tag, name, value, true)
}
F
function

RawTag

RawTag builds a host variable element without escaping the value. It is the
explicit trust API for markup values: only pass HTML you generated or
sanitized yourself, never user-derived data.

Parameters

tag
string
name
string
value
any

Returns

string
host/html.go:69-71
func RawTag(tag, name string, value any) string

{
	return hostVarTag(tag, name, value, false)
}
F
function

Raw

Raw marks a fragment as trusted HTML and returns it unchanged. It exists to
make raw injection points explicit at call sites: anything passed through
Raw ends up in the client DOM unescaped via InitSnapshot.HTML.

Parameters

html
string

Returns

string
host/html.go:76-78
func Raw(html string) string

{
	return html
}
F
function

Join

Join concatenates rendered host fragments.

Parameters

parts
...string

Returns

string
host/html.go:81-87
func Join(parts ...string) string

{
	var b strings.Builder
	for _, p := range parts {
		b.WriteString(p)
	}
	return b.String()
}