Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesPull RequestsActionsSecurityInsightsSettings
✨ AI
More
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.tsBlame67 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
04f6b7fClaude15// U+2028 (LINE SEPARATOR) and U+2029 (PARAGRAPH SEPARATOR) are valid
16// whitespace in HTML but not in JS string literals. Referenced via
17// String.fromCharCode so we never embed the raw codepoints in source.
18const LS = String.fromCharCode(0x2028);
19const PS = String.fromCharCode(0x2029);
20
febd4f0Claude21/**
22 * JSON-encode a value for safe inlining inside an HTML <script> block.
23 *
24 * Plain JSON.stringify is not sufficient: a string containing "</script>"
25 * would break out of the surrounding <script> tag. We additionally escape
26 * `<`, `>`, `&`, and U+2028/U+2029 so the result is safe to splice verbatim
27 * into server-rendered HTML.
28 */
29function safeJsonForScript(v: unknown): string {
30 return JSON.stringify(v)
04f6b7fClaude31 .split("<").join("\\u003C")
32 .split(">").join("\\u003E")
33 .split("&").join("\\u0026")
34 .split(LS).join("\\u2028")
35 .split(PS).join("\\u2029");
febd4f0Claude36}
37
38export function liveSubscribeScript(args: {
39 /** Topic name, e.g. "repo:abc" or "user:42" */
40 topic: string;
41 /** id of the DOM element that receives appended event HTML */
42 targetElementId: string;
43 /** Optional JS function body taking (event) and returning an HTML string.
44 * If omitted, a default escapes the JSON payload as text. */
45 formatFn?: string;
46}): string {
47 const topic = safeJsonForScript(args.topic);
48 const targetId = safeJsonForScript(args.targetElementId);
49 const formatFnBody =
50 args.formatFn ??
51 "return '<li>' + String(event && event.data || '').replace(/[&<>\"']/g,function(c){return {'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;',\"'\":'&#39;'}[c];}) + '</li>';";
52
53 // Keep compact to stay under 2KB.
54 return (
55 "(function(){try{" +
56 "if(typeof EventSource==='undefined')return;" +
57 "var t=" + topic + ",id=" + targetId + ";" +
58 "var el=document.getElementById(id);if(!el)return;" +
59 "function fmt(event){" + formatFnBody + "}" +
60 "var es,delay=1000;" +
61 "function connect(){" +
62 "try{es=new EventSource('/live-events/'+encodeURIComponent(t));}catch(e){setTimeout(connect,delay);return;}" +
63 "es.onmessage=function(m){try{var d=JSON.parse(m.data);var h=fmt(d);if(h&&el)el.insertAdjacentHTML('beforeend',h);}catch(e){}};" +
64 "es.onerror=function(){try{es.close();}catch(e){}setTimeout(connect,delay);};" +
65 "}connect();}catch(e){}})();"
66 );
67}