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:
@@ -265,12 +265,15 @@ pub async fn build_app(
|
||||
// to in-process SSE subscribers.
|
||||
let pubsub_repo = Arc::new(PostgresPubsubRepo::new(pool.clone()));
|
||||
// §11.6 D2: the shared-topic service reuses the same fan-out repo.
|
||||
let group_pubsub: Arc<dyn picloud_shared::GroupPubsubService> =
|
||||
Arc::new(GroupPubsubServiceImpl::new(
|
||||
let group_pubsub: Arc<dyn picloud_shared::GroupPubsubService> = Arc::new(
|
||||
GroupPubsubServiceImpl::new(
|
||||
pubsub_repo.clone(),
|
||||
Arc::new(PostgresGroupCollectionResolver::new(pool.clone())),
|
||||
authz.clone(),
|
||||
));
|
||||
)
|
||||
// §11.6 D2: fan a shared-topic publish to external SSE subscribers too.
|
||||
.with_realtime(broadcaster.clone()),
|
||||
);
|
||||
// §11.6 D3: the group shared-queue store + enqueue service.
|
||||
let group_queue_repo = Arc::new(PostgresGroupQueueRepo::new(pool.clone()));
|
||||
let group_dead_letter_repo = Arc::new(
|
||||
@@ -346,6 +349,8 @@ pub async fn build_app(
|
||||
topic_repo.clone(),
|
||||
app_secrets_repo.clone(),
|
||||
users.clone(),
|
||||
// §11.6 D2: resolves a shared topic to its owning group for SSE subscribe.
|
||||
Arc::new(PostgresGroupCollectionResolver::new(pool.clone())),
|
||||
));
|
||||
// v1.1.9 durable per-app queues. Producers write to queue_messages
|
||||
// via QueueService; the dispatcher's queue arm (commit 6) consumes
|
||||
|
||||
Reference in New Issue
Block a user