addons/realtime registers FeatureRealtime for browser-facing presentation
event fanout. Current support covers delivery of
contracts.PresentationEvent envelopes to browser clients, compiler metadata
for g:subscribe query regions, generated SSE fanout for bound subscriptions,
bounded generated client patches for query-owned regions, and explicit
domain-event to query invalidation refresh.
Use it with contract web adapters when commands emit presentation events:
import (
"github.com/cssbruno/gowdk"
"github.com/cssbruno/gowdk/addons/contracts"
"github.com/cssbruno/gowdk/addons/realtime"
)
var Config = gowdk.Config{
Addons: []gowdk.Addon{
contracts.Addon(),
realtime.Addon(),
},
}
gowdk add realtime inserts the same config entry.
.gwdk Subscriptions
Use g:subscribe on the same element as g:query to declare that a query-owned
region should listen for a presentation event:
view {
<section
g:query="patients.GetPatientPage"
g:subscribe="patients.PatientNotice">
<h1>Patients</h1>
</section>
}
Current behavior:
- Requires
realtime.Addon()ingowdk.config.go. - Adds a subscription record to
internal/gwdkir.Program.RealtimeSubscriptions. - Validates the event reference against scanned
runtime/contractspresentation-event registrations. - Rejects unknown events, invalid event registrations, domain/integration event
categories, and registrations that are not available to the
webrole. - Renders
data-gowdk-query,data-gowdk-subscribe, and validateddata-gowdk-subscribe-typemarkers for compiler-owned runtime hookup. - Adds
realtime_subscriptionevents togowdk-build-report.jsonwhen present. - Generated apps with bound subscriptions mount a dependency-free SSE stream at
/_gowdk/realtime/events. - Generated command adapters dispatch command-emitted presentation events to a subscription-filtered fanout, so only explicitly subscribed event types are streamed.
- Generated stream handlers run inherited subscribed-page guards before opening
an SSE response. They choose page guards from
?path=...or the request referer path when available; if no page path can be identified, guarded subscriptions fail closed by requiring the union of subscribed guard IDs. - Generated
gowdk.jsconnects pages with subscribed regions to the SSE stream and applies explicitreplaceHTMLpatches to matching query-owned elements. - Generated apps expose
RealtimeEventsPathandRegisterRealtimeFanout(realtime.PresentationFanout)for app-owned server setup.
Current limits:
- Subscriptions must be query-bounded;
g:subscribewithoutg:queryis rejected. - Only explicit
replaceHTMLpatches are supported in the generated client runtime; richer patch shapes are deferred. - Custom retry/backoff/replay, active server-side session-change stream revocation, richer patch shapes, and route-specific refresh endpoints remain follow-up work.
Query Invalidations
Use contracts.RegisterInvalidation[event, query] in Go when a backend-owned
domain event should refresh query-owned regions:
func Register(registry *contracts.Registry) {
contracts.RegisterQuery[GetPatientPage, PatientPageData](registry, LoadPatientPage, contracts.RoleWeb)
contracts.RegisterCommand[CreatePatient, CreatePatientResult](registry, HandleCreatePatient, contracts.RoleWeb)
contracts.RegisterDomainEvent[PatientCreated](registry, SendWelcomeEmail, contracts.RoleWorker)
contracts.RegisterInvalidation[PatientCreated, GetPatientPage](registry)
}
Current behavior:
- Requires
realtime.Addon()when the invalidated query is bound by.gwdk. - Scans invalidation edges beside normal contract registrations.
- Rejects edges that name an unknown query, an unknown domain event, or a domain event no scanned command emits.
- Joins edges with bound
g:queryreferences intoProgram.QueryInvalidations. - Adds
query_invalidationevents togowdk-build-report.json. - Prints invalidation edges in
gowdk graph. - Renders validated
data-gowdk-query-typemarkers. - Generated command adapters send a
gowdk.query.invalidatepresentation event after successful command event dispatch when captured domain events invalidate bound queries. - Generated
gowdk.jsrefetches the current document and replaces matching non-subscribed query regions. Regions withg:subscribeare left to explicit presentation patches so a document refetch does not overwrite a patch.
The generated invalidation event value is:
{
"queries": ["github.com/acme/clinic/patients.GetPatientPage"],
"events": ["domain:github.com/acme/clinic/patients.PatientCreated"]
}
Live Example
examples/contracts/patients.page.gwdk demonstrates the current live-update
contract:
.gwdkownsg:query="patients.GetPatientPage"andg:subscribe="patients.PatientNotice"on the live region, plus a non-subscribed query region refreshed through invalidation.- User Go owns the command, query, presentation-event registration, and the
server-generated
replaceHTMLpatch payload. It also registersRegisterInvalidation[PatientCreated, GetPatientPage]. - Generated Go owns the command/query web adapters, subscription-filtered SSE stream, invalidation presentation events, inherited guard checks, and command event sink composition.
- Generated
gowdk.jsowns the EventSource connection and applies the patch to the subscribed query region or refetches invalidated query regions.
Build and run it:
go run ./cmd/gowdk build --config examples/contracts/gowdk.config.go --out /tmp/gowdk-contracts-build --app /tmp/gowdk-contracts-app --bin /tmp/gowdk-contracts-site examples/contracts/patients.page.gwdk
/tmp/gowdk-contracts-site
Open http://127.0.0.1:8080/contracts/patients. With JavaScript enabled, the
generated runtime opens /_gowdk/realtime/events; submitting the form runs the
Go command, emits patients.PatientNotice, and replaces the subscribed status
region with the patch HTML from user Go. The same command emits
patients.PatientCreated, which triggers a generated query invalidation event
for the non-subscribed query region. Without JavaScript, the page still renders
the static query regions and the form posts to the generated command endpoint.
Useful smoke checks:
test -f /tmp/gowdk-contracts-build/assets/gowdk/gowdk.js
grep -F '"kind": "realtime_subscription"' /tmp/gowdk-contracts-build/gowdk-build-report.json
grep -F '"kind": "query_invalidation"' /tmp/gowdk-contracts-build/gowdk-build-report.json
grep -F '"event": "patients.PatientNotice"' /tmp/gowdk-contracts-build/gowdk-build-report.json
grep -F 'data-gowdk-subscribe-type=' /tmp/gowdk-contracts-build/contracts/patients/index.html
grep -F 'data-gowdk-query-type=' /tmp/gowdk-contracts-build/contracts/patients/index.html
go test ./examples/contracts/patients
Client Patch Payloads
Generated clients consume gowdk-presentation SSE messages whose envelope
Value contains either one patch object or a patches array:
{
"patch": {
"op": "replaceHTML",
"html": "<p>Updated</p>",
"swap": "innerHTML"
}
}
Supported patch fields:
op: must bereplaceHTML.html: replacement HTML string.swap: optional,innerHTMLby default;outerHTMLis also accepted.
The browser runtime applies patches only to regions whose validated
data-gowdk-subscribe-type matches the presentation event type. Unsupported
patch operations, missing HTML, malformed payloads, and unsupported swaps emit
gowdk:realtime-error and leave the DOM unchanged.
Stream Failure And Backpressure
Generated streams are ordinary same-origin SSE responses. Before the response is opened, generated code runs the guard IDs inherited by subscribed page regions. Guard failures return the existing no-store guard failure response instead of an SSE stream, so browsers do not receive protected events after access is denied.
The dependency-free SSE adapter sends retry: 1000, so browser EventSource
clients use a one-second reconnect delay for ordinary transport failures. GOWDK
does not add custom browser retry state in this slice.
Each SSE client has a bounded queue (16 messages by default, configurable
with realtime.WithSSEBufferSize). When a client queue is full, new events for
that client are dropped instead of blocking command execution or other clients.
Use a broker/outbox/replay path for applications that require guaranteed
delivery after disconnects or slow clients.
Active server-side session changes are enforced on the next stream open or reconnect. Immediate revocation of already-open streams remains app-owned or future GOWDK runtime work.
Transport Choice
Generated apps use SSE first when the browser only needs server-to-client presentation events. SSE is dependency-free in the root module, uses normal HTTP, and works well for notifications, progress updates, and invalidation signals.
For app-owned routes or custom setup outside generated subscriptions:
import (
"net/http"
"github.com/cssbruno/gowdk/addons/realtime"
"github.com/cssbruno/gowdk/runtime/contracts"
)
hub := realtime.NewSSE(realtime.WithSSEBufferSize(32))
http.Handle("/gowdk/events", hub)
gowdkapp.RegisterContractEventSink(
contracts.PresentationFanoutCommandEventSink(hub),
)
The browser receives event: gowdk-presentation messages whose data field is
a JSON contracts.EventEnvelope. Domain and integration events are filtered
out before delivery.
Use WebSocket when the app already needs a persistent bidirectional socket, a WebSocket-specific deployment path, or protocol-level client messages. The current GOWDK WebSocket adapter only fans presentation events out to connected clients; inbound WebSocket commands remain app-owned Go.
WebSocket support is a nested module so github.com/coder/websocket does not
enter the root module graph:
go get github.com/cssbruno/gowdk/runtime/contracts/websocketfanout
import (
"net/http"
"github.com/coder/websocket"
"github.com/cssbruno/gowdk/runtime/contracts"
"github.com/cssbruno/gowdk/runtime/contracts/websocketfanout"
)
hub := websocketfanout.New(websocketfanout.WithAcceptOptions(websocket.AcceptOptions{
OriginPatterns: []string{"https://example.com"},
}))
http.Handle("/gowdk/events/ws", hub)
gowdkapp.RegisterContractEventSink(
contracts.PresentationFanoutCommandEventSink(hub),
)
Each presentation event is written as one text JSON contracts.EventEnvelope.
Deployment Notes
- In-process SSE and WebSocket hubs only know about clients connected to the same process. Multi-instance deployments should pair fanout with a broker, outbox, or external pub/sub path when all clients must see the same event.
- SSE responses set
X-Accel-Buffering: no; reverse proxies may still need explicit buffering and timeout settings for long-lived streams. - WebSocket deployments should set origin checks and proxy upgrade headers.
- Presentation events are untrusted browser output. Do not treat client messages or presentation-event names as authorization.
Verification
go run ./cmd/gowdk add --list
go run ./cmd/gowdk add --list --registry
go test ./runtime/contracts/sse
go test ./internal/appgen -run 'TestGenerateGuardsRealtimeStreamForSubscribedPages|TestGeneratedBinaryRealtimeStreamGuardDenialClosesStream'
(cd runtime/contracts/websocketfanout && go test ./...)