This page records the design review for a possible binary receive API (for example receiveBytes$). It is not an implementation spec and does not ship a public Uint8Array receive stream in the current release.
Parent: #555 · Issue: #545 · Related: Supported data · Choosing receive$ / lines$ / terminalText$ · v1 → v2 Migration
Do not add receiveBytes$ (or any public wire-byte receive stream) for now.
Revisit only when concrete product demand shows that existing text APIs and application-side Web Serial usage cannot meet the need, and the complexity cost is justified.
| Decision | Status |
|---|---|
| Add binary receive to the public API now | No (defer) |
| Document current limits clearly | Done (Supported data, #540) |
| Preferred shape if revisited later | Additive opt-in (see Preferred additive sketch) |
| Implementation | Separate issue + separate PR from this design note |
This matches parent #555: do not expand the core API casually; prefer Recipes and existing streams until a gap is proven.
The library is UTF-8 text–first.
port.readable (ReadableStream<Uint8Array>), then immediately decodes with a streaming TextDecoder (UTF-8, fatal: false, stream: true).receive$, lines$, and terminalText$ are all Observable<string> derived from those decoded chunks.send$(Uint8Array) is supported; binary receive is not. Send and receive are asymmetric for binary payloads.receive$ means unframed decoded text chunks, not wire bytes.There is no public encoding option and no public byte stream today.
Ask whether raw bytes are required, or whether text streams already suffice.
| Need | Fit with current API |
|---|---|
| Newline / prompt text protocols, shells, logs | Use lines$ / receive$ / terminalText$ |
| Custom text framing on decoder chunks | Compose RxJS on receive$ (Advanced Usage) |
| Modbus RTU, COBS, SLIP, custom binary frames | Not a core concern — application-side (or outside this library) |
| Non-UTF-8 charsets (e.g. Shift_JIS) | Not supported; bytes-first would still need app decoding |
| Must preserve invalid UTF-8 / arbitrary octets | Requires wire bytes before TextDecoder — current pump cannot provide this |
Gate: add a library byte stream only when multiple real consumers need shared session lifecycle and cannot reasonably open Web Serial themselves or stay on decoded text.
Re-encoding decoded text (new TextEncoder().encode(chunk)) does not recover original wire bytes. It is lossy for invalid UTF-8 and binary protocols. Do not treat it as a substitute for receiveBytes$.
ReadableStream read sizesWeb Serial delivers Uint8Array chunks sized by the browser / OS, not by application protocol frames.
receiveBytes$ would still emit unframed byte chunks. Framing (length prefixes, CRC, COBS, etc.) remains application responsibility — same rule as “do not depend on chunk boundaries” for receive$ (stream selection).The port has a single readable stream and one active reader. Text and bytes cannot each own a separate getReader() without tearing down the other.
If a byte API were added later, the preferred internal shape is:
Uint8Array from the pump.TextDecoder for the existing text pipeline (receive$ → lines$ / terminalText$).Bytes must be taken before decoding. Passing binary through TextDecoder first and hoping to recover octets is incorrect.
Today the receive path uses a multicast Subject for decoded text: the pump keeps reading; there is no Observable backpressure into Web Serial. A slow receive$ subscriber does not pause reader.read().
A byte stream would inherit the same tension:
Preferred policy if revisited: keep pump-driven multicast with no unbounded replay; document that late subscribers miss past chunks (same as receive$); consider explicit caps / drop / error strategies only if real overload cases appear. Do not promise TCP-style backpressure through RxJS alone.
receive$ / lines$ / terminalText$| Stream | Role |
|---|---|
receive$ |
Unframed UTF-8 decoded chunks |
lines$ |
Newline-framed strings from the text pipeline |
terminalText$ |
Display-oriented fold of receive$ |
receiveBytes$ (hypothetical) |
Unframed wire Uint8Array chunks — parallel opt-in, not a replacement |
Do not replace or redefine receive$ as bytes. Keep text streams stable; any byte API must be additive.
Illustrative only — not implemented:
interface SerialSession {
readonly receive$: Observable<string>;
readonly lines$: Observable<string>;
readonly terminalText$: Observable<string>;
/** Hypothetical — not in this release */
readonly receiveBytes$: Observable<Uint8Array>;
}
Design constraints for a future implementation PR:
receive$ / lines$ / terminalText$.TextDecoder.Optional later refinements (also deferred): session option to disable text decoding when only bytes are needed; explicit buffer limits. None of these are required until go criteria are met.
client.bytes$v1 exposed client.bytes$. v2+ removed it; there is still no binary receive API. See Migrating to v2.
| Approach | Verdict |
|---|---|
Restore a drop-in bytes$ name for compatibility |
Not required; v2 migration already documents removal |
TextEncoder.encode on receive$ chunks |
Rejected — does not restore wire bytes |
New additive receiveBytes$ if demand appears |
Acceptable shape; not a silent revive of v1 semantics without design |
If a byte API ships later, migration notes should point here and to migration-v2.md, and must not claim lossless round-trip from decoded strings.
Use this table when reconsidering the feature.
| Criterion | Current assessment |
|---|---|
| Confirmed real-world use cases that cannot use text APIs or app-owned Web Serial | Not established for shipping in-core |
Clear reason receiveBytes$ belongs in this library vs app code |
Insufficient today |
Responsibilities vs receive$ / lines$ / terminalText$ documented |
Yes (this page) |
| Same-loop fan-out (bytes before decode) understood | Yes |
| Slow-subscriber / buffer policy stated | Yes (no unbounded promise) |
| Additive API possible without breaking change | Yes |
| Value outweighs API and pump complexity | No for an immediate add |
| Choosing not to implement remains valid | Yes — current decision |
All of the following should be true before an implementation issue is opened:
receive$ / Recipes / direct Web Serial.receiveBytes$ (or equivalent) is the public shape.receiveBytes$ in this documentation changeclient.bytes$ removal