router: align SSE to canonical /api/{domain}/events (drop sse.json, derive from x-sse) #120

Closed
opened 2026-06-14 15:08:08 +00:00 by mahmoud · 1 comment
Owner

Summary

hero_router's SSE handling diverges from the hero_lib macro/SDK canonical (GET /api/{domain}/events) and is internally inconsistent — it uses three different conventions, none of which match canonical. The fix is router-side alignment, not services growing sse.json stubs.

Evidence

Router (on development, post-#118) — three inconsistent conventions:

  • Probe: GET /api/main/sse.json, descriptor hardcoded path = "/api/main/sse.json" — one per socket, not per-domain. crates/hero_router/src/probe.rs:474-485, crates/hero_router/src/scanner.rs:371-385.
  • Multiplexer: streams from bare /events (no filter key). crates/hero_router/src/server/multiplex.rs:429.
  • Direct forward: renames external /api/{domain}/events → socket /api/{domain}/sse.json. crates/hero_router/src/server/proxy/http/http_dispatch.rs:296-298.
  • SseDescriptor doc even says "conforming services use /events" — contradicting its own probe. crates/hero_router/src/cache.rs:152-159.

hero_lib canonical (unambiguous) — GET /api/{domain}/events, forwarded verbatim:

  • hero_lib's own reference proxy routes /api/{domain}/events → socket /api/{domain}/events, no rename: hero_lib/crates/hero_lifecycle/src/webserver/components/openrpc_proxy/router.rs:245 (+ single-domain /api/events at :206).
  • Service proxy: GET /{prefix}/api/{domain}/events → socket GET /api/{domain}/events (.../service_proxy/mod.rs:16).
  • @sse(endpoint) defaults to /events; the spec carries an x-sse extension with endpoint, emits, done, and a filter key. Example (demo server generated/openrpc/openrpc_dom1.json): "x-sse": { "endpoint": "/events", filter=topic, ... }. There is no sse.json anywhere in the canonical.

Why NOT "fix" this on the service side

SSE is per-(domain, method) with a filter, not per-socket:

  • hero_proc streams are per-job (?job_sid=); there is no global proc stream.
  • demo dom1 is per-topic (?topic=); dom2 is an unfiltered firehose.

The router's probe (/api/main/sse.json) and multiplexer (bare /events) connect with no filter, so for proc there is nothing meaningful to emit. Satisfying them would mean adding an empty keepalive sse.json stub purely to pass the probe — cargo-culting a channel hero_lib already abandoned. Do not add sse.json stubs to hero_proc (or any service).

Proposed design (router-side)

Drive SSE from the cached per-domain spec (the per-domain domain_specs added in #118), not from a probe:

  • Drop the rename in the direct forward path — forward /api/{domain}/events verbatim to the socket (match hero_lib's proxy). http_dispatch.rs:296-298.
  • Replace the sse.json probe — derive SSE channels from each domain spec's x-sse extension (endpoint + filter key + emits/done), per (domain, method). Removes probe_events/EventsProbe blind probing. probe.rs, scanner.rs.
  • Rework SseDescriptor from one-per-socket (/api/main/sse.json) to per-(domain, method) carrying the filter key. cache.rs:152-159.
  • Fix the multiplexer — proxy a filtered per-domain stream GET /api/{domain}/events?<filter>=<value> on demand instead of a global bare /events. multiplex.rs:429. (The unfiltered/global case stays valid only where a domain actually exposes a firehose.)

Scope / notes

  • Router-owned; no service changes.
  • Separate from #118 (which deliberately scoped SSE out and did multi-domain RPC/MCP). This builds on #118's per-domain domain_specs cache.
  • Moderate size — touches probe + scanner + cache descriptor + multiplexer + forward; not a one-line rename.
## Summary hero_router's SSE handling diverges from the hero_lib macro/SDK canonical (`GET /api/{domain}/events`) and is internally inconsistent — it uses **three** different conventions, none of which match canonical. The fix is **router-side alignment**, not services growing `sse.json` stubs. ## Evidence **Router (on `development`, post-#118) — three inconsistent conventions:** - Probe: `GET /api/main/sse.json`, descriptor hardcoded `path = "/api/main/sse.json"` — one per socket, not per-domain. `crates/hero_router/src/probe.rs:474-485`, `crates/hero_router/src/scanner.rs:371-385`. - Multiplexer: streams from **bare `/events`** (no filter key). `crates/hero_router/src/server/multiplex.rs:429`. - Direct forward: renames external `/api/{domain}/events` → socket `/api/{domain}/sse.json`. `crates/hero_router/src/server/proxy/http/http_dispatch.rs:296-298`. - `SseDescriptor` doc even says *"conforming services use `/events`"* — contradicting its own probe. `crates/hero_router/src/cache.rs:152-159`. **hero_lib canonical (unambiguous) — `GET /api/{domain}/events`, forwarded verbatim:** - hero_lib's **own** reference proxy routes `/api/{domain}/events` → socket `/api/{domain}/events`, no rename: `hero_lib/crates/hero_lifecycle/src/webserver/components/openrpc_proxy/router.rs:245` (+ single-domain `/api/events` at `:206`). - Service proxy: `GET /{prefix}/api/{domain}/events → socket GET /api/{domain}/events` (`.../service_proxy/mod.rs:16`). - `@sse(endpoint)` defaults to `/events`; the spec carries an **`x-sse`** extension with `endpoint`, `emits`, `done`, and a **filter** key. Example (demo server `generated/openrpc/openrpc_dom1.json`): `"x-sse": { "endpoint": "/events", filter=topic, ... }`. There is **no `sse.json`** anywhere in the canonical. ## Why NOT "fix" this on the service side SSE is **per-(domain, method) with a filter**, not per-socket: - hero_proc streams are **per-job** (`?job_sid=`); there is no global proc stream. - demo `dom1` is **per-topic** (`?topic=`); `dom2` is an unfiltered firehose. The router's probe (`/api/main/sse.json`) and multiplexer (bare `/events`) connect with **no filter**, so for proc there is nothing meaningful to emit. Satisfying them would mean adding an **empty keepalive `sse.json` stub** purely to pass the probe — cargo-culting a channel hero_lib already abandoned. **Do not add `sse.json` stubs to hero_proc (or any service).** ## Proposed design (router-side) Drive SSE from the **cached per-domain spec** (the per-domain `domain_specs` added in #118), not from a probe: - [x] **Drop the rename** in the direct forward path — forward `/api/{domain}/events` verbatim to the socket (match hero_lib's proxy). `http_dispatch.rs:296-298`. - [x] **Replace the `sse.json` probe** — derive SSE channels from each domain spec's `x-sse` extension (endpoint + filter key + emits/done), per `(domain, method)`. Removes `probe_events`/`EventsProbe` blind probing. `probe.rs`, `scanner.rs`. - [x] **Rework `SseDescriptor`** from one-per-socket (`/api/main/sse.json`) to **per-(domain, method)** carrying the filter key. `cache.rs:152-159`. - [x] **Fix the multiplexer** — proxy a **filtered per-domain** stream `GET /api/{domain}/events?<filter>=<value>` on demand instead of a global bare `/events`. `multiplex.rs:429`. (The unfiltered/global case stays valid only where a domain actually exposes a firehose.) ## Scope / notes - Router-owned; **no service changes**. - Separate from #118 (which deliberately scoped SSE out and did multi-domain RPC/MCP). This builds on #118's per-domain `domain_specs` cache. - Moderate size — touches probe + scanner + cache descriptor + multiplexer + forward; not a one-line rename.
mahmoud self-assigned this 2026-06-14 15:08:53 +00:00
mahmoud added this to the now milestone 2026-06-14 15:09:01 +00:00
mahmoud added this to the ACTIVE project 2026-06-14 15:09:03 +00:00
Author
Owner

Implemented — PR #121 (development_router_sse_canonical → development).

  • forward /api/{domain}/events verbatim (dropped the sse.json rename)
  • derive SSE from cached x-sse (per-domain {channel,domain,endpoint,filter_key}; Vec per service); removed the sse.json probe + EventsProbe
  • multiplexer auto-exposes firehose channels only; streams the real endpoint; filtered streams on demand via ?<filter>=
  • unit tests for the derivation; 143 tests pass, clippy clean

No service changes (no sse.json stubs). Verified live against the real hero_demo_server: dom1 /api/dom1/events filter=topic, dom2 /api/dom2/events firehose; verbatim /events forwards 200; old sse.json 404.

**Implemented — PR #121** (development_router_sse_canonical → development). - forward `/api/{domain}/events` verbatim (dropped the `sse.json` rename) - derive SSE from cached `x-sse` (per-domain {channel,domain,endpoint,filter_key}; `Vec` per service); removed the `sse.json` probe + `EventsProbe` - multiplexer auto-exposes firehose channels only; streams the real endpoint; filtered streams on demand via `?<filter>=` - unit tests for the derivation; **143 tests pass**, clippy clean **No service changes** (no sse.json stubs). Verified live against the real `hero_demo_server`: dom1 `/api/dom1/events` filter=topic, dom2 `/api/dom2/events` firehose; verbatim `/events` forwards 200; old `sse.json` 404.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
lhumina_code/hero_router#120
No description provided.