feat(realtime): wire query-bound live subscriptions#440
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9fc770501e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| Init: define([]ast.Expr{id("_"), id("ok")}, call(sel("gowdkroute", "Match"), stringLit(route.Route), id("requestPath"))), | ||
| Cond: id("ok"), | ||
| Body: block(&ast.ReturnStmt{Results: []ast.Expr{stringSliceExpr(route.Guards)}}), |
There was a problem hiding this comment.
Require guards for all events on shared realtime streams
When an app has both a public subscribed page and a guarded subscribed page, the generated handler trusts the client-supplied ?path/Referer and returns only that route's guards before handing the request to the single global SSE hub. Because the fanout below filters only by event type, not by this route, an unauthenticated client can open /_gowdk/realtime/events?path=/public and still receive presentation envelopes for guarded subscriptions. Please either authorize against the union of subscribed guards or bind/filter each stream to the authorized subscription set.
Useful? React with 👍 / 👎.
| for _, subscription := range boundRealtimeSubscriptions(options) { | ||
| if subscription.OwnerKind != gwdkir.SourcePage { | ||
| continue |
There was a problem hiding this comment.
Derive guards for component-owned subscriptions
When g:subscribe is declared inside a component used from a guarded page, this branch drops it from the route-to-guard table; component templates do not inherit the page guards, so the subscription's own Guards are empty and the generated realtime stream can be opened with no guard at all. A direct request to /_gowdk/realtime/events can then receive that component's presentation events without satisfying the page guard. Derive guards from pages/layouts that render the component, or reject component subscriptions until they can be scoped safely.
Useful? React with 👍 / 👎.
| func generatedRealtimeEnabled(options Options) bool { | ||
| return !options.ProxyBackend && len(boundRealtimeSubscriptions(options)) > 0 | ||
| } |
There was a problem hiding this comment.
Proxy realtime streams in split frontend builds
When building a split app (--app plus --backend-app), this disables the realtime route in the frontend, and isBackendRoute only proxies adapter/contract routes, not /_gowdk/realtime/events; the backend app does mount the stream, but the browser's hard-coded EventSource URL is same-origin on the frontend. As a result realtime subscriptions silently fail in split deployments. Keep the frontend from owning backend code, but add the realtime path to the proxy route set when bound subscriptions exist.
Useful? React with 👍 / 👎.
| if route == "" { | ||
| continue | ||
| } | ||
| guards := runtimeGuardNames(subscription.Guards) |
There was a problem hiding this comment.
Preserve default-deny for guardless subscribed pages
For a subscribed page that declares no guard, runtimeGuardNames(subscription.Guards) collapses the subscription to an empty guard set, which makes the realtime stream open without checks. Elsewhere guardless build-time pages are intentionally denied by omission until the author adds guard public, so a direct request to /_gowdk/realtime/events can receive events for a page the generated app would not serve. Track whether the source had guard public versus no guard, and fail closed for omitted guards.
Useful? React with 👍 / 👎.
| usesRealtime, err := pageUsesRealtimeRuntime(page, viewSource, viewNodes, components) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| if pageUsesPartialRuntime(page, viewSource) || usesSPANavigation || usesRealtime { | ||
| scripts = append(scripts, gowdk.Script{Src: clientRuntimeHref}) |
There was a problem hiding this comment.
Load the realtime runtime for request-time pages
When an SSR/hybrid page uses g:subscribe, this newly added realtime detection only runs in the SPA branch because pageScripts returns earlier for request-time rendering, and ssrArtifact also passes no subscription type map into renderPage. The compiler accepts the subscription and the server stream may be generated, but the SSR HTML lacks the client runtime/type marker needed to open EventSource and apply patches. Either reject g:subscribe on request-time pages for now or include the same realtime marker/runtime path there.
Useful? React with 👍 / 👎.
| if event == "" || eventType == "" { | ||
| continue | ||
| } | ||
| types[event] = eventType |
There was a problem hiding this comment.
Key subscription type markers by resolved event
This global map is keyed only by the source text such as events.PatientNotice, so two GOWDK packages that import different Go packages under the same alias and subscribe to the same local event name overwrite each other's data-gowdk-subscribe-type. One page then renders a marker for the other package's event type and silently ignores its own presentation envelopes. Key the marker lookup by resolved import path/type or by subscription owner instead of the raw directive string.
Useful? React with 👍 / 👎.
| if (patch.swap === 'outerHTML') { | ||
| region.outerHTML = patch.html; | ||
| } else { | ||
| region.innerHTML = patch.html; | ||
| } |
There was a problem hiding this comment.
Close streams after subscribed regions are removed
When a realtime patch uses swap: "outerHTML" and the replacement no longer contains a data-gowdk-subscribe region, this mutates the DOM but never re-runs ensureRealtime(), so the EventSource stays connected even though the page no longer has live regions. The client then continues receiving and parsing presentation envelopes until a later navigation/partial update closes it, which contradicts the cleanup behavior this runtime adds for disappeared subscriptions. Re-check realtime regions after applying patches and close the stream when none remain.
Useful? React with 👍 / 👎.
Summary
g:subscribeon query-owned regions.g:subscribeinto IR, validates presentation-event contract bindings, emits diagnostics/build-report metadata, and renders validated subscription markers.replaceHTMLpatches, fail unsupported patch shapes safely, and clean up when subscribed regions disappear.examples/contractsrealtime flow and updated docs.Issue Closure
Closes #130
Closes #131
Closes #132
Closes #134
Related: #133
Related: #147
Verification
go test ./...scripts/test-go-modules.shgo build ./cmd/gowdkgit diff --checkscripts/test-go-modules.shwhen Go code or compiler behavior changed.go build ./cmd/gowdkwhen CLI, compiler, runtime, addon, or release behavior changed.node --check editors/vscode/extension.jswhen editor files changed. Not run; no editor files changed.request-time handlers, logs, diagnostics, embedded assets, editor commands,
WASM, contracts, and realtime behavior.
Known Gaps
LLM Assistance
main, preserving upstream SSR taint hardening and renumbering the realtime ADR to 0012.g:subscribeis intentionally query-bounded and presentation-event-only; generatedgowdk.jsis checked-in client runtime source, not generated output.