@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/src/call-port.ts
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { deserializeError, type SerializedError } from "@statewalker/webrun-streams";
|
|
2
|
+
import { getPortCloseSignal } from "./close-signal.js";
|
|
3
|
+
import type { MessageTarget } from "./message-target.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Pass as `timeout` to install no deadline at all. Used by the stream tier,
|
|
7
|
+
* where the deadline belongs to the stream rather than to one chunk (spec D8):
|
|
8
|
+
* a slow consumer is throttled, never failed.
|
|
9
|
+
*/
|
|
10
|
+
export const NO_TIMEOUT = Number.POSITIVE_INFINITY;
|
|
11
|
+
|
|
12
|
+
export interface CallPortOptions {
|
|
13
|
+
/**
|
|
14
|
+
* Timeout in ms after which the call rejects (default 1000). A value that is
|
|
15
|
+
* not a finite number greater than zero — {@link NO_TIMEOUT}, or 0 — installs
|
|
16
|
+
* no deadline, and the call then settles only on a reply, an abort, or the
|
|
17
|
+
* port's close signal.
|
|
18
|
+
*/
|
|
19
|
+
timeout?: number;
|
|
20
|
+
/** Channel name filter — peers with a different `channelName` ignore the message. */
|
|
21
|
+
channelName?: string;
|
|
22
|
+
/** Logging function; defaults to a no-op. */
|
|
23
|
+
log?: (...args: unknown[]) => void;
|
|
24
|
+
/** Override the call ID generator (default: `call-<timestamp>-<random>`). */
|
|
25
|
+
newCallId?: () => string;
|
|
26
|
+
/**
|
|
27
|
+
* Optional cancellation signal. Firing it rejects the pending call
|
|
28
|
+
* immediately (without waiting for the timeout) and cleans up the message
|
|
29
|
+
* listener. Use the signal's `reason` for the rejection if present.
|
|
30
|
+
*/
|
|
31
|
+
signal?: AbortSignal;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type ResponseEnvelope<T> =
|
|
35
|
+
| { type: "response:result"; channelName: string; callId: string; result: T }
|
|
36
|
+
| { type: "response:error"; channelName: string; callId: string; error: SerializedError };
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Asynchronous request/response over any `MessageTarget`.
|
|
40
|
+
*
|
|
41
|
+
* Sends `params` to the peer listening with `listenPort`, waits up to
|
|
42
|
+
* `timeout` ms for a matching reply, and either resolves with the result or
|
|
43
|
+
* rejects with the deserialised error.
|
|
44
|
+
*/
|
|
45
|
+
export function callPort<TResult = unknown, TParams = unknown>(
|
|
46
|
+
port: MessageTarget,
|
|
47
|
+
params: TParams,
|
|
48
|
+
{
|
|
49
|
+
timeout = 1000,
|
|
50
|
+
channelName = "",
|
|
51
|
+
log = () => {},
|
|
52
|
+
newCallId = () => `call-${Date.now()}-${String(Math.random()).substring(2)}`,
|
|
53
|
+
signal,
|
|
54
|
+
}: CallPortOptions = {},
|
|
55
|
+
): Promise<TResult> {
|
|
56
|
+
const callId = newCallId();
|
|
57
|
+
log("[callPort]", { channelName, callId, params });
|
|
58
|
+
// Combine the caller's signal (if any) with the port's transport-close
|
|
59
|
+
// signal (if the port is transport-backed). Either firing rejects the
|
|
60
|
+
// pending call. Without this, `callPort` would hang on transport-level
|
|
61
|
+
// disconnects until the per-call `timeout` fires (default 1 s, but
|
|
62
|
+
// higher-level operations like httpFetch dial it up to ~24 days).
|
|
63
|
+
const portCloseSignal = getPortCloseSignal(port);
|
|
64
|
+
const combinedSignal = combineSignals(signal, portCloseSignal);
|
|
65
|
+
let timerId: ReturnType<typeof setTimeout> | undefined;
|
|
66
|
+
let onMessage: ((event: MessageEvent) => void) | undefined;
|
|
67
|
+
let onAbort: (() => void) | undefined;
|
|
68
|
+
const promise = new Promise<TResult>((resolve, reject) => {
|
|
69
|
+
if (combinedSignal?.aborted) {
|
|
70
|
+
reject(abortReason(combinedSignal));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (Number.isFinite(timeout) && timeout > 0) {
|
|
74
|
+
timerId = setTimeout(() => reject(new Error(`Call timeout. CallId: "${callId}".`)), timeout);
|
|
75
|
+
}
|
|
76
|
+
onMessage = (event: MessageEvent) => {
|
|
77
|
+
const data = event.data as ResponseEnvelope<TResult> | undefined;
|
|
78
|
+
if (!data) return;
|
|
79
|
+
if (data.channelName !== channelName) return;
|
|
80
|
+
if (data.callId !== callId) return;
|
|
81
|
+
if (data.type === "response:error") reject(deserializeError(data.error));
|
|
82
|
+
else if (data.type === "response:result") resolve(data.result);
|
|
83
|
+
};
|
|
84
|
+
port.addEventListener("message", onMessage);
|
|
85
|
+
if (combinedSignal) {
|
|
86
|
+
onAbort = () => reject(abortReason(combinedSignal));
|
|
87
|
+
combinedSignal.addEventListener("abort", onAbort, { once: true });
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
// Swallow rejection on this side-branch so the cleanup .finally doesn't
|
|
91
|
+
// surface an unhandled rejection. The original `promise` keeps its
|
|
92
|
+
// rejection for the caller.
|
|
93
|
+
promise
|
|
94
|
+
.catch(() => {})
|
|
95
|
+
.finally(() => {
|
|
96
|
+
if (timerId !== undefined) clearTimeout(timerId);
|
|
97
|
+
if (onMessage) port.removeEventListener("message", onMessage);
|
|
98
|
+
if (onAbort && combinedSignal) combinedSignal.removeEventListener("abort", onAbort);
|
|
99
|
+
});
|
|
100
|
+
port.postMessage({ type: "request", channelName, callId, params });
|
|
101
|
+
return promise;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function abortReason(signal: AbortSignal): Error {
|
|
105
|
+
const reason = (signal as { reason?: unknown }).reason;
|
|
106
|
+
if (reason instanceof Error) return reason;
|
|
107
|
+
const err = new Error(reason === undefined ? "Aborted" : String(reason));
|
|
108
|
+
err.name = "AbortError";
|
|
109
|
+
return err;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function combineSignals(...signals: (AbortSignal | undefined)[]): AbortSignal | undefined {
|
|
113
|
+
const live = signals.filter((s): s is AbortSignal => s !== undefined);
|
|
114
|
+
if (live.length === 0) return undefined;
|
|
115
|
+
if (live.length === 1) return live[0];
|
|
116
|
+
// `AbortSignal.any` (ES2024) forwards `aborted` + `reason` from the first
|
|
117
|
+
// input that fires and auto-cleans up its listeners when the result is GC'd.
|
|
118
|
+
return AbortSignal.any(live);
|
|
119
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
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
|
+
|
|
13
|
+
export const CANCEL_CHANNEL_TYPE = "cancel-channel";
|
|
14
|
+
|
|
15
|
+
interface CancelChannelMessage {
|
|
16
|
+
type: typeof CANCEL_CHANNEL_TYPE;
|
|
17
|
+
channelName: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function postCancelChannel(port: MessageTarget, channelName: string): void {
|
|
21
|
+
if (!channelName) return;
|
|
22
|
+
try {
|
|
23
|
+
port.postMessage({ type: CANCEL_CHANNEL_TYPE, channelName } satisfies CancelChannelMessage);
|
|
24
|
+
} catch {
|
|
25
|
+
/* port may be closed — best effort */
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function listenCancelChannel(
|
|
30
|
+
port: MessageTarget,
|
|
31
|
+
channelName: string,
|
|
32
|
+
onCancel: () => void,
|
|
33
|
+
): () => void {
|
|
34
|
+
const handler = (event: MessageEvent) => {
|
|
35
|
+
const data = event.data as CancelChannelMessage | undefined;
|
|
36
|
+
if (!data || data.type !== CANCEL_CHANNEL_TYPE) return;
|
|
37
|
+
if (data.channelName !== channelName) return;
|
|
38
|
+
onCancel();
|
|
39
|
+
};
|
|
40
|
+
port.addEventListener("message", handler);
|
|
41
|
+
return () => port.removeEventListener("message", handler);
|
|
42
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-port AbortSignal that fires when the underlying transport adapter
|
|
3
|
+
* (libp2p stream, LiveKit participant, WebSocket, …) detects closure.
|
|
4
|
+
*
|
|
5
|
+
* Native `MessagePort` does NOT surface close events to either entangled
|
|
6
|
+
* half — neither a `close` event, nor a `messageerror`, nor any signal at
|
|
7
|
+
* all. Closing one half just silently stops message delivery on the other.
|
|
8
|
+
* A WHATWG proposal exists (https://github.com/whatwg/html/issues/1766) but
|
|
9
|
+
* no browser ships it.
|
|
10
|
+
*
|
|
11
|
+
* This module is the work-around: `bindBytesToPort` creates an
|
|
12
|
+
* `AbortController` per port and aborts it when `transport.onClose` fires.
|
|
13
|
+
* Consumers (`callPort`, `recieve`, `send`, application code) look up the
|
|
14
|
+
* signal via `getPortCloseSignal(port)` and combine it into their own
|
|
15
|
+
* abort plumbing — typically with `AbortSignal.any([userSignal, closeSignal])`
|
|
16
|
+
* — so pending awaits reject cleanly instead of hanging until a per-call
|
|
17
|
+
* timeout fires.
|
|
18
|
+
*
|
|
19
|
+
* Ports created via raw `new MessageChannel()` (i.e., without going through
|
|
20
|
+
* `bindBytesToPort`) return `undefined` from `getPortCloseSignal` — there's
|
|
21
|
+
* no underlying transport so there's nothing to track.
|
|
22
|
+
*/
|
|
23
|
+
import type { MessageTarget } from "./message-target.js";
|
|
24
|
+
|
|
25
|
+
const closeSignals = new WeakMap<MessageTarget, AbortSignal>();
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Internal: register a port's close signal. Called once by `bindBytesToPort`.
|
|
29
|
+
*
|
|
30
|
+
* @internal
|
|
31
|
+
*/
|
|
32
|
+
export function setPortCloseSignal(port: MessageTarget, signal: AbortSignal): void {
|
|
33
|
+
closeSignals.set(port, signal);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Return the AbortSignal that fires when `port`'s transport closes, or
|
|
38
|
+
* `undefined` if `port` is not transport-backed (e.g., a raw
|
|
39
|
+
* `MessageChannel().port1`).
|
|
40
|
+
*/
|
|
41
|
+
export function getPortCloseSignal(port: MessageTarget): AbortSignal | undefined {
|
|
42
|
+
return closeSignals.get(port);
|
|
43
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import type { Connect, Duplex, Serve } from "@statewalker/webrun-streams";
|
|
2
|
+
import {
|
|
3
|
+
type DuplexOverPortOptions,
|
|
4
|
+
duplexOverPort,
|
|
5
|
+
serveDuplexOverPort,
|
|
6
|
+
} from "./duplex-over-port.js";
|
|
7
|
+
import type { MessageTarget } from "./message-target.js";
|
|
8
|
+
import { multiplexPort } from "./multiplex-port.js";
|
|
9
|
+
import type { PortCodec, PortMux } from "./port-types.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Announced with every port this pair opens. Layer 1 never reads `meta` — it is
|
|
13
|
+
* here so a peer running something else over the same multiplexer can tell a
|
|
14
|
+
* stream port from whatever else it hands out, and so a packet capture says
|
|
15
|
+
* what the port is for.
|
|
16
|
+
*/
|
|
17
|
+
const STREAM_META = { kind: "stream" } as const;
|
|
18
|
+
|
|
19
|
+
/** What a mux calls when the peer opens a port. `false` rejects it. */
|
|
20
|
+
export type OnPort = (port: MessageTarget, meta?: unknown) => boolean | undefined;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A source of ports, built around the handler that will answer them.
|
|
24
|
+
*
|
|
25
|
+
* WHY A FACTORY AND NOT A `PortMux`. Every mux decides accept-or-reject
|
|
26
|
+
* **synchronously**, inside the `open` envelope, and tells the peer on the
|
|
27
|
+
* spot — `multiplexPort` posts `{type:"close", reason:"rejected"}` before
|
|
28
|
+
* returning. There is no "decide later", and layer 1 refuses to queue what it
|
|
29
|
+
* cannot deliver ("Drop, never queue").
|
|
30
|
+
*
|
|
31
|
+
* So a mux handed over already built has a window between its construction and
|
|
32
|
+
* its handler being attached, and a port arriving in that window is rejected
|
|
33
|
+
* outright — measured: zero messages delivered, and the peer told `rejected`
|
|
34
|
+
* for a call that was merely early. A factory closes the window by
|
|
35
|
+
* construction: the mux cannot exist before the thing that answers it.
|
|
36
|
+
*
|
|
37
|
+
* `connect` passes no handler, which is how a caller declines inbound ports.
|
|
38
|
+
* The same factory therefore serves both sides, and whichever of them called
|
|
39
|
+
* it owns the mux and closes it.
|
|
40
|
+
*/
|
|
41
|
+
export type PortMuxFactory = (onPort?: OnPort) => PortMux | Promise<PortMux>;
|
|
42
|
+
|
|
43
|
+
export interface PortParams {
|
|
44
|
+
/**
|
|
45
|
+
* Where ports come from. See {@link PortMuxFactory}, and `overPipe` /
|
|
46
|
+
* `overPorts` for the two this package ships.
|
|
47
|
+
*
|
|
48
|
+
* This used to be a `MessageTarget` plus `side` and a credit window, which
|
|
49
|
+
* forced one strategy — an id table — on every transport. A libp2p
|
|
50
|
+
* connection multiplexes already, so that stacked two multiplexers with no
|
|
51
|
+
* way to tell which one stalled; a transferable boundary moves real ports
|
|
52
|
+
* and needs no table at all. The strategy belongs to whoever knows the
|
|
53
|
+
* transport, which is never this file.
|
|
54
|
+
*/
|
|
55
|
+
mux: PortMuxFactory;
|
|
56
|
+
/** Per-stream inactivity timeout in ms, reset by any chunk in either direction.
|
|
57
|
+
*
|
|
58
|
+
* Unset — the default — means no timeout at all: a slow consumer is
|
|
59
|
+
* throttled, never failed. Set it on the side that must not hang. */
|
|
60
|
+
timeout?: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* One port source in, one caller `Duplex` out.
|
|
65
|
+
*
|
|
66
|
+
* A call is a port: the mux allocates one, `duplexOverPort` runs the single
|
|
67
|
+
* invocation on it, and closing the stream closes the port. No stream ids, no
|
|
68
|
+
* framing and no credit accounting live here — the mux owns the first, and
|
|
69
|
+
* `duplexOverPort` owns the one-chunk window that makes memory bounded.
|
|
70
|
+
*/
|
|
71
|
+
export const connect: Connect<PortParams> = async ({ mux: factory, timeout }) => {
|
|
72
|
+
// No handler: a caller answers no inbound calls, and an unexpected `open`
|
|
73
|
+
// from the peer is refused rather than silently accepted and then starved.
|
|
74
|
+
const mux = await factory();
|
|
75
|
+
const streamOptions: DuplexOverPortOptions = { maxMessageSize: mux.maxMessageSize, timeout };
|
|
76
|
+
|
|
77
|
+
const call: Duplex = (input) =>
|
|
78
|
+
// The port is opened on the consumer's first pull, not when `call` is
|
|
79
|
+
// invoked. A caller that builds a stream and then drops it without
|
|
80
|
+
// iterating therefore costs the peer nothing — under eager opening it
|
|
81
|
+
// would burn a port (and over libp2p, a whole stream) for a call that
|
|
82
|
+
// never arrives.
|
|
83
|
+
(async function* () {
|
|
84
|
+
const port = await mux.openPort(STREAM_META);
|
|
85
|
+
// `yield*` and not a hand-rolled pump: delegation already forwards
|
|
86
|
+
// `return()` and `throw()` into `duplexOverPort`'s generator, which is
|
|
87
|
+
// exactly the cancellation path `Duplex` specifies.
|
|
88
|
+
yield* duplexOverPort(port, streamOptions)(input);
|
|
89
|
+
})();
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
call,
|
|
93
|
+
async close() {
|
|
94
|
+
await mux.close();
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* One port source in, `handler` serving every call that arrives on it.
|
|
101
|
+
*
|
|
102
|
+
* Each inbound port is one invocation, so the factory's `onPort` is the accept
|
|
103
|
+
* loop: it installs `handler` on the new port and nothing else. The returned
|
|
104
|
+
* teardown abandons the streams still running *before* dropping the mux, so
|
|
105
|
+
* the peer's callers reject with "the peer abandoned the stream" instead of
|
|
106
|
+
* parking forever on a port that has silently gone inert — layer 1's close is
|
|
107
|
+
* not observable to layer 2, which is why the notice has to be posted
|
|
108
|
+
* deliberately.
|
|
109
|
+
*/
|
|
110
|
+
export const serve: Serve<PortParams> = async ({ mux: factory, timeout }, handler) => {
|
|
111
|
+
// Live streams only. A set that merely accumulated one entry per call would
|
|
112
|
+
// grow for the life of the connection — the same unbounded retention this
|
|
113
|
+
// stack exists to avoid, just moved from bytes to closures — so each entry is
|
|
114
|
+
// dropped the moment its handler is finished with.
|
|
115
|
+
const live = new Set<() => void>();
|
|
116
|
+
let streamOptions: DuplexOverPortOptions = { timeout };
|
|
117
|
+
|
|
118
|
+
const mux = await factory((port) => {
|
|
119
|
+
let off: (() => void) | undefined;
|
|
120
|
+
let finished = false;
|
|
121
|
+
// Wrapping the handler's *output* is how this side learns a stream is
|
|
122
|
+
// over: `sendChunks` drives that generator to completion, and an abort
|
|
123
|
+
// reaches it as `return()` through `throughAbort`, so the `finally` runs
|
|
124
|
+
// on every ending — normal exhaustion, handler throw, peer abort, local
|
|
125
|
+
// teardown. `serveDuplexOverPort` reports no completion of its own.
|
|
126
|
+
const tracked: Duplex = (input) =>
|
|
127
|
+
(async function* () {
|
|
128
|
+
try {
|
|
129
|
+
yield* handler(input);
|
|
130
|
+
} finally {
|
|
131
|
+
finished = true;
|
|
132
|
+
if (off) live.delete(off);
|
|
133
|
+
}
|
|
134
|
+
})();
|
|
135
|
+
off = serveDuplexOverPort(port, tracked, streamOptions);
|
|
136
|
+
// `serveDuplexOverPort` invokes the handler synchronously. The generator
|
|
137
|
+
// body above is lazy, so today it cannot have finished by the time `off`
|
|
138
|
+
// exists — but if it ever could, the `finally` would have run with `off`
|
|
139
|
+
// still unassigned and this line would add back an entry nothing will
|
|
140
|
+
// ever remove. The flag costs a word and closes that door.
|
|
141
|
+
if (!finished) live.add(off);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// Only knowable after the factory has run, and the first inbound port cannot
|
|
145
|
+
// arrive before it returns — the mux does not exist to receive one yet.
|
|
146
|
+
streamOptions = { maxMessageSize: mux.maxMessageSize, timeout };
|
|
147
|
+
|
|
148
|
+
let torn = false;
|
|
149
|
+
return async () => {
|
|
150
|
+
if (torn) return;
|
|
151
|
+
torn = true;
|
|
152
|
+
for (const off of [...live]) {
|
|
153
|
+
live.delete(off);
|
|
154
|
+
try {
|
|
155
|
+
off();
|
|
156
|
+
} catch {
|
|
157
|
+
// One stream's teardown failing must not strand the rest, nor the
|
|
158
|
+
// mux close below.
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
await mux.close();
|
|
162
|
+
};
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
export interface OverPipeOptions {
|
|
166
|
+
/** How envelopes are placed on the pipe. `structuredCodec` for a `MessagePort`. */
|
|
167
|
+
codec: PortCodec;
|
|
168
|
+
/**
|
|
169
|
+
* Id parity. The initiator allocates even ids, the responder odd, so both
|
|
170
|
+
* ends may open concurrently with no negotiation. The two ends of one pair
|
|
171
|
+
* must disagree, or their ids collide.
|
|
172
|
+
*/
|
|
173
|
+
side?: "initiator" | "responder";
|
|
174
|
+
/** Ceiling on concurrently open ports. Bounds the id table only. */
|
|
175
|
+
maxPorts?: number;
|
|
176
|
+
/**
|
|
177
|
+
* Largest payload one message may carry, if the pipe imposes a limit.
|
|
178
|
+
* Bodies are split to fit. Leave at least 256 bytes of margin below the
|
|
179
|
+
* transport's real ceiling — see `PortMuxOptions.maxMessageSize`.
|
|
180
|
+
*/
|
|
181
|
+
maxMessageSize?: number;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Ports over ONE PIPE OF BYTES — a `MessagePort`, a worker, a WebSocket.
|
|
186
|
+
*
|
|
187
|
+
* There is no second port to be had, so `multiplexPort`'s id table invents
|
|
188
|
+
* them. Use this when the transport gives you exactly one channel.
|
|
189
|
+
*/
|
|
190
|
+
export function overPipe(pipe: MessageTarget, options: OverPipeOptions): PortMuxFactory {
|
|
191
|
+
return (onPort) => multiplexPort(pipe, { ...options, onPort });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Ports from a source that already has them — a transferable boundary, or a
|
|
196
|
+
* transport that multiplexes on its own (libp2p's yamux, say).
|
|
197
|
+
*
|
|
198
|
+
* A pass-through, so an adapter that builds its own `PortMux` plugs in without
|
|
199
|
+
* this package learning what that transport is. It exists to make the
|
|
200
|
+
* three-way choice legible at the call site rather than to do work:
|
|
201
|
+
*
|
|
202
|
+
* ```ts
|
|
203
|
+
* await serve({ mux: overPorts((onPort) => libp2pPortMux({ node, onPort })) }, handler);
|
|
204
|
+
* ```
|
|
205
|
+
*/
|
|
206
|
+
export function overPorts(factory: PortMuxFactory): PortMuxFactory {
|
|
207
|
+
return factory;
|
|
208
|
+
}
|