resuma 1.3.0

Resuma — resumable SSR Rust web framework: zero hydration, islands, server actions, Flow (Axum).
Documentation
function X(e,t){let n=window.__resuma;if(!n?.action)throw new Error("Resuma core not loaded");return n.action(e,t)}var j=[],$=new Map,R=new Map,W=new WeakMap,H=new Map,_=new Set;function D(e){let t=W.get(e);return t||(t=new Set,W.set(e,t)),t}function Y(e){$.get(e)?.(),$.delete(e),R.delete(e);let t=H.get(e);t&&(t.listeners.clear(),t.es?.close(),t.es=null,H.delete(e))}function Z(e){e&&(Y(e),_.delete(e))}function k(e){e&&_.add(e);let t=H.get(e);t&&(t.terminal=!0,t.es?.close(),t.es=null)}function B(e){j.push(e)}function ee(){for(;j.length;){let e=j.pop();try{e?.()}catch{}}}function T(e){return e.getAttribute("data-r-flow-graph")??e.getAttribute("data-r-event-stream")??e.getAttribute("data-r-worker-panel")??""}function N(e){return e.getAttribute("data-r-graph-token")??""}function z(e,t){let n;return((...r)=>{n!==void 0&&clearTimeout(n),n=setTimeout(()=>e(...r),t)})}function K(e){let t=e.type??"event",n=e.timestamp_ms??0;switch(t){case"log":return`${t}:${e.message??""}`;case"progress":return`${t}:${e.value??""}`;case"result":return`${t}:${JSON.stringify(e.data??null)}`;case"node_start":{let r=F(e.node);return`${t}:${r}`}case"node_done":{let r=F(e.node),s=e.duration_ms??n;return`${t}:${r}:${s}`}case"graph_done":return`${t}:done`;default:return`${t}:${n}`}}function O(e){return e==="done"||e==="failed"}function F(e){return typeof e=="string"?e:e&&typeof e=="object"&&"0"in e?String(e[0]):"node"}function U(e){return(e.closest("[data-r-flow-execution]")?.querySelector("[data-r-flow-graph-status]")?.textContent??"").match(/Status:\s*(running|paused|done|failed)\b/i)?.[1]?.toLowerCase()}function S(e,t,n){let r=e.querySelector("[data-r-worker-status]"),s=e.querySelector("[data-r-worker-pause]"),c=e.querySelector("[data-r-worker-resume]"),o=e.querySelector("[data-r-worker-cancel]"),a=e.querySelector("[data-r-worker-replay]"),i=t==="done"||t==="failed",u=t==="paused",l=t==="running";s&&(s.disabled=!l),c&&(c.disabled=!u),o&&(o.disabled=!(l||u)),a&&(a.disabled=!1);for(let w of[s,c,o,a])w&&(w.disabled?w.setAttribute("aria-disabled","true"):w.removeAttribute("aria-disabled"));if(r){if(n){r.textContent=n;return}i?r.textContent=t==="done"?"Done \u2014 Pause/Cancel are off. Run worker again to retry.":`Graph ${t} \u2014 run worker again to start a new graph.`:u?r.textContent="Paused \u2014 Resume to continue or Cancel to abort.":l?r.textContent="Running \u2014 Pause or Cancel now (~25s window).":r.textContent="Syncing graph status\u2026"}}function Q(e){let t=e.type??"event";switch(t){case"log":return`[log] ${e.message}`;case"progress":return`[progress] ${e.value}%`;case"ai_thinking":return`[ai] ${e.content}`;case"tool_call":return`[tool] ${e.tool}`;case"node_start":return`[start] ${F(e.node)}`;case"node_done":return`[done] ${F(e.node)} (${e.duration_ms}ms)`;case"node_failed":return`[error] ${e.error}`;case"result":return`[result] ${JSON.stringify(e.data)}`;case"graph_done":return"[graph] complete";case"resync":return`[resync] missed ${e.skipped??"?"} events \u2014 replaying`;default:return`[${t}]`}}function M(e,t){if(!t)return e;let n=e.includes("?")?"&":"?";return`${e}${n}token=${encodeURIComponent(t)}`}function C(e){let t={};return e&&(t["X-Resuma-Graph-Token"]=e),t}function te(){let e=document.getElementById("resuma-state");if(!e?.textContent)return"";try{return JSON.parse(e.textContent).csrf_token??""}catch{return""}}function ne(e){let t=C(e),n=te();return n&&(t["x-resuma-csrf"]=n),t}function re(e,t){return`/_resuma/graph/${encodeURIComponent(e)}/${t}`}async function se(e){try{let t=await e.json();if(t.error)return t.error}catch{}return e.status===422?"Graph already finished \u2014 click Run worker again, then Pause while Running.":e.status===401||e.status===403?"Unauthorized \u2014 refresh the page and run the worker again.":`Control failed (HTTP ${e.status}).`}function ae(e){let t=Math.floor(e/1e3),n=Math.floor(t/3600),r=Math.floor(t%3600/60),s=t%60;return n>0?`${n}h ${r}m`:r>0?`${r}m ${s}s`:`${s}s`}function I(e){return e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;")}async function oe(){try{return await X("exec_status",[])}catch{}try{let e=await fetch("/_resuma/status",{credentials:"same-origin"});return e.ok?await e.json():null}catch{return null}}function J(e,t){let n=t.queues.reduce((a,i)=>a+i.pending,0),r=t.queues.reduce((a,i)=>a+i.processing,0),s=n>10?"r-flow-dash__badge r-flow-dash__badge--warn":t.ok?"r-flow-dash__badge":"r-flow-dash__badge r-flow-dash__badge--err",c=t.workers.names.length>0?t.workers.names.map(a=>`<span class="r-flow-dash__chip">${I(a)}</span>`).join(""):'<span class="r-flow-dash__chip">none registered</span>',o=t.queues.map(a=>{let i=a.pending+a.processing+a.done+a.failed||1,u=Math.round(a.processing/i*100);return`<tr>
        <td>${I(a.queue)}</td>
        <td>${a.pending}</td>
        <td>${a.processing}</td>
        <td>${a.done}</td>
        <td>${a.failed}</td>
        <td><div class="r-flow-dash__bar"><svg viewBox="0 0 100 6" preserveAspectRatio="none" aria-hidden="true"><rect class="r-flow-dash__bar-fill" width="${u}" height="6" rx="3"/></svg></div></td>
      </tr>`}).join("");e.innerHTML=`
    <header class="r-flow-dash__header">
      <h2 class="r-flow-dash__title">Resuma OS</h2>
      <span class="${s}">${t.ok?"healthy":"degraded"} \xB7 uptime ${ae(t.uptime_ms)}</span>
    </header>
    <div class="r-flow-dash__grid">
      <div class="r-flow-dash__stat"><p class="r-flow-dash__stat-label">Workers</p><p class="r-flow-dash__stat-value">${t.workers.registered}</p></div>
      <div class="r-flow-dash__stat"><p class="r-flow-dash__stat-label">Graphs running</p><p class="r-flow-dash__stat-value">${t.graphs.running}</p></div>
      <div class="r-flow-dash__stat"><p class="r-flow-dash__stat-label">Graphs paused</p><p class="r-flow-dash__stat-value">${t.graphs.paused}</p></div>
      <div class="r-flow-dash__stat"><p class="r-flow-dash__stat-label">Queue pending</p><p class="r-flow-dash__stat-value">${n}</p></div>
      <div class="r-flow-dash__stat"><p class="r-flow-dash__stat-label">Processing</p><p class="r-flow-dash__stat-value">${r}</p></div>
      <div class="r-flow-dash__stat"><p class="r-flow-dash__stat-label">Scheduler due</p><p class="r-flow-dash__stat-value">${t.scheduler.due}</p></div>
    </div>
    <section class="r-flow-dash__section">
      <h3>Workers</h3>
      <div class="r-flow-dash__chips">${c}</div>
    </section>
    <section class="r-flow-dash__section">
      <h3>Queues</h3>
      <table class="r-flow-dash__table">
        <thead><tr><th>Name</th><th>Pending</th><th>Active</th><th>Done</th><th>Failed</th><th>Load</th></tr></thead>
        <tbody>${o}</tbody>
      </table>
    </section>
    <section class="r-flow-dash__section">
      <h3>Scheduler</h3>
      <p class="r-flow-dash__meta">${t.scheduler.enabled} enabled \xB7 ${t.scheduler.total} total \xB7 ${t.scheduler.due} due now</p>
    </section>`}function ie(e){let t=e.querySelector("[data-r-flow-dashboard-root]")??e,n=Number(e.getAttribute("data-r-flow-dashboard-poll")??"5000")||5e3,r=e.getAttribute("data-r-flow-dashboard-init"),s=async()=>{let a=await oe();if(a){J(t,a);return}t.querySelector(".r-flow-dash__header")||(t.innerHTML='<p class="r-flow-dash__meta">Could not refresh ops status. The SSR snapshot above may be stale \u2014 reload the page.</p>')};if(r)try{J(t,JSON.parse(r))}catch{}s();let c=window.setInterval(()=>{s()},n),o=()=>clearInterval(c);e.addEventListener("resuma:disconnect",o,{once:!0}),B(o)}function G(e,t,n,r){if(!e||typeof EventSource>"u")return()=>{};if(_.has(e))return()=>{};let s=H.get(e);if(s||(s={listeners:new Set,es:null,refs:0,terminal:!1},H.set(e,s)),s.terminal)return()=>{};s.listeners.add(n),s.refs+=1,(()=>{if(s.terminal||s.es)return;let a=M(`/_resuma/graph/${encodeURIComponent(e)}/events`,t),i=new EventSource(a);s.es=i;let u=l=>{for(let w of s.listeners)w(l);l.type==="graph_done"&&(s.terminal=!0,i.close(),s.es=null)};i.onmessage=l=>{try{u(JSON.parse(l.data))}catch{}},i.addEventListener("resync",l=>{try{let w=JSON.parse(l.data);u({...w,type:"resync"})}catch{u({type:"resync"})}}),i.onerror=()=>{i.close(),s.es=null}})();let o=!1;return()=>{o||(o=!0,s.listeners.delete(n),s.refs-=1,s.refs<=0&&(s.es?.close(),H.delete(e)))}}function ce(e,t){let n=e.querySelector("[data-r-flow-graph-track]"),r=e.querySelector("[data-r-flow-graph-status]"),s=t.nodes??[];if(n&&(s.length?(n.innerHTML="",s.forEach((o,a)=>{if(a>0){let l=document.createElement("span");l.className="r-flow-graph__arrow",l.textContent="\u2192",n.appendChild(l)}let i=document.createElement("span");i.className=`r-flow-graph__node r-flow-graph__node--${o.status}`;let u=o.status==="done"?"\u2713":o.status==="running"?"\u25CF":o.status==="failed"?"\u2717":o.status==="paused"?"\u2016":"\u25CB";i.textContent=`${u} ${o.label}`,n.appendChild(i)})):n.textContent="No nodes"),r){let o=t.worker?` \xB7 ${t.worker}`:"",a=t.status??"unknown";r.textContent=`Status: ${a}${o}`}let c=e.closest("[data-r-flow-execution]")?.querySelector("[data-r-worker-panel]");c&&T(c)===T(e)&&S(c,(t.status??"unknown").toLowerCase())}async function A(e,t,n){let r=e.querySelector("[data-r-flow-graph-status]");if(_.has(t)){r&&(r.textContent="Graph finished.");return}let s=M(`/_resuma/graph/${encodeURIComponent(t)}`,n);try{let c=await fetch(s,{headers:C(n),credentials:"same-origin"});if(!c.ok){r&&(c.status===401||c.status===403?(k(t),r.textContent=_.has(t)?"Graph finished.":"Unauthorized \u2014 refresh the page and run the worker again."):r.textContent=`Failed to load graph (HTTP ${c.status}).`);return}let o=await c.json();ce(e,o),O(o.status)&&k(t)}catch{r&&(r.textContent="Network error loading graph.")}}function de(e){let t=T(e),n=N(e);if(!t)return;e.dataset.rFlowMounted==="1"&&e.dispatchEvent(new Event("resuma:disconnect")),e.dataset.rFlowMounted="1";let r=!1,s=null,c=()=>{r=!0,s?.(),s=null,delete e.dataset.rFlowMounted};e.addEventListener("resuma:disconnect",c,{once:!0}),B(c);let o=z(()=>{r||A(e,t,n)},400);(async()=>{if(!(r||(await A(e,t,n),r)||!(e.getAttribute("data-r-flow-graph-live")==="true"))){try{let i=M(`/_resuma/graph/${encodeURIComponent(t)}`,n),u=await fetch(i,{headers:C(n),credentials:"same-origin"});if(r)return;if(u.ok){let l=await u.json();if(O(l.status))return}}catch{}r||(s=G(t,n,i=>{if(!r){if(i.type==="resync"){A(e,t,n);return}if(!(i.type==="progress"||i.type==="log"||i.type==="ai_thinking")){if(i.type==="graph_done"||i.type==="node_done"||i.type==="node_start"||i.type==="node_failed"){A(e,t,n);return}o()}}}))}})()}function V(e){return e.querySelector("[data-r-event-stream-viewport]")??e.querySelector(".r-event-stream__viewport")??e.querySelector(".r-event-stream-list")??e}function P(e,t=!0){let n=()=>{let r=e.scrollHeight;t&&"scrollTo"in e?e.scrollTo({top:r,behavior:"smooth"}):e.scrollTop=r};requestAnimationFrame(()=>{n(),requestAnimationFrame(n)})}function le(e,t){if(typeof ResizeObserver>"u")return()=>{};let n=()=>{e.scrollTop=e.scrollHeight},r=new ResizeObserver(n);return r.observe(t),()=>r.disconnect()}function ue(e){let t=T(e),n=N(e);if(!t)return;let r=V(e),s=e.querySelector("ul")??e;s.innerHTML="",W.delete(s);let c=1e3,o=(R.get(t)??0)+1;R.set(t,o);let a=()=>R.get(t)===o,i=D(s),u=le(r,s),l=(d,h=!1)=>{if(!a())return;let f=document.createElement("li");for(f.textContent=d,s.appendChild(f);s.children.length>c;)s.removeChild(s.firstChild);P(r,h)},w=(d,h=!1)=>{if(!a()||e.dataset.rStreamTerminal==="1"&&d.type!=="graph_done")return;let f=K(d);i.has(f)||(i.add(f),l(Q(d),h),d.type==="graph_done"&&(e.dataset.rStreamTerminal="1",k(t)))};$.get(t)?.(),$.delete(t),e.dataset.rFlowMounted==="1"&&e.dispatchEvent(new Event("resuma:disconnect")),e.dataset.rFlowMounted="1";let E=!1,L=null,b=()=>{E=!0,u(),L?.(),L=null,W.delete(s),delete e.dataset.rFlowMounted,R.get(t)===o&&R.delete(t),$.get(t)===b&&$.delete(t)};$.set(t,b),e.addEventListener("resuma:disconnect",b,{once:!0}),B(b),(async()=>{let d=M(`/_resuma/graph/${encodeURIComponent(t)}/replay`,n),h=C(n),f=!1;try{let p=await fetch(d,{headers:h,credentials:"same-origin"});if(!a()||E)return;if(p.ok){let g=await p.json();s.innerHTML="",i.clear();for(let y of g)w(y,!1);P(r,!1),f=g.some(y=>y.type==="graph_done"),f&&(e.dataset.rStreamTerminal="1",k(t))}}catch{}if(!(!a()||E||f||_.has(t))){try{let p=M(`/_resuma/graph/${encodeURIComponent(t)}`,n),g=await fetch(p,{headers:h,credentials:"same-origin"});if(!a()||E)return;if(g.ok){let y=await g.json();if(O(y.status)){e.dataset.rStreamTerminal="1",k(t);return}}}catch{}!a()||E||(L=G(t,n,p=>{if(!(!a()||E)){if(p.type==="resync"){(async()=>{try{let g=await fetch(d,{headers:h,credentials:"same-origin"});if(!a()||E||!g.ok)return;let y=await g.json();s.innerHTML="",i.clear();for(let x of y)w(x,!1);P(r,!1),l("[resync] replayed missed events",!0)}catch{}})();return}w(p)}}))}})()}async function v(e,t,n,r){if(_.has(t)){S(e,"done",r);return}let s=M(`/_resuma/graph/${encodeURIComponent(t)}`,n);try{let c=await fetch(s,{headers:C(n),credentials:"same-origin"});if(!c.ok){let i=U(e);i?S(e,i,r):S(e,"unknown",r??`Could not load graph (HTTP ${c.status}).`);return}let a=((await c.json()).status??"unknown").toLowerCase();(a==="done"||a==="failed")&&k(t),S(e,a,r)}catch{let c=U(e);c?S(e,c,r):S(e,"unknown",r??"Network error loading graph status.")}}function fe(e){let t=T(e),n=N(e);if(!t)return;e.dataset.rFlowMounted==="1"&&e.dispatchEvent(new Event("resuma:disconnect")),e.dataset.rFlowMounted="1";let r=new AbortController,s=!1,c=null,o,a=()=>{s=!0,r.abort(),o!==void 0&&clearInterval(o),l?.disconnect(),c?.(),c=null,delete e.dataset.rFlowMounted};e.addEventListener("resuma:disconnect",a,{once:!0}),B(a),v(e,t,n);let i=z(()=>{s||v(e,t,n)},250),u=e.closest("[data-r-flow-execution]")?.querySelector("[data-r-flow-graph-status]"),l;u&&typeof MutationObserver<"u"&&(l=new MutationObserver(()=>{if(s)return;let d=U(e);d?S(e,d):i()}),l.observe(u,{characterData:!0,childList:!0,subtree:!0})),o=window.setInterval(()=>{if(s||_.has(t)){o!==void 0&&clearInterval(o),v(e,t,n);return}v(e,t,n)},1500),(async()=>{try{let d=M(`/_resuma/graph/${encodeURIComponent(t)}`,n),h=await fetch(d,{headers:C(n),credentials:"same-origin"});if(s)return;if(h.ok){let f=await h.json();if(O(f.status)){k(t),await v(e,t,n);return}}}catch{}s||(c=G(t,n,d=>{if(!s){if(d.type==="graph_done"){k(t),v(e,t,n);return}(d.type==="node_start"||d.type==="node_done"||d.type==="node_failed"||d.type==="progress"||d.type==="resync")&&i()}}))})();let w=()=>({method:"POST",credentials:"same-origin",headers:ne(n)}),E=e.closest("[data-r-flow-execution]"),L=async d=>{let h=e.querySelector("[data-r-worker-status]"),f=e.querySelector("[data-r-worker-pause]"),p=e.querySelector("[data-r-worker-resume]"),g=e.querySelector("[data-r-worker-cancel]");if(_.has(t)){await v(e,t,n,"Graph finished \u2014 run the worker again to try Pause/Cancel.");return}if(f?.disabled&&p?.disabled&&g?.disabled){await v(e,t,n);return}f&&(f.disabled=!0),p&&(p.disabled=!0),g&&(g.disabled=!0),h&&(h.textContent=`${d}\u2026`);let y=await fetch(re(t,d),w());if(s)return;if(!y.ok){let m=await se(y);await v(e,t,n,m);return}let x={pause:"Paused",resume:"Resumed",cancel:"Cancelled"};if(d==="resume"){_.delete(t);let m=H.get(t);m&&(m.terminal=!1,m.es?.close(),m.es=null)}let q=E?.querySelector("[data-r-flow-graph]");q&&A(q,t,n),await v(e,t,n,`${x[d]} \u2014 refreshing\u2026`),d==="resume"&&!s&&(c?.(),c=G(t,n,m=>{if(!s){if(m.type==="graph_done"){k(t),v(e,t,n);return}(m.type==="node_start"||m.type==="node_done"||m.type==="node_failed"||m.type==="progress"||m.type==="resync")&&i()}}),window.setTimeout(()=>{s||v(e,t,n)},200))},b=r.signal;e.querySelector("[data-r-worker-pause]")?.addEventListener("click",()=>{L("pause")},{signal:b}),e.querySelector("[data-r-worker-resume]")?.addEventListener("click",()=>{L("resume")},{signal:b}),e.querySelector("[data-r-worker-cancel]")?.addEventListener("click",()=>{L("cancel")},{signal:b}),e.querySelector("[data-r-worker-replay]")?.addEventListener("click",async()=>{let d=await fetch(M(`/_resuma/graph/${encodeURIComponent(t)}/replay`,n),{headers:C(n),credentials:"same-origin"});if(!d.ok)return;let h=await d.json(),f=e.closest("[data-r-flow-execution]")?.querySelector("[data-r-event-stream]"),p=f?.querySelector("ul"),g=f?V(f):null;if(!p)return;let y=D(p);y.clear(),p.innerHTML="",delete f.dataset.rStreamTerminal;for(let x of h){let q=K(x);if(y.has(q))continue;y.add(q);let m=document.createElement("li");m.textContent=Q(x),p.appendChild(m)}g&&P(g,!1),await v(e,t,n,"Replay loaded.")},{signal:b})}function me(e=document){e.querySelectorAll("[data-r-worker-panel]").forEach(t=>{let n=T(t),r=N(t);n&&v(t,n,r)})}function pe(e=document){let t=new Set;e.querySelectorAll("[data-r-flow-dashboard], [data-r-flow-graph], [data-r-event-stream], [data-r-worker-panel]").forEach(n=>{let r=T(n);r&&t.add(r),n.dispatchEvent(new Event("resuma:disconnect"))});for(let n of t)Z(n)}function he(e=document,t){let n=t?.exclude?.trim(),r=o=>!n||!(o instanceof HTMLElement&&o.closest(n));t?.flush!==!1?ee():pe(e),e.querySelectorAll("[data-r-flow-dashboard]").forEach(o=>{r(o)&&ie(o)});let s=new Set,c=(o,a,i)=>{if(!r(a))return;let u=T(a);if(!u){i(a);return}let l=`${o}:${u}`;s.has(l)||(s.add(l),i(a))};e.querySelectorAll("[data-r-flow-graph]").forEach(o=>{c("graph",o,de)}),e.querySelectorAll("[data-r-event-stream]").forEach(o=>{c("stream",o,ue)}),e.querySelectorAll("[data-r-worker-panel]").forEach(o=>{c("panel",o,fe)})}export{pe as disconnectFlowWidgets,he as initFlowWidgets,me as syncFlowControls,Z as teardownGraph};