Best viewed on desktop Mobile Friendly tutorial coming soon!

Single-slot SSE

⚠️ If the preview looks stale — reload it
This lesson runs two apps in the preview, and it doesn't always refresh on its own. After you Solve a step, switch lessons, or if a notice's × button stops working, hit reload on the preview — or reload the whole page. Everything also works if you download the lesson and run it locally.

So far the host pulled a remote once with <micro-frame>. Sometimes you want the remote to push — a live stream of updates over one connection. That’s Server-Sent Events (SSE), and app federation handles it with a second pair of tags:

  • <micro-frame-sse> is the faucet. It opens one SSE stream to a URL and renders nothing itself — it just feeds slots. Put it near the top so the stream starts early.
  • <micro-frame-slot> is a labeled drop-box. Each streamed chunk carries a label (a slot id); the chunk lands in the box whose slot matches. The slot’s from ties it back to the faucet’s name.

Open the remote’s code in the file tree to follow along — remote/src/routes/remote-app-sse-single-slot/+handler.js. It reads a mock SSE feed (remote/src/routes/api), turns that byte stream into an event stream with createMessageEmitter (remote/src/message-emitter.js), and streams back rendered notices as SSE frames tagged with the slot id. On the host, the read function turns each raw event into [slotId, html, isDone]:

<micro-frame-sse timeout=0 name="notices"
src=`http://localhost:3001/remote-app-sse-single-slot?dt=${Date.now()}&slotName=main`
read(ev) { return [ev.lastEventId, JSON.parse(ev.data), false]; }
/>

The faucet is already in the page. Your job: add the box. Drop a <micro-frame-slot> where the notices should appear, wire it to the faucet, and give it a loading placeholder:

  1. Add <micro-frame-slot from="notices" slot="main" timeout=0 client-reorder> where the comment is.
  2. Give it a <@loading> message and a <@catch|err|> fallback.

Load the page: five notices stream in one by one, each into that same slot, accumulating as they arrive. client-reorder lets the page paint immediately and fill the slot as chunks land. Each notice is a real interactive component — dismiss any of them with its ×, independently.

How the notices stream (and stay independent)

Look at remote/src/routes/remote-app-sse-single-slot/+page.marko. It renders one page, but that page recurses:

<define/Wait>
<try>
<await|messages|=Promise.any([once(messageStream, "done"), once(messageStream, "data")])>
<if=messages && Array.isArray(messages) && messages.length>
<Notice message=messages[0].message/>
<Wait/> <!-- render the next one -->
</if>
</await>
<@placeholder>Waiting for the stream…</@placeholder>
</try>
</>
<Wait/>

<define> is Marko 6’s replacement for Marko 5’s <macro> — a reusable snippet, here named Wait. It <await>s the next stream event; on a data event it renders one <Notice> and then invokes itself (<Wait/>), so the next notice nests inside the last. On done, the <if> fails and the recursion stops.

The payoff: every notice is a distinct branch of one render tree, so each gets its own client-side identity and hydrates independently — that’s why each × dismisses only its own notice. (Rendering the same component as five separate top-level renders would make them collide.)

The render API, and renderId

The handler streams that page with template.render(...). In Marko 5 the book used a separate template.stream(...) here — this SSE page is about the only place that API shows up. Marko 6 removed template.stream and folded it into template.render, which returns a value you can either for await (→ a stream of chunks, used here) or await (→ a finished string).

The render also sets a renderId on $global, which namespaces the whole render’s scope ids. With a single render it isn’t doing much yet — the next lesson renders several separate fragments onto one page, and that’s where renderId (Marko 5’s componentIdPrefix) earns its keep.

Powered by WebContainers Built with TutorialKit Built with ❤️ by defunkt-dev
Files
Preparing Environment
  • Installing the host app
  • Installing the remote app
  • Building the remote, then starting both apps