Rat docs

Inverse Tail — calling JS libraries

Tail handles "server-side Rat calls foreign code"; Inverse Tail handles "browser-side Rat (page-tier, compiled to JS) calls foreign code that already lives in the page." Same name.method(args) dispatch, no IPC needed — the call compiles to a direct JS member access on window.__ratJS. The chart below is a real Chart.js render driven by an Inverse Tail call.

Live: a real Chart.js render

Renders on page ready; click randomize to repaint

The chart is mounted automatically by lang/js/auto.js on DOMContentLoaded. Chart.js itself loads lazily from esm.sh the first time anything calls chart.bar(...) — page weight stays small until the demo actually needs it. The randomize button mutates the page-tier values array and repaints; "current values" updates reactively because every [values] read participates in the data-bind graph.

Result

Rendered with Chart.js — loaded lazily from esm.sh on first call.

current values: [12, 19, 7, 24]

Live: multi-series line chart

Three datasets on one axis

Same wrapper, second function. chart.line(canvas, labels, series) takes an array of \{label, values, color\} entries — one per line — and Chart.js handles the legend and color allocation. Shuffle re-randomizes all three series and re-renders; the bar chart above stays untouched, proving the two instances coexist on the same page.

Result

Plan vs actual vs forecast — same labels, three datasets.

The JS wrapper

site/lang/js/chart.js

The wrapper is a thin ESM module — two named exports, both pure JS. The lazy import() means Chart.js only ships down the wire when a page actually paints a chart; subsequent calls reuse the loaded module. Destroying the previous Chart instance before each render keeps the canvas clean across repeated paints.

// site/lang/js/chart.js
let _chartMod = null;
let _instance = null;

async function loadChart() { if (_chartMod) return _chartMod; _chartMod = await import('https://esm.sh/chart.js@4.4.0/auto'); return _chartMod; }

export async function bar(canvasId, labels, values, color) { const mod = await loadChart(); const Chart = mod.default || mod.Chart || mod; const el = document.getElementById(canvasId); if (!el) return null; if (_instance) _instance.destroy(); _instance = new Chart(el, { type: 'bar', data: { labels, datasets: [{ data: values, backgroundColor: color }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } } }, }); return true; }

export function random_values(n, max) { const out = []; for (let i = 0; i < n; i++) out.push(Math.floor(Math.random() * max) + 1); return out; }

// Multi-series line chart. `series` is [{label, values, color}, ...]
let _lineInstance = null;
export async function line(canvasId, labels, series) { const mod = await loadChart(); const Chart = mod.default || mod.Chart || mod; const el = document.getElementById(canvasId); if (!el) return null; if (_lineInstance) _lineInstance.destroy(); _lineInstance = new Chart(el, { type: 'line', data: { labels, datasets: series.map(s => ({ label: s.label, data: s.values, borderColor: s.color, backgroundColor: s.color, tension: 0.3, borderWidth: 2, })) }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom' } } }, }); return true; }

The auto-mount module

site/lang/js/auto.js

Every .js file in lang/js/ is auto-imported by the renderer's bootstrap. Side-effect modules — like this one — use that import to run setup code without exporting anything. Here it listens for DOMContentLoaded, checks for the demo canvas, and paints it with values pulled from window.__rat (the page snapshot).

// site/lang/js/auto.js
import * as chart from './chart.js';

function mount() { const canvas = document.getElementById('sales-canvas'); if (!canvas) return; const snap = window.__rat || {}; chart.bar('sales-canvas', snap.labels || [], snap.values || [], '#b8341c'); }

if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', mount); } else { setTimeout(mount, 0); }

The page

this file — pages/documentation/advanced/inverse_tail.rat

Page-tier state at the top, markup in the middle, page-tier functions at the bottom. The [page] flag on each function compiles its body to JS — that's the layer that knows to rewrite chart.bar(...) as window.__ratJS.chart.bar(...). A handler-attribute call to a bare JS namespace (without going through a [page] function) is the one shape the compiler can't see, so the wrapper pattern is the canonical way to bridge.

> page
labels: ['Q1', 'Q2', 'Q3', 'Q4']
values: [12, 19, 7, 24]

<canvas id['sales-canvas']>
<button on_click[randomize_chart('sales-canvas', labels)]> randomize
<button on_click[render_chart('sales-canvas', labels, values)]> re-render
<p> current values: [values]

> render_chart[canvas, ls, vs] [page]
    chart.bar(canvas, ls, vs, '#b8341c')

> randomize_chart[canvas, ls] [page]
    [vs] << chart.random_values(4, 25)
    values << vs
    chart.bar(canvas, ls, vs, '#b8341c')

Why it composes

Every npm UI lib, one wrapper away

Charting (Chart.js, Plotly, ECharts), editors (Monaco, TipTap, CodeMirror), 3D (Three.js, Babylon), maps (Leaflet, Mapbox), animation (GSAP, anime.js), platform APIs (fetch, IndexedDB, Clipboard) — all become Rat-native by writing one small lang/js/<name>.js wrapper. The framework ships zero default wrappers; the dialect doesn't need to know which libraries exist.

What gets generated

One bootstrap script per project

At boot the site loader walks lang/js/, reads the exported function names from each .js file, and emits a bootstrap script the renderer injects into every page. Modules with no exports (like auto.js) still get imported — their side effects run.

<!-- injected by the renderer at page boot -->
<script type="module">
  import * as chart from '/__rat__/lang/js/chart.js';
  import * as auto  from '/__rat__/lang/js/auto.js';
  window.__ratJS = window.__ratJS || {};
  window.__ratJS.chart = chart;
  window.__ratJS.auto  = auto;
</script>

Server-side calls fail loudly

No silent fallback

A > server body that tries to call a lang/js namespace raises a clear error rather than returning null. The renderer registers JS namespaces as forbidden server stubs so the tier mistake surfaces immediately — move the call into a > page body or a [page] function instead.

# WRONG — chart only exists in the browser
> server
preview: chart.bar(...)

# RIGHT — page-tier wrapper
> render_chart[id, ls, vs] [page]
    chart.bar(id, ls, vs, '#b8341c')

Next: Middleware — services that hook into the request lifecycle (with a live page-view counter).