Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
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.

sse-client.tsBlame61 lines · 1 contributor
febd4f0Claude1/**
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 */
23function 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
32export 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 {'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;',\"'\":'&#39;'}[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}