# Mostrar un run en vivo en tu UI

Esperar diez segundos mirando un spinner se siente roto aunque no lo esté. Meteor entrega
el run como **Server-Sent Events**, y esta receta lo lleva hasta el navegador.

El punto que no se puede saltar: **la key es un secreto de servidor.** El navegador nunca
habla con la API de Meteor; habla con tu backend, y tu backend retransmite.

## Antes de empezar

Tu key necesita `runs:execute`.

## 1. El relay en tu backend

```ts
import express from 'express';
import Met from '@meteor.ia/sdk';

const met = new Met(process.env.MET_API_KEY!, {
  workspaceId: Number(process.env.MET_WORKSPACE_ID),
});

const app = express();

app.get('/api/preguntar', async (req, res) => {
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');
  res.flushHeaders();

  try {
    for await (const ev of met.runs.stream({ input: String(req.query.q) })) {
      res.write(`event: ${ev.type}\ndata: ${JSON.stringify(ev.data)}\n\n`);
    }
  } catch (err) {
    res.write(`event: error\ndata: ${JSON.stringify({ message: 'se cortó' })}\n\n`);
  } finally {
    res.end();
  }
});

app.listen(3000);
```

El SDK te entrega objetos `{ type, data }` ya parseados. El stream emite un conjunto
cerrado de eventos:

| evento | cuándo |
|---|---|
| `run.started` | el run arrancó |
| `run.step` | un paso del Met, en versión curada |
| `run.output` | un fragmento de la respuesta |
| `run.completed` | terminó bien |
| `run.failed` | terminó con error |

## 2. El navegador

```html
<p id="respuesta"></p>
<script>
  const es = new EventSource('/api/preguntar?q=' + encodeURIComponent(pregunta));
  const destino = document.getElementById('respuesta');

  es.addEventListener('run.output', (e) => {
    destino.textContent += JSON.parse(e.data);   // llega por fragmentos
  });
  es.addEventListener('run.completed', () => es.close());
  es.addEventListener('run.failed', () => { destino.textContent = 'No pude responder.'; es.close(); });
</script>
```

`EventSource` reconecta solo cuando se cae la conexión, y ese comportamiento aquí juega en
contra: reconectar a `/api/preguntar` **vuelve a ejecutar el Met**, con su costo. Por eso
se cierra explícitamente al terminar.

## 3. No pierdas el resultado si el stream se corta

Un stream cortado a la mitad no se reanuda de forma transparente. Si lo que estás
mostrando también tiene que quedar guardado, no dependas del stream para eso: el run
existe en la API con o sin stream.

```ts
const run = await met.runs.retrieve(runId);   // la fuente de verdad, sin apuro
```

El patrón para lógica que importa es usar el stream **solo para pintar** y confirmar
contra `retrieve()` cuando el usuario ya se fue. Si nunca guardas el resultado, un usuario
que cierra la pestaña a los ocho segundos deja un run que te cobraron y nadie leyó.

## Verlo desde la terminal

```bash
curl -N https://api.met.meteor.com.co/api/v1/workspaces/7/runs \
  -H "Authorization: Bearer $MET_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: text/event-stream" \
  -d '{"input":"Hola","stream":true}'
```

`-N` desactiva el buffering: sin él ves todo junto al final y parece que el streaming no
funciona.

## Y después

- El esquema de eventos y qué expone `run.step`: [Streaming (SSE)](streaming.html).
- Para eventos del workspace en vez de los de un run: [Eventos en vivo](eventos.html).
