Skip to content

Commit 0a03aaa

Browse files
authored
Merge pull request #297 from bbopen/refactor/0.9-transport-collapse
refactor(runtime)!: collapse the subprocess transport stack — single-attempt requests, merged pool, always-on framing
2 parents 353e7e6 + a6a2914 commit 0a03aaa

29 files changed

Lines changed: 1210 additions & 2014 deletions

docs/public/llms-full.txt

Lines changed: 105 additions & 175 deletions
Large diffs are not rendered by default.

docs/reference/env-vars.md

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,24 +16,6 @@ These affect the Python bridge or decoded runtime behavior.
1616
| `TYWRAP_ALLOWED_MODULES` | Python bridge | unset | Comma- and/or whitespace-separated import allowlist; blank is unset, while a non-empty value permits only named modules plus bridge-required stdlib modules. See [SECURITY.md](https://github.com/bbopen/tywrap/blob/main/SECURITY.md). |
1717
| `TYWRAP_ALLOW_PRIVATE_ATTRS` | Python bridge | off | Set to `1`, `true`, or `yes` to allow underscore-prefixed attribute access; otherwise it is blocked |
1818

19-
## Chunked transport (`tywrap-frame/1`)
20-
21-
These negotiate the large-payload chunked transport. They are set **by the Node
22-
subprocess transport on the Python bridge it spawns** when `enableChunking` is
23-
requested — you do not normally set them by hand. Chunking is
24-
**subprocess-only** (it is the only backend with a real frame ceiling, the JSONL
25-
line-length limit); HTTP and Pyodide are single-frame and ignore these. If the
26-
bridge does not see all three agree, it stays single-frame and an oversize
27-
payload fails loud — there is no silent single-frame fallback. See
28-
[Transport framing](../transport-framing.md) and the
29-
[capability matrix](../transport-capabilities.md).
30-
31-
| Variable | Scope | Default | Purpose |
32-
| ---------------------------------- | ------------- | ------- | ----------------------------------------------------------------------------------------------- |
33-
| `TYWRAP_TRANSPORT_CHUNKING` | Python bridge | unset | Set to `1`/`true`/`yes` to enable `tywrap-frame/1` chunked framing on the bridge |
34-
| `TYWRAP_TRANSPORT_FRAME_PROTOCOL` | Python bridge | unset | The framing protocol the JS side advertises; must equal `tywrap-frame/1` for chunking to enable |
35-
| `TYWRAP_TRANSPORT_MAX_FRAME_BYTES` | Python bridge | unset | Per-frame UTF-8 byte ceiling for each frame's data slice (the JS side's JSONL `maxLineLength`) |
36-
3719
## Logging
3820

3921
These control the structured logger in the JavaScript runtime.

docs/transport-capabilities.md

Lines changed: 42 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,20 @@
22

33
Every tywrap transport exposes a static, transport-level capability descriptor
44
via `Transport.capabilities()`. The same descriptor is surfaced by
5-
`RpcClient.capabilities()` (it delegates to the held transport). It tells callers
6-
what the wire channel can carry and how it frames messages **without** a network
7-
round-trip.
5+
`RpcClient.capabilities()` (it delegates to the held transport). It tells
6+
callers what the wire channel can carry and how it frames messages **without** a
7+
network round-trip.
88

99
This descriptor is deliberately separate from the bridge `meta` report
1010
(`BridgeInfo`, fetched via `RpcClient.getBridgeInfo()`):
1111

1212
- The **transport descriptor** is authoritative for transport-level flags — what
13-
bytes the channel moves and how it frames them. It does not depend on lifecycle
14-
state, so it is safe to read before `init()` and after `dispose()`.
15-
- The **`BridgeInfo` meta report** is authoritative for the *Python environment*
16-
which optional libraries (`arrowAvailable`, `scipyAvailable`, `torchAvailable`,
17-
`sklearnAvailable`) happen to be importable in the running interpreter.
13+
bytes the channel moves and how it frames them. It does not depend on
14+
lifecycle state, so it is safe to read before `init()` and after `dispose()`.
15+
- The **`BridgeInfo` meta report** is authoritative for the _Python environment_
16+
— which optional libraries (`arrowAvailable`, `scipyAvailable`,
17+
`torchAvailable`, `sklearnAvailable`) happen to be importable in the running
18+
interpreter.
1819

