Types
The types shared by both ports: the frame model that every codec encodes, the constants that limit it, and the callable types the peer accepts as hooks.
Field names are not abbreviated and are snake_case in both languages: last_stream is last_stream in TypeScript too, because it is a wire name.
Frame
One logical protocol unit, and one WebSocket message. Python's is a frozen slotted dataclass compared by value; TypeScript's is a readonly interface compared with framesEqual.
A frame has twelve fields; most frames set three or four. A field at its default is omitted from the envelope, so the JSON form of a ping is {"type":"ping","nonce":"771c698c25d5c0d1"}: 42 bytes and two keys.
The envelope fields
| Field | Type | Default | What it carries |
|---|---|---|---|
type | str / string | required | The frame type. A v1 peer sends open, data, reset, ping, pong or goaway. Always emitted, even though it has no default to compare against. |
stream | int | None / number | null | None / null | Which stream this frame belongs to. Absent on ping, pong and goaway, which are connection-level. Odd ids are the dialer's, even ids the acceptor's. |
payload | Any / unknown | Absent | ABSENT | The application value. The only field whose default is not null: ABSENT omits the key, while None / null emits "payload": null. "No payload" and "a payload that is null" are different frames; the sentinel distinguishes them. |
fragment | str | bytes | None / string | ArrayBuffer | null | None / null | One slice of an encoded payload. A frame carries payload or fragment, never both. |
more | bool / boolean | False / false | True on every fragment but the last. The receiver concatenates until it sees a fragment without it. |
headers | dict[str, Any] | None / Record<string, unknown> | null | None / null | Per-stream metadata, sent once with the open. Carried on the first fragment and never split across fragments. |
end | bool / boolean | False / false | This side is done sending. A flag on a frame, not a frame type: there is no "end" frame type. Carried on the last fragment. |
trailers | dict[str, Any] | None / Record<string, unknown> | null | None / null | Metadata known only after the payloads, such as a row count or a checksum. Carried on the end frame, on its last fragment only. |
code | int | None / number | null | None / null | A ResetCode value, on reset and goaway. Carried as an integer, so a code this generation does not define is preserved. |
reason | str | None / string | null | None / null | Human-readable text accompanying code. Never parsed for meaning. |
nonce | str | None / string | null | None / null | Correlates a pong with the ping it answers. Echoed back verbatim. |
last_stream | int | None / number | null | None / null | On goaway: the highest stream id, of the remote's parity, that this peer has dispatched. Streams above it were not processed and can be retried elsewhere. |
Envelope keys are emitted in a fixed order — type, then stream, then the rest alphabetically — so that two implementations produce byte-identical output for the same frame.
Unknown keys arriving on the wire are dropped, not preserved. An unrecognised type is decoded as an ordinary Frame; the peer, not the codec, ignores it. A later generation can therefore add a field or a frame type without breaking a 1.0 peer.
Signature
@dataclass(frozen=True, slots=True)
class Frame:
type: str
stream: int | None = None
payload: Any = ABSENT
fragment: str | bytes | None = None
more: bool = False
headers: dict[str, Any] | None = None
end: bool = False
trailers: dict[str, Any] | None = None
code: int | None = None
reason: str | None = None
nonce: str | None = None
last_stream: int | None = Noneexport interface Frame {
readonly type: string;
readonly stream?: number | null;
readonly payload?: unknown | Absent;
readonly fragment?: string | ArrayBuffer | null;
readonly more?: boolean;
readonly headers?: Record<string, unknown> | null;
readonly end?: boolean;
readonly trailers?: Record<string, unknown> | null;
readonly code?: number | null;
readonly reason?: string | null;
readonly nonce?: string | null;
readonly last_stream?: number | null;
}Parameters
Every constructor parameter is an envelope field; see the table above for what each carries.
| Name | Type | Default | What it does |
|---|---|---|---|
type | str / string | required | The only field with no default. |
stream | int | None / number | null | None / null | See the field table. |
payload | Any / unknown | Absent | ABSENT | See the field table. In TypeScript, omitting the property and setting it to ABSENT mean the same thing. |
fragment | str | bytes | None / string | ArrayBuffer | null | None / null | See the field table. |
more | bool / boolean | False / false | See the field table. |
headers | dict[str, Any] | None / Record<string, unknown> | null | None / null | See the field table. |
end | bool / boolean | False / false | See the field table. |
trailers | dict[str, Any] | None / Record<string, unknown> | null | None / null | See the field table. |
code | int | None / number | null | None / null | See the field table. |
reason | str | None / string | null | None / null | See the field table. |
nonce | str | None / string | null | None / null | See the field table. |
last_stream | int | None / number | null | None / null | See the field table. Snake_case in both languages. |
All twelve are positional-or-keyword in Python, in the order above.
Return
Python: a frozen Frame instance; assigning to a field raises FrozenInstanceError. TypeScript: Frame is an interface, so an object literal is a frame.
Raises
Raises: nothing. Python's dataclass raises TypeError for a missing type or an argument that is not a field.
Example
import dataclasses
from muxws import ABSENT, Frame, JsonCodec
codec = JsonCodec()
opening = Frame("open", stream=1, payload={"path": "/reports"}, headers={"trace": "abc"})
print(codec.encode(opening))
closing = Frame("data", stream=1, payload={"rows": 3}, end=True, trailers={"checksum": "9f"})
print(codec.encode(closing))
print(codec.encode(Frame("ping", nonce="n-1")))
print(codec.encode(Frame("goaway", code=0, reason="restart", last_stream=4)))
# ABSENT omits the key; None emits it.
print(codec.encode(Frame("data", stream=1, payload=ABSENT, end=True)))
print(codec.encode(Frame("data", stream=1, payload=None, end=True)))
try:
setattr(opening, "type", "data") # noqa: B010 - the example shows that this fails
except dataclasses.FrozenInstanceError:
print("frames are frozen; dataclasses.replace() builds a new one instead")
print(dataclasses.replace(opening, type="data").type)import { ABSENT, type Frame, JsonCodec } from 'muxws';
const codec = new JsonCodec();
const opening: Frame = { type: 'open', stream: 1, payload: { path: '/reports' }, headers: { trace: 'abc' } };
console.log(codec.encode(opening));
console.log(codec.encode({ type: 'data', stream: 1, payload: { rows: 3 }, end: true, trailers: { checksum: '9f' } }));
console.log(codec.encode({ type: 'ping', nonce: 'n-1' }));
console.log(codec.encode({ type: 'goaway', code: 0, reason: 'restart', last_stream: 4 }));
// ABSENT omits the key; null emits it.
console.log(codec.encode({ type: 'data', stream: 1, payload: ABSENT, end: true }));
console.log(codec.encode({ type: 'data', stream: 1, payload: null, end: true }));ABSENT and Absent
The sentinel that distinguishes "no payload" from an explicit null. It is the default of Frame.payload, and it is what Assembler.feed returns while more fragments are expected.
Python's is a single module-level instance of a private class. It is falsy, so if frame.payload: is false for it, and its repr is ABSENT. copy.copy and copy.deepcopy return the same instance. TypeScript's is a unique symbol: no other value equals it, JSON serialization does not emit it, and it prints as Symbol(ABSENT). Absent is its type.
Compare with identity (is ABSENT / === ABSENT), not with ==.
Signature
ABSENT: Any = _Absent()export const ABSENT: unique symbol;
export type Absent = typeof ABSENT;Parameters
| Name | Type | Default | What it does |
|---|---|---|---|
| (none) | — | — | A value, not a callable. There is one instance per process. |
Return
Nothing — it is a constant.
Raises
Raises: nothing.
Example
import copy
from muxws import ABSENT, Frame, JsonCodec
print(repr(ABSENT), "| falsy:", not ABSENT)
print("one instance:", copy.deepcopy(ABSENT) is ABSENT)
codec = JsonCodec()
print("omitted:", codec.encode(Frame("data", stream=1, payload=ABSENT, end=True)))
print("explicit null:", codec.encode(Frame("data", stream=1, payload=None, end=True)))
print("round trip keeps them apart:", codec.decode(codec.encode(Frame("ping"))).payload is ABSENT)import { ABSENT, type Absent, JsonCodec } from 'muxws';
console.log(String(ABSENT));
const codec = new JsonCodec();
console.log('omitted:', codec.encode({ type: 'data', stream: 1, payload: ABSENT, end: true }));
console.log('explicit null:', codec.encode({ type: 'data', stream: 1, payload: null, end: true }));
console.log('round trip keeps them apart:', codec.decode(codec.encode({ type: 'ping' })).payload === ABSENT);
const nothing: Absent = ABSENT;
console.log('the type has one inhabitant:', nothing === ABSENT);V1_FRAME_TYPES
The frame types a v1 peer sends. A receiver ignores a frame whose type is not in this set; it is not an error, so a later generation can add a frame type without breaking a 1.0 peer.
window_update is reserved and unimplemented. There is no settings frame: no limit is negotiated on this wire.
TypeScript exports it from the package root; in Python, import it from muxws.frames.
Signature
V1_FRAME_TYPES: Final[frozenset[str]] = frozenset({"open", "data", "reset", "ping", "pong", "goaway"})export const V1_FRAME_TYPES: ReadonlySet<string>;Parameters
| Name | Type | Default | What it does |
|---|---|---|---|
| (none) | — | — | A constant set, not a callable. |
Return
Nothing — it is a constant. Python's is a frozenset; TypeScript's is a ReadonlySet (readonly to the type checker; the underlying Set is not frozen at runtime).
Raises
Raises: nothing.
Example
from muxws.frames import V1_FRAME_TYPES
print(sorted(V1_FRAME_TYPES))
print("window_update is reserved:", "window_update" in V1_FRAME_TYPES)
print("there is no settings frame:", "settings" in V1_FRAME_TYPES)import { V1_FRAME_TYPES } from 'muxws';
console.log([...V1_FRAME_TYPES].sort());
console.log('window_update is reserved:', V1_FRAME_TYPES.has('window_update'));
console.log('there is no settings frame:', V1_FRAME_TYPES.has('settings'));to_mapping / toMapping
Renders a frame as an envelope: a plain dict / object for a codec to serialize. Every field at its default is omitted; keys are ordered type, then stream, then the rest alphabetically.
Codecs call this function. An application can call it to inspect a frame without a codec.
Signature
def to_mapping(frame: Frame) -> dict[str, Any]: ...export function toMapping(frame: Frame): Record<string, unknown>;Parameters
| Name | Type | Default | What it does |
|---|---|---|---|
frame | Frame | required | The frame to render. It is not mutated. |
Return
A new dict[str, Any] / Record<string, unknown> holding only the fields that differ from their defaults, plus type always. payload appears when it is anything other than ABSENT, including None / null.
Raises
Raises: nothing.
Example
from muxws import ABSENT, Frame, to_mapping
print(to_mapping(Frame("ping", nonce="n-1")))
print(to_mapping(Frame("open", stream=3, payload={"q": 1}, headers={"trace": "abc"}, end=True)))
print(to_mapping(Frame("data", stream=3, payload=ABSENT, end=True)))
print(to_mapping(Frame("data", stream=3, payload=None, end=True)))
print("key order:", list(to_mapping(Frame("reset", stream=3, code=1, reason="stop"))))import { ABSENT, toMapping } from 'muxws';
console.log(toMapping({ type: 'ping', nonce: 'n-1' }));
console.log(toMapping({ type: 'open', stream: 3, payload: { q: 1 }, headers: { trace: 'abc' }, end: true }));
console.log(toMapping({ type: 'data', stream: 3, payload: ABSENT, end: true }));
console.log(toMapping({ type: 'data', stream: 3, payload: null, end: true }));
console.log('key order:', Object.keys(toMapping({ type: 'reset', stream: 3, code: 1, reason: 'stop' })));from_mapping / fromMapping
Builds a Frame from a decoded envelope. The inverse of to_mapping / toMapping, with two differences: unknown keys are dropped rather than preserved, and an unrecognised type is kept as an ordinary frame.
Because unknown keys are dropped, decode(encode(frame)) == frame cannot pass on a frame carrying extra keys.
Signature
def from_mapping(mapping: Mapping[str, Any]) -> Frame: ...export function fromMapping(mapping: Record<string, unknown>): Frame;Parameters
| Name | Type | Default | What it does |
|---|---|---|---|
mapping | Mapping[str, Any] / Record<string, unknown> | required | A decoded envelope. Keys that are not envelope fields are ignored. When payload is absent from the mapping, the frame's payload becomes ABSENT. |
Return
A Frame.
Raises
ProtocolError when the mapping has no type key, when it carries both payload and fragment (a frame carries one or the other), or when an envelope field has a value of the wrong type:
| Field | Accepted values |
|---|---|
type | a string |
stream, last_stream | a non-negative integer, or null |
code | an integer, or null |
fragment | a string or bytes (ArrayBuffer), or null |
more, end | a boolean |
headers, trailers | a mapping with string keys (a plain object), or null |
reason, nonce | a string, or null |
payload accepts any value. In Python an integral float in an integer field (1.0) is returned as int; a JavaScript decoder does not distinguish 1.0 from 1. A codec decoding a message through this function raises the same ProtocolError, which the peer treats as a message it cannot decode (WSM-FRM-005).
Example
from muxws import ABSENT, from_mapping, ProtocolError
frame = from_mapping({"type": "data", "stream": 3, "payload": {"n": 1}, "end": True, "future_field": "ignored"})
print(frame.type, frame.stream, frame.payload, frame.end)
print("unknown keys are dropped:", not hasattr(frame, "future_field"))
print("unknown types survive:", from_mapping({"type": "window_update", "stream": 3}).type)
print("no payload key means ABSENT:", from_mapping({"type": "ping"}).payload is ABSENT)
for bad in ({"stream": 1}, {"type": "data", "payload": 1, "fragment": "a"}):
try:
from_mapping(bad)
except ProtocolError as exc:
print("rejected:", exc)import { ABSENT, ProtocolError, fromMapping } from 'muxws';
const frame = fromMapping({ type: 'data', stream: 3, payload: { n: 1 }, end: true, future_field: 'ignored' });
console.log(frame.type, frame.stream, frame.payload, frame.end);
console.log('unknown keys are dropped:', !('future_field' in frame));
console.log('unknown types survive:', fromMapping({ type: 'window_update', stream: 3 }).type);
console.log('no payload key means ABSENT:', fromMapping({ type: 'ping' }).payload === ABSENT);
[{ stream: 1 }, { type: 'data', payload: 1, fragment: 'a' }].forEach((bad) => {
try {
fromMapping(bad);
} catch (error) {
if (error instanceof ProtocolError) console.log('rejected:', error.message);
}
});framesEqual
TypeScript only. Structural equality over the twelve envelope fields, with defaults normalised first, so a frame written { type: 'ping' } compares equal to one written { type: 'ping', stream: null, more: false }.
Python has no equivalent: Frame is a frozen dataclass, and == compares by value.
ArrayBuffers are compared byte by byte, so a round-tripped bytes payload compares equal to the original.
Signature
export function framesEqual(a: Frame, b: Frame): boolean;Parameters
| Name | Type | Default | What it does |
|---|---|---|---|
a | Frame | required | The left frame. |
b | Frame | required | The right frame. |
Return
true when every one of the twelve fields is equal after defaults are normalised, otherwise false.
Raises
Raises: nothing.
Example
import { JsonCodec, framesEqual } from 'muxws';
console.log(framesEqual({ type: 'ping' }, { type: 'ping', stream: null, more: false, end: false }));
console.log(framesEqual({ type: 'ping' }, { type: 'pong' }));
const codec = new JsonCodec();
const original = { type: 'data' as const, stream: 3, payload: { rows: [1, 2, 3] }, end: true };
console.log('round trip:', framesEqual(original, codec.decode(codec.encode(original))));MAX_FRAME_BYTES
The largest encoded message a sender may emit: 65 536 bytes, exactly 64 KiB.
It is a protocol constant, not a setting. It is not negotiated, not announced, not read from configuration and not configurable. The cap limits how long one stream can occupy a socket that has a single global message order: a 1 MB frame would delay every other stream until it was sent.
A receiver accepts any message up to this size and may accept larger ones; a sender always fragments at this size, whatever the remote accepts.
The peer's constructor takes a max_frame_bytes / maxFrameBytes that defaults to this value. The conformance runner lowers it to exercise fragmentation with small fixtures. It is not a tuning parameter.
Signature
MAX_FRAME_BYTES: Final[int] = 65_536export const MAX_FRAME_BYTES = 65_536;Parameters
| Name | Type | Default | What it does |
|---|---|---|---|
| (none) | — | — | A constant, not a callable. |
Return
Nothing — it is a constant int / number.
Raises
Raises: nothing.
Example
from muxws import encoded_length, Frame, JsonCodec, MAX_FRAME_BYTES, split_frame
print(MAX_FRAME_BYTES, "bytes =", MAX_FRAME_BYTES // 1024, "KiB")
codec = JsonCodec()
big = Frame("data", stream=1, payload={"blob": "x" * 200_000}, end=True)
fragments = split_frame(big, MAX_FRAME_BYTES, codec)
print("fragments:", len(fragments))
print("largest:", max(encoded_length(codec.encode(part)) for part in fragments), "<=", MAX_FRAME_BYTES)import { type Frame, JsonCodec, MAX_FRAME_BYTES, encodedLength, splitFrame } from 'muxws';
console.log(MAX_FRAME_BYTES, 'bytes =', MAX_FRAME_BYTES / 1024, 'KiB');
const codec = new JsonCodec();
const big: Frame = { type: 'data', stream: 1, payload: { blob: 'x'.repeat(200_000) }, end: true };
const fragments = splitFrame(big, MAX_FRAME_BYTES, codec);
console.log('fragments:', fragments.length);
const largest = Math.max(...fragments.map((part) => encodedLength(codec.encode(part))));
console.log('largest:', largest, '<=', MAX_FRAME_BYTES);__version__ / VERSION
The installed library version, as a string. Python names it muxws.__version__; TypeScript names it VERSION and exports the same string from all three entry points: muxws, muxws/node and muxws/msgpack.
The two packages share one version number: the test suite asserts that this value equals the version field of package.json, so a Python 0.1.0 and an npm 0.1.0 are the same release.
It is not the protocol version and is never sent on the wire. The only version on the wire is the v1 in the muxws.v1.<codec> subprotocol, which changes only for a breaking wire change. A 1.7.0 peer and a 0.1.0 peer both speak muxws.v1.json, and neither learns the other's package version.
Signature
__version__: strexport const VERSION: string;Parameters
| Name | Type | Default | What it does |
|---|---|---|---|
| (none) | — | — | A constant, not a callable. |
Return
Nothing — it is a constant str / string.
Raises
Raises: nothing.
Example
import muxws
from muxws.subprotocol import PREFIX
print(muxws.__version__)
print("three components:", len(muxws.__version__.split(".")) == 3)
# The package version is not the wire version; the wire version is in the subprotocol name.
print("the wire says:", f"{PREFIX}json")import { PREFIX, VERSION } from 'muxws';
import { VERSION as NODE_VERSION } from 'muxws/node';
console.log(VERSION);
console.log('one version stream:', VERSION === NODE_VERSION);
// The package version is not the wire version; the wire version is in the subprotocol name.
console.log('the wire says:', `${PREFIX}json`);encoded_length / encodedLength
The byte length of an encoded message: UTF-8 bytes for text, buffer length for bytes.
Every size limit in muxws is measured with this function, in bytes of encoded output, not characters. A JavaScript string's .length counts UTF-16 code units and differs from the UTF-8 byte count for every non-ASCII character.
Signature
def encoded_length(message: str | bytes) -> int: ...export function encodedLength(message: string | ArrayBuffer): number;Parameters
| Name | Type | Default | What it does |
|---|---|---|---|
message | str | bytes / string | ArrayBuffer | required | An encoded message. A str / string is measured as UTF-8; bytes are measured as they are. |
Return
int / number: the byte count.
Raises
Raises: nothing.
Example
from muxws import encoded_length
print(encoded_length("abc"), encoded_length(b"abc"))
print("a non-BMP character:", len("\U0001f600"), "code point,", encoded_length("\U0001f600"), "bytes")
print("accented text:", len("café"), "code points,", encoded_length("café"), "bytes")import { encodedLength } from 'muxws';
console.log(encodedLength('abc'), encodedLength(new Uint8Array([1, 2, 3]).buffer));
console.log('a non-BMP character:', '\u{1f600}'.length, 'code units,', encodedLength('\u{1f600}'), 'bytes');
console.log('accented text:', 'café'.length, 'code units,', encodedLength('café'), 'bytes');split_frame / splitFrame
Returns every fragment of a frame. Returns the frame unchanged, as a one-element list, when it fits under the cap.
Fragmentation is automatic and mandatory: the writer fragments every send, one slice at a time, so a 1 MB export does not delay a 200-byte progress update on another stream. split_frame performs the same computation as a pure function, for tests, the conformance corpus and inspection of fragment boundaries.
It is a pure function of (frame, cap, codec): it reads nothing else and mutates no argument. It encodes the logical payload with the codec, slices the encoded form, and puts each slice into a frame that the codec encodes again, so both ports cut at the same places. Text is cut on whole Unicode code points, never inside a multi-byte sequence.
headers are on the first fragment; end and trailers are on the last; more is true on every fragment except the last.
Signature
def split_frame(frame: Frame, cap: int = MAX_FRAME_BYTES, codec: Codec | None = None) -> list[Frame]: ...export function splitFrame(frame: Frame, cap: number = MAX_FRAME_BYTES, codec?: Codec): Frame[];Parameters
| Name | Type | Default | What it does |
|---|---|---|---|
frame | Frame | required | The frame to split. Not mutated. |
cap | int / number | MAX_FRAME_BYTES (65 536) | The largest encoded frame to produce, in bytes. Only tests and the conformance runner pass a smaller value; it is never read from the wire. |
codec | Codec | None / Codec | undefined | None / undefined | The codec whose output the boundaries are measured over. Required despite the default: omitting it raises ProtocolError, because fragment boundaries are defined over the codec's output. |
Return
list[Frame] / Frame[]: either [frame] when it already fits, or two or more fragment frames carrying fragment instead of payload. The last one always has more false, even when it carries no bytes at all.
Raises
ProtocolError when codec is omitted; when cap is below 2; when the frame exceeds the cap but carries no payload to fragment (headers are never fragmented); when the cap cannot hold the envelope plus one indivisible unit of payload; and when the closing fragment does not fit even with no payload, because it carries trailers whole.
Example
from muxws import ABSENT, Frame, JsonCodec, ProtocolError, split_frame
codec = JsonCodec()
small = Frame("data", stream=1, payload={"n": 1}, end=True)
print("already fits:", len(split_frame(small, 1024, codec)))
big = Frame("data", stream=1, payload={"blob": "y" * 3000}, headers={"trace": "abc"}, end=True, trailers={"rows": 1})
parts = split_frame(big, 1024, codec)
print("fragments:", len(parts))
print("first carries headers:", parts[0].headers, "| more:", parts[0].more)
print("last carries trailers:", parts[-1].trailers, "| end:", parts[-1].end, "| more:", parts[-1].more)
print("no fragment carries a payload:", all(part.payload is ABSENT for part in parts))
try:
split_frame(big, 1024)
except ProtocolError as exc:
print("no codec:", exc)import { type Frame, JsonCodec, ProtocolError, splitFrame } from 'muxws';
const codec = new JsonCodec();
const small: Frame = { type: 'data', stream: 1, payload: { n: 1 }, end: true };
console.log('already fits:', splitFrame(small, 1024, codec).length);
const big: Frame = {
type: 'data',
stream: 1,
payload: { blob: 'y'.repeat(3000) },
headers: { trace: 'abc' },
end: true,
trailers: { rows: 1 },
};
const parts = splitFrame(big, 1024, codec);
console.log('fragments:', parts.length);
console.log('first carries headers:', parts[0].headers, '| more:', parts[0].more);
const last = parts[parts.length - 1];
console.log('last carries trailers:', last.trailers, '| end:', last.end, '| more:', last.more);
try {
splitFrame(big, 1024);
} catch (error) {
if (error instanceof ProtocolError) console.log('no codec:', error.message);
}Assembler
The receive side of fragmentation: concatenates fragment values and decodes them when the last one arrives. Each stream holds one assembler. Applications rarely construct one; the type is public for use by other implementations, and because a receiver's payload limit is checked against byte_length / byteLength.
The limit is checked as fragments arrive, not after reassembly, so an oversized payload is rejected before it is fully buffered.
Signature
class Assembler:
def __init__(self) -> None: ...
@property
def in_progress(self) -> bool: ...
@property
def byte_length(self) -> int: ...
def reset(self) -> None: ...
def feed(self, frame: Frame, codec: Codec) -> Any: ...export class Assembler {
get inProgress(): boolean;
get byteLength(): number;
reset(): void;
feed(frame: Frame, codec: Codec): unknown;
}Parameters
| Name | Type | Default | What it does |
|---|---|---|---|
| (constructor) | — | — | Takes no arguments. |
in_progress / inProgress | bool / boolean | False / false | True between the first fragment and the one that arrives without more. |
byte_length / byteLength | int / number | 0 | Bytes accumulated so far, measured the way encoded_length measures them. |
reset() | () -> None / () => void | — | Drops the partial buffer and releases the bytes with it. Called when the stream is reset. |
feed(frame, codec) | frame: Frame, codec: Codec | both required | Absorbs one fragment frame. codec is used only on the closing fragment, to decode the concatenation. |
Return
feed() returns ABSENT while more fragments are expected, and the decoded payload on the frame that closes the sequence. reset() returns None / void.
Raises
ProtocolError from feed() when the frame carries no fragment, and whatever the codec's decode_payload / decodePayload raises — ProtocolError for both shipped codecs — when the reassembled bytes do not decode.
Example
from muxws import ABSENT, Assembler, Frame, JsonCodec, split_frame
codec = JsonCodec()
parts = split_frame(Frame("data", stream=1, payload={"blob": "z" * 3000}, end=True), 1024, codec)
assembler = Assembler()
print("before:", assembler.in_progress, assembler.byte_length)
for part in parts:
result = assembler.feed(part, codec)
print("fed a fragment ->", "ABSENT" if result is ABSENT else type(result).__name__, "|", assembler.byte_length)
print("payload length:", len(result["blob"]), "| after:", assembler.in_progress, assembler.byte_length)import { ABSENT, Assembler, type Frame, JsonCodec, splitFrame } from 'muxws';
const codec = new JsonCodec();
const source: Frame = { type: 'data', stream: 1, payload: { blob: 'z'.repeat(3000) }, end: true };
const parts = splitFrame(source, 1024, codec);
const assembler = new Assembler();
console.log('before:', assembler.inProgress, assembler.byteLength);
let result: unknown = ABSENT;
parts.forEach((part) => {
result = assembler.feed(part, codec);
console.log('fed a fragment ->', result === ABSENT ? 'ABSENT' : typeof result, '|', assembler.byteLength);
});
console.log('payload length:', (result as { blob: string }).blob.length);
console.log('after:', assembler.inProgress, assembler.byteLength);CloseReason
Why a socket ended. One type per language, used for every socket loss and passed to every on_close / onClose handler. It has the same four fields in both languages.
Python's is a frozen slotted dataclass; TypeScript's is an interface.
Signature
@dataclass(frozen=True, slots=True)
class CloseReason:
code: int
reason: str
was_clean: bool
will_retry: bool = Falseexport interface CloseReason {
readonly code: number;
readonly reason: string;
readonly wasClean: boolean;
readonly willRetry: boolean;
}Parameters
| Name | Type | Default | What it does |
|---|---|---|---|
code | int / number | required | The WebSocket close code. 1000 for a clean close, 1006 when the socket died without a close frame, 1008 for a policy violation such as a subprotocol mismatch. |
reason | str / string | required | The close frame's reason text, or "" when there was none. |
was_clean / wasClean | bool / boolean | required | Whether a close handshake completed. |
will_retry / willRetry | bool / boolean | False in Python; required in TypeScript | Whether the reconnect helper will dial again. False when the attempt budget is exhausted, when close() was called, and on any peer without the reconnect helper. A handler that tears down application state should check it: a close with will_retry true is followed by a reconnect attempt. |
The default on will_retry is the only difference between the two: Python's dataclass has one, and a TypeScript interface cannot declare defaults.
Return
Python: a frozen CloseReason. TypeScript: an interface, so an object literal is one.
Raises
Raises: nothing.
Example
import asyncio
from muxws import CloseReason, JsonCodec, Peer
from muxws.transports.memory import memory_pair
print(CloseReason(code=1000, reason="bye", was_clean=True))
print(CloseReason(1006, "", False).will_retry)
async def main() -> None:
left, right = memory_pair()
codec = JsonCodec()
dialer = Peer(left, codec=codec, is_dialer=True)
acceptor = Peer(right, codec=codec, is_dialer=False)
@dialer.on_close
def closed(reason: CloseReason) -> None:
print("closed:", reason.code, repr(reason.reason), reason.was_clean, reason.will_retry)
tasks = [asyncio.create_task(dialer.serve()), asyncio.create_task(acceptor.serve())]
await asyncio.sleep(0.05)
await left.drop()
await asyncio.sleep(0.05)
for task in tasks:
task.cancel()
asyncio.run(main())import { type CloseReason, JsonCodec, Peer, memoryPair } from 'muxws';
const literal: CloseReason = { code: 1000, reason: 'bye', wasClean: true, willRetry: false };
console.log(literal);
async function main(): Promise<void> {
const [left, right] = memoryPair();
const codec = new JsonCodec();
const dialer = new Peer(left, { codec, isDialer: true });
const acceptor = new Peer(right, { codec, isDialer: false });
dialer.onClose((reason: CloseReason) => {
console.log('closed:', reason.code, JSON.stringify(reason.reason), reason.wasClean, reason.willRetry);
});
void dialer.serve();
void acceptor.serve();
await new Promise((resolve) => setTimeout(resolve, 50));
left.drop();
await new Promise((resolve) => setTimeout(resolve, 50));
}
void main();StreamHandler
The type of a peer's incoming-stream handler (one per peer): the argument of peer.on_stream(...) / peer.onStream(...), connect(on_stream=...) and serve(handler=...).
It is called once per stream the remote opens, with the opening payload and the Stream. It is usually a coroutine function / returns a promise. Its return value is discarded: a unary handler answers by calling stream.reply(...).
The type is the same on both ends: an acceptor's handler receives a client's request, and a dialer's handler receives a server push, with the same signature and semantics.
A handler that raises produces reset(APPLICATION_ERROR) on that stream, with the reason text and the error_serializer's payload. It never produces REFUSED, which is reserved for streams that were definitively not processed. Only one handler is registered per peer; registering a second replaces the first and logs a warning.
Signature
StreamHandler = Callable[[Any, Stream], Any]export type StreamHandler = (payload: any, stream: Stream) => void | Promise<void>;Parameters
The parameters of the callable itself:
| Name | Type | Default | What it does |
|---|---|---|---|
payload | Any / any | none; positional | The opening payload, already decoded and reassembled. None / null when the remote opened with none. |
stream | Stream | none; positional | The stream. Reply on it, iterate it, send on it, or reset it. Its headers are the opening frame's. |
Return
Python's alias returns Any and the value is ignored; a coroutine function is the normal case. TypeScript's returns void | Promise<void>. A handler answers with stream.reply(...), not with a return value.
Raises
The type raises nothing. An implementation that raises causes reset(APPLICATION_ERROR) on its stream; the connection stays open.
Example
import asyncio
from muxws import JsonCodec, Peer, Stream, StreamHandler
from muxws.transports.memory import memory_pair
async def handle(payload, stream: Stream) -> None:
"""One handler serving both a unary request and a streamed response."""
if (payload or {}).get("action") == "stream":
for index in range(3):
await stream.send({"chunk": index})
await stream.end({"chunk": "done"})
else:
await stream.reply({"echo": payload})
async def main() -> None:
registered: StreamHandler = handle
left, right = memory_pair()
codec = JsonCodec()
dialer = Peer(left, codec=codec, is_dialer=True)
acceptor = Peer(right, codec=codec, is_dialer=False)
acceptor.on_stream(registered)
tasks = [asyncio.create_task(dialer.serve()), asyncio.create_task(acceptor.serve())]
print(await dialer.request({"n": 1}, timeout=2.0))
async for chunk in dialer.open({"action": "stream"}):
print(chunk)
await dialer.close(drain=0.1)
for task in tasks:
task.cancel()
asyncio.run(main())import { JsonCodec, Peer, type Stream, type StreamHandler, memoryPair } from 'muxws';
/** One handler serving both a unary request and a streamed response. */
const handle: StreamHandler = async (payload: { action?: string } | null, stream: Stream) => {
if (payload?.action === 'stream') {
for (const index of [0, 1, 2]) await stream.send({ chunk: index });
await stream.end({ payload: { chunk: 'done' } });
} else {
await stream.reply({ echo: payload });
}
};
async function main(): Promise<void> {
const [left, right] = memoryPair();
const codec = new JsonCodec();
const dialer = new Peer(left, { codec, isDialer: true });
const acceptor = new Peer(right, { codec, isDialer: false });
acceptor.onStream(handle);
void dialer.serve();
void acceptor.serve();
console.log(await dialer.request({ n: 1 }, { timeoutMs: 2000 }));
for await (const chunk of dialer.open({ action: 'stream' })) console.log(chunk);
await dialer.close({ drainMs: 100 });
}
void main();ErrorSerializer
The type of the hook that turns a handler's failure into the payload of the reset(APPLICATION_ERROR) frame reporting it. Passed as error_serializer= / errorSerializer: to connect() and accept(), and defaulting to default_error_serializer / defaultErrorSerializer.
It is set per peer, not globally, so one process can redact on a browser-facing connection and not on an internal one.
WARNING
The return value is sent on the wire. The default includes the exception's message text, and exception messages often contain file paths, SQL and identifiers that must not reach the remote. A public-facing deployment should pass a redacting implementation.
Its return value must be representable by the configured codec; under JSON it cannot contain bytes.
Signature
ErrorSerializer = Callable[[BaseException], Any]export type ErrorSerializer = (error: unknown) => unknown;Parameters
The parameters of the callable itself:
| Name | Type | Default | What it does |
|---|---|---|---|
exc / error | BaseException / unknown | none; positional | The exception the handler raised. Python types it BaseException rather than Exception; TypeScript types it unknown because JavaScript permits throwing anything. |
Return
Any / unknown: the value that becomes the reset frame's payload, and that arrives at the remote as RemoteError.payload. Return None / null to send no structured payload; the reason text is still sent.
Raises
The type raises nothing. If an implementation raises, the reset(APPLICATION_ERROR) is still sent, with the reason text and no payload.
Example
import asyncio
from muxws import ErrorSerializer, JsonCodec, Peer, RemoteError
from muxws.transports.memory import memory_pair
def redacting(exc: BaseException) -> dict[str, str]:
"""Class name only. The message never leaves the process."""
return {"type": type(exc).__name__}
async def main() -> None:
serializer: ErrorSerializer = redacting
left, right = memory_pair()
codec = JsonCodec()
dialer = Peer(left, codec=codec, is_dialer=True)
acceptor = Peer(right, codec=codec, is_dialer=False, error_serializer=serializer)
@acceptor.on_stream
async def handle(payload, stream):
raise ValueError("SELECT * FROM accounts WHERE id = 7")
tasks = [asyncio.create_task(dialer.serve()), asyncio.create_task(acceptor.serve())]
try:
await dialer.request({"n": 1}, timeout=2.0)
except RemoteError as exc:
print("payload:", exc.payload, "| reason:", exc)
await dialer.close(drain=0.1)
for task in tasks:
task.cancel()
asyncio.run(main())import { type ErrorSerializer, JsonCodec, Peer, RemoteError, memoryPair } from 'muxws';
/** Class name only. The message never leaves the process. */
const redacting: ErrorSerializer = (error: unknown) => ({
type: error instanceof Error ? error.name : typeof error,
});
async function main(): Promise<void> {
const [left, right] = memoryPair();
const codec = new JsonCodec();
const dialer = new Peer(left, { codec, isDialer: true });
const acceptor = new Peer(right, { codec, isDialer: false, errorSerializer: redacting });
acceptor.onStream(() => {
throw new Error('SELECT * FROM accounts WHERE id = 7');
});
void dialer.serve();
void acceptor.serve();
try {
await dialer.request({ n: 1 }, { timeoutMs: 2000 });
} catch (error) {
if (error instanceof RemoteError) console.log('payload:', error.payload, '| reason:', error.message);
}
await dialer.close({ drainMs: 100 });
}
void main();OpenInterceptor
The type of an interceptor registered with peer.intercept_open(...) / peer.interceptOpen(...). It is called synchronously inside every open(), notify() and request() on that peer, before a stream id is allocated, and shapes the headers the open frame carries.
It receives a fresh mapping holding a shallow copy of the caller's headers and mutates it in place: adding, replacing or deleting keys. It cannot touch the rest of the frame. The reconnect helper's hello does not pass through it; the leading headers this peer answers with pass through a ReplyHeadersInterceptor instead.
Signature
OpenInterceptor = Callable[[dict[str, Any], Any], None]export type OpenInterceptor = (headers: Record<string, unknown>, payload: unknown) => void;Parameters
The parameters of the callable itself:
| Name | Type | Default | What it does |
|---|---|---|---|
headers | dict[str, Any] / Record<string, unknown> | none; positional | The headers the open frame will carry, as left by the interceptors registered before this one. Mutate it; keys must be strings. Changes made after the call returned have no effect. |
payload | Any / unknown | none; positional | The opening payload, None / null when the caller passed none. For reading only. |
Return
None / void; any other value is ignored, except an awaitable (Python) or a thenable (TypeScript), which fails the open with TypeError.
Raises
The type raises nothing. An implementation that raises fails the open() with that exception: no later interceptor runs, no stream id is allocated, and nothing is sent.
Example
import asyncio
from muxws import JsonCodec, OpenInterceptor, Peer, Stream
from muxws.transports.memory import memory_pair
def tag_connection(headers: dict, _payload: object) -> None:
headers["connection"] = "tab-7"
async def main() -> None:
interceptor: OpenInterceptor = tag_connection
left, right = memory_pair()
codec = JsonCodec()
dialer = Peer(left, codec=codec, is_dialer=True)
acceptor = Peer(right, codec=codec, is_dialer=False)
dialer.intercept_open(interceptor)
async def handler(_payload: object, stream: Stream) -> None:
await stream.reply(dict(stream.headers))
acceptor.on_stream(handler)
tasks = [asyncio.create_task(dialer.serve()), asyncio.create_task(acceptor.serve())]
print(await dialer.request("hi", headers={"trace": "t-1"}, timeout=2.0))
await dialer.close(drain=0.1)
for task in tasks:
task.cancel()
asyncio.run(main())import { JsonCodec, type OpenInterceptor, Peer, type Stream, memoryPair } from 'muxws';
const tagConnection: OpenInterceptor = (headers) => {
headers.connection = 'tab-7';
};
const [left, right] = memoryPair();
const codec = new JsonCodec();
const dialer = new Peer(left, { codec, isDialer: true });
const acceptor = new Peer(right, { codec, isDialer: false });
dialer.interceptOpen(tagConnection);
acceptor.onStream(async (_payload: unknown, stream: Stream) => stream.reply({ ...stream.headers }));
void dialer.serve();
void acceptor.serve();
console.log(await dialer.request('hi', { headers: { trace: 't-1' }, timeoutMs: 2000 }));
await dialer.close({ drainMs: 100 });ReplyHeadersInterceptor
The type of an interceptor registered with peer.intercept_reply_headers(...) / peer.interceptReplyHeaders(...). It is called once per stream the remote opened, synchronously, by the call that sends this peer's first frame on it, and shapes the leading headers that frame carries.
It receives a fresh mapping holding a shallow copy of the headers that call was given and mutates it in place. It cannot touch the rest of the frame.
Signature
ReplyHeadersInterceptor = Callable[[dict[str, Any], Stream], None]export type ReplyHeadersInterceptor = (headers: Record<string, unknown>, stream: Stream) => void;Parameters
The parameters of the callable itself:
| Name | Type | Default | What it does |
|---|---|---|---|
headers | dict[str, Any] / Record<string, unknown> | none; positional | The headers this peer's first frame on the stream will carry, as left by the interceptors registered before this one. Mutate it; keys must be strings. Changes made after the call returned have no effect. |
stream | Stream | none; positional | The stream being answered: stream.headers and stream.payload are the opener's. For reading. |
Return
None / void; any other value is ignored, except an awaitable (Python) or a thenable (TypeScript), which fails the sending call with TypeError.
Raises
The type raises nothing. An implementation that raises fails the call that sends the first frame with that exception: no later interceptor runs, nothing is sent, and the first frame stays unspent. In the implicit end of a handler that returns, the stream is reset with APPLICATION_ERROR instead.
Example
import asyncio
from muxws import JsonCodec, Peer, ReplyHeadersInterceptor, Stream
from muxws.transports.memory import memory_pair
def tag_region(headers: dict, _stream: Stream) -> None:
headers["region"] = "eu-1"
async def main() -> None:
interceptor: ReplyHeadersInterceptor = tag_region
left, right = memory_pair()
codec = JsonCodec()
dialer = Peer(left, codec=codec, is_dialer=True)
acceptor = Peer(right, codec=codec, is_dialer=False)
acceptor.intercept_reply_headers(interceptor)
async def handler(_payload: object, stream: Stream) -> None:
await stream.reply("ok")
acceptor.on_stream(handler)
tasks = [asyncio.create_task(dialer.serve()), asyncio.create_task(acceptor.serve())]
stream = dialer.open("hi", end=True)
await stream
print(stream.reply_headers) # {'region': 'eu-1'}
await dialer.close(drain=0.1)
for task in tasks:
task.cancel()
asyncio.run(main())import { JsonCodec, Peer, type ReplyHeadersInterceptor, type Stream, memoryPair } from 'muxws';
const tagRegion: ReplyHeadersInterceptor = (headers) => {
headers.region = 'eu-1';
};
const [left, right] = memoryPair();
const codec = new JsonCodec();
const dialer = new Peer(left, { codec, isDialer: true });
const acceptor = new Peer(right, { codec, isDialer: false });
acceptor.interceptReplyHeaders(tagRegion);
acceptor.onStream(async (_payload: unknown, stream: Stream) => stream.reply('ok'));
void dialer.serve();
void acceptor.serve();
const stream = dialer.open('hi', { end: true });
await stream;
console.log(stream.replyHeaders); // { region: 'eu-1' }
await dialer.close({ drainMs: 100 });IncomingHeadersInterceptor
The type of an interceptor registered with peer.intercept_incoming_headers(...) / peer.interceptIncomingHeaders(...). It is called once per stream, synchronously inside the read loop, with the leading headers of the remote's first frame on that stream: the open of a stream the remote opened, or the first data of a stream this peer opened.
Signature
IncomingHeadersInterceptor = Callable[[Mapping[str, Any], Stream], None]export type IncomingHeadersInterceptor = (headers: Readonly<Record<string, unknown>>, stream: Stream) => void;Parameters
The parameters of the callable itself:
| Name | Type | Default | What it does |
|---|---|---|---|
headers | Mapping[str, Any] / Readonly<Record<string, unknown>> | none; positional | A read-only view of the headers that frame carried: a MappingProxyType in Python, a frozen copy in TypeScript. Empty when the frame carried none. |
stream | Stream | none; positional | The stream. stream.local is false for a stream the remote opened (the headers are stream.headers) and true for the answer to one this peer opened (the headers are stream.reply_headers / stream.replyHeaders). |
Return
None / void; the value is ignored. A returned awaitable (Python) or thenable (TypeScript) is not awaited and is logged.
Raises
The type raises nothing. An implementation that raises is logged; the frame, the stream and the connection are unaffected.
Example
import asyncio
from muxws import IncomingHeadersInterceptor, JsonCodec, Peer, Stream
from muxws.transports.memory import memory_pair
def show(headers, stream: Stream) -> None:
print("local" if stream.local else "remote", "stream", stream.id, dict(headers))
async def main() -> None:
interceptor: IncomingHeadersInterceptor = show
left, right = memory_pair()
codec = JsonCodec()
dialer = Peer(left, codec=codec, is_dialer=True)
acceptor = Peer(right, codec=codec, is_dialer=False)
acceptor.intercept_incoming_headers(interceptor)
async def handler(_payload: object, stream: Stream) -> None:
await stream.reply("ok")
acceptor.on_stream(handler)
tasks = [asyncio.create_task(dialer.serve()), asyncio.create_task(acceptor.serve())]
await dialer.request("hi", headers={"trace": "t-1"}, timeout=2.0)
await dialer.close(drain=0.1)
for task in tasks:
task.cancel()
asyncio.run(main())import { type IncomingHeadersInterceptor, JsonCodec, Peer, type Stream, memoryPair } from 'muxws';
const show: IncomingHeadersInterceptor = (headers, stream: Stream) => {
console.log(stream.local ? 'local' : 'remote', 'stream', stream.id, headers);
};
const [left, right] = memoryPair();
const codec = new JsonCodec();
const dialer = new Peer(left, { codec, isDialer: true });
const acceptor = new Peer(right, { codec, isDialer: false });
acceptor.interceptIncomingHeaders(show);
acceptor.onStream(async (_payload: unknown, stream: Stream) => stream.reply('ok'));
void dialer.serve();
void acceptor.serve();
await dialer.request('hi', { headers: { trace: 't-1' }, timeoutMs: 2000 });
await dialer.close({ drainMs: 100 });logger and LogLevel (TypeScript)
TypeScript's replacement for Python's standard logging module.
Python has no counterpart: it uses a named logger (logging.getLogger("muxws.frames")) that the application configures. The browser entry point has no dependencies (WSM-PKG-003), so the TypeScript port provides a four-method wrapper over console, and setting logger.level is the only way to enable frame logging.
The default level is 'warn', matching an unconfigured Python logger: logger.info and logger.debug print nothing until the application lowers the level.
Frame lines are emitted at 'debug' and never carry payload contents at any level (WSM-OBS-002).
Signature
export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'silent';
export const logger: {
level: LogLevel;
isEnabledFor(level: LogLevel): boolean;
debug(message: string, ...rest: unknown[]): void;
info(message: string, ...rest: unknown[]): void;
warn(message: string, ...rest: unknown[]): void;
error(message: string, ...rest: unknown[]): void;
};Parameters
| Name | Type | Default | What it does |
|---|---|---|---|
level | LogLevel | 'warn' | The threshold. Assign to it to change what prints; 'silent' stops everything. |
message | string | required | The line, on each of the four emitters. |
...rest | unknown[] | [] | Extra values passed through to the matching console method. |
Return
logger.isEnabledFor returns a boolean; the four emitters return void. logger is a single mutable object, not a factory; assigning level changes it for the whole process.
Raises
Raises: nothing. An unknown level string is a type error at compile time; at runtime it disables every level, and nothing is printed.
Example
import { logger } from 'muxws';
console.log('default level:', logger.level);
console.log('debug enabled by default:', logger.isEnabledFor('debug'));
logger.level = 'debug';
console.log('debug enabled now:', logger.isEnabledFor('debug'));
logger.level = 'silent';
console.log('silent stops even errors:', logger.isEnabledFor('error'));
logger.level = 'warn';FrameDirection (TypeScript)
Which way a frame was travelling, as handed to peer.onFrame (WSM-OBS-003).
'tx' is reported before the codec encodes, 'rx' after it decodes, so a handler receives a logical Frame, not bytes, in both directions. Python's on_frame takes the same two strings and needs no type alias for them.
Signature
export type FrameDirection = 'tx' | 'rx';Parameters
| Name | Type | Default | What it does |
|---|---|---|---|
| (none) | — | — | A string-literal union, not a callable. |
Return
Nothing — it is a type. It exists at compile time only and is absent from the emitted JavaScript.
Raises
Raises: nothing.
Example
import { JsonCodec, type FrameDirection, MemorySocket, Peer, memoryPair } from 'muxws';
const [dialerSide, acceptorSide] = memoryPair();
const seen: FrameDirection[] = [];
const acceptor = new Peer(acceptorSide, { codec: new JsonCodec(), isDialer: false });
acceptor.onStream(() => undefined);
void acceptor.serve().catch(() => undefined);
const dialer = new Peer(dialerSide, { codec: new JsonCodec(), isDialer: true });
dialer.onFrame((direction: FrameDirection) => seen.push(direction));
void dialer.serve().catch(() => undefined);
dialer.open({ hello: true }, { end: true });
await new Promise((resolve) => setTimeout(resolve, 50));
console.log('directions observed:', [...new Set(seen)].sort());
await dialer.close();
await acceptor.close();PREFIX
The subprotocol prefix, muxws.v1.. A dialer offers `${PREFIX}${codecName}` as its first subprotocol entry (WSM-CDC-020), and the acceptor accepts only that exact value.
The v1 in it is the only version on the wire (WSM-CON-009): it identifies the breaking-change generation, not the package's semver. muxws 2.0.0 can still speak muxws.v1., and a v1 acceptor rejects a peer offering muxws.v2.json at the handshake (WSM-CDC-025).
It is exported for applications that build a subprotocol list by hand. The functions that build and read the offer are internal and not exported.
TypeScript exports it from the package root; in Python, import it from muxws.subprotocol.
Signature
PREFIX = "muxws.v1."export const PREFIX = 'muxws.v1.';Parameters
| Name | Type | Default | What it does |
|---|---|---|---|
| (none) | — | — | A constant string, not a callable. |
Return
Nothing — it is a constant.
Raises
Raises: nothing.
Example
from muxws.subprotocol import PREFIX
print(PREFIX)
print("what a json dialer offers first:", f"{PREFIX}json")
print("the only version on the wire is the generation:", PREFIX.split(".")[1])import { PREFIX } from 'muxws';
console.log(PREFIX);
console.log('what a json dialer offers first:', `${PREFIX}json`);
console.log('the only version on the wire is the generation:', PREFIX.split('.')[1]);See also
- Codec — what turns a
Frameinto bytes, andCodec.encode_payload, whose outputsplit_frameslices. - Errors —
default_error_serializer, theErrorSerializeryou get if you pass none. - Peer —
on_stream,on_close,on_frameand theintercept_*methods, which take these types. - Stream — what a
StreamHandleris handed.