Show a run live in your UI
Relay the Met's stream from your backend to the browser, word by word, without exposing the key and without losing the result if the stream drops.
Waiting ten seconds at a spinner feels broken even when it isn't. Meteor delivers the run as Server-Sent Events, and this recipe carries it all the way to the browser.
The part you can't skip: the key is a server-side secret. The browser never talks to Meteor's API; it talks to your backend, and your backend relays.
Before you start
Your key needs runs:execute.
1. The relay in your backend
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/ask', 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: 'dropped' })}\n\n`);
} finally {
res.end();
}
});
app.listen(3000);
The SDK hands you parsed { type, data } objects. The stream emits a closed set of events:
| event | when |
|---|---|
run.started | the run began |
run.step | a step of the Met, in curated form |
run.output | a fragment of the answer |
run.completed | finished cleanly |
run.failed | finished with an error |
2. The browser
<p id="answer"></p>
<script>
const es = new EventSource('/api/ask?q=' + encodeURIComponent(question));
const target = document.getElementById('answer');
es.addEventListener('run.output', (e) => {
target.textContent += JSON.parse(e.data); // arrives in fragments
});
es.addEventListener('run.completed', () => es.close());
es.addEventListener('run.failed', () => { target.textContent = "Couldn't answer."; es.close(); });
</script>
EventSource reconnects on its own when the connection drops, and here that works against you: reconnecting to /api/ask runs the Met again, at its cost. That's why it closes explicitly when it's done.
3. Don't lose the result if the stream drops
A stream cut in half doesn't resume transparently. If what you're showing also has to be stored, don't rely on the stream for that: the run exists in the API with or without one.
const run = await met.runs.retrieve(runId); // the source of truth, unhurried
The pattern for logic that matters is to use the stream only to paint and confirm against retrieve() after the user is gone. If you never store the result, a user who closes the tab at eight seconds leaves behind a run you paid for and nobody read.
Watching it from the terminal
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":"Hello","stream":true}'
-N turns off buffering: without it you see everything at once at the end and it looks like streaming isn't working.
Next
- The event schema and what
run.stepexposes: Streaming (SSE). - For workspace events rather than a run's: Live events.