1920
When you need both questions answered — "can this transport carry Arrow **and**
2021
does this Python have pyarrow?" — consult both: the transport descriptor for the
@@ -35,47 +36,34 @@ interface TransportCapabilities {
3536

3637
## Matrix
3738

38-
| Backend (transport) | `backend` | `supportsArrow` | `supportsBinary` | `supportsChunking` | `supportsStreaming` | `maxFrameBytes` |
39-
| ------------------------------ | ------------- | --------------- | ---------------- | -------------------------- | ------------------- | -------------------------------- |
40-
| `SubprocessTransport` (Node) | `subprocess` | `true` | `true` | `true` when configured\* | `false` | JSONL line limit (default 100 MB) |
41-
| `HttpTransport` | `http` | `true` | `true` | `false` | `false` | `Number.POSITIVE_INFINITY` |
42-
| `PyodideTransport` (WASM) | `pyodide` | `false` | `true` | `false` | `false` | `Number.POSITIVE_INFINITY` |
43-
44-
\* `SubprocessTransport.supportsChunking` reports the **configured** capability:
45-
`true` when the transport was created with `enableChunking: true` (the default for
46-
`NodeBridge`), `false` otherwise. Like `supportsArrow`, it is a static,
47-
lifecycle-independent property of the transport — it does **not** make a round
48-
trip and does **not** change across `init()`/`dispose()`. Whether the *connected*
49-
bridge actually advertised the `tywrap-frame/1` block is the negotiated fact,
50-
surfaced separately on `BridgeInfo.transport.supportsChunking` (from the `meta`
51-
probe). "Will chunking actually happen for an oversize payload" needs **both**
52-
`true`; against an old/incapable bridge the oversize payload fails loud — never a
53-
silent single-frame fallback. Chunking is subprocess-only — it is the only backend
54-
with a real frame ceiling (the JSONL line-length limit). See
39+
| Backend (transport) | `backend` | `supportsArrow` | `supportsBinary` | `supportsChunking` | `supportsStreaming` | `maxFrameBytes` |
40+
| ---------------------------- | ------------ | --------------- | ---------------- | -------------------------- | ------------------- | ---------------------------------- |
41+
| `SubprocessTransport` (Node) | `subprocess` | `true` | `true` | `true` (always-on framing) | `false` | Per-frame ceiling (default 100 MB) |
42+
| `HttpTransport` | `http` | `true` | `true` | `false` | `false` | `Number.POSITIVE_INFINITY` |
43+
| `PyodideTransport` (WASM) | `pyodide` | `false` | `true` | `false` | `false` | `Number.POSITIVE_INFINITY` |
44+
45+
`SubprocessTransport.supportsChunking` is always `true`. The npm package ships
46+
the JS and Python framing peers together, so no runtime negotiation is needed.
47+
Chunking is subprocess-only; HTTP and Pyodide have no JSONL line ceiling. See
5548
[Transport framing](./transport-framing.md).
5649

5750
`PooledTransport` (the multi-process Node path) reports the capabilities of the
5851
worker transport it distributes across — in practice `SubprocessTransport`. Its
5952
`capabilities()` is a **static** descriptor read from an un-initialized probe
60-
worker built by the same factory, so it reports the configured
61-
`supportsChunking` the workers carry (e.g. `true` when the pool's workers are
62-
created with `enableChunking: true`). The pool builds no live worker to answer a
63-
capability query. Each leased worker negotiates `tywrap-frame/1` independently
64-
inside its own `init()` — that per-worker negotiated fact lives on each worker's
65-
`BridgeInfo.transport.supportsChunking`, and a chunked request or response routed
66-
through a pool lease reassembles correctly.
53+
worker built by the same factory. Each leased worker always uses
54+
`tywrap-frame/1`, and chunked traffic through a lease reassembles correctly.
6755

6856
## Notes per flag
6957

7058
### `supportsArrow`
7159

72-
Whether the transport can carry Arrow-encoded payloads (binary IPC frames) on the
73-
wire.
60+
Whether the transport can carry Arrow-encoded payloads (binary IPC frames) on
61+
the wire.
7462

75-
- **subprocess / http**: `true`. The channel can move Arrow bytes. Whether Arrow is
76-
actually *used* for a given response still depends on the Python side
77-
(`BridgeInfo.arrowAvailable`) and the `TYWRAP_CODEC_FALLBACK` setting — those are
78-
runtime/codec concerns, not transport-level ones.
63+
- **subprocess / http**: `true`. The channel can move Arrow bytes. Whether Arrow
64+
is actually _used_ for a given response still depends on the Python side
65+
(`BridgeInfo.arrowAvailable`) and the `TYWRAP_CODEC_FALLBACK` setting — those
66+
are runtime/codec concerns, not transport-level ones.
7967
- **pyodide**: `false`. pyarrow is unavailable in WASM, so the Pyodide bootstrap
8068
forces JSON markers (`force_json_markers=True`) and reports
8169
`arrowAvailable: false`. The channel is JSON-only.
@@ -87,33 +75,29 @@ Whether the transport can carry arbitrary binary data (e.g. Python `bytes`).
8775

8876
### `supportsChunking` and `supportsStreaming`
8977

90-
`supportsChunking` is implemented for the **subprocess** backend as of **0.8.0**:
91-
it splits one logical message across multiple `tywrap-frame/1` frames so a payload
92-
can exceed the JSONL line ceiling. The capability reports the **configured** path
93-
(`enableChunking`, `true` by default on `NodeBridge`) and is static (see the table
94-
note above); the per-bridge negotiated fact lives on
95-
`BridgeInfo.transport.supportsChunking`. HTTP and Pyodide stay `false` — they have
96-
no line ceiling and buffer the whole payload in one frame. See
97-
[Transport framing](./transport-framing.md) for the wire format and negotiation
98-
handshake.
78+
`supportsChunking` is implemented for the **subprocess** backend as of
79+
**0.8.0**: it splits one logical message across multiple `tywrap-frame/1` frames
80+
so a payload can exceed the JSONL line ceiling. The capability is statically
81+
`true` for subprocess. HTTP and Pyodide stay `false`. See
82+
[Transport framing](./transport-framing.md) for the wire format.
9983

100-
`supportsStreaming` (incremental results for a single request) is `false` on every
101-
backend; it is not implemented in 0.8.0.
84+
`supportsStreaming` (incremental results for a single request) is `false` on
85+
every backend; it is not implemented in 0.8.0.
10286

10387
### `maxFrameBytes`
10488

10589
Maximum size, in bytes, of a single wire frame the transport itself will accept.
106-
`Number.POSITIVE_INFINITY` means the transport imposes no frame ceiling of its own
107-
(a higher layer — e.g. the codec's payload limit, default 10 MB — may still cap
108-
the size).
90+
`Number.POSITIVE_INFINITY` means the transport imposes no frame ceiling of its
91+
own (a higher layer — e.g. the codec's payload limit, default 10 MB — may still
92+
cap the size).
10993

11094
- **subprocess**: the JSONL line-length limit (`maxLineLength`, default
111-
`100 * 1024 * 1024` = 100 MB). A response line larger than this raises a protocol
112-
error.
113-
- **http**: `Number.POSITIVE_INFINITY`. The whole response body is read in one shot;
114-
the transport imposes no frame limit.
115-
- **pyodide**: `Number.POSITIVE_INFINITY`. Calls are in-memory string passing with no
116-
framing.
95+
`100 * 1024 * 1024` = 100 MB). A response line larger than this raises a
96+
protocol error.
97+
- **http**: `Number.POSITIVE_INFINITY`. The whole response body is read in one
98+
shot; the transport imposes no frame limit.
99+
- **pyodide**: `Number.POSITIVE_INFINITY`. Calls are in-memory string passing
100+
with no framing.
117101

118102
## Example
119103

0 commit comments

Comments
 (0)