Skip to content

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 ​

FieldTypeDefaultWhat it carries
typestr / stringrequiredThe frame type. A v1 peer sends open, data, reset, ping, pong or goaway. Always emitted, even though it has no default to compare against.
streamint | None / number | nullNone / nullWhich 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.
payloadAny / unknown | AbsentABSENTThe 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.
fragmentstr | bytes | None / string | ArrayBuffer | nullNone / nullOne slice of an encoded payload. A frame carries payload or fragment, never both.
morebool / booleanFalse / falseTrue on every fragment but the last. The receiver concatenates until it sees a fragment without it.
headersdict[str, Any] | None / Record<string, unknown> | nullNone / nullPer-stream metadata, sent once with the open. Carried on the first fragment and never split across fragments.
endbool / booleanFalse / falseThis side is done sending. A flag on a frame, not a frame type: there is no "end" frame type. Carried on the last fragment.
trailersdict[str, Any] | None / Record<string, unknown> | nullNone / nullMetadata known only after the payloads, such as a row count or a checksum. Carried on the end frame, on its last fragment only.
codeint | None / number | nullNone / nullA ResetCode value, on reset and goaway. Carried as an integer, so a code this generation does not define is preserved.
reasonstr | None / string | nullNone / nullHuman-readable text accompanying code. Never parsed for meaning.
noncestr | None / string | nullNone / nullCorrelates a pong with the ping it answers. Echoed back verbatim.
last_streamint | None / number | nullNone / nullOn 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 ​

python
@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 = None
ts
export 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.

NameTypeDefaultWhat it does
typestr / stringrequiredThe only field with no default.
streamint | None / number | nullNone / nullSee the field table.
payloadAny / unknown | AbsentABSENTSee the field table. In TypeScript, omitting the property and setting it to ABSENT mean the same thing.
fragmentstr | bytes | None / string | ArrayBuffer | nullNone / nullSee the field table.
morebool / booleanFalse / falseSee the field table.
headersdict[str, Any] | None / Record<string, unknown> | nullNone / nullSee the field table.
endbool / booleanFalse / falseSee the field table.
trailersdict[str, Any] | None / Record<string, unknown> | nullNone / nullSee the field table.
codeint | None / number | nullNone / nullSee the field table.
reasonstr | None / string | nullNone / nullSee the field table.
noncestr | None / string | nullNone / nullSee the field table.
last_streamint | None / number | nullNone / nullSee 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 ​

python
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)
ts
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 ​

python
ABSENT: Any = _Absent()
ts
export const ABSENT: unique symbol;
export type Absent = typeof ABSENT;

Parameters ​

NameTypeDefaultWhat it does
(none)——A value, not a callable. There is one instance per process.

Return ​

Nothing — it is a constant.

Raises ​

Raises: nothing.

Example ​

python
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)
ts
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 ​

python
V1_FRAME_TYPES: Final[frozenset[str]] = frozenset({"open", "data", "reset", "ping", "pong", "goaway"})
ts
export const V1_FRAME_TYPES: ReadonlySet<string>;

Parameters ​

NameTypeDefaultWhat 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 ​

python
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)
ts
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 ​

python
def to_mapping(frame: Frame) -> dict[str, Any]: ...
ts
export function toMapping(frame: Frame): Record<string, unknown>;

Parameters ​

NameTypeDefaultWhat it does
frameFramerequiredThe 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 ​

python
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"))))
ts
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 ​

python
def from_mapping(mapping: Mapping[str, Any]) -> Frame: ...
ts
export function fromMapping(mapping: Record<string, unknown>): Frame;

Parameters ​

NameTypeDefaultWhat it does
mappingMapping[str, Any] / Record<string, unknown>requiredA 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:

FieldAccepted values
typea string
stream, last_streama non-negative integer, or null
codean integer, or null
fragmenta string or bytes (ArrayBuffer), or null
more, enda boolean
headers, trailersa mapping with string keys (a plain object), or null
reason, noncea 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 ​

python
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)
ts
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 ​

ts
export function framesEqual(a: Frame, b: Frame): boolean;

Parameters ​

NameTypeDefaultWhat it does
aFramerequiredThe left frame.
bFramerequiredThe right frame.

Return ​

true when every one of the twelve fields is equal after defaults are normalised, otherwise false.

Raises ​

Raises: nothing.

Example ​

ts
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 ​

python
MAX_FRAME_BYTES: Final[int] = 65_536
ts
export const MAX_FRAME_BYTES = 65_536;

Parameters ​

NameTypeDefaultWhat it does
(none)——A constant, not a callable.

Return ​

Nothing — it is a constant int / number.

Raises ​

Raises: nothing.

Example ​

python
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)
ts
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 ​

python
__version__: str
ts
export const VERSION: string;

Parameters ​

NameTypeDefaultWhat it does
(none)——A constant, not a callable.

Return ​

Nothing — it is a constant str / string.

Raises ​

Raises: nothing.

Example ​

python
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")
ts
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 ​

python
def encoded_length(message: str | bytes) -> int: ...
ts
export function encodedLength(message: string | ArrayBuffer): number;

Parameters ​

