SSE Streaming
One @get — many updates. The endpoint keeps the response open with
sseStream() and pushes patchSignals() (progress %) and
patchElements() (appended log lines) as work happens.
Long-running job
0%
The demo
---
/** SSE streaming demo: one @get, many patches. */
import JobLog from '../JobLog.astro';
---
<section class="demo" data-signals="{progress: 0, running: false}">
<h2>Long-running job</h2>
<div class="row">
<button
data-on:click="@get('/api/stream')"
data-attr:disabled="$running"
data-text="$running ? 'Working…' : 'Start job'"
>
Start job
</button>
<span data-text="$progress + '%'">0%</span>
</div>
<progress max="100" data-attr:value="$progress"></progress>
<JobLog />
</section>The shared component
---
/** Streaming job log: empty <ul> reset, or one appended line. */
interface Props {
line?: string;
}
const { line } = Astro.props;
---
{line ? <li>{line}</li> : <ul id="job-log" class="job-log"></ul>}The endpoint
import {
patchElements,
patchSignals,
sseStream,
} from '@wrux/astro-datastar/server';
import type { APIRoute } from 'astro';
import { experimental_AstroContainer as AstroContainer } from 'astro/container';
import JobLog from '../../components/JobLog.astro';
export const prerender = false;
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
const steps = [
'Fetching records…',
'Validating…',
'Transforming…',
'Writing output…',
'Done.',
];
export const GET: APIRoute = async () => {
const container = await AstroContainer.create();
return sseStream(async (stream) => {
stream.send(patchSignals({ running: true, progress: 0 }));
stream.send(
patchElements(await container.renderToString(JobLog), {
selector: '#job-log',
}),
);
for (const [i, step] of steps.entries()) {
await sleep(500);
stream.send(
patchSignals({ progress: Math.round(((i + 1) / steps.length) * 100) }),
);
stream.send(
patchElements(
await container.renderToString(JobLog, { props: { line: step } }),
{ selector: '#job-log', mode: 'append' },
),
);
}
stream.send(patchSignals({ running: false }));
});
};