Transport
The transport layer handles how messages are sent and received between clients and servers. Teleportal’s transport system is built on async iterables, making it composable and allowing multiple transports to be chained together.
Why Async Iterables?
Section titled “Why Async Iterables?”Teleportal uses async iterables (AsyncIterable<Message[]>) as the foundation for its transport layer rather than raw callbacks, event emitters, or Web Streams. This is a deliberate architectural choice that enables three properties:
- Composability: Transports are just iterables and write functions, so middleware (encryption, rate limiting, logging, validation) can be layered by wrapping one transport in another. Each layer is independent and testable in isolation. You can stack them in any order without the middleware needing to know about each other.
- Backpressure: The pull-based async iterable model naturally pauses the producer when the consumer is slow. The consumer pulls the next batch only when it is ready, preventing unbounded memory growth from buffered messages.
- Runtime portability: Async iterables are a language-level primitive available in every JavaScript runtime — browsers, Bun, Deno, Node.js, and Cloudflare Workers. By building on a language standard rather than a runtime-specific abstraction, the transport layer works everywhere without polyfills.
This means that adding a new transport (say, WebTransport or a custom binary protocol) only requires implementing an async iterable source and a write function — all existing middleware automatically applies to it.
Overview
Section titled “Overview”A Transport combines a Source (for reading messages) and a Sink (for writing messages):
type Source<Context> = { source: AsyncIterable<Message<Context>[]>;};
type Sink<Context> = { write(message: Message<Context>): void | Promise<void>; close(): void;};
type Transport<Context> = Source<Context> & Sink<Context>;The source yields batches (arrays) of messages — preserving the natural batching of the underlying transport (a single WebSocket frame might carry multiple protocol messages). The write function sends a single message. The close function tears down the transport.
Architecture
Section titled “Architecture”The transport system follows a composable architecture:
- Base transports handle the actual communication (HTTP, SSE, WebSocket, PubSub)
- Middleware transports wrap other transports to add functionality (encryption, rate limiting, logging, validation)
- Utility functions help compose, connect, and transform transports
Rate Limiting
Section titled “Rate Limiting”Throttles messages using a token bucket algorithm:
import { withRateLimit } from "teleportal/transports/rate-limiter";
const rateLimitedTransport = withRateLimit(transport, { rules: [ { id: "per-user", maxMessages: 100, windowMs: 1000, trackBy: "user", }, ],});Message Encryption
Section titled “Message Encryption”Wraps a transport with content-level end-to-end encryption:
import { getEncryptedTransport } from "teleportal/transports";
const encryptedTransport = getEncryptedTransport(handler);You rarely call this directly — the Provider applies it automatically because content encryption is the default. Pass an encryptionKey (a CryptoKey) when creating the provider, or encryptionKey: false to opt a document out into plaintext. The encrypted transport keeps the CRDT structure update in plaintext (so the server can still merge and sync) and encrypts only the document content into sidecars.
Message Validation
Section titled “Message Validation”Adds authorization checks to message reading and writing:
import { withMessageValidator } from "teleportal/transports";
const validatedTransport = withMessageValidator(transport, { isAuthorized: async (message, type) => { // Your authorization logic. Note: for server-side transports the framework // always passes type "write" (both directions are checked with "write"), // so derive read/write from the message payload, not the `type` argument. return true; },});Logging
Section titled “Logging”Logs all messages for debugging:
import { withLogger } from "teleportal/transports";
const loggedTransport = withLogger(transport);ACK Support
Section titled “ACK Support”Adds acknowledgment message support for reliable message delivery:
import { withAckSink, withAckTrackingSink } from "teleportal/transports";
// Server: Send ACKs automaticallyconst ackSink = withAckSink(sink, { pubSub, ackTopic: "acks", sourceId: "server-1",});
// Client: Track ACKsconst trackingSink = withAckTrackingSink(sink, { pubSub, ackTopic: "acks", sourceId: "client-1", ackTimeout: 10000,});Transport Composition
Section titled “Transport Composition”Transports can be composed in layers:
// Base transportlet transport = getBaseTransport();
// Add encryptiontransport = getEncryptedTransport(handler);
// Add rate limitingtransport = withRateLimit(transport, { rules: myRules });
// Add loggingtransport = withLogger(transport);
// Add message validationtransport = withMessageValidator(transport, { isAuthorized: async (message, type) => { // Authorization logic return true; },});Y.js Document Transport
Section titled “Y.js Document Transport”The YDoc transport integrates Y.js documents with the Teleportal transport system:
import { getYTransportFromYDoc } from "teleportal/transports";
const transport = getYTransportFromYDoc({ ydoc, awareness, document: "my-document", context: { clientId: "client-1" },});
// Start synchronizationawait transport.handler.start();Utility Functions
Section titled “Utility Functions”Compose
Section titled “Compose”Combines a Source and Sink into a Transport:
import { compose } from "teleportal/transports";
const transport = compose(source, sink);Connect
Section titled “Connect”Drains all messages from a source and writes them to a sink (one-directional):
import { connect } from "teleportal/transports";
await connect(source, sink);Bidirectionally connects two transports — each transport’s source feeds the other’s sink:
import { sync } from "teleportal/transports";
await sync(transportA, transportB);Transform Helpers
Section titled “Transform Helpers”Transform messages flowing through a source. Each helper takes the per-item
function and returns a transform (source) => source that operates over the
batched AsyncIterable (a fully-filtered batch is skipped, never yielded empty):
import { mapMessages, filterMessages, flatMapMessages } from "teleportal/transports";
// Transform each message (return null/undefined to drop it)const mapped = mapMessages((msg) => transform(msg))(source);
// Drop messages that don't match a predicateconst filtered = filterMessages((msg) => msg.type === "doc")(source);
// Expand each message into zero or more outputsconst expanded = flatMapMessages((msg) => [msg, derivedMsg])(source);Binary Conversion
Section titled “Binary Conversion”Convert between message-level and binary transports:
import { toBinaryTransport, fromBinaryTransport } from "teleportal/transports";
// Message transport → binary (for wire transmission)const binaryTransport = toBinaryTransport(transport, context);
// Binary transport → message (for protocol processing)const messageTransport = fromBinaryTransport(binaryTransport, context);Best Practices
Section titled “Best Practices”- Compose from bottom up: Start with base transports, then add middleware
- Handle errors: Transports can error, ensure proper error handling
- Clean up resources: Call
close()when done - Use appropriate transports: Choose transports based on your use case
- Rate limit client transports: Always rate limit client-side transports to prevent abuse
- Validate messages: Use message validators for authorization checks
- Monitor with logging: Use logger transport during development
Next Steps
Section titled “Next Steps”- Server - Learn how the server uses transports
- Provider - See how clients use transports
- Advanced: Custom Transport - Implement your own transport