@statewalker/webrun-rpc 0.7.0 → 0.8.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 +33 -2
- package/dist/connect-serve.d.ts +2 -0
- package/dist/connect-serve.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +60 -16
- package/dist/multiplex-port.d.ts +2 -0
- package/dist/multiplex-port.d.ts.map +1 -1
- package/dist/port-types.d.ts +13 -0
- package/dist/port-types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/connect-serve.ts +20 -9
- package/src/duplex-over-port.ts +8 -0
- package/src/index.ts +6 -1
- package/src/multiplex-port.ts +83 -11
- package/src/port-types.ts +13 -0
package/README.md
CHANGED
|
@@ -298,6 +298,7 @@ port cannot grow memory here.
|
|
|
298
298
|
| `signal` | The transport's life. On abort the mux closes with `signal.reason`; an already-aborted signal gives a closed mux. See below. |
|
|
299
299
|
| `window` | Per-port flow control in bytes, off when unset. See "A port with a window holds only its own bytes" below. |
|
|
300
300
|
| `frameSize` | Fair framing in bytes, off when unset; needs `window`. See "Large messages take turns with small ones" below. |
|
|
301
|
+
| `maxReassembledBytes` | Ceiling on what the peer's framed messages hold past their ports' windows, summed over all ports, default `DEFAULT_MAX_REASSEMBLED_BYTES` (64 MiB). Past it, or past what a port's window leaves, the port closes. See "Large messages take turns with small ones" below. |
|
|
301
302
|
| `keepalive` | `{ intervalMs, misses? }`, off when unset: ping the peer and close the mux after `misses` unanswered pings from a peer that answered before. See "A port lives until its transport or its user lets go" below. |
|
|
302
303
|
| `onLeak` | Development aid, off when unset: called with a port's `PortInfo` when its consumer dropped it without `close()`, and the port is freed. See "A port lives until its transport or its user lets go" below. |
|
|
303
304
|
|
|
@@ -374,11 +375,35 @@ frames of messages posted together interleave.
|
|
|
374
375
|
It also needs `window` and byte codecs (`encode` / `decode`) on both ends. A side frames only on a
|
|
375
376
|
port whose window is in force with exact sizes agreed (`exact: true` in the `open` and in its
|
|
376
377
|
acknowledgement), and accepts frames only there; a frame anywhere else is dropped. An older peer,
|
|
377
|
-
or one whose codec cannot decode, gets whole messages.
|
|
378
|
-
same message sent whole would. If the pipe's `whenDrained()` rejects, the mux closes with that
|
|
378
|
+
or one whose codec cannot decode, gets whole messages. If the pipe's `whenDrained()` rejects, the mux closes with that
|
|
379
379
|
error, so the ports waiting behind it fail instead of hanging. 16-64 KiB frames cost one small envelope each (about 15 bytes with
|
|
380
380
|
`msgpackCodec`).
|
|
381
381
|
|
|
382
|
+
**A peer cannot make this side hold frames without limit.** Two limits apply as frames arrive:
|
|
383
|
+
|
|
384
|
+
- a message begun while this side still owed the port credit (delivered or consumed bytes not yet
|
|
385
|
+
granted back) must fit what the window left, since an honest sender counts at least that much as
|
|
386
|
+
outstanding; one begun with nothing owed may be larger than the window, as a whole message may;
|
|
387
|
+
- what messages larger than the window hold past it, summed across every port, is at most
|
|
388
|
+
`maxReassembledBytes` (64 MiB by default), so opening many ports does not multiply it. A partial
|
|
389
|
+
message within its window is not charged: the window bounds it already, as it bounds unframed
|
|
390
|
+
traffic, so many ports framing honest messages at once never trip the cap.
|
|
391
|
+
|
|
392
|
+
A frame past either limit closes its port: the partial message is dropped, the port's close signal
|
|
393
|
+
aborts with a `RangeError` ("webrun-rpc: a framed message passed what the window left …" or
|
|
394
|
+
"… passed maxReassembledBytes …"), and the peer's port closes as on any peer close. On the wire
|
|
395
|
+
the close carries reason `"max-reassembled-bytes"`. A port that closes with messages still queued
|
|
396
|
+
sends them whole, not framed, so they never trip the window check on the way out.
|
|
397
|
+
|
|
398
|
+
The cap is the receiver's: a sender with `frameSize` that frames a message more than the
|
|
399
|
+
peer's `maxReassembledBytes` past the window has its port closed, where the same message sent whole would arrive.
|
|
400
|
+
Set it above the largest message the peer legitimately frames; a message just past it looks like
|
|
401
|
+
a closed port, not a lost message.
|
|
402
|
+
|
|
403
|
+
Frames carry no sequence number. Reassembly relies on what every `MessageTarget` already
|
|
404
|
+
promises: an ordered, reliable pipe. Each port's frames leave from its own queue in order, and
|
|
405
|
+
other ports' frames interleave but are told apart by port id.
|
|
406
|
+
|
|
382
407
|
`openPort(meta?)` allocates a port, announces it and returns the local end without waiting for the
|
|
383
408
|
peer to accept. That keeps layer 1 free of round trips: messages posted before a rejection are
|
|
384
409
|
dropped at the far end and the port goes inert. It is asynchronous because a natively multiplexed
|
|
@@ -589,6 +614,12 @@ caller without one does not.
|
|
|
589
614
|
An abort unwinds a producing handler through `iter.return()`, not by throwing into it. A handler's
|
|
590
615
|
`catch` never sees the abort reason; only `finally` runs. Put cleanup in `finally`.
|
|
591
616
|
|
|
617
|
+
`serve` forwards that `return()` to the handler's output at once, even while the handler is still
|
|
618
|
+
working on its first chunk, with or without `maxMessageSize` (whose chunker would queue it too). Its own tracking wrapper would otherwise queue it behind that pending
|
|
619
|
+
`next()`, as any async generator does. A handler that wraps its output to watch for `return()`
|
|
620
|
+
therefore sees the abandonment when it happens, not when it next yields:
|
|
621
|
+
`@statewalker/webrun-http-streams` aborts the served `Request.signal` that way.
|
|
622
|
+
|
|
592
623
|
### `transferPortMux` moves real ports
|
|
593
624
|
|
|
594
625
|
Each `openPort` creates a `MessageChannel`, posts `{ type: PORT_TRANSFER, meta }` with one end in
|
package/dist/connect-serve.d.ts
CHANGED
|
@@ -86,6 +86,8 @@ export interface OverPipeOptions {
|
|
|
86
86
|
window?: number;
|
|
87
87
|
/** Fair framing in bytes — see `PortMuxOptions.frameSize`. */
|
|
88
88
|
frameSize?: number;
|
|
89
|
+
/** Ceiling on a message reassembled from frames — see `PortMuxOptions.maxReassembledBytes`. */
|
|
90
|
+
maxReassembledBytes?: number;
|
|
89
91
|
/** Liveness of the peer by ping/pong — see `PortMuxOptions.keepalive`. */
|
|
90
92
|
keepalive?: {
|
|
91
93
|
intervalMs: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connect-serve.d.ts","sourceRoot":"","sources":["../src/connect-serve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAU,KAAK,EAAE,MAAM,6BAA6B,CAAC;AAM1E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,OAAO,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAW1E,uEAAuE;AACvE,MAAM,MAAM,MAAM,GAAG,CAAC,IAAI,EAAE,aAAa,EAAE,IAAI,CAAC,EAAE,OAAO,KAAK,OAAO,GAAG,SAAS,CAAC;AAElF;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,MAAM,CAAC,EAAE,MAAM,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;AAE7E,MAAM,WAAW,UAAU;IACzB;;;;;;;;;;OAUG;IACH,GAAG,EAAE,cAAc,CAAC;IACpB;;;yEAGqE;IACrE,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,OAAO,EAAE,OAAO,CAAC,UAAU,CAyCvC,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,KAAK,EAAE,KAAK,CAAC,UAAU,
|
|
1
|
+
{"version":3,"file":"connect-serve.d.ts","sourceRoot":"","sources":["../src/connect-serve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAU,KAAK,EAAE,MAAM,6BAA6B,CAAC;AAM1E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,OAAO,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAW1E,uEAAuE;AACvE,MAAM,MAAM,MAAM,GAAG,CAAC,IAAI,EAAE,aAAa,EAAE,IAAI,CAAC,EAAE,OAAO,KAAK,OAAO,GAAG,SAAS,CAAC;AAElF;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,MAAM,CAAC,EAAE,MAAM,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;AAE7E,MAAM,WAAW,UAAU;IACzB;;;;;;;;;;OAUG;IACH,GAAG,EAAE,cAAc,CAAC;IACpB;;;yEAGqE;IACrE,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,OAAO,EAAE,OAAO,CAAC,UAAU,CAyCvC,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,KAAK,EAAE,KAAK,CAAC,UAAU,CA8DnC,CAAC;AAEF,MAAM,WAAW,eAAe;IAC9B,mFAAmF;IACnF,KAAK,EAAE,SAAS,CAAC;IACjB;;;;OAIG;IACH,IAAI,CAAC,EAAE,WAAW,GAAG,WAAW,CAAC;IACjC,oEAAoE;IACpE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,gFAAgF;IAChF,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,iFAAiF;IACjF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,8DAA8D;IAC9D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,+FAA+F;IAC/F,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,0EAA0E;IAC1E,SAAS,CAAC,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACpD,6FAA6F;IAC7F,MAAM,CAAC,EAAE,cAAc,CAAC,QAAQ,CAAC,CAAC;CACnC;AAED;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,aAAa,EAAE,OAAO,EAAE,eAAe,GAAG,cAAc,CAEtF;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,cAAc,GAAG,cAAc,CAEjE"}
|
package/dist/index.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ export * from "./io-send.js";
|
|
|
10
10
|
export * from "./listen-bidi.js";
|
|
11
11
|
export * from "./listen-port.js";
|
|
12
12
|
export * from "./message-target.js";
|
|
13
|
-
export { DEFAULT_MAX_PORTS, MUX_CLOSED_MESSAGE, multiplexPort } from "./multiplex-port.js";
|
|
13
|
+
export { DEFAULT_MAX_PORTS, DEFAULT_MAX_REASSEMBLED_BYTES, MUX_CLOSED_MESSAGE, multiplexPort, } from "./multiplex-port.js";
|
|
14
14
|
export { type PortSendState, portBufferedAmount, whenPortReady } from "./port-credit.js";
|
|
15
15
|
export type { OpenPortOptions, PortCodec, PortEnvelope, PortInfo, PortMux, PortMuxOptions, } from "./port-types.js";
|
|
16
16
|
export * from "./recieve.js";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,0BAA0B,EAAE,MAAM,mBAAmB,CAAC;AAC/D,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,qBAAqB,CAAC;AACpC,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAC3E,OAAO,EACL,OAAO,EACP,KAAK,MAAM,EACX,KAAK,eAAe,EACpB,QAAQ,EACR,SAAS,EACT,KAAK,cAAc,EACnB,KAAK,UAAU,EACf,KAAK,GACN,MAAM,oBAAoB,CAAC;AAC5B,cAAc,uBAAuB,CAAC;AACtC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,cAAc,CAAC;AAC7B,cAAc,kBAAkB,CAAC;AACjC,cAAc,kBAAkB,CAAC;AACjC,cAAc,qBAAqB,CAAC;AACpC,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,0BAA0B,EAAE,MAAM,mBAAmB,CAAC;AAC/D,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,qBAAqB,CAAC;AACpC,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAC3E,OAAO,EACL,OAAO,EACP,KAAK,MAAM,EACX,KAAK,eAAe,EACpB,QAAQ,EACR,SAAS,EACT,KAAK,cAAc,EACnB,KAAK,UAAU,EACf,KAAK,GACN,MAAM,oBAAoB,CAAC;AAC5B,cAAc,uBAAuB,CAAC;AACtC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,cAAc,CAAC;AAC7B,cAAc,kBAAkB,CAAC;AACjC,cAAc,kBAAkB,CAAC;AACjC,cAAc,qBAAqB,CAAC;AACpC,OAAO,EACL,iBAAiB,EACjB,6BAA6B,EAC7B,kBAAkB,EAClB,aAAa,GACd,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,KAAK,aAAa,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACzF,YAAY,EACV,eAAe,EACf,SAAS,EACT,YAAY,EACZ,QAAQ,EACR,OAAO,EACP,cAAc,GACf,MAAM,iBAAiB,CAAC;AACzB,cAAc,cAAc,CAAC;AAC7B,cAAc,WAAW,CAAC;AAC1B,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,cAAc,wBAAwB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -825,7 +825,11 @@ function receiveChunks(port, channelName, controller, touch) {
|
|
|
825
825
|
* outstanding at a time (spec D11/D12), with no per-chunk deadline (spec D8).
|
|
826
826
|
*/
|
|
827
827
|
async function sendChunks(port, channelName, output, { maxMessageSize, log }, signal, touch) {
|
|
828
|
-
const
|
|
828
|
+
const framed = maxMessageSize ? toChunks(maxMessageSize)(output) : output;
|
|
829
|
+
const stream = throughAbort(framed, signal);
|
|
830
|
+
const producer = output;
|
|
831
|
+
const returnProducer = () => void Promise.resolve(producer.return?.()).catch(() => {});
|
|
832
|
+
if (framed !== output) signal.addEventListener("abort", returnProducer, { once: true });
|
|
829
833
|
try {
|
|
830
834
|
await sendIterator(async ({ done, value, error }) => {
|
|
831
835
|
if (signal.aborted) return;
|
|
@@ -849,6 +853,8 @@ async function sendChunks(port, channelName, output, { maxMessageSize, log }, si
|
|
|
849
853
|
} catch (err) {
|
|
850
854
|
if (signal.aborted) return;
|
|
851
855
|
throw err;
|
|
856
|
+
} finally {
|
|
857
|
+
signal.removeEventListener("abort", returnProducer);
|
|
852
858
|
}
|
|
853
859
|
}
|
|
854
860
|
//#endregion
|
|
@@ -984,6 +990,8 @@ const KEEPALIVE_ID = Number.MAX_SAFE_INTEGER;
|
|
|
984
990
|
const MAX_TIMER_MS = 2 ** 31 - 1;
|
|
985
991
|
/** Ceiling on concurrently open virtual ports. Bounds the id table only. */
|
|
986
992
|
const DEFAULT_MAX_PORTS = 1024;
|
|
993
|
+
/** Default ceiling on one message reassembled from frames (`maxReassembledBytes`). */
|
|
994
|
+
const DEFAULT_MAX_REASSEMBLED_BYTES = 67108864;
|
|
987
995
|
/**
|
|
988
996
|
* What a message costs a port's window when the codec cannot say
|
|
989
997
|
* (`PortCodec.encode`): its bytes and string lengths, found in its values up
|
|
@@ -1013,10 +1021,18 @@ function estimateMessageSize(payload) {
|
|
|
1013
1021
|
*/
|
|
1014
1022
|
function multiplexPort(port, options) {
|
|
1015
1023
|
const { codec, onPort, side = "initiator", maxPorts = DEFAULT_MAX_PORTS, maxMessageSize, signal, window, frameSize, keepalive, onLeak } = options;
|
|
1024
|
+
const maxReassembledBytes = options.maxReassembledBytes ?? 67108864;
|
|
1016
1025
|
const misses = keepalive?.misses ?? 3;
|
|
1017
1026
|
if (keepalive && !(Number.isFinite(keepalive.intervalMs) && keepalive.intervalMs > 0 && keepalive.intervalMs <= MAX_TIMER_MS && Number.isSafeInteger(misses) && misses > 0)) throw new RangeError(`webrun-rpc: keepalive needs an intervalMs in (0, ${MAX_TIMER_MS}] and a positive integer misses`);
|
|
1018
1027
|
if (frameSize != null && !(Number.isSafeInteger(frameSize) && frameSize > 0)) throw new RangeError(`webrun-rpc: frameSize must be a positive integer, got ${frameSize}`);
|
|
1028
|
+
if (!(Number.isSafeInteger(maxReassembledBytes) && maxReassembledBytes > 0)) throw new RangeError(`webrun-rpc: maxReassembledBytes must be a positive integer, got ${maxReassembledBytes}`);
|
|
1019
1029
|
const open = /* @__PURE__ */ new Map();
|
|
1030
|
+
let reassembling = 0;
|
|
1031
|
+
const freePartial = (entry) => {
|
|
1032
|
+
if (!entry?.partial) return;
|
|
1033
|
+
reassembling -= entry.partial.charged;
|
|
1034
|
+
entry.partial = void 0;
|
|
1035
|
+
};
|
|
1020
1036
|
const leaks = onLeak ? new FinalizationRegistry(({ id, record, release }) => {
|
|
1021
1037
|
const entry = open.get(id);
|
|
1022
1038
|
if (entry?.handle !== record) return;
|
|
@@ -1164,12 +1180,12 @@ function multiplexPort(port, options) {
|
|
|
1164
1180
|
...envelope,
|
|
1165
1181
|
payload: structuredClone(payload, { transfer })
|
|
1166
1182
|
} : void 0;
|
|
1167
|
-
const send = () => {
|
|
1183
|
+
const send = (whole = false) => {
|
|
1168
1184
|
if (!bytes) {
|
|
1169
1185
|
sendOnPort(id, held ? () => post(held) : () => post(envelope, transfer));
|
|
1170
1186
|
return;
|
|
1171
1187
|
}
|
|
1172
|
-
if (frameSize != null && c.enforced && c.exact && bytes.byteLength > frameSize) {
|
|
1188
|
+
if (!whole && frameSize != null && c.enforced && c.exact && bytes.byteLength > frameSize) {
|
|
1173
1189
|
for (let at = 0; at < bytes.byteLength; at += frameSize) {
|
|
1174
1190
|
const data = bytes.subarray(at, at + frameSize);
|
|
1175
1191
|
const frame = at + frameSize >= bytes.byteLength ? {
|
|
@@ -1199,10 +1215,11 @@ function multiplexPort(port, options) {
|
|
|
1199
1215
|
record.markClosed(reason);
|
|
1200
1216
|
});
|
|
1201
1217
|
function release(reason) {
|
|
1218
|
+
freePartial(open.get(id));
|
|
1202
1219
|
open.delete(id);
|
|
1203
1220
|
leaks?.unregister(record);
|
|
1204
1221
|
if (c?.queue.length) {
|
|
1205
|
-
for (const item of c.queue.splice(0)) item.send();
|
|
1222
|
+
for (const item of c.queue.splice(0)) item.send(true);
|
|
1206
1223
|
c.queued = 0;
|
|
1207
1224
|
}
|
|
1208
1225
|
sendOnPort(id, () => post({
|
|
@@ -1341,15 +1358,38 @@ function multiplexPort(port, options) {
|
|
|
1341
1358
|
if (envelope.type === "frame") {
|
|
1342
1359
|
if (!entry?.credit?.exact || !codec.decode || !(envelope.data instanceof Uint8Array)) return;
|
|
1343
1360
|
touch(envelope.id);
|
|
1361
|
+
const c = entry.credit;
|
|
1362
|
+
const owed = c.unconsumed + c.toGrant;
|
|
1344
1363
|
const partial = entry.partial ?? {
|
|
1345
1364
|
chunks: [],
|
|
1346
|
-
bytes: 0
|
|
1365
|
+
bytes: 0,
|
|
1366
|
+
owed,
|
|
1367
|
+
charged: 0,
|
|
1368
|
+
...owed > 0 ? { room: c.window - owed } : {}
|
|
1347
1369
|
};
|
|
1348
1370
|
entry.partial = partial;
|
|
1371
|
+
const size = envelope.data.byteLength;
|
|
1372
|
+
const charged = partial.room == null ? Math.max(0, partial.bytes + size - c.window) : 0;
|
|
1373
|
+
const overrun = partial.room != null && partial.bytes + size > partial.room ? `what the window left (${partial.room} of ${c.window} bytes, ${partial.owed} still owed)` : reassembling - partial.charged + charged > maxReassembledBytes ? `maxReassembledBytes (${maxReassembledBytes} bytes past the window, across all ports)` : void 0;
|
|
1374
|
+
if (overrun) {
|
|
1375
|
+
freePartial(entry);
|
|
1376
|
+
dropOutgoing(envelope.id);
|
|
1377
|
+
open.delete(envelope.id);
|
|
1378
|
+
releaseWaiters(c);
|
|
1379
|
+
entry.handle.markClosed(/* @__PURE__ */ new RangeError(`webrun-rpc: a framed message passed ${overrun}`));
|
|
1380
|
+
post({
|
|
1381
|
+
type: "close",
|
|
1382
|
+
id: envelope.id,
|
|
1383
|
+
reason: "max-reassembled-bytes"
|
|
1384
|
+
});
|
|
1385
|
+
return;
|
|
1386
|
+
}
|
|
1387
|
+
partial.bytes += size;
|
|
1388
|
+
reassembling += charged - partial.charged;
|
|
1389
|
+
partial.charged = charged;
|
|
1349
1390
|
partial.chunks.push(envelope.data);
|
|
1350
|
-
partial.bytes += envelope.data.byteLength;
|
|
1351
1391
|
if (!envelope.last) return;
|
|
1352
|
-
entry
|
|
1392
|
+
freePartial(entry);
|
|
1353
1393
|
const whole = new Uint8Array(partial.bytes);
|
|
1354
1394
|
let at = 0;
|
|
1355
1395
|
for (const chunk of partial.chunks) {
|
|
@@ -1363,6 +1403,7 @@ function multiplexPort(port, options) {
|
|
|
1363
1403
|
}
|
|
1364
1404
|
dropOutgoing(envelope.id);
|
|
1365
1405
|
if (!entry) return;
|
|
1406
|
+
freePartial(entry);
|
|
1366
1407
|
open.delete(envelope.id);
|
|
1367
1408
|
releaseWaiters(entry.credit);
|
|
1368
1409
|
entry.handle.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the peer closed the port"));
|
|
@@ -1555,14 +1596,17 @@ const serve = async ({ mux: factory, timeout }, handler) => {
|
|
|
1555
1596
|
const mux = await factory((port) => {
|
|
1556
1597
|
let off;
|
|
1557
1598
|
let finished = false;
|
|
1558
|
-
const tracked = (input) =>
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1599
|
+
const tracked = (input) => {
|
|
1600
|
+
const output = handler(input);
|
|
1601
|
+
return returningAtOnce((async function* () {
|
|
1602
|
+
try {
|
|
1603
|
+
yield* output;
|
|
1604
|
+
} finally {
|
|
1605
|
+
finished = true;
|
|
1606
|
+
if (off) live.delete(off);
|
|
1607
|
+
}
|
|
1608
|
+
})(), () => void output.return(void 0).catch(() => {}));
|
|
1609
|
+
};
|
|
1566
1610
|
off = serveDuplexOverPort(port, tracked, streamOptions);
|
|
1567
1611
|
if (!finished) live.add(off);
|
|
1568
1612
|
});
|
|
@@ -1780,4 +1824,4 @@ function transferPortMux(target, options = {}) {
|
|
|
1780
1824
|
};
|
|
1781
1825
|
}
|
|
1782
1826
|
//#endregion
|
|
1783
|
-
export { CANCEL_CHANNEL_TYPE, DEFAULT_MAX_PORTS, MUX_CLOSED_MESSAGE, NO_TIMEOUT, PORT_TRANSFER, STREAM_ABORT, byteChannelFromMessagePort, callBidi, callPort, connect, duplexOverPort, getPortCloseSignal, ioHandle, ioSend, listenBidi, listenCancelChannel, listenPort, multiplexPort, overPipe, overPorts, portBufferedAmount, postCancelChannel, recieve, send, serve, serveDuplexOverPort, setPortCloseSignal, structuredCodec, transferPortMux, whenPortReady };
|
|
1827
|
+
export { CANCEL_CHANNEL_TYPE, DEFAULT_MAX_PORTS, DEFAULT_MAX_REASSEMBLED_BYTES, MUX_CLOSED_MESSAGE, NO_TIMEOUT, PORT_TRANSFER, STREAM_ABORT, byteChannelFromMessagePort, callBidi, callPort, connect, duplexOverPort, getPortCloseSignal, ioHandle, ioSend, listenBidi, listenCancelChannel, listenPort, multiplexPort, overPipe, overPorts, portBufferedAmount, postCancelChannel, recieve, send, serve, serveDuplexOverPort, setPortCloseSignal, structuredCodec, transferPortMux, whenPortReady };
|
package/dist/multiplex-port.d.ts
CHANGED
|
@@ -7,6 +7,8 @@ import type { PortInfo, PortMux, PortMuxOptions } from "./port-types.js";
|
|
|
7
7
|
export declare const MUX_CLOSED_MESSAGE = "webrun-rpc: the multiplexer was closed";
|
|
8
8
|
/** Ceiling on concurrently open virtual ports. Bounds the id table only. */
|
|
9
9
|
export declare const DEFAULT_MAX_PORTS = 1024;
|
|
10
|
+
/** Default ceiling on one message reassembled from frames (`maxReassembledBytes`). */
|
|
11
|
+
export declare const DEFAULT_MAX_REASSEMBLED_BYTES: number;
|
|
10
12
|
/**
|
|
11
13
|
* What a message costs a port's window when the codec cannot say
|
|
12
14
|
* (`PortCodec.encode`): its bytes and string lengths, found in its values up
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"multiplex-port.d.ts","sourceRoot":"","sources":["../src/multiplex-port.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,OAAO,KAAK,EAGV,QAAQ,EACR,OAAO,EACP,cAAc,EACf,MAAM,iBAAiB,CAAC;AAGzB;;;GAGG;AACH,eAAO,MAAM,kBAAkB,2CAA2C,CAAC;AAY3E,4EAA4E;AAC5E,eAAO,MAAM,iBAAiB,OAAO,CAAC;AAEtC;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAc5D;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,aAAa,EACnB,OAAO,EAAE,cAAc,GACtB,OAAO,GAAG;IAAE,KAAK,IAAI,QAAQ,EAAE,CAAA;CAAE,
|
|
1
|
+
{"version":3,"file":"multiplex-port.d.ts","sourceRoot":"","sources":["../src/multiplex-port.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,OAAO,KAAK,EAGV,QAAQ,EACR,OAAO,EACP,cAAc,EACf,MAAM,iBAAiB,CAAC;AAGzB;;;GAGG;AACH,eAAO,MAAM,kBAAkB,2CAA2C,CAAC;AAY3E,4EAA4E;AAC5E,eAAO,MAAM,iBAAiB,OAAO,CAAC;AAEtC,sFAAsF;AACtF,eAAO,MAAM,6BAA6B,QAAmB,CAAC;AAE9D;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAc5D;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,aAAa,EACnB,OAAO,EAAE,cAAc,GACtB,OAAO,GAAG;IAAE,KAAK,IAAI,QAAQ,EAAE,CAAA;CAAE,CA0rBnC"}
|
package/dist/port-types.d.ts
CHANGED
|
@@ -147,6 +147,19 @@ export interface PortMuxOptions {
|
|
|
147
147
|
* whole. 16-64 KiB is a reasonable range.
|
|
148
148
|
*/
|
|
149
149
|
frameSize?: number;
|
|
150
|
+
/**
|
|
151
|
+
* Ceiling, in bytes, on what this side holds of a peer's framed messages
|
|
152
|
+
* past their ports' windows, summed over all ports (default 64 MiB).
|
|
153
|
+
* A partial message counts against its port's window as it arrives: one
|
|
154
|
+
* begun while this side still owed credit must fit what the window left,
|
|
155
|
+
* and one begun with nothing owed may pass the window, its excess charged
|
|
156
|
+
* to this cap. A frame past either limit makes the mux drop that port's
|
|
157
|
+
* partial message, close the port with a `RangeError` and send the peer a
|
|
158
|
+
* close with reason `"max-reassembled-bytes"`. A framed message more than
|
|
159
|
+
* this cap past the window never arrives, though the same message sent
|
|
160
|
+
* whole would.
|
|
161
|
+
*/
|
|
162
|
+
maxReassembledBytes?: number;
|
|
150
163
|
/**
|
|
151
164
|
* Liveness of the peer (off when unset). Every `intervalMs` the mux pings
|
|
152
165
|
* and the peer answers; anything from it counts as an answer. After `misses`
|
package/dist/port-types.d.ts.map
CHANGED
|
@@ -1 +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;;;;;;;GAOG;AACH,MAAM,MAAM,YAAY,GACpB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,GAC9E;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;AACjD,gFAAgF;GAC9E;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE;AAChE,8EAA8E;GAC5E;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,UAAU,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAA;CAAE;AACjE,oGAAoG;GAClG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GAC5B;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC;AAEjC;;;;;;;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;IACpD;;;;;OAKG;IACH,MAAM,CAAC,CAAC,QAAQ,EAAE,YAAY,GAAG,UAAU,CAAC;IAC5C,gFAAgF;IAChF,MAAM,CAAC,CAAC,IAAI,EAAE,OAAO,GAAG,YAAY,GAAG,SAAS,CAAC;CAClD;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;IACxB;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;;;;;;;;;;;;;;;;;;;OAmBG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;;;;;OASG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;;;;;;OAUG;IACH,SAAS,CAAC,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACpD;;;;;;;;;OASG;IACH,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,QAAQ,KAAK,IAAI,CAAC;CACnC;AAED,wDAAwD;AACxD,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,6CAA6C;IAC7C,QAAQ,EAAE,MAAM,CAAC;IACjB,4DAA4D;IAC5D,YAAY,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,8DAA8D;IAC9D,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,sEAAsE;IACtE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,eAAe;IAC9B,iFAAiF;IACjF,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,mCAAmC;AACnC,MAAM,WAAW,OAAO;IACtB,8DAA8D;IAC9D,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;IAC5E;;;;OAIG;IACH,KAAK,CAAC,MAAM,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvC,0DAA0D;IAC1D,KAAK,CAAC,IAAI,QAAQ,EAAE,CAAC;IACrB,iDAAiD;IACjD,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;CAClC"}
|
|
1
|
+
{"version":3,"file":"port-types.d.ts","sourceRoot":"","sources":["../src/port-types.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/connect-serve.ts
CHANGED
|
@@ -139,15 +139,24 @@ export const serve: Serve<PortParams> = async ({ mux: factory, timeout }, handle
|
|
|
139
139
|
// reaches it as `return()` through `throughAbort`, so the `finally` runs
|
|
140
140
|
// on every ending — normal exhaustion, handler throw, peer abort, local
|
|
141
141
|
// teardown. `serveDuplexOverPort` reports no completion of its own.
|
|
142
|
-
const tracked: Duplex = (input) =>
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
142
|
+
const tracked: Duplex = (input) => {
|
|
143
|
+
const output = handler(input);
|
|
144
|
+
// `return()` on this wrapper is queued behind its pending `next()`, as on
|
|
145
|
+
// any async generator, so the abandonment would reach the handler only
|
|
146
|
+
// once it answers. Forwarded at once, a handler still waiting (an HTTP
|
|
147
|
+
// long poll) learns now that its caller left.
|
|
148
|
+
return returningAtOnce(
|
|
149
|
+
(async function* () {
|
|
150
|
+
try {
|
|
151
|
+
yield* output;
|
|
152
|
+
} finally {
|
|
153
|
+
finished = true;
|
|
154
|
+
if (off) live.delete(off);
|
|
155
|
+
}
|
|
156
|
+
})(),
|
|
157
|
+
() => void output.return(undefined).catch(() => {}),
|
|
158
|
+
);
|
|
159
|
+
};
|
|
151
160
|
off = serveDuplexOverPort(port, tracked, streamOptions);
|
|
152
161
|
// `serveDuplexOverPort` invokes the handler synchronously. The generator
|
|
153
162
|
// body above is lazy, so today it cannot have finished by the time `off`
|
|
@@ -201,6 +210,8 @@ export interface OverPipeOptions {
|
|
|
201
210
|
window?: number;
|
|
202
211
|
/** Fair framing in bytes — see `PortMuxOptions.frameSize`. */
|
|
203
212
|
frameSize?: number;
|
|
213
|
+
/** Ceiling on a message reassembled from frames — see `PortMuxOptions.maxReassembledBytes`. */
|
|
214
|
+
maxReassembledBytes?: number;
|
|
204
215
|
/** Liveness of the peer by ping/pong — see `PortMuxOptions.keepalive`. */
|
|
205
216
|
keepalive?: { intervalMs: number; misses?: number };
|
|
206
217
|
/** Development aid: report ports dropped without `close()` — see `PortMuxOptions.onLeak`. */
|
package/src/duplex-over-port.ts
CHANGED
|
@@ -481,6 +481,12 @@ async function sendChunks(
|
|
|
481
481
|
): Promise<void> {
|
|
482
482
|
const framed = maxMessageSize ? toChunks(maxMessageSize)(output) : output;
|
|
483
483
|
const stream = throughAbort(framed, signal);
|
|
484
|
+
// `throughAbort` returns `framed`; when chunking wraps the output, that
|
|
485
|
+
// `return()` is queued behind the chunker's pending `next()`, so the abort
|
|
486
|
+
// would reach the producer only once it yields. Return the producer itself.
|
|
487
|
+
const producer = output as Partial<AsyncIterator<Uint8Array>>;
|
|
488
|
+
const returnProducer = () => void Promise.resolve(producer.return?.()).catch(() => {});
|
|
489
|
+
if (framed !== output) signal.addEventListener("abort", returnProducer, { once: true });
|
|
484
490
|
try {
|
|
485
491
|
await sendIterator<Uint8Array>(async ({ done, value, error }) => {
|
|
486
492
|
if (signal.aborted) return;
|
|
@@ -502,5 +508,7 @@ async function sendChunks(
|
|
|
502
508
|
// anything else is a genuine transport failure worth surfacing.
|
|
503
509
|
if (signal.aborted) return;
|
|
504
510
|
throw err;
|
|
511
|
+
} finally {
|
|
512
|
+
signal.removeEventListener("abort", returnProducer);
|
|
505
513
|
}
|
|
506
514
|
}
|
package/src/index.ts
CHANGED
|
@@ -21,7 +21,12 @@ export * from "./io-send.js";
|
|
|
21
21
|
export * from "./listen-bidi.js";
|
|
22
22
|
export * from "./listen-port.js";
|
|
23
23
|
export * from "./message-target.js";
|
|
24
|
-
export {
|
|
24
|
+
export {
|
|
25
|
+
DEFAULT_MAX_PORTS,
|
|
26
|
+
DEFAULT_MAX_REASSEMBLED_BYTES,
|
|
27
|
+
MUX_CLOSED_MESSAGE,
|
|
28
|
+
multiplexPort,
|
|
29
|
+
} from "./multiplex-port.js";
|
|
25
30
|
export { type PortSendState, portBufferedAmount, whenPortReady } from "./port-credit.js";
|
|
26
31
|
export type {
|
|
27
32
|
OpenPortOptions,
|
package/src/multiplex-port.ts
CHANGED
|
@@ -29,6 +29,9 @@ const MAX_TIMER_MS = 2 ** 31 - 1;
|
|
|
29
29
|
/** Ceiling on concurrently open virtual ports. Bounds the id table only. */
|
|
30
30
|
export const DEFAULT_MAX_PORTS = 1024;
|
|
31
31
|
|
|
32
|
+
/** Default ceiling on one message reassembled from frames (`maxReassembledBytes`). */
|
|
33
|
+
export const DEFAULT_MAX_REASSEMBLED_BYTES = 64 * 1024 * 1024;
|
|
34
|
+
|
|
32
35
|
/**
|
|
33
36
|
* What a message costs a port's window when the codec cannot say
|
|
34
37
|
* (`PortCodec.encode`): its bytes and string lengths, found in its values up
|
|
@@ -73,6 +76,7 @@ export function multiplexPort(
|
|
|
73
76
|
keepalive,
|
|
74
77
|
onLeak,
|
|
75
78
|
} = options;
|
|
79
|
+
const maxReassembledBytes = options.maxReassembledBytes ?? DEFAULT_MAX_REASSEMBLED_BYTES;
|
|
76
80
|
const misses = keepalive?.misses ?? 3;
|
|
77
81
|
if (
|
|
78
82
|
keepalive &&
|
|
@@ -92,6 +96,11 @@ export function multiplexPort(
|
|
|
92
96
|
if (frameSize != null && !(Number.isSafeInteger(frameSize) && frameSize > 0)) {
|
|
93
97
|
throw new RangeError(`webrun-rpc: frameSize must be a positive integer, got ${frameSize}`);
|
|
94
98
|
}
|
|
99
|
+
if (!(Number.isSafeInteger(maxReassembledBytes) && maxReassembledBytes > 0)) {
|
|
100
|
+
throw new RangeError(
|
|
101
|
+
`webrun-rpc: maxReassembledBytes must be a positive integer, got ${maxReassembledBytes}`,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
95
104
|
|
|
96
105
|
/**
|
|
97
106
|
* Flow control of one port (`PortMuxOptions.window`): the send side's queue
|
|
@@ -117,7 +126,7 @@ export function multiplexPort(
|
|
|
117
126
|
*/
|
|
118
127
|
exact: boolean;
|
|
119
128
|
outstanding: number;
|
|
120
|
-
queue: { send: () => void; size: number }[];
|
|
129
|
+
queue: { send: (whole?: boolean) => void; size: number }[];
|
|
121
130
|
queued: number;
|
|
122
131
|
unconsumed: number;
|
|
123
132
|
waiters: { below: number; resolve: () => void }[];
|
|
@@ -138,10 +147,28 @@ export function multiplexPort(
|
|
|
138
147
|
interface Entry extends PortInfo {
|
|
139
148
|
handle: PortRecord;
|
|
140
149
|
credit?: Credit;
|
|
141
|
-
/**
|
|
142
|
-
|
|
150
|
+
/**
|
|
151
|
+
* The frames of a message still arriving. `room`: what the window left
|
|
152
|
+
* when the first came, if anything was still owed back then. `charged`:
|
|
153
|
+
* its bytes past the window, counted in `reassembling`.
|
|
154
|
+
*/
|
|
155
|
+
partial?: {
|
|
156
|
+
chunks: Uint8Array[];
|
|
157
|
+
bytes: number;
|
|
158
|
+
room?: number;
|
|
159
|
+
owed: number;
|
|
160
|
+
charged: number;
|
|
161
|
+
};
|
|
143
162
|
}
|
|
144
163
|
const open = new Map<number, Entry>();
|
|
164
|
+
// Bytes of partial messages held past their port's window, across every
|
|
165
|
+
// port (`maxReassembledBytes`). What fits a window is bounded by it already.
|
|
166
|
+
let reassembling = 0;
|
|
167
|
+
const freePartial = (entry: Entry | undefined): void => {
|
|
168
|
+
if (!entry?.partial) return;
|
|
169
|
+
reassembling -= entry.partial.charged;
|
|
170
|
+
entry.partial = undefined;
|
|
171
|
+
};
|
|
145
172
|
// A port collected while still open: report it, then free its slot on both
|
|
146
173
|
// ends as its close would. Only with `onLeak` — a registry costs per port.
|
|
147
174
|
const leaks = onLeak
|
|
@@ -346,14 +373,22 @@ export function multiplexPort(
|
|
|
346
373
|
!bytes && waits
|
|
347
374
|
? { ...envelope, payload: structuredClone(payload, { transfer }) }
|
|
348
375
|
: undefined;
|
|
349
|
-
|
|
376
|
+
// `whole`: at close, what is still queued leaves past the window, and
|
|
377
|
+
// a frame past the window is what the peer refuses (`maxReassembledBytes`).
|
|
378
|
+
const send = (whole = false): void => {
|
|
350
379
|
if (!bytes) {
|
|
351
380
|
sendOnPort(id, held ? () => post(held) : () => post(envelope, transfer));
|
|
352
381
|
return;
|
|
353
382
|
}
|
|
354
383
|
// Frames only once the peer has acknowledged exact sizing: before
|
|
355
384
|
// that, it may be a peer that cannot reassemble them.
|
|
356
|
-
if (
|
|
385
|
+
if (
|
|
386
|
+
!whole &&
|
|
387
|
+
frameSize != null &&
|
|
388
|
+
c.enforced &&
|
|
389
|
+
c.exact &&
|
|
390
|
+
bytes.byteLength > frameSize
|
|
391
|
+
) {
|
|
357
392
|
for (let at = 0; at < bytes.byteLength; at += frameSize) {
|
|
358
393
|
const data = bytes.subarray(at, at + frameSize);
|
|
359
394
|
const frame: PortEnvelope =
|
|
@@ -376,6 +411,7 @@ export function multiplexPort(
|
|
|
376
411
|
},
|
|
377
412
|
);
|
|
378
413
|
function release(reason: unknown): void {
|
|
414
|
+
freePartial(open.get(id));
|
|
379
415
|
open.delete(id);
|
|
380
416
|
leaks?.unregister(record);
|
|
381
417
|
// What is queued goes out now, before the close and regardless of the
|
|
@@ -384,7 +420,7 @@ export function multiplexPort(
|
|
|
384
420
|
// kept for a port nobody holds. Without a window it would all have
|
|
385
421
|
// gone out at once anyway.
|
|
386
422
|
if (c?.queue.length) {
|
|
387
|
-
for (const item of c.queue.splice(0)) item.send();
|
|
423
|
+
for (const item of c.queue.splice(0)) item.send(true);
|
|
388
424
|
c.queued = 0;
|
|
389
425
|
}
|
|
390
426
|
// Behind this port's frames, so the peer gets the whole message first.
|
|
@@ -520,16 +556,51 @@ export function multiplexPort(
|
|
|
520
556
|
|
|
521
557
|
if (envelope.type === "frame") {
|
|
522
558
|
// Only where this end advertised exact sizing — the one place a peer may
|
|
523
|
-
// frame.
|
|
524
|
-
//
|
|
559
|
+
// frame. What a partial message may hold is bounded below, so frames
|
|
560
|
+
// that never end cannot make this side buffer without limit.
|
|
525
561
|
if (!entry?.credit?.exact || !codec.decode || !(envelope.data instanceof Uint8Array)) return;
|
|
526
562
|
touch(envelope.id);
|
|
527
|
-
const
|
|
563
|
+
const c = entry.credit;
|
|
564
|
+
// What this side still owes back (delivered and unconsumed, or consumed
|
|
565
|
+
// and not yet granted) is never more than what the sender counted as
|
|
566
|
+
// outstanding. So an honest sender starts a message only when owed plus
|
|
567
|
+
// the message fits the window, or when nothing is owed at all: a message
|
|
568
|
+
// larger than the window, bounded by `maxReassembledBytes` alone.
|
|
569
|
+
const owed = c.unconsumed + c.toGrant;
|
|
570
|
+
const partial = entry.partial ?? {
|
|
571
|
+
chunks: [],
|
|
572
|
+
bytes: 0,
|
|
573
|
+
owed,
|
|
574
|
+
charged: 0,
|
|
575
|
+
...(owed > 0 ? { room: c.window - owed } : {}),
|
|
576
|
+
};
|
|
528
577
|
entry.partial = partial;
|
|
578
|
+
const size = envelope.data.byteLength;
|
|
579
|
+
// Only a message begun with nothing owed may pass the window; what it
|
|
580
|
+
// holds past it is what the cap, shared by all ports, bounds.
|
|
581
|
+
const charged = partial.room == null ? Math.max(0, partial.bytes + size - c.window) : 0;
|
|
582
|
+
const overrun =
|
|
583
|
+
partial.room != null && partial.bytes + size > partial.room
|
|
584
|
+
? `what the window left (${partial.room} of ${c.window} bytes, ${partial.owed} still owed)`
|
|
585
|
+
: reassembling - partial.charged + charged > maxReassembledBytes
|
|
586
|
+
? `maxReassembledBytes (${maxReassembledBytes} bytes past the window, across all ports)`
|
|
587
|
+
: undefined;
|
|
588
|
+
if (overrun) {
|
|
589
|
+
// A peer that frames past what it may send: hold none of it.
|
|
590
|
+
freePartial(entry);
|
|
591
|
+
dropOutgoing(envelope.id);
|
|
592
|
+
open.delete(envelope.id);
|
|
593
|
+
releaseWaiters(c);
|
|
594
|
+
entry.handle.markClosed(new RangeError(`webrun-rpc: a framed message passed ${overrun}`));
|
|
595
|
+
post({ type: "close", id: envelope.id, reason: "max-reassembled-bytes" });
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
partial.bytes += size;
|
|
599
|
+
reassembling += charged - partial.charged;
|
|
600
|
+
partial.charged = charged;
|
|
529
601
|
partial.chunks.push(envelope.data);
|
|
530
|
-
partial.bytes += envelope.data.byteLength;
|
|
531
602
|
if (!envelope.last) return;
|
|
532
|
-
entry
|
|
603
|
+
freePartial(entry);
|
|
533
604
|
const whole = new Uint8Array(partial.bytes);
|
|
534
605
|
let at = 0;
|
|
535
606
|
for (const chunk of partial.chunks) {
|
|
@@ -545,6 +616,7 @@ export function multiplexPort(
|
|
|
545
616
|
// The peer closed the port: whatever it still had to receive is moot.
|
|
546
617
|
dropOutgoing(envelope.id);
|
|
547
618
|
if (!entry) return;
|
|
619
|
+
freePartial(entry);
|
|
548
620
|
open.delete(envelope.id);
|
|
549
621
|
releaseWaiters(entry.credit);
|
|
550
622
|
entry.handle.markClosed(new Error("webrun-rpc: the peer closed the port"));
|
package/src/port-types.ts
CHANGED
|
@@ -124,6 +124,19 @@ export interface PortMuxOptions {
|
|
|
124
124
|
* whole. 16-64 KiB is a reasonable range.
|
|
125
125
|
*/
|
|
126
126
|
frameSize?: number;
|
|
127
|
+
/**
|
|
128
|
+
* Ceiling, in bytes, on what this side holds of a peer's framed messages
|
|
129
|
+
* past their ports' windows, summed over all ports (default 64 MiB).
|
|
130
|
+
* A partial message counts against its port's window as it arrives: one
|
|
131
|
+
* begun while this side still owed credit must fit what the window left,
|
|
132
|
+
* and one begun with nothing owed may pass the window, its excess charged
|
|
133
|
+
* to this cap. A frame past either limit makes the mux drop that port's
|
|
134
|
+
* partial message, close the port with a `RangeError` and send the peer a
|
|
135
|
+
* close with reason `"max-reassembled-bytes"`. A framed message more than
|
|
136
|
+
* this cap past the window never arrives, though the same message sent
|
|
137
|
+
* whole would.
|
|
138
|
+
*/
|
|
139
|
+
maxReassembledBytes?: number;
|
|
127
140
|
/**
|
|
128
141
|
* Liveness of the peer (off when unset). Every `intervalMs` the mux pings
|
|
129
142
|
* and the peer answers; anything from it counts as an answer. After `misses`
|