CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history
sse-client.ts
Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.
| febd4f0 | 1 | /** |
| 2 | * SSE client helper — builds a plain-JS initialization snippet that can be | |
| 3 | * dropped into an SSR'd view via a <script> tag. Intentionally returns a | |
| 4 | * string (not a function export) so it works without any bundler. | |
| 5 | * | |
| 6 | * The returned snippet: | |
| 7 | * - opens an EventSource on /live-events/<topic> | |
| 8 | * - for each message event, parses JSON and calls the user-supplied | |
| 9 | * formatFn (expected to return an HTML string) | |
| 10 | * - appends the HTML to the target element | |
| 11 | * - reconnects with a 1-second backoff on error | |
| 12 | * - no-ops gracefully if EventSource is not supported | |
| 13 | */ | |
| 14 | ||
| 15 | /** | |
| 16 | * JSON-encode a value for safe inlining inside an HTML <script> block. | |
| 17 | * | |
| 18 | * Plain JSON.stringify is not sufficient: a string containing "</script>" | |
| 19 | * would break out of the surrounding <script> tag. We additionally escape | |
| 20 | * `<`, `>`, `&`, and U+2028/U+2029 so the result is safe to splice verbatim | |
| 21 | * into server-rendered HTML. | |
| 22 | */ | |
| 23 | function safeJsonForScript(v: unknown): string { | |
| 24 | return JSON.stringify(v) | |
| 25 | .replace(/</g, "\\u003C") | |
| 26 | .replace(/>/g, "\\u003E") | |
| 27 | .replace(/&/g, "\\u0026") | |
| 28 | .replace(/ /g, "\\u2028") | |
| 29 | .replace(/ /g, "\\u2029"); | |
| 30 | } | |
| 31 | ||
| 32 | export function liveSubscribeScript(args: { | |
| 33 | /** Topic name, e.g. "repo:abc" or "user:42" */ | |
| 34 | topic: string; | |
| 35 | /** id of the DOM element that receives appended event HTML */ | |
| 36 | targetElementId: string; | |
| 37 | /** Optional JS function body taking (event) and returning an HTML string. | |
| 38 | * If omitted, a default escapes the JSON payload as text. */ | |
| 39 | formatFn?: string; | |
| 40 | }): string { | |
| 41 | const topic = safeJsonForScript(args.topic); | |
| 42 | const targetId = safeJsonForScript(args.targetElementId); | |
| 43 | const formatFnBody = | |
| 44 | args.formatFn ?? | |
| 45 | "return '<li>' + String(event && event.data || '').replace(/[&<>\"']/g,function(c){return {'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c];}) + '</li>';"; | |
| 46 | ||
| 47 | // Keep compact to stay under 2KB. | |
| 48 | return ( | |
| 49 | "(function(){try{" + | |
| 50 | "if(typeof EventSource==='undefined')return;" + | |
| 51 | "var t=" + topic + ",id=" + targetId + ";" + | |
| 52 | "var el=document.getElementById(id);if(!el)return;" + | |
| 53 | "function fmt(event){" + formatFnBody + "}" + | |
| 54 | "var es,delay=1000;" + | |
| 55 | "function connect(){" + | |
| 56 | "try{es=new EventSource('/live-events/'+encodeURIComponent(t));}catch(e){setTimeout(connect,delay);return;}" + | |
| 57 | "es.onmessage=function(m){try{var d=JSON.parse(m.data);var h=fmt(d);if(h&&el)el.insertAdjacentHTML('beforeend',h);}catch(e){}};" + | |
| 58 | "es.onerror=function(){try{es.close();}catch(e){}setTimeout(connect,delay);};" + | |
| 59 | "}connect();}catch(e){}})();" | |
| 60 | ); | |
| 61 | } |