transport-io
One connection. Independent streams and unreliable datagrams. Choose reliability per message.
Getting startedRead the limitations first

stream() is its own QUIC stream, so the stop is a reset on one of them and costs the other nothing.The whole thing
Section titled “The whole thing”One file says what every event is and whether it can be dropped. Both ends import it.
import { defineContract, type MapOf, reliable, unreliable } from 'transport-io'
export const contract = defineContract({ chat: reliable<{ from: string; body: string }>(), cursor: unreliable<{ x: number; y: number }>(),})
export interface AppMap extends MapOf<typeof contract> {}import { createServer } from 'transport-io'import { listenDev } from 'transport-io/node-transport'
const server = createServer<AppMap>({ contract })
server.onSession((peer) => { void peer.join('lobby') peer.on('chat', (m) => void server.to('lobby').emit('chat', m))})
await server.listen(await listenDev())import { devClient } from 'transport-io/dev-transport'
const client = await devClient<AppMap>({ contract })
client.on('chat', (m) => console.log(m.from, m.body))
client.emit('chat', { from: 'me', body: 'hi' }) // arrivesclient.emit('cursor', { x: 12, y: 40 }) // may notchat arrives. cursor may not. The contract is the only place that says so, and both
sides infer from it: a wrong event name or payload does not compile.
AppMap is passed once on each end, the way you would pass a router type to a typed client.
Streaming responses
Section titled “Streaming responses”The server yields, the client consumes, and the loop ends when the server stops. Stopping
early is a QUIC stream reset: the handler’s ctx.signal fires and its finally runs.
import { type Client, defineContract, type MapOf, streaming } from 'transport-io'
const gen = defineContract({ ask: streaming<{ prompt: string }, string>(),})interface GenMap extends MapOf<typeof gen> {}
declare const gpt: Client<GenMap>declare const prompt: string
for await (const token of gpt.stream('ask', { prompt })) { console.log(token)}import type { Server } from 'transport-io'
declare const gptServer: Server<GenMap>declare function model(text: string): AsyncIterable<string>
gptServer.handle('ask', async function* ({ prompt }) { for await (const token of model(prompt)) { yield token }})Each stream() and each call() owns its own QUIC stream, so a stalled one does not block
the others.
Run it locally in one command: npx transport-io dev —demo,
then open the printed URL in two tabs. There is no live demo on this page: the server needs
raw UDP ingress, which GitHub Pages does not have.
Before you start
Section titled “Before you start”These are properties of the library, not things it will grow out of.
- Chrome and Firefox over WebTransport. Safari establishes a session and then never sends; the fallback reaches it after 5 seconds.
- One fallback, emits only. A WebSocket carries the emit lane where the contract declares it; calls and streams need WebTransport.
- It needs raw UDP ingress to your process. Many managed platforms do not provide it. Check yours before building on this.
0.x, so a minor bump may break you. A minor release is allowed to change the wire, so two peers on different minors may refuse each other. Pin the minor.