feat(realtime): external SSE subscription for group shared topics (§11.6 D2 / Track A M6)
Shared TOPICS fanned out only to in-cluster trigger handlers; external clients
could not subscribe (per-app topics already can). Add SSE for shared topics.
- RealtimeBroadcaster gains a parallel (group_id, topic) channel map:
subscribe_group / publish_group / drop_group_topic (default no-ops so
NoopRealtimeBroadcaster + test doubles are untouched). InProcessBroadcaster
implements them with a second map; GC + channel_count span both.
- Route GET /realtime/shared/topics/{topic}: Host->app dispatch (as the per-app
route), then RealtimeAuthority::authorize_subscribe_shared resolves the OWNING
GROUP from the app's chain (kind=topic, root segment). Reads-open model — the
resolution IS the authorization, consistent with in-script shared reads; a
foreign-subtree app never resolves (404, the isolation boundary). No principal
machinery needed.
- GroupPubsubServiceImpl::with_realtime bridges a shared-topic publish to the
owning-group channel (best-effort) after the durable trigger fan-out.
- Host wires the broadcaster into the group pubsub service + the collection
resolver into the authority.
Auth-model note: chose reads-open (subtree app's Host is the grant) over
"authenticated principal + GroupKvRead" — it's both simpler and faithful to how
shared-collection reads already work. Pinned by realtime broadcaster group-map
tests, realtime_api shared-route tests (404 + stream), and
group_pubsub_service::publish_bridges_to_the_group_broadcaster. No migration.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1314,6 +1314,17 @@ Resolved items now live inline next to their topic. What genuinely remains:
|
||||
> topic pattern's ROOT segment (`events.*` → `events`) be a declared `kind='topic'` collection
|
||||
> (group-only) — the `shared` flag + owning-group chain walk are the isolation boundary. No message store
|
||||
> (topics are events, not persistence). Pinned by `tests/shared_topics.rs` + the `shared_topics` journey.
|
||||
> **External SSE subscription shipped (Track A M6):** `GET /realtime/shared/topics/{topic}` streams a
|
||||
> shared topic to external clients. Host→app dispatch (as the per-app SSE route) identifies the subscriber
|
||||
> app; `RealtimeAuthority::authorize_subscribe_shared` resolves the OWNING GROUP from that app's chain
|
||||
> (kind=`topic`, the topic's root segment) — the reads-open model: the resolution IS the authorization (a
|
||||
> foreign-subtree app never resolves → 404, the isolation boundary), consistent with in-script shared-read
|
||||
> semantics. The `RealtimeBroadcaster` gained a parallel `(group_id, topic)` channel map
|
||||
> (`subscribe_group`/`publish_group`/`drop_group_topic`, default no-ops so non-realtime bundles are
|
||||
> unchanged); `GroupPubsubServiceImpl::with_realtime` fans a publish to the owning-group channel (best-
|
||||
> effort) after the durable trigger fan-out. Pinned by `realtime` broadcaster + `realtime_api` SSE tests +
|
||||
> `group_pubsub_service::publish_bridges_to_the_group_broadcaster`. Deferred: multi-node propagation
|
||||
> (cluster mode swaps the in-process broadcaster for LISTEN/NOTIFY behind the same trait).
|
||||
>
|
||||
> **Shipped — D3 shared durable QUEUES (competing consumers).** A group declares a `queue` shared
|
||||
> collection; any subtree app enqueues into ONE group-keyed store (`group_queue_messages`, `0065`) via
|
||||
|
||||
Reference in New Issue
Block a user