Commitd4ac5c3unknown_key
feat(sse): publish live events on issue + PR comment create
feat(sse): publish live events on issue + PR comment create
The SSE foundation (src/lib/sse.ts + GET /live-events/:topic) was
shipped with the in-flight batch but no producer ever called
publish() — subscribers hooked up but received nothing. This wires
the two highest-traffic comment paths so connected browser tabs
get live nudges instead of polling.
Changes:
- src/routes/issues.tsx — POST .../issues/:n/comment now uses
.returning() and publishes:
topic: repo:<repoId>:issue:<n>
event: "issue-comment"
data: {issueId, commentId, authorId, authorUsername}
- src/routes/pulls.tsx — POST .../pulls/:n/comment mirrors the
same shape under topic repo:<repoId>:pr:<n> with event
"pr-comment".
Both call sites are fire-and-forget — wrapped in try/catch and
imported dynamically so a missing/disabled SSE module can never
block the comment redirect. Per the existing locked invariant the
publisher swallows handler exceptions.
UI consumers are not added here (subscribe via EventSource on the
detail page is a follow-up). The publish side stands on its own;
once consumers land, every tab on the same node gets live updates
without page refresh. Cross-node fanout (Redis/NATS) still tracked
in src/lib/sse.ts TODO(scale).
Tests: existing SSE + comment route suites still pass. Total suite
1143 pass / 8 skip / 0 fail (unchanged).
https://claude.ai/code/session_0163vJChUuZqqtBBznUW6xBU2 files changed+53−10d4ac5c3d3c336913ddbb03e34bf38681aa46a882
2 changed files+53−10
Modifiedsrc/routes/issues.tsx+27−5View fileUnifiedSplit
@@ -460,11 +460,33 @@ issueRoutes.post(
460460
461461 if (!issue) return c.redirect(`/${ownerName}/${repoName}/issues`);
462462
463 await db.insert(issueComments).values({
464 issueId: issue.id,
465 authorId: user.id,
466 body: commentBody,
467 });
463 const [inserted] = await db
464 .insert(issueComments)
465 .values({
466 issueId: issue.id,
467 authorId: user.id,
468 body: commentBody,
469 })
470 .returning();
471
472 // Live update: nudge any browser tabs subscribed to this issue. Pure
473 // fanout — never blocks the redirect, never throws into the request.
474 if (inserted) {
475 try {
476 const { publish } = await import("../lib/sse");
477 publish(`repo:${resolved.repo.id}:issue:${issueNum}`, {
478 event: "issue-comment",
479 data: {
480 issueId: issue.id,
481 commentId: inserted.id,
482 authorId: user.id,
483 authorUsername: user.username,
484 },
485 });
486 } catch {
487 /* SSE is best-effort */
488 }
489 }
468490
469491 return c.redirect(
470492 `/${ownerName}/${repoName}/issues/${issueNum}`
Modifiedsrc/routes/pulls.tsx+26−5View fileUnifiedSplit
@@ -638,11 +638,32 @@ pulls.post(
638638
639639 if (!pr) return c.redirect(`/${ownerName}/${repoName}/pulls`);
640640
641 await db.insert(prComments).values({
642 pullRequestId: pr.id,
643 authorId: user.id,
644 body: commentBody,
645 });
641 const [inserted] = await db
642 .insert(prComments)
643 .values({
644 pullRequestId: pr.id,
645 authorId: user.id,
646 body: commentBody,
647 })
648 .returning();
649
650 // Live update: nudge any browser tabs subscribed to this PR.
651 if (inserted) {
652 try {
653 const { publish } = await import("../lib/sse");
654 publish(`repo:${resolved.repo.id}:pr:${prNum}`, {
655 event: "pr-comment",
656 data: {
657 pullRequestId: pr.id,
658 commentId: inserted.id,
659 authorId: user.id,
660 authorUsername: user.username,
661 },
662 });
663 } catch {
664 /* SSE is best-effort */
665 }
666 }
646667
647668 return c.redirect(`/${ownerName}/${repoName}/pulls/${prNum}`);
648669 }
649670