@statewalker/webrun-rpc 0.8.0 → 0.10.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 +156 -10
- package/dist/duplex-over-port.d.ts +3 -1
- package/dist/duplex-over-port.d.ts.map +1 -1
- package/dist/index.js +446 -95
- package/dist/multiplex-port.d.ts.map +1 -1
- package/dist/port-credit.d.ts +51 -0
- package/dist/port-credit.d.ts.map +1 -1
- package/dist/port-types.d.ts +24 -2
- package/dist/port-types.d.ts.map +1 -1
- package/dist/structured-codec.d.ts.map +1 -1
- package/package.json +5 -1
- package/src/duplex-over-port.ts +337 -40
- package/src/multiplex-port.ts +161 -16
- package/src/port-credit.ts +86 -0
- package/src/port-types.ts +19 -3
- package/src/structured-codec.ts +5 -1
package/src/multiplex-port.ts
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import { getPortCloseSignal } from "./close-signal.js";
|
|
2
2
|
import type { MessageTarget } from "./message-target.js";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
nextTask,
|
|
5
|
+
type PortCap,
|
|
6
|
+
setPortAgreement,
|
|
7
|
+
setPortEndState,
|
|
8
|
+
setPortSendState,
|
|
9
|
+
} from "./port-credit.js";
|
|
4
10
|
import type {
|
|
5
11
|
OpenPortOptions,
|
|
12
|
+
PortCodec,
|
|
6
13
|
PortEnvelope,
|
|
7
14
|
PortInfo,
|
|
8
15
|
PortMux,
|
|
@@ -26,6 +33,23 @@ const KEEPALIVE_ID = Number.MAX_SAFE_INTEGER;
|
|
|
26
33
|
/** The longest delay a timer holds; past it `setInterval` fires at once. */
|
|
27
34
|
const MAX_TIMER_MS = 2 ** 31 - 1;
|
|
28
35
|
|
|
36
|
+
/**
|
|
37
|
+
* What a windowed port offers in its `open`: every capability this end
|
|
38
|
+
* speaks. `end` only over a codec that reads it (`PortCodec.halfClose`).
|
|
39
|
+
*/
|
|
40
|
+
function ownCaps(codec: PortCodec): readonly PortCap[] {
|
|
41
|
+
return codec.halfClose ? ["pipeline", "end"] : ["pipeline"];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The capabilities in a peer's `caps` that this end speaks too, or
|
|
46
|
+
* `undefined` when the peer listed none (it is then treated as an older peer).
|
|
47
|
+
*/
|
|
48
|
+
function sharedCaps(own: readonly PortCap[], listed: unknown): Set<PortCap> | undefined {
|
|
49
|
+
if (!Array.isArray(listed)) return undefined;
|
|
50
|
+
return new Set(own.filter((cap) => listed.includes(cap)));
|
|
51
|
+
}
|
|
52
|
+
|
|
29
53
|
/** Ceiling on concurrently open virtual ports. Bounds the id table only. */
|
|
30
54
|
export const DEFAULT_MAX_PORTS = 1024;
|
|
31
55
|
|
|
@@ -125,6 +149,8 @@ export function multiplexPort(
|
|
|
125
149
|
* port wedged.
|
|
126
150
|
*/
|
|
127
151
|
exact: boolean;
|
|
152
|
+
/** What both ends speak on this port; empty until the handshake says otherwise. */
|
|
153
|
+
caps: Set<PortCap>;
|
|
128
154
|
outstanding: number;
|
|
129
155
|
queue: { send: (whole?: boolean) => void; size: number }[];
|
|
130
156
|
queued: number;
|
|
@@ -132,6 +158,18 @@ export function multiplexPort(
|
|
|
132
158
|
waiters: { below: number; resolve: () => void }[];
|
|
133
159
|
toGrant: number;
|
|
134
160
|
grantScheduled: boolean;
|
|
161
|
+
/** This side's `end` is queued: anything posted afterwards is dropped. */
|
|
162
|
+
ending: boolean;
|
|
163
|
+
/** This side's `end` has left, behind the port's frames. */
|
|
164
|
+
endSent: boolean;
|
|
165
|
+
/** The peer's `end` has arrived. */
|
|
166
|
+
peerEnded: boolean;
|
|
167
|
+
/**
|
|
168
|
+
* Resolves `whenPortEnded`, once someone waits for it. Held weakly: the
|
|
169
|
+
* waiter's continuations would otherwise keep a dropped port reachable
|
|
170
|
+
* from the mux, and `onLeak` would never see it.
|
|
171
|
+
*/
|
|
172
|
+
onPeerEnd?: WeakRef<() => void>;
|
|
135
173
|
}
|
|
136
174
|
/**
|
|
137
175
|
* What the mux keeps of a port: never the consumer's `port` object itself,
|
|
@@ -202,6 +240,8 @@ export function multiplexPort(
|
|
|
202
240
|
let muxClosed = false;
|
|
203
241
|
/** This end counts encoded bytes and reassembles frames. */
|
|
204
242
|
const exactCodec = codec.encode != null && codec.decode != null;
|
|
243
|
+
/** What this end offers on a windowed port. */
|
|
244
|
+
const caps = ownCaps(codec);
|
|
205
245
|
|
|
206
246
|
const post = (envelope: PortEnvelope, transfer?: Transferable[]): void => {
|
|
207
247
|
if (muxClosed) return;
|
|
@@ -277,6 +317,29 @@ export function multiplexPort(
|
|
|
277
317
|
}
|
|
278
318
|
};
|
|
279
319
|
|
|
320
|
+
/**
|
|
321
|
+
* Both directions have ended: free the id as a close would, with no `close`
|
|
322
|
+
* on the wire (each end frees it on its own once it has seen both ends).
|
|
323
|
+
* Aborting the close signal is how the port's consumer learns it is gone.
|
|
324
|
+
*/
|
|
325
|
+
const freeIfEnded = (id: number, c: Credit): void => {
|
|
326
|
+
const entry = open.get(id);
|
|
327
|
+
if (!c.endSent || !c.peerEnded || entry?.credit !== c) return;
|
|
328
|
+
freePartial(entry);
|
|
329
|
+
open.delete(id);
|
|
330
|
+
leaks?.unregister(entry.handle);
|
|
331
|
+
entry.handle.markClosed(new Error("webrun-rpc: the port ended in both directions"));
|
|
332
|
+
};
|
|
333
|
+
/** A peer that broke the port's protocol: hold none of its traffic, and close. */
|
|
334
|
+
const failPort = (id: number, entry: Entry, error: Error, reason: string): void => {
|
|
335
|
+
freePartial(entry);
|
|
336
|
+
dropOutgoing(id);
|
|
337
|
+
open.delete(id);
|
|
338
|
+
releaseWaiters(entry.credit);
|
|
339
|
+
entry.handle.markClosed(error);
|
|
340
|
+
post({ type: "close", id, reason });
|
|
341
|
+
};
|
|
342
|
+
|
|
280
343
|
const releaseWaiters = (c: Credit | undefined): void => {
|
|
281
344
|
for (const w of c?.waiters.splice(0) ?? []) w.resolve();
|
|
282
345
|
};
|
|
@@ -285,8 +348,10 @@ export function multiplexPort(
|
|
|
285
348
|
while (c.queue.length) {
|
|
286
349
|
// biome-ignore lint/style/noNonNullAssertion: the loop condition checks the queue is not empty.
|
|
287
350
|
const next = c.queue[0]!;
|
|
288
|
-
// A message larger than the window goes out once nothing else is
|
|
289
|
-
|
|
351
|
+
// A message larger than the window goes out once nothing else is
|
|
352
|
+
// outstanding. An `end` (size 0) costs no window and never waits for credit.
|
|
353
|
+
if (next.size > 0 && c.enforced && c.outstanding > 0 && c.outstanding + next.size > c.window)
|
|
354
|
+
break;
|
|
290
355
|
c.queue.shift();
|
|
291
356
|
c.queued -= next.size;
|
|
292
357
|
c.outstanding += next.size;
|
|
@@ -339,6 +404,7 @@ export function multiplexPort(
|
|
|
339
404
|
enforced: false,
|
|
340
405
|
opener,
|
|
341
406
|
exact,
|
|
407
|
+
caps: new Set(),
|
|
342
408
|
outstanding: 0,
|
|
343
409
|
queue: [],
|
|
344
410
|
queued: 0,
|
|
@@ -346,10 +412,15 @@ export function multiplexPort(
|
|
|
346
412
|
waiters: [],
|
|
347
413
|
toGrant: 0,
|
|
348
414
|
grantScheduled: false,
|
|
415
|
+
ending: false,
|
|
416
|
+
endSent: false,
|
|
417
|
+
peerEnded: false,
|
|
349
418
|
}
|
|
350
419
|
: undefined;
|
|
351
420
|
const self = newVirtualPort(
|
|
352
421
|
(payload, transfer) => {
|
|
422
|
+
// This side's direction has ended: nothing more goes out on it.
|
|
423
|
+
if (c?.ending) return;
|
|
353
424
|
touch(id);
|
|
354
425
|
const envelope: PortEnvelope = { type: "message", id, payload };
|
|
355
426
|
// Without a window nothing frames or waits: post at once, so the
|
|
@@ -447,6 +518,40 @@ export function multiplexPort(
|
|
|
447
518
|
else c.waiters.push({ below, resolve: () => nextTask(resolve) });
|
|
448
519
|
}),
|
|
449
520
|
});
|
|
521
|
+
setPortEndState(self.port, {
|
|
522
|
+
end: () => {
|
|
523
|
+
if (isClosed() || !c.caps.has("end")) return false;
|
|
524
|
+
if (c.ending) return true;
|
|
525
|
+
c.ending = true;
|
|
526
|
+
// Behind what this side already posted: first in the window queue
|
|
527
|
+
// (a size-0 item), then in the fair-framing ring behind its frames.
|
|
528
|
+
c.queue.push({
|
|
529
|
+
size: 0,
|
|
530
|
+
send: () =>
|
|
531
|
+
sendOnPort(id, () => {
|
|
532
|
+
// A port closed meanwhile was abandoned, not ended: its peer
|
|
533
|
+
// hears the close alone, never an end that looks like a normal finish.
|
|
534
|
+
if (open.get(id)?.credit !== c) return;
|
|
535
|
+
post({ type: "end", id });
|
|
536
|
+
c.endSent = true;
|
|
537
|
+
freeIfEnded(id, c);
|
|
538
|
+
}),
|
|
539
|
+
});
|
|
540
|
+
flush(c);
|
|
541
|
+
return true;
|
|
542
|
+
},
|
|
543
|
+
watchPeerEnd: (resolve) => {
|
|
544
|
+
if (!c.caps.has("end")) return false;
|
|
545
|
+
if (c.peerEnded) resolve();
|
|
546
|
+
else c.onPeerEnd = new WeakRef(resolve);
|
|
547
|
+
return true;
|
|
548
|
+
},
|
|
549
|
+
});
|
|
550
|
+
setPortAgreement(self.port, () => ({
|
|
551
|
+
window: c.window,
|
|
552
|
+
caps: c.caps,
|
|
553
|
+
enforced: c.enforced,
|
|
554
|
+
}));
|
|
450
555
|
}
|
|
451
556
|
const record: PortRecord = {
|
|
452
557
|
deliver: self.deliver,
|
|
@@ -494,13 +599,18 @@ export function multiplexPort(
|
|
|
494
599
|
// Acknowledge the window before `onPort` runs: what `onPort` posts then
|
|
495
600
|
// reaches the opener after the ack, so both ends count it the same way
|
|
496
601
|
// (exact bytes or the estimate). A rejection's close follows the ack.
|
|
602
|
+
// Only an opener that listed capabilities hears them answered; they
|
|
603
|
+
// hold from here for what it sends. This end's own sends still wait for
|
|
604
|
+
// the opener's answer to the ack before the window holds them: that
|
|
605
|
+
// answer is what shows this end's codec reads credit at all.
|
|
497
606
|
const credit = open.get(envelope.id)?.credit;
|
|
607
|
+
const listed = sharedCaps(caps, envelope.caps);
|
|
498
608
|
if (credit) {
|
|
499
|
-
|
|
500
|
-
credit.exact
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
);
|
|
609
|
+
const ack: PortEnvelope = credit.exact
|
|
610
|
+
? { type: "credit", id: envelope.id, bytes: 0, exact: true }
|
|
611
|
+
: { type: "credit", id: envelope.id, bytes: 0 };
|
|
612
|
+
if (listed) credit.caps = listed;
|
|
613
|
+
post(listed ? { ...ack, caps: [...listed] } : ack);
|
|
504
614
|
}
|
|
505
615
|
let accepted = false;
|
|
506
616
|
if (onPort) {
|
|
@@ -533,6 +643,7 @@ export function multiplexPort(
|
|
|
533
643
|
c.exact = false;
|
|
534
644
|
c.outstanding = 0;
|
|
535
645
|
}
|
|
646
|
+
c.caps = sharedCaps(caps, envelope.caps) ?? new Set();
|
|
536
647
|
// Show the responder its codec reads credit, so it may hold back too.
|
|
537
648
|
post({ type: "credit", id: envelope.id, bytes: 0 });
|
|
538
649
|
}
|
|
@@ -542,6 +653,19 @@ export function multiplexPort(
|
|
|
542
653
|
return;
|
|
543
654
|
}
|
|
544
655
|
|
|
656
|
+
// Nothing follows a peer's `end` on its direction but credit: an honest
|
|
657
|
+
// sender drops what is posted after it. Delivering more would hand the
|
|
658
|
+
// consumer data after it was told the input is over.
|
|
659
|
+
if ((envelope.type === "message" || envelope.type === "frame") && entry?.credit?.peerEnded) {
|
|
660
|
+
failPort(
|
|
661
|
+
envelope.id,
|
|
662
|
+
entry,
|
|
663
|
+
new Error("webrun-rpc: the peer sent on a port after its end"),
|
|
664
|
+
"protocol",
|
|
665
|
+
);
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
|
|
545
669
|
if (envelope.type === "message") {
|
|
546
670
|
// Drop, never queue. An id that was rejected, never opened, or already
|
|
547
671
|
// closed has no consumer, and holding its traffic would be exactly the
|
|
@@ -587,12 +711,12 @@ export function multiplexPort(
|
|
|
587
711
|
: undefined;
|
|
588
712
|
if (overrun) {
|
|
589
713
|
// A peer that frames past what it may send: hold none of it.
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
714
|
+
failPort(
|
|
715
|
+
envelope.id,
|
|
716
|
+
entry,
|
|
717
|
+
new RangeError(`webrun-rpc: a framed message passed ${overrun}`),
|
|
718
|
+
"max-reassembled-bytes",
|
|
719
|
+
);
|
|
596
720
|
return;
|
|
597
721
|
}
|
|
598
722
|
partial.bytes += size;
|
|
@@ -613,6 +737,27 @@ export function multiplexPort(
|
|
|
613
737
|
return;
|
|
614
738
|
}
|
|
615
739
|
|
|
740
|
+
if (envelope.type === "end") {
|
|
741
|
+
if (!entry) return;
|
|
742
|
+
const c = entry.credit;
|
|
743
|
+
// Only where `end` was agreed. Anywhere else the peer broke the protocol,
|
|
744
|
+
// as with a pipelined chunk where pipelining was not agreed.
|
|
745
|
+
if (!c?.caps.has("end")) {
|
|
746
|
+
failPort(
|
|
747
|
+
envelope.id,
|
|
748
|
+
entry,
|
|
749
|
+
new Error("webrun-rpc: the peer ended a port where end was not agreed"),
|
|
750
|
+
"protocol",
|
|
751
|
+
);
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
touch(envelope.id);
|
|
755
|
+
c.peerEnded = true;
|
|
756
|
+
c.onPeerEnd?.deref()?.();
|
|
757
|
+
freeIfEnded(envelope.id, c);
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
|
|
616
761
|
// The peer closed the port: whatever it still had to receive is moot.
|
|
617
762
|
dropOutgoing(envelope.id);
|
|
618
763
|
if (!entry) return;
|
|
@@ -724,8 +869,8 @@ export function multiplexPort(
|
|
|
724
869
|
post(
|
|
725
870
|
window
|
|
726
871
|
? exactCodec
|
|
727
|
-
? { type: "open", id, meta, window, exact: true }
|
|
728
|
-
: { type: "open", id, meta, window }
|
|
872
|
+
? { type: "open", id, meta, window, exact: true, caps: [...caps] }
|
|
873
|
+
: { type: "open", id, meta, window, caps: [...caps] }
|
|
729
874
|
: { type: "open", id, meta },
|
|
730
875
|
);
|
|
731
876
|
if (portSignal) {
|
package/src/port-credit.ts
CHANGED
|
@@ -59,3 +59,89 @@ export function portBufferedAmount(port: MessageTarget): number {
|
|
|
59
59
|
export function whenPortReady(port: MessageTarget, below = 0): Promise<void> {
|
|
60
60
|
return states.get(port)?.ready(below) ?? new Promise<void>((resolve) => nextTask(resolve));
|
|
61
61
|
}
|
|
62
|
+
|
|
63
|
+
/** A capability two ends agree on for one windowed port, in its `open` and the ack. */
|
|
64
|
+
export type PortCap = "pipeline" | "end";
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* What two ends have agreed for one windowed port so far. `caps`: what both
|
|
68
|
+
* speak, and so what the peer may send; empty until the handshake settles it
|
|
69
|
+
* (the opener learns it from the ack, the responder from the `open`).
|
|
70
|
+
* `enforced`: the window holds this end's own sends — what this end needs
|
|
71
|
+
* before it relies on the window instead of a confirmation per message.
|
|
72
|
+
*/
|
|
73
|
+
export interface PortAgreement {
|
|
74
|
+
window: number;
|
|
75
|
+
caps: ReadonlySet<PortCap>;
|
|
76
|
+
enforced: boolean;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const agreements = new WeakMap<MessageTarget, () => PortAgreement | undefined>();
|
|
80
|
+
|
|
81
|
+
/** @internal Registered by the multiplexer for each port with a window. */
|
|
82
|
+
export function setPortAgreement(port: MessageTarget, agreement: () => PortAgreement | undefined) {
|
|
83
|
+
agreements.set(port, agreement);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @internal What has been agreed with the peer on `port` so far; `undefined`
|
|
88
|
+
* on a port without a window. Not exported from the package.
|
|
89
|
+
*/
|
|
90
|
+
export function portAgreement(port: MessageTarget): PortAgreement | undefined {
|
|
91
|
+
return agreements.get(port)?.();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** @internal The half-close of one windowed port (capability `"end"`). */
|
|
95
|
+
export interface PortEndState {
|
|
96
|
+
/** Queue this side's `end`; `false` when `end` was not agreed. */
|
|
97
|
+
end(): boolean;
|
|
98
|
+
/**
|
|
99
|
+
* Calls `resolve` once the peer has ended its direction (at once if it has
|
|
100
|
+
* already); `false`, and `resolve` is never called, when `end` was not
|
|
101
|
+
* agreed. The mux holds `resolve` weakly, so a port dropped while someone
|
|
102
|
+
* waits on it can still be collected (`onLeak`).
|
|
103
|
+
*/
|
|
104
|
+
watchPeerEnd(resolve: () => void): boolean;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const ends = new WeakMap<MessageTarget, PortEndState>();
|
|
108
|
+
|
|
109
|
+
/** @internal Registered by the multiplexer for each port with a window. */
|
|
110
|
+
export function setPortEndState(port: MessageTarget, state: PortEndState): void {
|
|
111
|
+
ends.set(port, state);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* @internal End this side's direction of `port`: an `end` is queued behind
|
|
116
|
+
* everything already posted on it, and anything posted afterwards is dropped.
|
|
117
|
+
* `false`, and nothing changes, when `end` was not agreed (no window, the
|
|
118
|
+
* handshake not answered yet, or a peer that does not speak it): the caller
|
|
119
|
+
* then closes the port. Never waits on the peer. Not exported from the package.
|
|
120
|
+
*/
|
|
121
|
+
export function endPort(port: MessageTarget): boolean {
|
|
122
|
+
return ends.get(port)?.end() ?? false;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* @internal Resolves once the peer has ended its direction of `port`. It never
|
|
127
|
+
* settles otherwise: not on a port where `end` is not agreed when it is asked
|
|
128
|
+
* (no window, the handshake not answered yet, a peer or codec without it), and
|
|
129
|
+
* not when the port closes first, so a caller races it against the port's
|
|
130
|
+
* close signal. Not exported from the package.
|
|
131
|
+
*/
|
|
132
|
+
export function whenPortEnded(port: MessageTarget): Promise<void> {
|
|
133
|
+
let waiting = peerEnds.get(port);
|
|
134
|
+
if (!waiting) {
|
|
135
|
+
let resolve: () => void = () => {};
|
|
136
|
+
const promise = new Promise<void>((r) => {
|
|
137
|
+
resolve = r;
|
|
138
|
+
});
|
|
139
|
+
if (!ends.get(port)?.watchPeerEnd(resolve)) return promise;
|
|
140
|
+
// Held from the port only: what waits on it lives as long as the port.
|
|
141
|
+
waiting = { promise, resolve };
|
|
142
|
+
peerEnds.set(port, waiting);
|
|
143
|
+
}
|
|
144
|
+
return waiting.promise;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const peerEnds = new WeakMap<MessageTarget, { promise: Promise<void>; resolve: () => void }>();
|
package/src/port-types.ts
CHANGED
|
@@ -9,11 +9,18 @@ import type { MessageTarget } from "./message-target.js";
|
|
|
9
9
|
* inspects, because stream semantics belong above this layer.
|
|
10
10
|
*/
|
|
11
11
|
export type PortEnvelope =
|
|
12
|
-
|
|
12
|
+
/** `caps` (with a window): the capabilities the opener speaks on this port. */
|
|
13
|
+
| { type: "open"; id: number; meta?: unknown; window?: number; exact?: boolean; caps?: string[] }
|
|
13
14
|
| { type: "message"; id: number; payload: unknown }
|
|
14
15
|
| { type: "close"; id: number; reason?: unknown }
|
|
15
|
-
/**
|
|
16
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Per-port flow control (`window`): the receiver has consumed `bytes` more.
|
|
18
|
+
* The responder's zero-byte ack carries `caps`, the shared subset, when the
|
|
19
|
+
* opener listed some.
|
|
20
|
+
*/
|
|
21
|
+
| { type: "credit"; id: number; bytes: number; exact?: boolean; caps?: string[] }
|
|
22
|
+
/** Half-close (capability `"end"`): the sender's direction of the port has ended. */
|
|
23
|
+
| { type: "end"; id: number }
|
|
17
24
|
/** Fair framing (`frameSize`): one piece of an encoded `message` envelope. */
|
|
18
25
|
| { type: "frame"; id: number; data: Uint8Array; last?: boolean }
|
|
19
26
|
/** Keepalive (`keepalive`): the mux's own liveness probe and its answer, on an id no port takes. */
|
|
@@ -42,6 +49,15 @@ export interface PortCodec {
|
|
|
42
49
|
encode?(envelope: PortEnvelope): Uint8Array;
|
|
43
50
|
/** Optional, with `encode`: an envelope back from its bytes, or `undefined`. */
|
|
44
51
|
decode?(data: unknown): PortEnvelope | undefined;
|
|
52
|
+
/**
|
|
53
|
+
* Optional: this codec's `read` (and `decode`) accept the `end` envelope.
|
|
54
|
+
* Only then does a windowed port offer the `end` capability. A codec that
|
|
55
|
+
* dropped `end` would leave a port that ended normally in the peer's table
|
|
56
|
+
* (`ports()`) until the mux closes, with no error anywhere. A codec spread
|
|
57
|
+
* from a built-in (`{ ...structuredCodec, read }`) inherits the flag: its
|
|
58
|
+
* own `read` must then accept `end` too, or it must drop the flag.
|
|
59
|
+
*/
|
|
60
|
+
halfClose?: true;
|
|
45
61
|
}
|
|
46
62
|
|
|
47
63
|
export interface PortMuxOptions {
|
package/src/structured-codec.ts
CHANGED
|
@@ -12,7 +12,8 @@ function isEnvelope(value: unknown): value is PortEnvelope {
|
|
|
12
12
|
candidate.type === "credit" ||
|
|
13
13
|
candidate.type === "frame" ||
|
|
14
14
|
candidate.type === "ping" ||
|
|
15
|
-
candidate.type === "pong"
|
|
15
|
+
candidate.type === "pong" ||
|
|
16
|
+
candidate.type === "end"
|
|
16
17
|
);
|
|
17
18
|
}
|
|
18
19
|
|
|
@@ -26,6 +27,9 @@ function isEnvelope(value: unknown): value is PortEnvelope {
|
|
|
26
27
|
* not send anything a byte codec could not also carry.
|
|
27
28
|
*/
|
|
28
29
|
export const structuredCodec: PortCodec = {
|
|
30
|
+
// `read` accepts `end`, so windowed ports over this codec offer it.
|
|
31
|
+
halfClose: true,
|
|
32
|
+
|
|
29
33
|
post(port, envelope, transfer) {
|
|
30
34
|
// An empty transfer list is not the same as no transfer list — some
|
|
31
35
|
// implementations reject the former.
|