@statewalker/webrun-rpc 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +549 -0
- package/dist/byte-channel.d.ts +15 -0
- package/dist/byte-channel.d.ts.map +1 -0
- package/dist/call-bidi.d.ts +25 -0
- package/dist/call-bidi.d.ts.map +1 -0
- package/dist/call-port.d.ts +37 -0
- package/dist/call-port.d.ts.map +1 -0
- package/dist/cancel-channel.d.ts +15 -0
- package/dist/cancel-channel.d.ts.map +1 -0
- package/dist/close-signal.d.ts +36 -0
- package/dist/close-signal.d.ts.map +1 -0
- package/dist/connect-serve.d.ts +104 -0
- package/dist/connect-serve.d.ts.map +1 -0
- package/dist/duplex-over-port.d.ts +49 -0
- package/dist/duplex-over-port.d.ts.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1180 -0
- package/dist/io-handle.d.ts +17 -0
- package/dist/io-handle.d.ts.map +1 -0
- package/dist/io-send.d.ts +26 -0
- package/dist/io-send.d.ts.map +1 -0
- package/dist/listen-bidi.d.ts +11 -0
- package/dist/listen-bidi.d.ts.map +1 -0
- package/dist/listen-port.d.ts +16 -0
- package/dist/listen-port.d.ts.map +1 -0
- package/dist/message-target.d.ts +16 -0
- package/dist/message-target.d.ts.map +1 -0
- package/dist/multiplex-port.d.ts +12 -0
- package/dist/multiplex-port.d.ts.map +1 -0
- package/dist/port-types.d.ts +86 -0
- package/dist/port-types.d.ts.map +1 -0
- package/dist/recieve.d.ts +35 -0
- package/dist/recieve.d.ts.map +1 -0
- package/dist/send.d.ts +23 -0
- package/dist/send.d.ts.map +1 -0
- package/dist/structured-codec.d.ts +12 -0
- package/dist/structured-codec.d.ts.map +1 -0
- package/dist/through-abort.d.ts +8 -0
- package/dist/through-abort.d.ts.map +1 -0
- package/dist/transfer-port-mux.d.ts +39 -0
- package/dist/transfer-port-mux.d.ts.map +1 -0
- package/dist/virtual-port.d.ts +19 -0
- package/dist/virtual-port.d.ts.map +1 -0
- package/package.json +51 -0
- package/src/byte-channel.ts +109 -0
- package/src/call-bidi.ts +60 -0
- package/src/call-port.ts +119 -0
- package/src/cancel-channel.ts +42 -0
- package/src/close-signal.ts +43 -0
- package/src/connect-serve.ts +208 -0
- package/src/duplex-over-port.ts +471 -0
- package/src/index.ts +29 -0
- package/src/io-handle.ts +40 -0
- package/src/io-send.ts +70 -0
- package/src/listen-bidi.ts +31 -0
- package/src/listen-port.ts +47 -0
- package/src/message-target.ts +18 -0
- package/src/multiplex-port.ts +134 -0
- package/src/port-types.ts +80 -0
- package/src/recieve.ts +89 -0
- package/src/send.ts +60 -0
- package/src/structured-codec.ts +30 -0
- package/src/through-abort.ts +32 -0
- package/src/transfer-port-mux.ts +106 -0
- package/src/virtual-port.ts +71 -0
package/README.md
ADDED
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
# @statewalker/webrun-rpc
|
|
2
|
+
|
|
3
|
+
Ports and RPC over them, in the `webrun-streams-*` family: two port
|
|
4
|
+
multiplexers that turn one `MessageTarget` into many, a `Duplex` stream tier
|
|
5
|
+
that runs one stream over one port, a `Connect` / `Serve` adapter over any
|
|
6
|
+
single port, and a typed-JSON RPC tier that runs over any `MessageTarget`.
|
|
7
|
+
|
|
8
|
+
## What this is
|
|
9
|
+
|
|
10
|
+
Four pieces, one dependency:
|
|
11
|
+
|
|
12
|
+
- **Port multiplexing** (`multiplexPort` / `PortMux`, and `transferPortMux`) —
|
|
13
|
+
turns one `MessageTarget` into many virtual ones. Each virtual port is itself
|
|
14
|
+
a `MessageTarget`, so a multiplexer composes over another multiplexer's port,
|
|
15
|
+
and everything below can run directly on top of it. `multiplexPort` emulates
|
|
16
|
+
multiplexing over any single port; `transferPortMux` hands the peer real
|
|
17
|
+
transferred `MessagePort`s where the platform provides them. See
|
|
18
|
+
[Port multiplexing](#port-multiplexing) and
|
|
19
|
+
[Transferring ports](#transferring-ports).
|
|
20
|
+
- **Stream tier** (`duplexOverPort` / `serveDuplexOverPort`) — one
|
|
21
|
+
[`webrun-streams`](../webrun-streams) `Duplex` over one port, with
|
|
22
|
+
backpressure that is a property of the protocol rather than a configured
|
|
23
|
+
buffer. **This is what new code should use.** See
|
|
24
|
+
[Streams over a port](#streams-over-a-port).
|
|
25
|
+
- **Connect/serve adapter** (`connect` / `serve`) — the `webrun-streams`
|
|
26
|
+
`Connect` / `Serve` seam over a single port, so a port pair drops into the
|
|
27
|
+
same slot as a socket or a data channel. It is the two pieces above wired
|
|
28
|
+
together and nothing else: `multiplexPort` allocates one virtual port per
|
|
29
|
+
call and `duplexOverPort` runs the call on it, which is why it inherits the
|
|
30
|
+
stream tier's bounded memory rather than a buffer ceiling of its own. See
|
|
31
|
+
[Connect and serve](#connect-and-serve).
|
|
32
|
+
- **Typed-JSON RPC tier** (`callPort` / `listenPort` / `callBidi` /
|
|
33
|
+
`listenBidi` / `ioSend` / `ioHandle` / `send` / `recieve`) — request/response
|
|
34
|
+
with typed JSON arguments per call, relocated here from the retired
|
|
35
|
+
`webrun-streams-port` package. It types against `MessageTarget` rather than
|
|
36
|
+
`MessagePort`, so it also runs directly over a virtual port from
|
|
37
|
+
`multiplexPort` — one raw port fans out into many independent RPC channels
|
|
38
|
+
with no byte-stream layer in between. Use this when you want plain JSON
|
|
39
|
+
messaging and do not need byte-stream semantics.
|
|
40
|
+
|
|
41
|
+
All four are exported from the package root. The only runtime dependency is
|
|
42
|
+
[`@statewalker/webrun-streams`](../webrun-streams).
|
|
43
|
+
|
|
44
|
+
## Why it exists
|
|
45
|
+
|
|
46
|
+
`MessagePort` is the browser's universal in-process seam — Workers,
|
|
47
|
+
SharedWorkers, ServiceWorkers, iframes, `MessageChannel` pipes between two
|
|
48
|
+
modules in the same tab. It is also the most primitive: `postMessage` fires and
|
|
49
|
+
forgets. There is no request, no correlation, no backpressure, no half-close,
|
|
50
|
+
and an exception on the far side simply never arrives.
|
|
51
|
+
|
|
52
|
+
The stream tier makes a port indistinguishable from any other transport in
|
|
53
|
+
this family, which is what lets an in-browser back-end be tested in-process and
|
|
54
|
+
then moved behind a real socket unchanged. The RPC tier exists because a lot of
|
|
55
|
+
port traffic is not a byte stream at all — it is one typed call with one typed
|
|
56
|
+
answer, and forcing that through a `Duplex` is more machinery than the job
|
|
57
|
+
needs. The multiplexer exists because a single `MessagePort` is usually the
|
|
58
|
+
only pipe you're handed — one `MessageChannel` per logical stream doesn't
|
|
59
|
+
scale — so `multiplexPort` fans it out into as many independent
|
|
60
|
+
`MessageTarget`s as the RPC tier or byte-stream tier need.
|
|
61
|
+
|
|
62
|
+
## Install
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
npm install @statewalker/webrun-rpc
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
No peer dependencies. `MessagePort` and `MessageChannel` are platform globals in
|
|
69
|
+
browsers and in Node ≥ 15 (`node:worker_threads`).
|
|
70
|
+
|
|
71
|
+
## Getting started
|
|
72
|
+
|
|
73
|
+
### Connect and serve
|
|
74
|
+
|
|
75
|
+
Reach for this when you want the family's `Connect` / `Serve` shape over a
|
|
76
|
+
source of ports; reach for [Streams over a port](#streams-over-a-port) directly
|
|
77
|
+
when you are already holding a port per call.
|
|
78
|
+
|
|
79
|
+
`connect` and `serve` take a **port factory**, not a port. They ask it for one
|
|
80
|
+
port per call and run a stream on it — so *how* ports are made belongs to
|
|
81
|
+
whoever knows the transport:
|
|
82
|
+
|
|
83
|
+
| Transport | Factory | Id table? |
|
|
84
|
+
|---|---|---|
|
|
85
|
+
| one pipe of bytes (`MessagePort`, worker, WebSocket) | `overPipe(pipe, { codec })` | **yes** — there is no second port to be had |
|
|
86
|
+
| a transferable boundary | `overPorts((onPort) => transferPortMux(target, { onPort }))` | no — the platform moves real ports |
|
|
87
|
+
| a transport that multiplexes already (libp2p's yamux) | `overPorts((onPort) => yourMux({ onPort }))` | no — it did the work |
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
import { connect, overPipe, serve, structuredCodec } from "@statewalker/webrun-rpc";
|
|
91
|
+
|
|
92
|
+
const channel = new MessageChannel();
|
|
93
|
+
const codec = structuredCodec;
|
|
94
|
+
|
|
95
|
+
const stop = await serve(
|
|
96
|
+
{ mux: overPipe(channel.port2, { codec, side: "responder" }) },
|
|
97
|
+
async function* echo(input) {
|
|
98
|
+
for await (const chunk of input) yield chunk;
|
|
99
|
+
},
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
const { call, close } = await connect({
|
|
103
|
+
mux: overPipe(channel.port1, { codec, side: "initiator" }),
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
for await (const chunk of call([new TextEncoder().encode("ping")])) {
|
|
107
|
+
console.log(new TextDecoder().decode(chunk)); // "ping"
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
await close();
|
|
111
|
+
await stop();
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Across a Worker boundary, transfer one end and keep the other:
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
const { port1, port2 } = new MessageChannel();
|
|
118
|
+
worker.postMessage({ port: port2 }, [port2]);
|
|
119
|
+
const { call } = await connect({ mux: overPipe(port1, { codec: structuredCodec }) });
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
#### Why a factory and not a `PortMux`
|
|
123
|
+
|
|
124
|
+
Every mux decides accept-or-reject **synchronously**, inside the `open`
|
|
125
|
+
envelope, and tells the peer on the spot — `multiplexPort` posts
|
|
126
|
+
`{type:"close", reason:"rejected"}` before returning. There is no "decide
|
|
127
|
+
later", and layer 1 refuses to queue what it cannot deliver.
|
|
128
|
+
|
|
129
|
+
So handing `serve` a mux that already exists would leave a window between its
|
|
130
|
+
construction and its handler being attached, and a port arriving in that window
|
|
131
|
+
is rejected outright — **measured: zero messages delivered**, with the peer told
|
|
132
|
+
`rejected` for a call whose only fault was being early. A factory closes the
|
|
133
|
+
window by construction: the mux cannot exist before the thing that answers it.
|
|
134
|
+
`port-factory-no-race.test.ts` measures both halves.
|
|
135
|
+
|
|
136
|
+
`connect` passes no handler, which is how a caller declines inbound ports —
|
|
137
|
+
your factory receives `undefined`. Whichever of the two called the factory owns
|
|
138
|
+
the mux and closes it.
|
|
139
|
+
|
|
140
|
+
#### Port-id sides
|
|
141
|
+
|
|
142
|
+
`side: "initiator" | "responder"` belongs to `overPipe`, because it is
|
|
143
|
+
`multiplexPort`'s id parity: initiator takes even port ids, responder odd. The
|
|
144
|
+
two ends of one pipe must disagree, or their ids collide on the first
|
|
145
|
+
concurrent call. A transport that multiplexes on its own has no ids and no
|
|
146
|
+
parity, which is why this is not a `connect`/`serve` option any more.
|
|
147
|
+
|
|
148
|
+
#### Tuning
|
|
149
|
+
|
|
150
|
+
`overPipe` takes `maxPorts` (concurrent in-flight calls) and `maxMessageSize`
|
|
151
|
+
(payload split size, if the transport caps a message); `connect`/`serve` take
|
|
152
|
+
`timeout` (per-stream inactivity, off by default). `maxMessageSize` is
|
|
153
|
+
*reported* by the mux to the stream tier, so a transport with no message
|
|
154
|
+
ceiling — a libp2p stream — simply does not set one and nothing is split.
|
|
155
|
+
There is deliberately **no buffer size**. The stream tier withholds a chunk's confirmation until the
|
|
156
|
+
consumer has pulled past it, so a producer is at most one chunk ahead of its
|
|
157
|
+
consumer and the ceiling is one chunk per open port — a number there is nothing
|
|
158
|
+
to tune. `connect-serve-backpressure.test.ts` measures it: a 5000-chunk
|
|
159
|
+
producer against a handler that reads one chunk and stops gets exactly one
|
|
160
|
+
chunk out.
|
|
161
|
+
|
|
162
|
+
### Typed-JSON RPC tier
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
import { callPort, listenPort } from "@statewalker/webrun-rpc";
|
|
166
|
+
|
|
167
|
+
// Responder
|
|
168
|
+
const off = listenPort(port2, async ({ a, b }) => ({ sum: a + b }));
|
|
169
|
+
|
|
170
|
+
// Caller
|
|
171
|
+
const { sum } = await callPort(port1, { a: 2, b: 3 }, { timeout: 2000 });
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
`callBidi` / `listenBidi` extend this to a streaming outer call, and `ioSend` /
|
|
175
|
+
`ioHandle` carry an async iterator across the port.
|
|
176
|
+
|
|
177
|
+
### Carrying HTTP over a port
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
import { fetchOverDuplex } from "@statewalker/webrun-http-streams";
|
|
181
|
+
|
|
182
|
+
const response = await fetchOverDuplex(call, new Request("http://local/api/todo"));
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Streams over a port
|
|
186
|
+
|
|
187
|
+
`duplexOverPort(port, options)` runs one `Duplex` over one port;
|
|
188
|
+
`serveDuplexOverPort(port, handler, options)` is its serving half. Each
|
|
189
|
+
direction is one `callPort` per chunk on its own channel (`"in"` for the
|
|
190
|
+
caller's input, `"out"` for the handler's output), and the reply to a chunk
|
|
191
|
+
*is* the confirmation that the consumer pulled past it.
|
|
192
|
+
|
|
193
|
+
**One stream per port.** A stream port carries exactly one invocation — open
|
|
194
|
+
one port per call. Nothing enforces this: invoking the same `duplexOverPort`
|
|
195
|
+
result twice on one port makes both invocations cross-talk on the same two
|
|
196
|
+
channel names.
|
|
197
|
+
|
|
198
|
+
```js
|
|
199
|
+
import {
|
|
200
|
+
duplexOverPort,
|
|
201
|
+
multiplexPort,
|
|
202
|
+
serveDuplexOverPort,
|
|
203
|
+
structuredCodec,
|
|
204
|
+
} from "@statewalker/webrun-rpc";
|
|
205
|
+
|
|
206
|
+
const channel = new MessageChannel();
|
|
207
|
+
channel.port1.start();
|
|
208
|
+
channel.port2.start();
|
|
209
|
+
|
|
210
|
+
// The serving end: every stream port the peer opens runs one echo handler.
|
|
211
|
+
const server = multiplexPort(channel.port2, {
|
|
212
|
+
codec: structuredCodec,
|
|
213
|
+
side: "responder",
|
|
214
|
+
onPort: (port) => {
|
|
215
|
+
serveDuplexOverPort(port, async function* echo(input) {
|
|
216
|
+
for await (const chunk of input) yield chunk;
|
|
217
|
+
});
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// The calling end: one port per stream.
|
|
222
|
+
const client = multiplexPort(channel.port1, {
|
|
223
|
+
codec: structuredCodec,
|
|
224
|
+
side: "initiator",
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
const streamPort = await client.openPort({ kind: "stream" });
|
|
228
|
+
const call = duplexOverPort(streamPort, { maxMessageSize: client.maxMessageSize });
|
|
229
|
+
|
|
230
|
+
const parts = [];
|
|
231
|
+
for await (const chunk of call([new TextEncoder().encode("ping")])) {
|
|
232
|
+
parts.push(new TextDecoder().decode(chunk));
|
|
233
|
+
}
|
|
234
|
+
console.log(parts.join("")); // "ping"
|
|
235
|
+
|
|
236
|
+
await client.close();
|
|
237
|
+
await server.close();
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### `DuplexOverPortOptions`
|
|
241
|
+
|
|
242
|
+
| option | type | meaning |
|
|
243
|
+
| --- | --- | --- |
|
|
244
|
+
| `maxMessageSize` | `number` | Largest payload one chunk may carry, normally `PortMux.maxMessageSize`. Bodies are split to fit with `toChunks`. Unset means no limit and no splitting. |
|
|
245
|
+
| `timeout` | `number` | Inactivity timeout for the **whole stream**, in ms. See below. Unset — the default — means no timeout at all. |
|
|
246
|
+
| `log` | `(...args) => void` | Logging hook; defaults to a no-op. |
|
|
247
|
+
|
|
248
|
+
### Flow control: a window of one
|
|
249
|
+
|
|
250
|
+
Within one direction the next chunk is never sent until the previous one has
|
|
251
|
+
been delivered **and** pulled past by the consumer. There is no credit window
|
|
252
|
+
and no buffer ceiling to tune, because there is nothing to tune: **one open
|
|
253
|
+
port holds at most one chunk.** Over `multiplexPort` that composes into a
|
|
254
|
+
whole-mux bound of `maxPorts × one chunk`, because `maxPorts` caps concurrent
|
|
255
|
+
ports. `transferPortMux` has no `maxPorts` (see
|
|
256
|
+
[Transferring ports](#transferring-ports)), so the per-port bound is the same
|
|
257
|
+
but the mux-wide one is yours to impose by bounding how many ports you open.
|
|
258
|
+
|
|
259
|
+
A peer that sends a second chunk before the first is confirmed has that call
|
|
260
|
+
refused and **that port closed** — the penalty is scoped to the offending port
|
|
261
|
+
and every other port on the mux is untouched.
|
|
262
|
+
|
|
263
|
+
The honest cost, from the design's own numbers: single-stream throughput is
|
|
264
|
+
`chunk ÷ RTT`. In-process that is negligible. Over a 50 ms WAN round trip a
|
|
265
|
+
10 MiB body is **~43 s**, because it is 854 sequential round trips.
|
|
266
|
+
Concurrency does not come from pipelining one stream — it comes from running
|
|
267
|
+
many streams, which are genuinely independent because each owns its own port.
|
|
268
|
+
|
|
269
|
+
### The timeout
|
|
270
|
+
|
|
271
|
+
There is **no timeout by default**, and that is deliberate: a per-chunk
|
|
272
|
+
deadline fails a consumer that is merely slow, which is a bug rather than a
|
|
273
|
+
policy. `callPort` gained `NO_TIMEOUT` for the same reason, and the stream tier
|
|
274
|
+
uses it for every chunk call.
|
|
275
|
+
|
|
276
|
+
The `timeout` option is an inactivity timeout for the whole stream: any chunk
|
|
277
|
+
in either direction resets it, and elapsing aborts the stream.
|
|
278
|
+
|
|
279
|
+
**Know what you are buying if you set it.** The clock is only reset once a
|
|
280
|
+
chunk call *returns*, and that reply is withheld until the consumer has pulled
|
|
281
|
+
past the value. The inactivity clock therefore cannot distinguish "the peer is
|
|
282
|
+
slow" from "the peer is dead": with an explicit `timeout`, a consumer slower
|
|
283
|
+
than it **is** failed. The default of none is why a slow consumer is safe out
|
|
284
|
+
of the box.
|
|
285
|
+
|
|
286
|
+
### Cancellation
|
|
287
|
+
|
|
288
|
+
Layer 1's close is invisible to layer 2 (see
|
|
289
|
+
[Port multiplexing](#port-multiplexing)): a closed port drops its listeners
|
|
290
|
+
silently and is indistinguishable from a working port nobody is answering. So a
|
|
291
|
+
side that abandons a stream posts an out-of-band `STREAM_ABORT` notice on the
|
|
292
|
+
port — which is the only signal the peer can act on.
|
|
293
|
+
|
|
294
|
+
**Three routes post that notice; two do not.** The notice is posted from
|
|
295
|
+
exactly two places in the implementation — the teardown returned by
|
|
296
|
+
`serveDuplexOverPort`, and the caller's own `finally` as its generator unwinds
|
|
297
|
+
— and everything else that ends a stream simply aborts locally. Measured:
|
|
298
|
+
|
|
299
|
+
| what ends the stream | notice posted? | what the peer actually observes |
|
|
300
|
+
| --- | --- | --- |
|
|
301
|
+
| the caller stops iterating (`break`, `return`, `throw`) | **yes** | `STREAM_ABORT`; the handler unwinds through `iter.return()` |
|
|
302
|
+
| the caller's inactivity `timeout` elapses | **yes** — the abort rejects the caller's stream, and its `finally` posts on the way out | `STREAM_ABORT` |
|
|
303
|
+
| you call the teardown `serveDuplexOverPort` returned | **yes** | `STREAM_ABORT`; the caller's stream rejects with `the peer abandoned the stream` |
|
|
304
|
+
| the **serve side's** inactivity `timeout` elapses | **no** | nothing on the `out` half — a caller parked there waits forever. A caller still *sending* has its chunk calls answered `webrun-rpc: the stream is closed`, but `duplexOverPort` surfaces only the inbound half, so that rejection never reaches its consumer either |
|
|
305
|
+
| a **window violation** (a second unconfirmed chunk), on either side | **no** | the offender gets `response:error` on the channel it violated, naming the violation. Nothing else: the enforcer closes the port, and that close is exactly the layer 1 signal layer 2 cannot see. A peer parked on the other half waits forever |
|
|
306
|
+
|
|
307
|
+
So the notice is what makes a *cooperative* abandonment observable. It is not a
|
|
308
|
+
general liveness mechanism, and the two rows without one are a known gap rather
|
|
309
|
+
than a subtlety of the wording: **give the side that must not hang its own
|
|
310
|
+
`timeout`.** A caller with a `timeout` set detects both of the silent rows on
|
|
311
|
+
its own clock; a caller without one does not detect them at all.
|
|
312
|
+
|
|
313
|
+
**An abort unwinds a producing handler through `iter.return()`, not by
|
|
314
|
+
throwing into it.** A handler's `catch` never sees the abort reason; only its
|
|
315
|
+
`finally` runs. Put cleanup in `finally`.
|
|
316
|
+
|
|
317
|
+
## API
|
|
318
|
+
|
|
319
|
+
### Stream tier — exports
|
|
320
|
+
|
|
321
|
+
| Export | Kind | Purpose |
|
|
322
|
+
| --- | --- | --- |
|
|
323
|
+
| `duplexOverPort(port, options?)` | function | Returns a `Duplex` running one stream on `port`. |
|
|
324
|
+
| `serveDuplexOverPort(port, handler, options?)` | function | Installs `handler` as the serving half. Returns an idempotent teardown that abandons the stream and notifies the peer. |
|
|
325
|
+
| `STREAM_ABORT` | constant | The `type` of the out-of-band notice a side posts when it abandons a stream. Exported because tests and adapters assert on it. |
|
|
326
|
+
| `DuplexOverPortOptions` | type | `maxMessageSize`, `timeout`, `log` — see [above](#duplexoverportoptions). |
|
|
327
|
+
| `NO_TIMEOUT` | constant | Pass as `callPort`'s `timeout` to install no deadline at all. |
|
|
328
|
+
|
|
329
|
+
### Connect/serve tier — exports
|
|
330
|
+
|
|
331
|
+
| Export | Kind | Purpose |
|
|
332
|
+
| --- | --- | --- |
|
|
333
|
+
| `connect(params)` | `Connect<PortParams>` | Resolves `{ call, close }`. One port per call, opened on the consumer's first pull. |
|
|
334
|
+
| `serve(params, handler)` | `Serve<PortParams>` | Registers a `Duplex` handler. Returns an idempotent teardown that abandons the live streams before releasing the mux. |
|
|
335
|
+
| `PortParams` | type | `{ mux: PortMuxFactory; timeout?: number }`. |
|
|
336
|
+
| `PortMuxFactory` | type | `(onPort?) => PortMux \| Promise<PortMux>`. Why a factory: [above](#why-a-factory-and-not-a-portmux). |
|
|
337
|
+
| `overPipe(pipe, options)` | `PortMuxFactory` | Ports over one pipe of bytes, via `multiplexPort`. Takes `codec`, `side`, `maxPorts`, `maxMessageSize`. |
|
|
338
|
+
| `overPorts(factory)` | `PortMuxFactory` | Pass-through, for a mux this package knows nothing about. |
|
|
339
|
+
| `OnPort` | type | `(port, meta?) => boolean \| undefined`. `false` rejects. |
|
|
340
|
+
|
|
341
|
+
`byteChannelFromMessagePort(port)` is still exported, and is now the only thing
|
|
342
|
+
in the package that touches `ByteChannel`: it wraps a port for driving
|
|
343
|
+
`emulateMux` yourself. Nothing here uses it. It goes away with `emulateMux` in
|
|
344
|
+
Plan C.
|
|
345
|
+
|
|
346
|
+
### Typed-JSON RPC tier — exports
|
|
347
|
+
|
|
348
|
+
| Export | Purpose |
|
|
349
|
+
| --- | --- |
|
|
350
|
+
| `callPort(port, args, options?)` | One typed call, one typed answer. |
|
|
351
|
+
| `listenPort(port, handler, options?)` | Answer `callPort` requests. Returns an unsubscribe. |
|
|
352
|
+
| `callBidi(port, args)` / `listenBidi(port, handler)` | Streaming outer call in both directions. |
|
|
353
|
+
| `ioSend(...)` / `ioHandle(...)` | Ship an async iterator across the port. |
|
|
354
|
+
| `send(...)` / `recieve(...)` | The low-level message primitives underneath. |
|
|
355
|
+
| `CallPortOptions` | `timeout` (default 1000 ms; `NO_TIMEOUT`, or any value that is not a finite number above zero, installs no deadline), `channelName`, `log`, `newCallId`, `signal`. |
|
|
356
|
+
| `CallBidiOptions` / `CallBidiArgs` | `bidiTimeout` for the outer stream. |
|
|
357
|
+
| `ListenPortOptions`, `PortHandler`, `BidiHandler`, `IoSendOptions`, `RecieveOptions`, `SendOptions` | Supporting types. |
|
|
358
|
+
|
|
359
|
+
### Cancellation and close signalling
|
|
360
|
+
|
|
361
|
+
| Export | Purpose |
|
|
362
|
+
| --- | --- |
|
|
363
|
+
| `postCancelChannel(...)` / `listenCancelChannel(...)` | Out-of-band cancellation for an in-flight call. |
|
|
364
|
+
| `CANCEL_CHANNEL_TYPE` | The reserved message type used for it. |
|
|
365
|
+
| `setPortCloseSignal(port, signal)` / `getPortCloseSignal(port)` | Attach an `AbortSignal` that marks a port as closed, so pending calls reject instead of hanging. |
|
|
366
|
+
|
|
367
|
+
## Port multiplexing
|
|
368
|
+
|
|
369
|
+
`multiplexPort` turns one message port into many. Each virtual port is itself a
|
|
370
|
+
`MessageTarget` — the same shape as a `MessagePort` — so whatever runs on top
|
|
371
|
+
cannot tell a virtual port from a real one, and a multiplexer composes over
|
|
372
|
+
another multiplexer's port.
|
|
373
|
+
|
|
374
|
+
**A port sends and receives messages. That is all.** No backpressure,
|
|
375
|
+
acknowledgements, credit or buffering ceiling — deliberately, matching
|
|
376
|
+
`MessagePort` semantics. Waiting strategies belong above. The one safety
|
|
377
|
+
property it does hold: **a message for a port with no consumer is dropped, never
|
|
378
|
+
queued**, so a peer flooding an unaccepted port cannot grow memory here.
|
|
379
|
+
|
|
380
|
+
### `multiplexPort(port, options): PortMux`
|
|
381
|
+
|
|
382
|
+
The default implementation, which emulates multiplexing over a single port. A
|
|
383
|
+
transport that already multiplexes natively supplies its own `PortMux` instead.
|
|
384
|
+
|
|
385
|
+
| option | type | meaning |
|
|
386
|
+
| --- | --- | --- |
|
|
387
|
+
| `codec` | `PortCodec` | How envelopes reach the wire. Required. |
|
|
388
|
+
| `onPort` | `(port, meta?) => boolean \| undefined` | Called when the peer opens a port. Return `false` to reject. **Without it, inbound ports are rejected.** |
|
|
389
|
+
| `side` | `"initiator" \| "responder"` | Id parity — initiator allocates even, responder odd, so both ends may open concurrently. Defaults to `"initiator"`. |
|
|
390
|
+
| `maxPorts` | `number` | Ceiling on **concurrently** open ports. Defaults to `1024`. Closing a port frees its slot immediately, so a long-lived mux can open unboundedly many over its lifetime — this bounds the id table, never the total. It also never delays a message. |
|
|
391
|
+
| `maxMessageSize` | `number` | Largest **payload** a chunk may carry, if the transport has a limit. Reported, not enforced. It bounds the payload, **not the frame**: the layer above chunks the payload to it and the envelope framing is added on top afterwards — measured at 123–128 bytes over `msgpackCodec` (the 128 at a 64 KiB cap), with a modelled ceiling of 134. **Set this ~256 bytes below the transport's real limit**, or a full-size chunk overruns it (a 12 KiB setting was measured producing 12,413-byte frames). |
|
|
392
|
+
|
|
393
|
+
### `PortMux`
|
|
394
|
+
|
|
395
|
+
| member | meaning |
|
|
396
|
+
| --- | --- |
|
|
397
|
+
| `openPort(meta?)` | Allocate a port, announce it, and return the local end. **Asynchronous** — a natively multiplexed transport cannot produce a port synchronously, and the two must share a shape. Does not wait for the peer to accept. **Rejects** with `RangeError` past `maxPorts`, and rejects on a closed mux — it does not throw synchronously, so a bare `try`/`catch` around the call will not catch either. |
|
|
398
|
+
| `close()` | Close every virtual port, then release the underlying port. |
|
|
399
|
+
| `maxMessageSize` | See above. |
|
|
400
|
+
|
|
401
|
+
`openPort` returning a promise that resolves before acceptance is deliberate:
|
|
402
|
+
it keeps layer 1 free of round trips. Messages posted before the peer accepts
|
|
403
|
+
are sent, and dropped at the far end if it rejects; the port becomes inert.
|
|
404
|
+
Guard failures — `maxPorts` exceeded, the mux already closed — reject the
|
|
405
|
+
returned promise rather than throwing synchronously, so a caller wrapping a
|
|
406
|
+
bare `openPort(...)` call in `try`/`catch` will not catch them.
|
|
407
|
+
|
|
408
|
+
**No close is observable at this layer — not a rejection, and not an orderly close
|
|
409
|
+
either.** `MessageTarget` has no close event and the port you hold exposes no
|
|
410
|
+
queryable state, so when a port closes its listeners are simply cleared and
|
|
411
|
+
`postMessage` becomes a silent no-op. You get no event, no error, and no callback,
|
|
412
|
+
and a `close` envelope's `reason` is discarded rather than delivered. A closed port
|
|
413
|
+
is indistinguishable from a working one that nobody is answering.
|
|
414
|
+
|
|
415
|
+
This is deliberate — a real `MessagePort` behaves the same way, and a close event
|
|
416
|
+
would make this something other than a port — but it means **the layer above must
|
|
417
|
+
carry its own end-of-stream signal** as an ordinary message, sent before the port
|
|
418
|
+
closes, rather than relying on the close being seen.
|
|
419
|
+
|
|
420
|
+
### `structuredCodec`
|
|
421
|
+
|
|
422
|
+
For ports whose messages are structured values. Envelopes pass through
|
|
423
|
+
unencoded, so nothing is serialised and `ArrayBuffer`s move zero-copy through the
|
|
424
|
+
transfer list.
|
|
425
|
+
|
|
426
|
+
A byte transport needs a codec that encodes; that one ships with
|
|
427
|
+
`@statewalker/webrun-msgpack`, so this package keeps no dependencies.
|
|
428
|
+
|
|
429
|
+
### `PortEnvelope`
|
|
430
|
+
|
|
431
|
+
What crosses the wire: `{ type: "open", id, meta? }`, `{ type: "message", id,
|
|
432
|
+
payload }`, `{ type: "close", id, reason? }`.
|
|
433
|
+
|
|
434
|
+
`reason` is opaque: layer 1 never inspects it — and, as implemented, never surfaces
|
|
435
|
+
it either. Setting one accomplishes nothing observable at this layer today; it
|
|
436
|
+
exists so the wire format does not have to change when a layer above starts
|
|
437
|
+
carrying it.
|
|
438
|
+
|
|
439
|
+
## Transferring ports
|
|
440
|
+
|
|
441
|
+
`transferPortMux(target, options)` is a second `PortMux` with the same
|
|
442
|
+
`openPort` / `close` / `maxMessageSize` shape, so the stream tier above it is
|
|
443
|
+
identical — but its ports are **real, transferred `MessagePort`s**. Each
|
|
444
|
+
`openPort` creates a `MessageChannel`, transfers one end to the peer over
|
|
445
|
+
`target`, and returns the other. There is no id table, no `maxPorts` and no
|
|
446
|
+
envelope overhead per message, because the platform does the multiplexing.
|
|
447
|
+
|
|
448
|
+
```ts
|
|
449
|
+
import { transferPortMux } from "@statewalker/webrun-rpc";
|
|
450
|
+
|
|
451
|
+
const mux = transferPortMux(worker, {
|
|
452
|
+
onPort: (port, meta) => {
|
|
453
|
+
// `meta` is `unknown` — layer 1 never inspects it, so you narrow it.
|
|
454
|
+
if ((meta as { kind?: string })?.kind !== "stream") return false; // reject
|
|
455
|
+
serveDuplexOverPort(port, handler);
|
|
456
|
+
},
|
|
457
|
+
});
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
| option | type | meaning |
|
|
461
|
+
| --- | --- | --- |
|
|
462
|
+
| `onPort` | `(port, meta?) => boolean \| undefined` | Called when the peer transfers a port in. Return `false` to reject it — the port is closed and nothing further arrives on it. Any other return value, `undefined` included, accepts. **Without it, inbound ports are rejected**, matching `multiplexPort`. |
|
|
463
|
+
| `maxMessageSize` | `number` | Reported to the layer above, never enforced. A `MessagePort` normally has no limit. |
|
|
464
|
+
|
|
465
|
+
`PORT_TRANSFER` is the `type` of the envelope that carries a port to the peer.
|
|
466
|
+
A message with that `type` and no attached port is malformed and is dropped, so
|
|
467
|
+
a shared parent port is not corrupted.
|
|
468
|
+
|
|
469
|
+
**It needs structured clone with transferables**, so it exists in browsers,
|
|
470
|
+
workers and iframes and nowhere else — not over a byte transport. The caller
|
|
471
|
+
selects it explicitly rather than by capability sniffing: use `multiplexPort`
|
|
472
|
+
where the transport is one pipe of bytes.
|
|
473
|
+
|
|
474
|
+
What it buys over emulation: a transferred port can cross an origin or a worker
|
|
475
|
+
boundary and be handed to code that never saw `target`, which is what a relay
|
|
476
|
+
handing a live connection to a third party needs. An emulated port id is
|
|
477
|
+
meaningless outside its own mux.
|
|
478
|
+
|
|
479
|
+
`target` must be a full `MessageTarget`. Reaching a send-only `MessageSink` —
|
|
480
|
+
a `ServiceWorkerClient`, say — is a real use of port transfer but needs a
|
|
481
|
+
different entry point, and is not part of this interface.
|
|
482
|
+
|
|
483
|
+
**Caveat: the issued-port set never shrinks.** Every port `transferPortMux`
|
|
484
|
+
opens or accepts is retained until `close()`, and nothing removes a port from
|
|
485
|
+
that set when it closes. There is no `maxPorts` here for it to exhaust, so
|
|
486
|
+
nothing fails — the set just grows, holding dead `MessagePort` handles for as
|
|
487
|
+
long as the mux lives. Bounding it needs a per-port `close`-event listener, a
|
|
488
|
+
newer platform surface this implementation otherwise avoids; it is a known,
|
|
489
|
+
deliberately deferred gap rather than an oversight. In practice: **scope the
|
|
490
|
+
mux to the lifetime of the thing it multiplexes** — a worker, an iframe, a
|
|
491
|
+
connection — rather than making one process-wide mux and opening streams
|
|
492
|
+
through it forever.
|
|
493
|
+
|
|
494
|
+
## Message passing
|
|
495
|
+
|
|
496
|
+
| Export | Kind | Purpose |
|
|
497
|
+
| --- | --- | --- |
|
|
498
|
+
| `MessageTarget` | interface | Full-duplex structural view of a message endpoint — a `MessagePort`, a `Worker`, or a ServiceWorker bridge. Extends `MessageSource` and `MessageSink`. |
|
|
499
|
+
| `MessageSource` | interface | `addEventListener`/`removeEventListener` for `"message"`, plus optional `start()`. |
|
|
500
|
+
| `MessageSink` | interface | `postMessage(message, transfer?)`. |
|
|
501
|
+
| `MessageListener` | type | `(event: MessageEvent) => void \| Promise<void>`. |
|
|
502
|
+
|
|
503
|
+
These are types only — no runtime code — so pulling in just the RPC tier costs
|
|
504
|
+
nothing extra. A `MessagePort` satisfies `MessageTarget` structurally; no
|
|
505
|
+
adapter is needed.
|
|
506
|
+
|
|
507
|
+
## Conformance
|
|
508
|
+
|
|
509
|
+
The unmodified L0–L6 suite of
|
|
510
|
+
[`@statewalker/webrun-streams-conformance`](../webrun-streams-conformance) runs
|
|
511
|
+
**twice**, 11 tests each, against two entry points into the same stack:
|
|
512
|
+
|
|
513
|
+
| run | stack |
|
|
514
|
+
| --- | --- |
|
|
515
|
+
| `webrun-rpc (MessageChannel pair)` | `connect` / `serve` — the adapter, driving both pieces below |
|
|
516
|
+
| `webrun-rpc (multiplexPort + duplexOverPort)` | the two pieces wired by hand in the test |
|
|
517
|
+
|
|
518
|
+
The suite itself has never been changed to accommodate either run.
|
|
519
|
+
|
|
520
|
+
```sh
|
|
521
|
+
pnpm --filter @statewalker/webrun-rpc test
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
**L6 does not prove flow control on either run.** `PairTuning` is a credit
|
|
525
|
+
window (`mtu`, `maxStreamBuffer`) and this stack has none to size. The
|
|
526
|
+
`connect`/`serve` pair translates `mtu` to `maxMessageSize`, so its L6 at least
|
|
527
|
+
runs the 256 KiB body as 64 frames rather than one, and drops
|
|
528
|
+
`maxStreamBuffer`; the hand-wired pair ignores the tuning outright, and with
|
|
529
|
+
`maxMessageSize` unset its L6 body crosses as exactly one chunk in each
|
|
530
|
+
direction. Both greens say the body round-trips. The bounded-memory property
|
|
531
|
+
is measured in `tests/connect-serve-backpressure.test.ts`, which counts how far
|
|
532
|
+
a 5000-chunk producer gets against a handler that reads one chunk and stops —
|
|
533
|
+
one chunk here, 5000 against the `emulateMux` implementation this replaced.
|
|
534
|
+
The rest of the flow-control coverage is in
|
|
535
|
+
`tests/duplex-over-port-timeout.test.ts` and
|
|
536
|
+
`tests/duplex-over-port-hostile.test.ts`.
|
|
537
|
+
|
|
538
|
+
## Dependencies
|
|
539
|
+
|
|
540
|
+
| Dependency | Kind | Why |
|
|
541
|
+
| --- | --- | --- |
|
|
542
|
+
| [`@statewalker/webrun-streams`](../webrun-streams) | runtime | The `Duplex` / `Connect` / `Serve` seam, the iterator primitives underneath the stream tier, and — for `byteChannelFromMessagePort` alone — `ByteChannel` and `emulateMux`. |
|
|
543
|
+
|
|
544
|
+
No runtime dependencies outside the workspace, no peer dependencies. ESM only
|
|
545
|
+
(`"type": "module"`).
|
|
546
|
+
|
|
547
|
+
## License
|
|
548
|
+
|
|
549
|
+
MIT © statewalker — see [LICENSE](../../LICENSE).
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ByteChannel } from "@statewalker/webrun-streams";
|
|
2
|
+
import type { MessageTarget } from "./message-target.js";
|
|
3
|
+
/**
|
|
4
|
+
* Wrap any `MessageTarget` — a real `MessagePort`, a worker, or a virtual
|
|
5
|
+
* port over some other transport — as a `ByteChannel`. Outbound bytes are emitted via
|
|
6
|
+
* `port.postMessage(uint8Array)` (the structured-clone path); inbound bytes
|
|
7
|
+
* are taken from `message` events whose `data` is a `Uint8Array` (or
|
|
8
|
+
* coerceable byte-like value).
|
|
9
|
+
*
|
|
10
|
+
* The port must already be started (`port.start()` if manually constructed).
|
|
11
|
+
* This adapter assumes the port carries only byte payloads — non-byte messages
|
|
12
|
+
* are ignored.
|
|
13
|
+
*/
|
|
14
|
+
export declare function byteChannelFromMessagePort(port: MessageTarget): ByteChannel;
|
|
15
|
+
//# sourceMappingURL=byte-channel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"byte-channel.d.ts","sourceRoot":"","sources":["../src/byte-channel.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAC/D,OAAO,KAAK,EAAmB,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAE1E;;;;;;;;;;GAUG;AACH,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,aAAa,GAAG,WAAW,CA8F3E"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type CallPortOptions } from "./call-port.js";
|
|
2
|
+
import type { MessageTarget } from "./message-target.js";
|
|
3
|
+
export interface CallBidiOptions extends CallPortOptions {
|
|
4
|
+
/** Timeout for the outer stream (default: `Number.MAX_SAFE_INTEGER` / max int). */
|
|
5
|
+
bidiTimeout?: number;
|
|
6
|
+
}
|
|
7
|
+
export interface CallBidiArgs {
|
|
8
|
+
options?: CallBidiOptions;
|
|
9
|
+
[key: string]: unknown;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Initiates a full-duplex stream call: ships `input` values to the peer and
|
|
13
|
+
* yields the values returned by `listenBidi`'s handler.
|
|
14
|
+
*
|
|
15
|
+
* Internally allocates a fresh sub-channel name, announces it to the peer
|
|
16
|
+
* via `callPort`, and then runs {@link ioSend} on that sub-channel.
|
|
17
|
+
*
|
|
18
|
+
* If the outer `callPort` rejects (e.g., the peer's handler threw and
|
|
19
|
+
* `listenPort` surfaced the error as `response:error`), the inner `ioSend`'s
|
|
20
|
+
* recieveIterator is force-closed via an internal cancel signal so the
|
|
21
|
+
* consumer doesn't hang waiting for chunks that will never come. The outer
|
|
22
|
+
* error is then re-thrown to the caller.
|
|
23
|
+
*/
|
|
24
|
+
export declare function callBidi<TIn, TOut>(port: MessageTarget, input: AsyncIterable<TIn> | Iterable<TIn>, { options, ...params }?: CallBidiArgs): AsyncGenerator<TOut>;
|
|
25
|
+
//# sourceMappingURL=call-bidi.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"call-bidi.d.ts","sourceRoot":"","sources":["../src/call-bidi.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,eAAe,EAAY,MAAM,gBAAgB,CAAC;AAEhE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,MAAM,WAAW,eAAgB,SAAQ,eAAe;IACtD,mFAAmF;IACnF,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,CAAC,EAAE,eAAe,CAAC;IAC1B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAuB,QAAQ,CAAC,GAAG,EAAE,IAAI,EACvC,IAAI,EAAE,aAAa,EACnB,KAAK,EAAE,aAAa,CAAC,GAAG,CAAC,GAAG,QAAQ,CAAC,GAAG,CAAC,EACzC,EAAE,OAAY,EAAE,GAAG,MAAM,EAAE,GAAE,YAAiB,GAC7C,cAAc,CAAC,IAAI,CAAC,CA4BtB"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { MessageTarget } from "./message-target.js";
|
|
2
|
+
/**
|
|
3
|
+
* Pass as `timeout` to install no deadline at all. Used by the stream tier,
|
|
4
|
+
* where the deadline belongs to the stream rather than to one chunk (spec D8):
|
|
5
|
+
* a slow consumer is throttled, never failed.
|
|
6
|
+
*/
|
|
7
|
+
export declare const NO_TIMEOUT: number;
|
|
8
|
+
export interface CallPortOptions {
|
|
9
|
+
/**
|
|
10
|
+
* Timeout in ms after which the call rejects (default 1000). A value that is
|
|
11
|
+
* not a finite number greater than zero — {@link NO_TIMEOUT}, or 0 — installs
|
|
12
|
+
* no deadline, and the call then settles only on a reply, an abort, or the
|
|
13
|
+
* port's close signal.
|
|
14
|
+
*/
|
|
15
|
+
timeout?: number;
|
|
16
|
+
/** Channel name filter — peers with a different `channelName` ignore the message. */
|
|
17
|
+
channelName?: string;
|
|
18
|
+
/** Logging function; defaults to a no-op. */
|
|
19
|
+
log?: (...args: unknown[]) => void;
|
|
20
|
+
/** Override the call ID generator (default: `call-<timestamp>-<random>`). */
|
|
21
|
+
newCallId?: () => string;
|
|
22
|
+
/**
|
|
23
|
+
* Optional cancellation signal. Firing it rejects the pending call
|
|
24
|
+
* immediately (without waiting for the timeout) and cleans up the message
|
|
25
|
+
* listener. Use the signal's `reason` for the rejection if present.
|
|
26
|
+
*/
|
|
27
|
+
signal?: AbortSignal;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Asynchronous request/response over any `MessageTarget`.
|
|
31
|
+
*
|
|
32
|
+
* Sends `params` to the peer listening with `listenPort`, waits up to
|
|
33
|
+
* `timeout` ms for a matching reply, and either resolves with the result or
|
|
34
|
+
* rejects with the deserialised error.
|
|
35
|
+
*/
|
|
36
|
+
export declare function callPort<TResult = unknown, TParams = unknown>(port: MessageTarget, params: TParams, { timeout, channelName, log, newCallId, signal, }?: CallPortOptions): Promise<TResult>;
|
|
37
|
+
//# sourceMappingURL=call-port.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"call-port.d.ts","sourceRoot":"","sources":["../src/call-port.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD;;;;GAIG;AACH,eAAO,MAAM,UAAU,QAA2B,CAAC;AAEnD,MAAM,WAAW,eAAe;IAC9B;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qFAAqF;IACrF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6CAA6C;IAC7C,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAC;IACnC,6EAA6E;IAC7E,SAAS,CAAC,EAAE,MAAM,MAAM,CAAC;IACzB;;;;OAIG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAMD;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,OAAO,GAAG,OAAO,EAAE,OAAO,GAAG,OAAO,EAC3D,IAAI,EAAE,aAAa,EACnB,MAAM,EAAE,OAAO,EACf,EACE,OAAc,EACd,WAAgB,EAChB,GAAc,EACd,SAA4E,EAC5E,MAAM,GACP,GAAE,eAAoB,GACtB,OAAO,CAAC,OAAO,CAAC,CA+ClB"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire-level "cancel this sub-channel" signal.
|
|
3
|
+
*
|
|
4
|
+
* Used by `ioSend` when the consumer breaks out of the bidi stream early
|
|
5
|
+
* (`iter.return()`) so the producer-side `ioHandle` can stop generating
|
|
6
|
+
* chunks immediately instead of waiting for `callPort` timeouts to fire.
|
|
7
|
+
*
|
|
8
|
+
* Format: `port.postMessage({ type: "cancel-channel", channelName })`.
|
|
9
|
+
* Fire-and-forget; no callId, no response.
|
|
10
|
+
*/
|
|
11
|
+
import type { MessageTarget } from "./message-target.js";
|
|
12
|
+
export declare const CANCEL_CHANNEL_TYPE = "cancel-channel";
|
|
13
|
+
export declare function postCancelChannel(port: MessageTarget, channelName: string): void;
|
|
14
|
+
export declare function listenCancelChannel(port: MessageTarget, channelName: string, onCancel: () => void): () => void;
|
|
15
|
+
//# sourceMappingURL=cancel-channel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cancel-channel.d.ts","sourceRoot":"","sources":["../src/cancel-channel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,eAAO,MAAM,mBAAmB,mBAAmB,CAAC;AAOpD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,GAAG,IAAI,CAOhF;AAED,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,aAAa,EACnB,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,IAAI,GACnB,MAAM,IAAI,CASZ"}
|