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

    src/components/demos/StreamDemo.astro
    ---
    /** 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

    src/components/JobLog.astro
    ---
    /** 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

    src/pages/api/stream.ts
    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 }));
      });
    };