Skip to content

feat(realtime): wire query-bound live subscriptions#440

Merged
cssbruno merged 2 commits into
mainfrom
codex/m14-live-realtime
Jun 15, 2026
Merged

feat(realtime): wire query-bound live subscriptions#440
cssbruno merged 2 commits into
mainfrom
codex/m14-live-realtime

Conversation

@cssbruno

Copy link
Copy Markdown
Owner

Summary

  • Adds ADR 0012 and the M14 realtime reactivity spec/plan for explicit g:subscribe on query-owned regions.
  • Lowers g:subscribe into IR, validates presentation-event contract bindings, emits diagnostics/build-report metadata, and renders validated subscription markers.
  • Generates subscription-filtered SSE fanout for bound presentation events, including inherited guard checks before stream open and public fanout registration hooks.
  • Extends the checked-in client runtime source to open the generated EventSource stream, apply explicit replaceHTML patches, fail unsupported patch shapes safely, and clean up when subscribed regions disappear.
  • Adds retry/drop behavior for the dependency-free SSE adapter plus a live examples/contracts realtime flow and updated docs.

Issue Closure

Closes #130
Closes #131
Closes #132
Closes #134

Related: #133
Related: #147

Verification

  • I ran the relevant tests, lint, and build commands.
    • go test ./...
    • scripts/test-go-modules.sh
    • go build ./cmd/gowdk
    • git diff --check
  • I ran scripts/test-go-modules.sh when Go code or compiler behavior changed.
  • I ran go build ./cmd/gowdk when CLI, compiler, runtime, addon, or release behavior changed.
  • I ran node --check editors/vscode/extension.js when editor files changed. Not run; no editor files changed.
  • I updated docs for behavior, setup, or architecture changes.
  • I added or updated tests for changed behavior.
  • I considered security-sensitive surfaces such as auth, CSRF, redirects,
    request-time handlers, logs, diagnostics, embedded assets, editor commands,
    WASM, contracts, and realtime behavior.

Known Gaps

LLM Assistance

  • LLM session summary: Codex implemented the M14 realtime subscription surface, generated SSE fanout, guarded stream behavior, client patch loop, live contracts example, docs, and tests; then rebased onto current main, preserving upstream SSR taint hardening and renumbering the realtime ADR to 0012.
  • Human-reviewed assumptions: g:subscribe is intentionally query-bounded and presentation-event-only; generated gowdk.js is checked-in client runtime source, not generated output.
  • Follow-up work: Finish [Reactivity] Reconnect, backpressure, guard-gated streams #133 active stream revocation/custom replay policy and evaluate feat: compiler-derived event → query invalidation for automatic fragment refresh #147 derived invalidation after explicit subscriptions are proven.

@cssbruno cssbruno marked this pull request as ready for review June 15, 2026 12:46

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +262 to +264
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)}}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment on lines +73 to +75
for _, subscription := range boundRealtimeSubscriptions(options) {
if subscription.OwnerKind != gwdkir.SourcePage {
continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment on lines +14 to +16
func generatedRealtimeEnabled(options Options) bool {
return !options.ProxyBackend && len(boundRealtimeSubscriptions(options)) > 0
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment on lines +251 to 256
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})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +551 to +555
if (patch.swap === 'outerHTML') {
region.outerHTML = patch.html;
} else {
region.innerHTML = patch.html;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@cssbruno cssbruno merged commit df812f4 into main Jun 15, 2026
5 checks passed
@cssbruno cssbruno deleted the codex/m14-live-realtime branch June 15, 2026 13:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant