a13n
AI 에이전트를 만들고 클라우드에서 실행합니다
WebTransport 기반 실시간 앱, 하나의 연결로 독립 스트림·비신뢰형 데이터그램, 메시지별 신뢰성 선택
Real-time apps over WebTransport. One connection, independent streams and unreliable datagrams, reliability chosen per message.
개발 도구TypeScript · ShellGitHub @transport-io온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기Typed real-time for WebTransport. Reliable streams and unreliable datagrams on one connection, chosen per message in the contract.
저장소 README에서 가져옴
README 전체를 불러오는 중입니다.
README 전문 보기 ↗Real-time apps over WebTransport. Socket.IO's shape, on a transport with multiple streams and datagrams. Two lanes, one contract, one fallback.
import { defineContract, reliable, unreliable } from 'transport-io'
import { z } from 'zod'
export const teaser = defineContract({
chat: reliable(z.object({ room: z.string(), body: z.string() })),
cursor: unreliable(z.object({ x: z.number(), y: z.number() })),
})
chat will arrive. cursor may not. The contract is the only place that says so, and both
sides infer from it. The reliable lane is carried on QUIC streams and the unreliable lane on
QUIC datagrams.
Two streams on one session, one of them stopped, the other not noticing. Each stream() is
its own QUIC stream, so the stop is a reset on one of them and costs the other nothing. That
page is examples/chat, and both lanes are one command away with no project
and no certificate:
npx transport-io dev --demo
Framing, length prefixes, buffer accumulation and stream lifecycle are handled for you. Reliability is declared in the contract, so "this message may be dropped" is visible in the type system.
// contract.ts - the whole surface, in one file
import { defineContract, type MapOf, reliable, rpc, streaming, unreliable } from 'transport-io'
export const contract = defineContract({
chat: reliable<{ from: string; body: string }>(),
cursor: unreliable<{ x: number; y: number }>(),
save: rpc<{ text: string }, { revision: number }>(),
ask: streaming<{ prompt: string }, string>(),
})
export interface AppMap extends MapOf<typeof contract> {}
reliable and unreliable take the payload. rpc and streaming take the payload and
what comes back. bytes() in any slot carries a Uint8Array as bytes, never through JSON.
Write the AppMap interface line. Without it, every hover shows the whole contract with
your validator's internals in it. AppMap is what each end is given,
browserClient<AppMap> and createServer<AppMap> below, and never inferred from
contract. Registering the map globally instead is opt-in; see
Registering the map.
A type argument describes the payload and costs nothing at runtime: nothing checks it, so a peer that sends the wrong shape reaches your handler. Pass a Standard Schema validator instead - zod, valibot, arktype - and every inbound payload, replies and stream elements included, is validated on arrival, at one check per message:
import { defineContract, reliable, unreliable } from 'transport-io'
import { z } from 'zod'
export const validated = defineContract({
chat: reliable(z.object({ from: z.string(), body: z.string().max(2000) })),
cursor: unreliable(z.object({ x: z.number(), y: z.number() })),
})
The payload types are inferred either way; nothing downstream changes. Use a schema wherever an untrusted peer can reach, which for a server is every client. Use a type argument where both ends are yours and the traffic is high, such as cursor positions at pointer rate.
// server
import { createServer } from 'transport-io'
import { listenHttp3 } from 'transport-io/node-transport'
// `cert` and `privKey` are the PEM text, not paths to it.
export async function serve(cert: string, privKey: string): Promise<void> {
const server = createServer<AppMap>({ contract })
server.handle('save', async ({ text }) => ({ revision: text.length }))
server.handle('ask', async function* ({ prompt }) {
for (const word of prompt.split(' ')) yield word
})
server.onSession((peer) => {
void peer.join('lobby')
peer.on('chat', (msg) => void server.to('lobby').emit('chat', msg))
peer.on('cursor', (pos) => void server.to('lobby').except(peer.id).emit('cursor', pos))
})
const listener = await listenHttp3({ port: 4433, host: '127.0.0.1', cert, privKey, path: '/' })
await server.listen(listener)
}
listen(listener) owns the accept loop, and a handshake that fails is counted in
server.acceptErrors rather than thrown away. Call listen() with no argument and drive
accept() yourself only when a connection has to be inspected before it is accepted.
// client
import { browserClient } from 'transport-io/browser-transport'
export async function run(url: string): Promise<number> {
// No `certificateHash`, so the certificate is validated against the platform's CA store
// like any other HTTPS origin. Pass one only to pin a self-signed certificate in
// development, which is what `transport-io dev` sets up for you.
const client = await browserClient<AppMap>({ contract, url })
client.emit('chat', { from: 'me', body: 'hello' }) // arrives
client.emit('cursor', { x: 12, y: 40 }) // may not
const { revision } = await client.call('save', { text: 'hi' })
return revision
}
browserClient resolves to a connected client; devClient and http3Client are the same
shape for local development and for Node. Use new Client({ contract, connect }) when you
need the client before it is connected: your own transport, React, or a page that renders
connecting. There is no default call timeout; pass AbortSignal.timeout(ms) for one.
An event can answer with a sequence instead of a value. Declare yields instead of
returns, write an async generator, consume an async iterable:
import type { Client } from 'transport-io'
export async function render(client: Client<AppMap>, prompt: string, show: (t: string) => void): Promise<void> {
for await (const token of client.stream('ask', { prompt })) {
show(token)
}
}
The loop ends when the server stops. Leaving it early resets the QUIC stream and the
handler's finally runs. The producer runs at most 32 frames ahead of what the consumer has
taken. Stopping from a button, deadlines and the helpers are in the
guide.
Where UDP does not get through, or the browser has no WebTransport, a WebSocket fallback
carries the emit lane and nothing else: withFallback on the client, server.withFallback
on the server, and an unreliable event crosses it only where the contract declares
fallback: 'newest'. The fallback
has the whole thing.
A wrong event name or payload fails to compile. The error names the event instead of unrolling the contract type.
React: @transport-io/react is the
binding, published separately.
For your own project, transport-io dev ./server.ts mints the certificate, publishes its
hash for the browser to pin, and hands the certificate to your server. It does not bundle
your browser code: keep running your own bundler and point --static at the output.
None of these are going to change. KNOWN-ISSUES.md has the reasoning
and the measurements behind each one, and is worth reading before you build on this.
Not in this version: namespaces (a room-name prefix covers it), presence, middleware chains (auth is one hook), server-initiated streaming (a response shape only), and the Redis adapter.
Socket.IO has supported WebTransport since 4.7.0, released in June 2023. Everything in this section comes from Socket.IO's own documentation and source, linked at the end, so each line can be checked.
Where Socket.IO is the better choice. Socket.IO falls back to WebSocket and then to HTTP
long-polling with everything it offers intact, so Safari works and so do networks that block
UDP; transport-io's fallback carries emits and declared unreliable events, and nothing else.
For calls or streams on Safari, or where UDP cannot reach your server, use Socket.IO. Socket.IO
also guarantees ordering across a transport upgrade, buffers client events across a
reconnection, offers at-least-once client-to-server delivery with retries, and has
namespaces, middleware, a Redis adapter and years of production use. transport-io starts a new
session on reconnect, keeps no buffer, and has rooms and an in-memory adapter.
What is the same. Both servers need the same native QUIC package,
@fails-components/webtransport, because Node has no WebTransport of its own. Both are bound
by the same rule for a pinned self-signed certificate: ECDSA, at most fourteen days.
Neither reaches Safari over WebTransport.
The differences that can be checked in their source:
| Socket.IO | transport-io | |
|---|---|---|
| Streams | One bidirectional stream per session, carrying every Engine.IO packet behind a length header. The protocol forbids a client from opening a second one. | The emit lane on one stream per direction, and a fresh bidirectional stream for every call and stream. |
| Datagrams | Not used. | The unreliable lane. |
| Acknowledgements | An incrementing id per request and a map from id to callback on each side; the ACK packet carries the id back. | The stream is the correlation. No id, no map. |
| Cancelling a request in flight | timeout() rejects locally after a delay and removes the map entry. Nothing is sent to the peer, whose handler continues. | An AbortSignal or cancel() resets the QUIC stream. The responder's signal fires and its generator's finally runs. |
| Reliability in the types | Events are typed as function signatures and nothing marks delivery. volatile is a runtime modifier: the event is discarded if the transport is not writable at that moment, and sent normally otherwise. | reliable or unreliable on each event in the contract. The lane is a type. |
| Head-of-line blocking | Every packet on the session is one ordered sequence, so a lost packet stalls what is queued behind it. | Each call and each stream is its own QUIC stream; one stalled call does not delay another. |
| Typed setup | Four interfaces, ServerToClientEvents, ClientToServerEvents, InterServerEvents and SocketData, with the two event interfaces passed in opposite order on the server and the client. | One contract, one MapOf line, and the same name at each end. |
Sources: the 4.7.0 changelog, the
WebTransport guide, the
Engine.IO protocol and its
client
and server
transports, the parser
that writes the length header, the
client
and server
socket classes for acknowledgements and timeout(), the
emitting events page for volatile, the
TypeScript guide, and the
delivery guarantees page.
npm install transport-io
Browsers need nothing extra. The server needs a native QUIC package, and the details below are what will surprise you in CI.
Installing from git does not work. The repository root is a private package called
transport-io-monorepo, so npm install github:transport-io/transport-io installs that
instead and import … from 'transport-io' fails. To work on the library itself, clone it:
git clone https://github.com/transport-io/transport-io
cd transport-io && npm install && npm run build
The server also needs the native QUIC transport, which is a separate, deliberate install:
npm install @fails-components/webtransport-transport-http3-quiche
It is not a dependency of anything, only a dynamic import, so no package manager will pull it in for you.
Two things about that native package:
trixie variant or Ubuntu 24.04. There is no musl build at all, so Alpine
falls back to a source compile.Consumers need Node 22 or newer, and TypeScript 5.0 or newer - gated by const type
parameters in the public types, and checked in CI.
Contributors additionally need Bun and openssl on PATH. Bun runs
the unit tests, the documentation gate and the example build; openssl mints the
short-lived certificate the browser needs for WebTransport. Neither is optional: npm install succeeds without Bun and then the first git commit fails, because the hooks shell
out to it. Development is supported on macOS and Linux only - Windows contributors should
use WSL.
KNOWN-ISSUES.md - read this before you start: what this library
refuses to do and will not changePROTOCOL.md - the wire format, written to be implemented in another languageAPI.md - the TypeScript surfaceDECISIONS.md - every question this project raised, answeredADR/ - the records a future contributor would want to reverseexamples/chat - both lanes in one page, and two streaming calls
running at once in another, where stopping one leaves the other untouchedexamples/react - the same chat on @transport-io/react, under ViteMemoryAdapter is the default; transport-io/testing exports HostileAdapter for testing
an adapter of your ownMIT, for the source code. The name and the marks in assets/brand are
covered separately by assets/brand/LICENSE: use them to refer to
this project, not as the mark of your own.