feat(project): SSE push updates for project board pages #7
Reference in New Issue
Block a user
Delete Branch "project-board-sse"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Adds Server-Sent Events push so project board pages auto-update when ANY board mutation happens (card move/add/remove + column CRUD + project edit/delete), without page refresh.
Design
/user/eventsSSE endpoint and theUserEventsSharedWorkerSharedWorker pattern. Topic isolation via event names likeproject-board.{project_id}— no new endpoints, no new auth.services/projects/{column,issue,project}.gothat call the underlying model funcs and then emit events. All routers (web + REST for repo/user/org scopes) were migrated to call the wrappers, so every mutation publishes exactly once regardless of entry point.connectedUIDsWithProjectAccessiterateseventsource.Manager.ConnectedUIDs()and checks per-user project read access before sending. No card content leaks to users without access.session_tagon load, attaches it asX-Session-Tagheader on every mutation request. Server stitches it into the published event payload. Frontend skips events where the tag matches its own — no duplicate animations from the originating tab's drag-drop.Event taxonomy
card.moved,card.linked,card.unlinked,column.created,column.updated,column.deleted,column.reordered,project.updated,project.deleted. Payloads carry only IDs + minimal fields needed for DOM patching;card.linkeddoes a targeted column re-fetch rather than embedding rendered HTML.Notable decisions
models → services/project_eventsimport cycle (project_events importsmodels/projectfor the access check). Wrappers avoid the cycle.ColumnDeletedemits per-issueCardMovedfollow-ups snapshotted before the delete, so the frontend can patch declaratively without an extra fetch.context.Valuerather than added params — propagates from middleware (routers/common.SessionTagMiddleware) without rippling through many signatures.Tests
services/project_events/events_test.go— 15 unit tests covering event-name format, JSON payload shape per type, session-tag context propagation, explicit-tag override, access-filter inclusion/exclusion, no-connections short-circuit, broadcast fan-out target list. All pass.golangci-lintclean on changed packages.tsc --noEmit,vite buildclean.Limitations
column.createdfalls back towindow.location.reload()as the first-pass DOM handler (rare event). Optimize later if needed.Manual smoke
Open project board in two browser tabs. Drag a card in tab A → tab B updates without refresh. Same for column edit, card link via API, etc.
New services/project_events package marshals typed payloads to JSON, wraps them in SSE events named project-board.{project_id}, and fans them out via the eventsource manager to every connected user that has read access to the project. Each Publish* helper runs the broadcast in a goroutine so request handlers stay responsive. Includes WithSessionTag / SessionTagFromContext for propagating an X-Session-Tag value down to the publisher (so the originating browser tab can suppress its own echo). Unit tests cover event-name format, payload JSON shape, session-tag propagation, the connected-uids access filter, and the broadcast fan-out path.The project board view now opens a SharedWorker EventSource subscription scoped to project-board.{id} and patches the DOM in response to incoming events: - card.moved: relocates the card to the destination column and refreshes both column count badges; falls back to a column refetch when the card isn't currently rendered (filtered out / new). - card.linked: refetches the destination column's issue list and updates the count badge. - card.unlinked: removes the card and updates the badge. - column.created: page reload (rare event, simplest path). - column.updated: in-place title + color/contrast updates. - column.deleted: removes the column element. - column.reordered: re-attaches columns in the new sort order. - project.updated: updates the header title + description text. - project.deleted: navigates up to the projects listing. The board template now exposes data-project-id, data-project-scope (repo/user/org), data-project-owner, and data-project-repo so the subscriber can build the right column-issues refetch URL. Each mutation request the page makes also carries a crypto.randomUUID-generated X-Session-Tag header; the receiving handler compares it against the incoming payload's session_tag to suppress own-tab echoes (the optimistic local update is already authoritative).