@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
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ListenPortOptions } from "./listen-port.js";
|
|
2
|
+
import type { MessageTarget } from "./message-target.js";
|
|
3
|
+
/**
|
|
4
|
+
* Server half of a full-duplex exchange over any `MessageTarget`.
|
|
5
|
+
*
|
|
6
|
+
* For each inbound stream, invokes `handler` with the stream, sends the
|
|
7
|
+
* handler's output back, and yields a counter. The generator never ends
|
|
8
|
+
* on its own — consumers break when they want to stop. Pairs with
|
|
9
|
+
* {@link ioSend}.
|
|
10
|
+
*
|
|
11
|
+
* If the peer (the consumer of our outbound send) posts a `cancel-channel`
|
|
12
|
+
* message on the same sub-channel, we abort `send` immediately. This makes
|
|
13
|
+
* `ioSend`'s `iter.return()` propagate cleanly without waiting for
|
|
14
|
+
* `callPort` timeouts.
|
|
15
|
+
*/
|
|
16
|
+
export declare function ioHandle<T, U = T>(port: MessageTarget, handler: (input: AsyncIterable<T>) => AsyncIterable<U> | Promise<AsyncIterable<U>>, options?: ListenPortOptions): AsyncGenerator<number>;
|
|
17
|
+
//# sourceMappingURL=io-handle.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"io-handle.d.ts","sourceRoot":"","sources":["../src/io-handle.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAC1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAIzD;;;;;;;;;;;;GAYG;AACH,wBAAuB,QAAQ,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,EACtC,IAAI,EAAE,aAAa,EACnB,OAAO,EAAE,CAAC,KAAK,EAAE,aAAa,CAAC,CAAC,CAAC,KAAK,aAAa,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,EAClF,OAAO,GAAE,iBAAsB,GAC9B,cAAc,CAAC,MAAM,CAAC,CAgBxB"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ListenPortOptions } from "./listen-port.js";
|
|
2
|
+
import type { MessageTarget } from "./message-target.js";
|
|
3
|
+
export interface IoSendOptions extends ListenPortOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Optional cancel signal threaded into `recieve` so the consumer can force
|
|
6
|
+
* an in-flight `input.next()` to resolve immediately (otherwise an
|
|
7
|
+
* AsyncGenerator's `.return()` queues behind the pending `.next()` and
|
|
8
|
+
* never preempts a hanging await). Used by `callBidi` to abort the inner
|
|
9
|
+
* stream when the outer call rejects.
|
|
10
|
+
*/
|
|
11
|
+
cancelSignal?: AbortSignal;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Client half of a full-duplex exchange over any `MessageTarget`.
|
|
15
|
+
*
|
|
16
|
+
* Concurrently reads one inbound stream from the peer and writes `output`
|
|
17
|
+
* to it. Yields each value received from the peer. Completes once both
|
|
18
|
+
* directions finish. Pairs with {@link ioHandle}.
|
|
19
|
+
*
|
|
20
|
+
* If the consumer breaks out of the `for await` (via `iter.return()` or
|
|
21
|
+
* loop `break`), `ioSend` posts a `cancel-channel` message on the same
|
|
22
|
+
* sub-channel so the peer can abort its `send` immediately rather than
|
|
23
|
+
* waiting for `callPort` timeouts to fire.
|
|
24
|
+
*/
|
|
25
|
+
export declare function ioSend<T, U = T>(port: MessageTarget, output: AsyncIterable<U> | Iterable<U>, options?: IoSendOptions): AsyncGenerator<T>;
|
|
26
|
+
//# sourceMappingURL=io-send.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"io-send.d.ts","sourceRoot":"","sources":["../src/io-send.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAC1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAIzD,MAAM,WAAW,aAAc,SAAQ,iBAAiB;IACtD;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,WAAW,CAAC;CAC5B;AAED;;;;;;;;;;;GAWG;AACH,wBAAuB,MAAM,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,EACpC,IAAI,EAAE,aAAa,EACnB,MAAM,EAAE,aAAa,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,EACtC,OAAO,GAAE,aAAkB,GAC1B,cAAc,CAAC,CAAC,CAAC,CAoCnB"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { MessageTarget } from "./message-target.js";
|
|
2
|
+
export type BidiHandler<TIn, TOut> = (input: AsyncIterable<TIn>, params: Record<string, unknown>) => AsyncIterable<TOut> | Promise<AsyncIterable<TOut>>;
|
|
3
|
+
/**
|
|
4
|
+
* Server half of {@link callBidi}: listens for stream-call requests on `port`
|
|
5
|
+
* and dispatches each accepted one to `action`.
|
|
6
|
+
*
|
|
7
|
+
* The optional `accept` predicate can inspect the incoming params and reject
|
|
8
|
+
* unwanted calls. Returns a cleanup function that removes the listener.
|
|
9
|
+
*/
|
|
10
|
+
export declare function listenBidi<TIn, TOut>(port: MessageTarget, action: BidiHandler<TIn, TOut>, accept?: (params: Record<string, unknown>) => boolean): () => void;
|
|
11
|
+
//# sourceMappingURL=listen-bidi.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"listen-bidi.d.ts","sourceRoot":"","sources":["../src/listen-bidi.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,MAAM,MAAM,WAAW,CAAC,GAAG,EAAE,IAAI,IAAI,CACnC,KAAK,EAAE,aAAa,CAAC,GAAG,CAAC,EACzB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAC5B,aAAa,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC;AAExD;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,IAAI,EAClC,IAAI,EAAE,aAAa,EACnB,MAAM,EAAE,WAAW,CAAC,GAAG,EAAE,IAAI,CAAC,EAC9B,MAAM,GAAE,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAoB,GAChE,MAAM,IAAI,CAUZ"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { MessageTarget } from "./message-target.js";
|
|
2
|
+
export interface ListenPortOptions {
|
|
3
|
+
/** Channel name filter — ignore messages whose `channelName` doesn't match. */
|
|
4
|
+
channelName?: string;
|
|
5
|
+
/** Logging function; defaults to a no-op. */
|
|
6
|
+
log?: (...args: unknown[]) => void;
|
|
7
|
+
}
|
|
8
|
+
export type PortHandler<TParams = unknown, TResult = unknown> = (params: TParams) => TResult | Promise<TResult>;
|
|
9
|
+
/**
|
|
10
|
+
* Installs `handler` as the server side of a `callPort` / `listenPort`
|
|
11
|
+
* request/response pair on `port`.
|
|
12
|
+
*
|
|
13
|
+
* Returns a cleanup function that removes the listener.
|
|
14
|
+
*/
|
|
15
|
+
export declare function listenPort<TParams = unknown, TResult = unknown>(port: MessageTarget, handler: PortHandler<TParams, TResult>, { channelName, log }?: ListenPortOptions): () => void;
|
|
16
|
+
//# sourceMappingURL=listen-port.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"listen-port.d.ts","sourceRoot":"","sources":["../src/listen-port.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,MAAM,WAAW,iBAAiB;IAChC,+EAA+E;IAC/E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6CAA6C;IAC7C,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAC;CACpC;AAED,MAAM,MAAM,WAAW,CAAC,OAAO,GAAG,OAAO,EAAE,OAAO,GAAG,OAAO,IAAI,CAC9D,MAAM,EAAE,OAAO,KACZ,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;AAEhC;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,OAAO,GAAG,OAAO,EAAE,OAAO,GAAG,OAAO,EAC7D,IAAI,EAAE,aAAa,EACnB,OAAO,EAAE,WAAW,CAAC,OAAO,EAAE,OAAO,CAAC,EACtC,EAAE,WAAgB,EAAE,GAAc,EAAE,GAAE,iBAAsB,GAC3D,MAAM,IAAI,CAsBZ"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type MessageListener = (event: MessageEvent) => void | Promise<void>;
|
|
2
|
+
/** An object we can listen for `"message"` events on. */
|
|
3
|
+
export interface MessageSource {
|
|
4
|
+
addEventListener(type: "message", listener: MessageListener): void;
|
|
5
|
+
removeEventListener(type: "message", listener: MessageListener): void;
|
|
6
|
+
start?(): void | Promise<void>;
|
|
7
|
+
}
|
|
8
|
+
/** An object we can post messages to (with optional transferable list). */
|
|
9
|
+
export interface MessageSink {
|
|
10
|
+
postMessage(message: unknown, transfer?: Transferable[]): void;
|
|
11
|
+
}
|
|
12
|
+
/** Full-duplex message target: both sends and receives. */
|
|
13
|
+
export interface MessageTarget extends MessageSource, MessageSink {
|
|
14
|
+
close?(): void | Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=message-target.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"message-target.d.ts","sourceRoot":"","sources":["../src/message-target.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,eAAe,GAAG,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAE5E,yDAAyD;AACzD,MAAM,WAAW,aAAa;IAC5B,gBAAgB,CAAC,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,eAAe,GAAG,IAAI,CAAC;IACnE,mBAAmB,CAAC,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,eAAe,GAAG,IAAI,CAAC;IACtE,KAAK,CAAC,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAChC;AAED,2EAA2E;AAC3E,MAAM,WAAW,WAAW;IAC1B,WAAW,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,CAAC,EAAE,YAAY,EAAE,GAAG,IAAI,CAAC;CAChE;AAED,2DAA2D;AAC3D,MAAM,WAAW,aAAc,SAAQ,aAAa,EAAE,WAAW;IAC/D,KAAK,CAAC,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAChC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { MessageTarget } from "./message-target.js";
|
|
2
|
+
import type { PortMux, PortMuxOptions } from "./port-types.js";
|
|
3
|
+
/** Ceiling on concurrently open virtual ports. Bounds the id table only. */
|
|
4
|
+
export declare const DEFAULT_MAX_PORTS = 1024;
|
|
5
|
+
/**
|
|
6
|
+
* The default `PortMux`: emulates multiplexing over a single port.
|
|
7
|
+
*
|
|
8
|
+
* A transport that already multiplexes natively supplies its own `PortMux`
|
|
9
|
+
* instead — this implementation is for transports that offer one pipe.
|
|
10
|
+
*/
|
|
11
|
+
export declare function multiplexPort(port: MessageTarget, options: PortMuxOptions): PortMux;
|
|
12
|
+
//# sourceMappingURL=multiplex-port.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"multiplex-port.d.ts","sourceRoot":"","sources":["../src/multiplex-port.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAgB,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAG7E,4EAA4E;AAC5E,eAAO,MAAM,iBAAiB,OAAO,CAAC;AAEtC;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,aAAa,EAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAwHnF"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { MessageTarget } from "./message-target.js";
|
|
2
|
+
/**
|
|
3
|
+
* What a multiplexer exchanges over the underlying port.
|
|
4
|
+
*
|
|
5
|
+
* Three types and nothing more. There is no DATA/ACK split, no credit and no
|
|
6
|
+
* error type: `close` carries an opaque `reason` that layer 1 never inspects,
|
|
7
|
+
* because stream semantics belong above this layer.
|
|
8
|
+
*/
|
|
9
|
+
export type PortEnvelope = {
|
|
10
|
+
type: "open";
|
|
11
|
+
id: number;
|
|
12
|
+
meta?: unknown;
|
|
13
|
+
} | {
|
|
14
|
+
type: "message";
|
|
15
|
+
id: number;
|
|
16
|
+
payload: unknown;
|
|
17
|
+
} | {
|
|
18
|
+
type: "close";
|
|
19
|
+
id: number;
|
|
20
|
+
reason?: unknown;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* How an envelope reaches the wire.
|
|
24
|
+
*
|
|
25
|
+
* This is the only place that knows the wire format. A port whose messages are
|
|
26
|
+
* structured values passes envelopes through untouched; a port whose messages
|
|
27
|
+
* are bytes encodes them. A transport with different constraints adds a codec,
|
|
28
|
+
* not a multiplexer.
|
|
29
|
+
*/
|
|
30
|
+
export interface PortCodec {
|
|
31
|
+
/** Place one envelope on the underlying port. */
|
|
32
|
+
post(port: MessageTarget, envelope: PortEnvelope, transfer?: Transferable[]): void;
|
|
33
|
+
/** Recover an envelope from a message event, or `undefined` to ignore it. */
|
|
34
|
+
read(event: MessageEvent): PortEnvelope | undefined;
|
|
35
|
+
}
|
|
36
|
+
export interface PortMuxOptions {
|
|
37
|
+
/** How envelopes are placed on the underlying port. */
|
|
38
|
+
codec: PortCodec;
|
|
39
|
+
/**
|
|
40
|
+
* Called when the peer opens a port. Return `false` to reject it: a `close`
|
|
41
|
+
* goes back and every later message for that id is dropped. Any other return
|
|
42
|
+
* value — including `undefined` — accepts.
|
|
43
|
+
*
|
|
44
|
+
* With no `onPort` at all, inbound ports are rejected. A port nobody holds
|
|
45
|
+
* has no consumer, and accepting one would mean dropping its traffic
|
|
46
|
+
* silently rather than telling the peer.
|
|
47
|
+
*/
|
|
48
|
+
onPort?: (port: MessageTarget, meta?: unknown) => boolean | undefined;
|
|
49
|
+
/**
|
|
50
|
+
* Id parity. The initiator allocates even ids, the responder odd, so both
|
|
51
|
+
* ends may open concurrently with no negotiation. Defaults to `"initiator"`.
|
|
52
|
+
*/
|
|
53
|
+
side?: "initiator" | "responder";
|
|
54
|
+
/**
|
|
55
|
+
* Ceiling on concurrently open virtual ports. Bounds the id table only — it
|
|
56
|
+
* never inspects, counts or delays a payload.
|
|
57
|
+
*/
|
|
58
|
+
maxPorts?: number;
|
|
59
|
+
/**
|
|
60
|
+
* Largest **payload** a port on this mux can carry, if the transport imposes
|
|
61
|
+
* a limit. Layer 1 does not enforce it; it reports it so layer 2 can chunk.
|
|
62
|
+
*
|
|
63
|
+
* It bounds the payload, **not the frame**. `duplexOverPort` applies
|
|
64
|
+
* `toChunks(maxMessageSize)` and the envelope framing — the chunk wrapper,
|
|
65
|
+
* `callPort`'s request, this mux's own envelope, then the codec — is added on
|
|
66
|
+
* top afterwards. Measured over `msgpackCodec` that framing is 123-128 bytes
|
|
67
|
+
* (modelled ceiling 134), and it is not constant: the call id's length varies
|
|
68
|
+
* per chunk, the port id's integer width adds up to 4, and the payload's
|
|
69
|
+
* length header widens at 64 KiB.
|
|
70
|
+
*
|
|
71
|
+
* So set this **at least 256 bytes below** the transport's real limit. Set to
|
|
72
|
+
* the limit exactly, a full-size chunk overruns it — which on LiveKit
|
|
73
|
+
* delivered a body as zero bytes with no error on either side.
|
|
74
|
+
*/
|
|
75
|
+
maxMessageSize?: number;
|
|
76
|
+
}
|
|
77
|
+
/** One port in, many ports out. */
|
|
78
|
+
export interface PortMux {
|
|
79
|
+
/** Allocate a port, announce it, and return the local end. */
|
|
80
|
+
openPort(meta?: unknown): Promise<MessageTarget>;
|
|
81
|
+
/** Close every virtual port, then release the underlying port. */
|
|
82
|
+
close(): Promise<void>;
|
|
83
|
+
/** See {@link PortMuxOptions.maxMessageSize}. */
|
|
84
|
+
readonly maxMessageSize?: number;
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=port-types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"port-types.d.ts","sourceRoot":"","sources":["../src/port-types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD;;;;;;GAMG;AACH,MAAM,MAAM,YAAY,GACpB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAA;CAAE,GAC5C;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,GACjD;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAEpD;;;;;;;GAOG;AACH,MAAM,WAAW,SAAS;IACxB,iDAAiD;IACjD,IAAI,CAAC,IAAI,EAAE,aAAa,EAAE,QAAQ,EAAE,YAAY,EAAE,QAAQ,CAAC,EAAE,YAAY,EAAE,GAAG,IAAI,CAAC;IACnF,6EAA6E;IAC7E,IAAI,CAAC,KAAK,EAAE,YAAY,GAAG,YAAY,GAAG,SAAS,CAAC;CACrD;AAED,MAAM,WAAW,cAAc;IAC7B,uDAAuD;IACvD,KAAK,EAAE,SAAS,CAAC;IACjB;;;;;;;;OAQG;IACH,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,aAAa,EAAE,IAAI,CAAC,EAAE,OAAO,KAAK,OAAO,GAAG,SAAS,CAAC;IACtE;;;OAGG;IACH,IAAI,CAAC,EAAE,WAAW,GAAG,WAAW,CAAC;IACjC;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;;;;;;;;;;;OAeG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,mCAAmC;AACnC,MAAM,WAAW,OAAO;IACtB,8DAA8D;IAC9D,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;IACjD,kEAAkE;IAClE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,iDAAiD;IACjD,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;CAClC"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type ListenPortOptions } from "./listen-port.js";
|
|
2
|
+
import type { MessageTarget } from "./message-target.js";
|
|
3
|
+
export interface RecieveOptions extends ListenPortOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Optional cancel signal. When fired, the currently-active
|
|
6
|
+
* `recieveIterator` is force-closed (its producer is signalled `done`
|
|
7
|
+
* with an `AbortError`) so any pending `.next()` resolves immediately
|
|
8
|
+
* and the consumer's iteration loop can unwind. Required to make
|
|
9
|
+
* consumer-side `iter.return()` propagate through `callBidi`/`yield*`
|
|
10
|
+
* without waiting on `callPort` timeouts.
|
|
11
|
+
*/
|
|
12
|
+
signal?: AbortSignal;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Async generator over async generators. Each outer yield is one inbound
|
|
16
|
+
* stream reconstructed from chunk-envelopes delivered by the peer's
|
|
17
|
+
* {@link send}.
|
|
18
|
+
*
|
|
19
|
+
* The outer generator itself never ends: break out of the outer loop when
|
|
20
|
+
* you've handled the streams you care about.
|
|
21
|
+
*
|
|
22
|
+
* When the outer for-await is interrupted (`break`, `return`, throw), the
|
|
23
|
+
* `finally` here both removes the underlying `listenPort` and closes the
|
|
24
|
+
* most recently yielded `recieveIterator`. The latter is required because
|
|
25
|
+
* a `listenPort` handler invocation that's already in flight (awaiting
|
|
26
|
+
* `deliver(chunk)`) would otherwise hang forever — `iterator.return()`
|
|
27
|
+
* triggers `drainQueue` which resolves all pending producer Promises.
|
|
28
|
+
*
|
|
29
|
+
* If `options.signal` is provided and fires while the consumer is awaiting
|
|
30
|
+
* a chunk, the active `recieveIterator` is force-delivered an end-of-stream
|
|
31
|
+
* marker so the consumer wakes up immediately rather than waiting for the
|
|
32
|
+
* next inbound chunk (or `callPort` timeout).
|
|
33
|
+
*/
|
|
34
|
+
export declare function recieve<T>(port: MessageTarget, options?: RecieveOptions): AsyncGenerator<AsyncGenerator<T>>;
|
|
35
|
+
//# sourceMappingURL=recieve.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recieve.d.ts","sourceRoot":"","sources":["../src/recieve.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,iBAAiB,EAAc,MAAM,kBAAkB,CAAC;AACtE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,MAAM,WAAW,cAAe,SAAQ,iBAAiB;IACvD;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAuB,OAAO,CAAC,CAAC,EAC9B,IAAI,EAAE,aAAa,EACnB,OAAO,GAAE,cAAmB,GAC3B,cAAc,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,CAiDnC"}
|
package/dist/send.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type CallPortOptions } from "./call-port.js";
|
|
2
|
+
import type { MessageTarget } from "./message-target.js";
|
|
3
|
+
export interface SendOptions extends CallPortOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Optional cancellation signal. When fired, `send` aborts cleanly:
|
|
6
|
+
* - the current iteration is interrupted (by calling `return()` on the
|
|
7
|
+
* underlying iterator, releasing any pending work in the producer);
|
|
8
|
+
* - no further `callPort` round-trips are issued;
|
|
9
|
+
* - the final `{done: true}` marker is **not** sent (the peer told us
|
|
10
|
+
* it's gone — there's no one to receive it).
|
|
11
|
+
*
|
|
12
|
+
* `send` resolves normally on abort; the caller can distinguish abort
|
|
13
|
+
* from normal completion by checking `options.signal.aborted` after.
|
|
14
|
+
*/
|
|
15
|
+
signal?: AbortSignal;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Send every value produced by `output` to `port`, one `callPort` round-trip
|
|
19
|
+
* per chunk. Resolves once the peer has acknowledged the final `{ done: true }`
|
|
20
|
+
* envelope, or once `options.signal` aborts (whichever comes first).
|
|
21
|
+
*/
|
|
22
|
+
export declare function send<T>(port: MessageTarget, output: AsyncIterable<T> | Iterable<T>, options?: SendOptions): Promise<void>;
|
|
23
|
+
//# sourceMappingURL=send.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"send.d.ts","sourceRoot":"","sources":["../src/send.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,eAAe,EAAY,MAAM,gBAAgB,CAAC;AAChE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAGzD,MAAM,WAAW,WAAY,SAAQ,eAAe;IAClD;;;;;;;;;;OAUG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED;;;;GAIG;AACH,wBAAsB,IAAI,CAAC,CAAC,EAC1B,IAAI,EAAE,aAAa,EACnB,MAAM,EAAE,aAAa,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,EACtC,OAAO,GAAE,WAAgB,GACxB,OAAO,CAAC,IAAI,CAAC,CA8Bf"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { PortCodec } from "./port-types.js";
|
|
2
|
+
/**
|
|
3
|
+
* For ports whose messages are structured values — a real `MessagePort`, a
|
|
4
|
+
* worker, an iframe.
|
|
5
|
+
*
|
|
6
|
+
* Envelopes are posted as-is, so nothing is encoded, `ArrayBuffer`s move
|
|
7
|
+
* zero-copy through the transfer list, and structured clone does the work the
|
|
8
|
+
* platform already does well. This is a performance choice only: layer 2 may
|
|
9
|
+
* not send anything a byte codec could not also carry.
|
|
10
|
+
*/
|
|
11
|
+
export declare const structuredCodec: PortCodec;
|
|
12
|
+
//# sourceMappingURL=structured-codec.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"structured-codec.d.ts","sourceRoot":"","sources":["../src/structured-codec.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAgB,MAAM,iBAAiB,CAAC;AAU/D;;;;;;;;GAQG;AACH,eAAO,MAAM,eAAe,EAAE,SAU7B,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wraps an async iterable so that an `AbortSignal` firing causes the wrapper
|
|
3
|
+
* to return cleanly, forwarding `return()` to the underlying iterator so the
|
|
4
|
+
* producer (e.g., a user-supplied generator) sees its own `finally` blocks
|
|
5
|
+
* run immediately rather than waiting for the next yield.
|
|
6
|
+
*/
|
|
7
|
+
export declare function throughAbort<T>(input: AsyncIterable<T> | Iterable<T>, signal: AbortSignal): AsyncGenerator<T>;
|
|
8
|
+
//# sourceMappingURL=through-abort.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"through-abort.d.ts","sourceRoot":"","sources":["../src/through-abort.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,wBAAuB,YAAY,CAAC,CAAC,EACnC,KAAK,EAAE,aAAa,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,EACrC,MAAM,EAAE,WAAW,GAClB,cAAc,CAAC,CAAC,CAAC,CAsBnB"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { MessageTarget } from "./message-target.js";
|
|
2
|
+
import type { PortMux } from "./port-types.js";
|
|
3
|
+
/** The `type` of the envelope that carries a transferred port to the peer. */
|
|
4
|
+
export declare const PORT_TRANSFER = "webrun-rpc:port-transfer";
|
|
5
|
+
export interface TransferPortMuxOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Called when the peer transfers a port in. Return `false` to reject it: the
|
|
8
|
+
* port is closed and nothing further arrives on it. Any other return value —
|
|
9
|
+
* including `undefined` — accepts.
|
|
10
|
+
*
|
|
11
|
+
* With no `onPort` at all, inbound ports are rejected, matching
|
|
12
|
+
* `multiplexPort`: a port nobody holds has no consumer.
|
|
13
|
+
*/
|
|
14
|
+
onPort?: (port: MessageTarget, meta?: unknown) => boolean | undefined;
|
|
15
|
+
/** Reported to layer 2, never enforced. A `MessagePort` normally has none. */
|
|
16
|
+
maxMessageSize?: number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* A `PortMux` whose ports are real, transferred `MessagePort`s (spec D23).
|
|
20
|
+
*
|
|
21
|
+
* `openPort` creates a `MessageChannel`, transfers one end to the peer over
|
|
22
|
+
* `target`, and returns the other. There is no id table, no `maxPorts` and no
|
|
23
|
+
* envelope overhead per message, because the platform does the multiplexing.
|
|
24
|
+
*
|
|
25
|
+
* **It needs structured clone with transferables**, so it exists in browsers,
|
|
26
|
+
* workers and iframes and nowhere else that lacks them. A caller selects it
|
|
27
|
+
* explicitly rather than by capability sniffing (spec D21): use
|
|
28
|
+
* `multiplexPort` where the transport is one pipe of bytes.
|
|
29
|
+
*
|
|
30
|
+
* What it buys over emulation: a transferred port can cross an origin or a
|
|
31
|
+
* worker boundary and be handed to code that never saw `target`, where an
|
|
32
|
+
* emulated port id is meaningless outside its own mux.
|
|
33
|
+
*
|
|
34
|
+
* `target` must be a full `MessageTarget`. Reaching a send-only `MessageSink`
|
|
35
|
+
* — a `ServiceWorkerClient`, say — is a real use of port transfer but needs a
|
|
36
|
+
* different entry point, and is not part of this interface.
|
|
37
|
+
*/
|
|
38
|
+
export declare function transferPortMux(target: MessageTarget, options?: TransferPortMuxOptions): PortMux;
|
|
39
|
+
//# sourceMappingURL=transfer-port-mux.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transfer-port-mux.d.ts","sourceRoot":"","sources":["../src/transfer-port-mux.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAE/C,8EAA8E;AAC9E,eAAO,MAAM,aAAa,6BAA6B,CAAC;AAExD,MAAM,WAAW,sBAAsB;IACrC;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,aAAa,EAAE,IAAI,CAAC,EAAE,OAAO,KAAK,OAAO,GAAG,SAAS,CAAC;IACtE,8EAA8E;IAC9E,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,aAAa,EACrB,OAAO,GAAE,sBAA2B,GACnC,OAAO,CA8DT"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { MessageTarget } from "./message-target.js";
|
|
2
|
+
export interface VirtualPortHandle {
|
|
3
|
+
/** The consumer-facing end. Indistinguishable from a real `MessagePort`. */
|
|
4
|
+
port: MessageTarget;
|
|
5
|
+
/** Multiplexer-only: hand an inbound payload to the consumer's listeners. */
|
|
6
|
+
deliver(payload: unknown): void;
|
|
7
|
+
/** Multiplexer-only: the port is finished; drop listeners and go inert. */
|
|
8
|
+
markClosed(): void;
|
|
9
|
+
isClosed(): boolean;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* One virtual port.
|
|
13
|
+
*
|
|
14
|
+
* `deliver` and `markClosed` are deliberately not on `port`: the consumer holds
|
|
15
|
+
* only a `MessageTarget`, so it cannot forge inbound traffic or close the port
|
|
16
|
+
* out from under the multiplexer's bookkeeping.
|
|
17
|
+
*/
|
|
18
|
+
export declare function newVirtualPort(send: (payload: unknown, transfer?: Transferable[]) => void, requestClose: (reason?: unknown) => void): VirtualPortHandle;
|
|
19
|
+
//# sourceMappingURL=virtual-port.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"virtual-port.d.ts","sourceRoot":"","sources":["../src/virtual-port.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAmB,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAE1E,MAAM,WAAW,iBAAiB;IAChC,4EAA4E;IAC5E,IAAI,EAAE,aAAa,CAAC;IACpB,6EAA6E;IAC7E,OAAO,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IAChC,2EAA2E;IAC3E,UAAU,IAAI,IAAI,CAAC;IACnB,QAAQ,IAAI,OAAO,CAAC;CACrB;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,CAAC,EAAE,YAAY,EAAE,KAAK,IAAI,EAC3D,YAAY,EAAE,CAAC,MAAM,CAAC,EAAE,OAAO,KAAK,IAAI,GACvC,iBAAiB,CAgDnB"}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@statewalker/webrun-rpc",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "Ports and RPC over them: a port multiplexer, and typed request/response and streaming primitives over any MessageTarget.",
|
|
7
|
+
"homepage": "https://github.com/statewalker/webrun-wire",
|
|
8
|
+
"author": {
|
|
9
|
+
"name": "Mikhail Kotelnikov",
|
|
10
|
+
"email": "mikhail.kotelnikov@gmail.com"
|
|
11
|
+
},
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git@github.com:statewalker/webrun-wire.git",
|
|
16
|
+
"directory": "packages/webrun-rpc"
|
|
17
|
+
},
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"import": "./dist/index.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist",
|
|
26
|
+
"src"
|
|
27
|
+
],
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@statewalker/webrun-streams": "0.2.0"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/node": "^26.2.0",
|
|
33
|
+
"rimraf": "^6.1.3",
|
|
34
|
+
"rolldown": "^1.2.4",
|
|
35
|
+
"typescript": "^7.0.2",
|
|
36
|
+
"vitest": "^4.1.10",
|
|
37
|
+
"@statewalker/webrun-streams-conformance": "0.2.0"
|
|
38
|
+
},
|
|
39
|
+
"sideEffects": false,
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public"
|
|
42
|
+
},
|
|
43
|
+
"types": "./dist/index.d.ts",
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "rimraf dist && rolldown -c && tsc --emitDeclarationOnly --declaration",
|
|
46
|
+
"test": "vitest run",
|
|
47
|
+
"typecheck": "tsc --noEmit",
|
|
48
|
+
"typecheck:tests": "tsc -p tsconfig.tests.json",
|
|
49
|
+
"lint": "biome check src tests"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { ByteChannel } from "@statewalker/webrun-streams";
|
|
2
|
+
import type { MessageListener, MessageTarget } from "./message-target.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Wrap any `MessageTarget` — a real `MessagePort`, a worker, or a virtual
|
|
6
|
+
* port over some other transport — as a `ByteChannel`. Outbound bytes are emitted via
|
|
7
|
+
* `port.postMessage(uint8Array)` (the structured-clone path); inbound bytes
|
|
8
|
+
* are taken from `message` events whose `data` is a `Uint8Array` (or
|
|
9
|
+
* coerceable byte-like value).
|
|
10
|
+
*
|
|
11
|
+
* The port must already be started (`port.start()` if manually constructed).
|
|
12
|
+
* This adapter assumes the port carries only byte payloads — non-byte messages
|
|
13
|
+
* are ignored.
|
|
14
|
+
*/
|
|
15
|
+
export function byteChannelFromMessagePort(port: MessageTarget): ByteChannel {
|
|
16
|
+
let closedResolve!: () => void;
|
|
17
|
+
const closed = new Promise<void>((r) => {
|
|
18
|
+
closedResolve = r;
|
|
19
|
+
});
|
|
20
|
+
let isClosed = false;
|
|
21
|
+
|
|
22
|
+
const queue: Uint8Array[] = [];
|
|
23
|
+
let pending: ((value: IteratorResult<Uint8Array>) => void) | null = null;
|
|
24
|
+
|
|
25
|
+
const deliver = (bytes: Uint8Array): void => {
|
|
26
|
+
if (isClosed) return;
|
|
27
|
+
if (pending) {
|
|
28
|
+
const r = pending;
|
|
29
|
+
pending = null;
|
|
30
|
+
r({ value: bytes, done: false });
|
|
31
|
+
} else {
|
|
32
|
+
queue.push(bytes);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const onMessage = (ev: MessageEvent): void => {
|
|
37
|
+
const data = ev.data as unknown;
|
|
38
|
+
if (data instanceof Uint8Array) {
|
|
39
|
+
deliver(new Uint8Array(data));
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (data instanceof ArrayBuffer) {
|
|
43
|
+
deliver(new Uint8Array(data));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (ArrayBuffer.isView(data)) {
|
|
47
|
+
const view = data as ArrayBufferView;
|
|
48
|
+
deliver(
|
|
49
|
+
new Uint8Array(view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength)),
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// MessagePort doesn't fire a "close" event natively; consumers signal close
|
|
55
|
+
// by calling the channel's close() (which we honour) or by tearing down the
|
|
56
|
+
// underlying port (which they must observe themselves).
|
|
57
|
+
port.addEventListener("message", onMessage as unknown as MessageListener);
|
|
58
|
+
port.start?.();
|
|
59
|
+
|
|
60
|
+
const recv: AsyncIterable<Uint8Array> = {
|
|
61
|
+
[Symbol.asyncIterator]() {
|
|
62
|
+
return {
|
|
63
|
+
next(): Promise<IteratorResult<Uint8Array>> {
|
|
64
|
+
if (queue.length > 0) {
|
|
65
|
+
return Promise.resolve({ value: queue.shift() as Uint8Array, done: false });
|
|
66
|
+
}
|
|
67
|
+
if (isClosed) {
|
|
68
|
+
return Promise.resolve({
|
|
69
|
+
value: undefined,
|
|
70
|
+
done: true,
|
|
71
|
+
} as IteratorResult<Uint8Array>);
|
|
72
|
+
}
|
|
73
|
+
return new Promise<IteratorResult<Uint8Array>>((resolve) => {
|
|
74
|
+
pending = resolve;
|
|
75
|
+
});
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
send(bytes) {
|
|
83
|
+
if (isClosed) return;
|
|
84
|
+
try {
|
|
85
|
+
port.postMessage(bytes);
|
|
86
|
+
} catch {
|
|
87
|
+
/* port closed by peer */
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
recv,
|
|
91
|
+
closed,
|
|
92
|
+
close() {
|
|
93
|
+
if (isClosed) return;
|
|
94
|
+
isClosed = true;
|
|
95
|
+
port.removeEventListener("message", onMessage as unknown as MessageListener);
|
|
96
|
+
try {
|
|
97
|
+
void port.close?.();
|
|
98
|
+
} catch {
|
|
99
|
+
/* ignore */
|
|
100
|
+
}
|
|
101
|
+
if (pending) {
|
|
102
|
+
const r = pending;
|
|
103
|
+
pending = null;
|
|
104
|
+
r({ value: undefined, done: true } as IteratorResult<Uint8Array>);
|
|
105
|
+
}
|
|
106
|
+
closedResolve();
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
package/src/call-bidi.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { type CallPortOptions, callPort } from "./call-port.js";
|
|
2
|
+
import { ioSend } from "./io-send.js";
|
|
3
|
+
import type { MessageTarget } from "./message-target.js";
|
|
4
|
+
|
|
5
|
+
export interface CallBidiOptions extends CallPortOptions {
|
|
6
|
+
/** Timeout for the outer stream (default: `Number.MAX_SAFE_INTEGER` / max int). */
|
|
7
|
+
bidiTimeout?: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface CallBidiArgs {
|
|
11
|
+
options?: CallBidiOptions;
|
|
12
|
+
[key: string]: unknown;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Initiates a full-duplex stream call: ships `input` values to the peer and
|
|
17
|
+
* yields the values returned by `listenBidi`'s handler.
|
|
18
|
+
*
|
|
19
|
+
* Internally allocates a fresh sub-channel name, announces it to the peer
|
|
20
|
+
* via `callPort`, and then runs {@link ioSend} on that sub-channel.
|
|
21
|
+
*
|
|
22
|
+
* If the outer `callPort` rejects (e.g., the peer's handler threw and
|
|
23
|
+
* `listenPort` surfaced the error as `response:error`), the inner `ioSend`'s
|
|
24
|
+
* recieveIterator is force-closed via an internal cancel signal so the
|
|
25
|
+
* consumer doesn't hang waiting for chunks that will never come. The outer
|
|
26
|
+
* error is then re-thrown to the caller.
|
|
27
|
+
*/
|
|
28
|
+
export async function* callBidi<TIn, TOut>(
|
|
29
|
+
port: MessageTarget,
|
|
30
|
+
input: AsyncIterable<TIn> | Iterable<TIn>,
|
|
31
|
+
{ options = {}, ...params }: CallBidiArgs = {},
|
|
32
|
+
): AsyncGenerator<TOut> {
|
|
33
|
+
const channelName = `${+String(Math.random()).substring(2)}`;
|
|
34
|
+
const { bidiTimeout = 2147483647 } = options;
|
|
35
|
+
const promise = callPort(port, { ...params, channelName }, { ...options, timeout: bidiTimeout });
|
|
36
|
+
|
|
37
|
+
const cancelInner = new AbortController();
|
|
38
|
+
const sendIter = ioSend<TOut, TIn>(port, input, {
|
|
39
|
+
...options,
|
|
40
|
+
channelName,
|
|
41
|
+
cancelSignal: cancelInner.signal,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
let outerError: unknown;
|
|
45
|
+
promise.catch((err) => {
|
|
46
|
+
outerError = err;
|
|
47
|
+
cancelInner.abort();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
yield* sendIter;
|
|
52
|
+
} finally {
|
|
53
|
+
try {
|
|
54
|
+
await promise;
|
|
55
|
+
} catch {
|
|
56
|
+
/* surfaced via outerError */
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (outerError !== undefined) throw outerError;
|
|
60
|
+
}
|