NameTypeDefaultWhat it does
messagestr | bytes / string | ArrayBufferrequiredAn 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 ​

python
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")
ts
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 ​

python
def split_frame(frame: Frame, cap: int = MAX_FRAME_BYTES, codec: Codec | None = None) -> list[Frame]: ...
ts
export function splitFrame(frame: Frame, cap: number = MAX_FRAME_BYTES, codec?: Codec): Frame[];

Parameters ​

NameTypeDefaultWhat it does
frameFramerequiredThe frame to split. Not mutated.
capint / numberMAX_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.
codecCodec | None / Codec | undefinedNone / undefinedThe 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 ​

python
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)
ts
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 ​

python
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: ...
ts
export class Assembler {
  get inProgress(): boolean;
  get byteLength(): number;
  reset(): void;
  feed(frame: Frame, codec: Codec): unknown;
}

Parameters ​

NameTypeDefaultWhat it does
(constructor)——Takes no arguments.
in_progress / inProgressbool / booleanFalse / falseTrue between the first fragment and the one that arrives without more.
byte_length / byteLengthint / number0Bytes 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: Codecboth requiredAbsorbs 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 ​

python
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)
ts
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 ​

python
@dataclass(frozen=True, slots=True)
class CloseReason:
    code: int
    reason: str
    was_clean: bool
    will_retry: bool = False
ts
export interface CloseReason {
  readonly code: number;
  readonly reason: string;
  readonly wasClean: boolean;
  readonly willRetry: boolean;
}

Parameters ​

NameTypeDefaultWhat it does
codeint / numberrequiredThe 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.
reasonstr / stringrequiredThe close frame's reason text, or "" when there was none.
was_clean / wasCleanbool / booleanrequiredWhether a close handshake completed.
will_retry / willRetrybool / booleanFalse in Python; required in TypeScriptWhether 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 ​

python
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())
ts
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 ​

python
StreamHandler = Callable[[Any, Stream], Any]
ts
export type StreamHandler = (payload: any, stream: Stream) => void | Promise<void>;

Parameters ​

The parameters of the callable itself:

NameTypeDefaultWhat it does
payloadAny / anynone; positionalThe opening payload, already decoded and reassembled. None / null when the remote opened with none.
streamStreamnone; positionalThe 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 ​

python
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())
ts
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 ​

python
ErrorSerializer = Callable[[BaseException], Any]
ts
export type ErrorSerializer = (error: unknown) => unknown;

Parameters ​

The parameters of the callable itself:

NameTypeDefaultWhat it does
exc / errorBaseException / unknownnone; positionalThe 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 ​

python
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())
ts
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 ​

python
OpenInterceptor = Callable[[dict[str, Any], Any], None]
ts
export type OpenInterceptor = (headers: Record<string, unknown>, payload: unknown) => void;

Parameters ​

The parameters of the callable itself:

NameTypeDefaultWhat it does
headersdict[str, Any] / Record<string, unknown>none; positionalThe 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.
payloadAny / unknownnone; positionalThe 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 ​

python
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())
ts
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 ​

python
ReplyHeadersInterceptor = Callable[[dict[str, Any], Stream], None]
ts
export type ReplyHeadersInterceptor = (headers: Record<string, unknown>, stream: Stream) => void;

Parameters ​

The parameters of the callable itself:

NameTypeDefaultWhat it does
headersdict[str, Any] / Record<string, unknown>none; positionalThe 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.
streamStreamnone; positionalThe 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 ​

python
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())
ts
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 ​

python
IncomingHeadersInterceptor = Callable[[Mapping[str, Any], Stream], None]
ts
export type IncomingHeadersInterceptor = (headers: Readonly<Record<string, unknown>>, stream: Stream) => void;

Parameters ​

The parameters of the callable itself:

NameTypeDefaultWhat it does
headersMapping[str, Any] / Readonly<Record<string, unknown>>none; positionalA read-only view of the headers that frame carried: a MappingProxyType in Python, a frozen copy in TypeScript. Empty when the frame carried none.
streamStreamnone; positionalThe 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 ​

python
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())
ts
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 ​

ts
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 ​

NameTypeDefaultWhat it does
levelLogLevel'warn'The threshold. Assign to it to change what prints; 'silent' stops everything.
messagestringrequiredThe line, on each of the four emitters.
...restunknown[][]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 ​

ts
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 ​

ts
export type FrameDirection = 'tx' | 'rx';

Parameters ​

NameTypeDefaultWhat 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 ​

ts
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 ​

python
PREFIX = "muxws.v1."
ts
export const PREFIX = 'muxws.v1.';

Parameters ​

NameTypeDefaultWhat it does
(none)——A constant string, not a callable.

Return ​

Nothing — it is a constant.

Raises ​

Raises: nothing.

Example ​

python
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])
ts
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 Frame into bytes, and Codec.encode_payload, whose output split_frame slices.
  • Errors — default_error_serializer, the ErrorSerializer you get if you pass none.
  • Peer — on_stream, on_close, on_frame and the intercept_* methods, which take these types.
  • Stream — what a StreamHandler is handed.

Released under the MIT License.