@mcpwarp/ws-mixer 0.6.0 → 0.7.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/CHANGELOG.md +63 -0
- package/README.md +29 -5
- package/dist/client.d.ts +4 -4
- package/dist/conn.d.ts +38 -6
- package/dist/errors.d.ts +11 -1
- package/dist/index.cjs +260 -84
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +260 -84
- package/dist/index.js.map +1 -1
- package/dist/stream.d.ts +59 -7
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -66,7 +66,7 @@ var WsMixerError = class extends Error {
|
|
|
66
66
|
/** The peer's observed WS close-frame reason, verbatim, under the same condition as `wsCode`. */
|
|
67
67
|
closeReason;
|
|
68
68
|
constructor(code, message, opts) {
|
|
69
|
-
super(message);
|
|
69
|
+
super(message, opts?.cause !== void 0 ? { cause: opts.cause } : void 0);
|
|
70
70
|
this.code = code;
|
|
71
71
|
this.fatal = opts?.fatal ?? false;
|
|
72
72
|
this.streamId = opts?.streamId;
|
|
@@ -523,7 +523,7 @@ var MixerStream = class extends Duplex {
|
|
|
523
523
|
/** Bytes queued via _write, waiting on credit; drives backpressure. */
|
|
524
524
|
writeQueue = [];
|
|
525
525
|
draining = false;
|
|
526
|
-
/** Write callbacks (_write's own terminal-error branch, or
|
|
526
|
+
/** Write callbacks (_write's own terminal-error branch, or failWrite()) whose error must wait until the read side has finished delivering buffered data and emitted 'end' -- see terminateNoThrow's doc comment. Non-null exactly while that wait is pending; flushed (and reset to null) from _destroy(), or from the 'end' listener failWrite() installs. */
|
|
527
527
|
heldWriteCallbacks = null;
|
|
528
528
|
/** Set once this stream is RESET -- by the peer (handleReset), the SDK (abort()), or the app itself (reset()); undefined until then. Set even when no `'error'` listener is attached. */
|
|
529
529
|
resetCode;
|
|
@@ -557,7 +557,7 @@ var MixerStream = class extends Duplex {
|
|
|
557
557
|
return;
|
|
558
558
|
}
|
|
559
559
|
if (this.state !== "open" && this.state !== "half_closed_remote") {
|
|
560
|
-
callback
|
|
560
|
+
this.failWrite(callback, new StreamError(ErrorCode.STREAM_CLOSED, this.id, "write on a stream that is not open for sending"));
|
|
561
561
|
return;
|
|
562
562
|
}
|
|
563
563
|
this.writeQueue.push({ chunk, callback });
|
|
@@ -595,6 +595,8 @@ var MixerStream = class extends Duplex {
|
|
|
595
595
|
try {
|
|
596
596
|
while (chunk.length > 0) {
|
|
597
597
|
const n = await this.reserveSendCredit(chunk.length);
|
|
598
|
+
const { done, error } = this.sendDone();
|
|
599
|
+
if (done) throw error;
|
|
598
600
|
const piece = chunk.subarray(0, n);
|
|
599
601
|
chunk = chunk.subarray(n);
|
|
600
602
|
await this.host.sendData(this.id, piece);
|
|
@@ -603,42 +605,66 @@ var MixerStream = class extends Duplex {
|
|
|
603
605
|
callback();
|
|
604
606
|
} catch (e) {
|
|
605
607
|
this.writeQueue.shift();
|
|
606
|
-
|
|
607
|
-
this.heldWriteCallbacks.push(callback);
|
|
608
|
-
continue;
|
|
609
|
-
}
|
|
610
|
-
if (this.state === "half_closed_remote") {
|
|
611
|
-
const failure = e;
|
|
612
|
-
if (!this.hasErrorListener()) {
|
|
613
|
-
this.on("error", NOOP_ERROR_LISTENER);
|
|
614
|
-
}
|
|
615
|
-
if (this.readableEnded) {
|
|
616
|
-
callback(failure);
|
|
617
|
-
continue;
|
|
618
|
-
}
|
|
619
|
-
this.heldWriteCallbacks = [(err) => callback(err ?? failure)];
|
|
620
|
-
this.once(
|
|
621
|
-
"end",
|
|
622
|
-
() => process.nextTick(() => {
|
|
623
|
-
if (this.destroyed || !this.heldWriteCallbacks) return;
|
|
624
|
-
if (this.state === "closed") {
|
|
625
|
-
this.destroy();
|
|
626
|
-
return;
|
|
627
|
-
}
|
|
628
|
-
const held = this.heldWriteCallbacks;
|
|
629
|
-
this.heldWriteCallbacks = null;
|
|
630
|
-
for (const cb of held) cb(this.terminalError ?? void 0);
|
|
631
|
-
})
|
|
632
|
-
);
|
|
633
|
-
continue;
|
|
634
|
-
}
|
|
635
|
-
if (!this.hasErrorListener()) {
|
|
636
|
-
this.on("error", NOOP_ERROR_LISTENER);
|
|
637
|
-
}
|
|
638
|
-
callback(e);
|
|
608
|
+
this.failWrite(callback, this.terminalError ?? e);
|
|
639
609
|
}
|
|
640
610
|
}
|
|
641
611
|
}
|
|
612
|
+
/**
|
|
613
|
+
* Settles a failed write's callback with `failure` -- now, or held until
|
|
614
|
+
* the read side can no longer be hurt by it. Calling a write callback with
|
|
615
|
+
* an error marks the READABLE side errored too (Node's own Writable/Duplex
|
|
616
|
+
* machinery), which drops whatever is still buffered and blocks 'end'.
|
|
617
|
+
*
|
|
618
|
+
* - terminateNoThrow is already holding write errors for the read side
|
|
619
|
+
* (`heldWriteCallbacks` non-null): join that hold -- see its doc comment.
|
|
620
|
+
* - The peer's CLOSE has arrived and 'end' hasn't fired yet, whatever this
|
|
621
|
+
* side's own write state (`half_closed_remote`, or `closed` after
|
|
622
|
+
* closeWrite()): the read side may still be delivering buffered data, so
|
|
623
|
+
* hold the callback until 'end', without relying on teardown to flush it
|
|
624
|
+
* -- there may be no teardown at all. A connection teardown that does
|
|
625
|
+
* come (it also reaches a stream closeWrite() retired) flushes it once
|
|
626
|
+
* nothing is left unread; so do destroy()/reset(). `failure` is the
|
|
627
|
+
* fallback for a flush with no terminalError of its own (always, on a
|
|
628
|
+
* stream CLOSE'd both ways -- see terminateNoThrow).
|
|
629
|
+
* - Otherwise (peer still sending, 'end' already fired or never will --
|
|
630
|
+
* destroyed, or already errored): fail it now.
|
|
631
|
+
*
|
|
632
|
+
* Node's Writable machinery re-emits a write callback's error as 'error',
|
|
633
|
+
* so -- same rationale as terminateNoThrow() -- the internal no-op listener
|
|
634
|
+
* is attached first if nothing else is listening: a failure is also
|
|
635
|
+
* reachable before terminateNoThrow() has run at all (the connection's
|
|
636
|
+
* writer failed this chunk's socket write, and teardown follows from the
|
|
637
|
+
* socket's own 'close'), and a dying connection must not crash a consumer
|
|
638
|
+
* that never attached its own 'error'.
|
|
639
|
+
*/
|
|
640
|
+
failWrite(callback, failure) {
|
|
641
|
+
if (this.heldWriteCallbacks) {
|
|
642
|
+
this.heldWriteCallbacks.push((err) => callback(err ?? failure));
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
if (!this.hasErrorListener()) {
|
|
646
|
+
this.on("error", NOOP_ERROR_LISTENER);
|
|
647
|
+
}
|
|
648
|
+
if (!this.remoteClosed || this.readableEnded || this.destroyed || this.errored) {
|
|
649
|
+
callback(failure);
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
this.heldWriteCallbacks = [(err) => callback(err ?? failure)];
|
|
653
|
+
this.host.pinRetired?.(this.id);
|
|
654
|
+
this.once(
|
|
655
|
+
"end",
|
|
656
|
+
() => process.nextTick(() => {
|
|
657
|
+
if (this.destroyed || !this.heldWriteCallbacks) return;
|
|
658
|
+
if (this.state === "closed") {
|
|
659
|
+
this.destroy();
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
const held = this.heldWriteCallbacks;
|
|
663
|
+
this.heldWriteCallbacks = null;
|
|
664
|
+
for (const cb of held) cb(this.terminalError ?? void 0);
|
|
665
|
+
})
|
|
666
|
+
);
|
|
667
|
+
}
|
|
642
668
|
/** Waits until send credit is available, then reserves and returns min(want, sendWindow, MAX_CHUNK). */
|
|
643
669
|
reserveSendCredit(want) {
|
|
644
670
|
return new Promise((resolve, reject) => {
|
|
@@ -751,12 +777,14 @@ var MixerStream = class extends Duplex {
|
|
|
751
777
|
* @internal
|
|
752
778
|
*/
|
|
753
779
|
handleClose() {
|
|
780
|
+
let closedNow = false;
|
|
754
781
|
switch (this.state) {
|
|
755
782
|
case "open":
|
|
756
783
|
this.state = "half_closed_remote";
|
|
757
784
|
break;
|
|
758
785
|
case "half_closed_local":
|
|
759
786
|
this.state = "closed";
|
|
787
|
+
closedNow = true;
|
|
760
788
|
this.host.retireStream(this.id);
|
|
761
789
|
break;
|
|
762
790
|
case "half_closed_remote":
|
|
@@ -766,6 +794,42 @@ var MixerStream = class extends Duplex {
|
|
|
766
794
|
}
|
|
767
795
|
this.remoteClosed = true;
|
|
768
796
|
this.maybeDeliverEOF();
|
|
797
|
+
if (closedNow) this.destroyWhenDone();
|
|
798
|
+
}
|
|
799
|
+
/**
|
|
800
|
+
* Both directions CLOSE'd cleanly (closeWrite() + the peer's CLOSE, in
|
|
801
|
+
* either order): the stream is retired, so only a connection teardown
|
|
802
|
+
* (which may never come) would otherwise reach it, and 'close' needs a
|
|
803
|
+
* destroy() on an `autoDestroy: false` Duplex. Mirrors
|
|
804
|
+
* what autoDestroy itself would do: destroy once the readable has emitted
|
|
805
|
+
* 'end' (a tick later, like failWrite()'s hold, so every 'end' listener
|
|
806
|
+
* runs first and 'end' always precedes 'close'), and, when the write side
|
|
807
|
+
* was closed by end(), once 'finish' has fired too -- destroying inside
|
|
808
|
+
* _final() would suppress 'finish' and turn finished()/pipeline() into a
|
|
809
|
+
* premature close. A readable that already errored never emits 'end', so
|
|
810
|
+
* that destroys right away. A consumer that never reads never sees 'end',
|
|
811
|
+
* and so never 'close' -- same as any other Readable.
|
|
812
|
+
*/
|
|
813
|
+
destroyWhenDone() {
|
|
814
|
+
if (this.destroyed) return;
|
|
815
|
+
if (this.errored) {
|
|
816
|
+
this.destroy();
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
if (this.writableEnded && !this.writableFinished) {
|
|
820
|
+
this.once("finish", () => this.destroyWhenDone());
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
if (this.readableEnded) {
|
|
824
|
+
this.destroy();
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
this.once(
|
|
828
|
+
"end",
|
|
829
|
+
() => process.nextTick(() => {
|
|
830
|
+
if (!this.destroyed) this.destroy();
|
|
831
|
+
})
|
|
832
|
+
);
|
|
769
833
|
}
|
|
770
834
|
// CLOSE preserves buffered data; EOF (push(null)) only follows once every
|
|
771
835
|
// buffered byte has been delivered to the application (WIRE.md section
|
|
@@ -778,7 +842,7 @@ var MixerStream = class extends Duplex {
|
|
|
778
842
|
this.push(null);
|
|
779
843
|
}
|
|
780
844
|
}
|
|
781
|
-
/** True once something is listening for `'error'`: gates attaching the internal no-op `'error'` listener (terminateNoThrow,
|
|
845
|
+
/** True once something is listening for `'error'`: gates attaching the internal no-op `'error'` listener (terminateNoThrow, failWrite). */
|
|
782
846
|
hasErrorListener() {
|
|
783
847
|
return this.listenerCount("error") > 0;
|
|
784
848
|
}
|
|
@@ -865,8 +929,8 @@ var MixerStream = class extends Duplex {
|
|
|
865
929
|
* too, permanently blocking 'end' regardless of whether an 'error'
|
|
866
930
|
* listener is attached. So instead of calling those callbacks here,
|
|
867
931
|
* `heldWriteCallbacks` (non-null for exactly this stretch) collects them
|
|
868
|
-
* -- pushed onto by
|
|
869
|
-
*
|
|
932
|
+
* -- pushed onto by failWrite() and by `_write`'s terminal-error
|
|
933
|
+
* branch -- and `_destroy()` flushes them with `err`
|
|
870
934
|
* right after `destroy()` actually runs (whether that's the 'end'-
|
|
871
935
|
* triggered `destroy()` below, or the read side never gets there because
|
|
872
936
|
* something else destroys the stream first): by then the readable side
|
|
@@ -893,9 +957,13 @@ var MixerStream = class extends Duplex {
|
|
|
893
957
|
* loop, so no separate queue-flushing step is needed here.
|
|
894
958
|
*
|
|
895
959
|
* A stream already fully, cleanly closed (both directions CLOSE'd) has
|
|
896
|
-
* already called `retireStream()
|
|
897
|
-
*
|
|
898
|
-
*
|
|
960
|
+
* already called `retireStream()`, but MixerConn keeps it for
|
|
961
|
+
* `rejectOutstanding` until it is destroyed: it lands in the branch above
|
|
962
|
+
* (`remoteClosed`), so its read side still ends cleanly, and 'close' fires
|
|
963
|
+
* once nothing is left unread. Its exchange already completed, so `err`
|
|
964
|
+
* is not adopted (no `terminalError`, no `resetCode`): a write callback it
|
|
965
|
+
* holds keeps its own failure (STREAM_CLOSED, or its socket send's error).
|
|
966
|
+
* Without a teardown, destroyWhenDone() is what ends it.
|
|
899
967
|
*
|
|
900
968
|
* `resetCode` is set here too, but only when `err` is a `StreamError`
|
|
901
969
|
* (mirrors handleReset()/applyReset(), which construct one for exactly
|
|
@@ -907,15 +975,18 @@ var MixerStream = class extends Duplex {
|
|
|
907
975
|
* otherwise wrongly discard a response that had already completed on
|
|
908
976
|
* every drain hand-over. From the stream's own perspective a `StreamError`
|
|
909
977
|
* IS a fourth way it ends in RESET, on top of the three the class doc
|
|
910
|
-
* above lists, distinct from an ordinary
|
|
911
|
-
*
|
|
912
|
-
* unset.
|
|
978
|
+
* above lists, distinct from an ordinary connection failure (always a
|
|
979
|
+
* `ConnError` by the time rejectOutstanding() hands it here), which is not
|
|
980
|
+
* RESET-shaped and leaves `resetCode` unset.
|
|
913
981
|
* @internal
|
|
914
982
|
*/
|
|
915
983
|
terminateNoThrow(err, opts) {
|
|
984
|
+
const closedCleanly = this.state === "closed" && !this.terminalError;
|
|
916
985
|
this.state = "closed";
|
|
917
|
-
|
|
918
|
-
|
|
986
|
+
if (!closedCleanly) {
|
|
987
|
+
this.terminalError = err;
|
|
988
|
+
if (err instanceof StreamError) this.resetCode = err.code;
|
|
989
|
+
}
|
|
919
990
|
const deferWriteErrors = !opts?.discardBuffered && this.remoteClosed;
|
|
920
991
|
if (deferWriteErrors) this.heldWriteCallbacks = this.heldWriteCallbacks ?? [];
|
|
921
992
|
this.wakeSendWaiters();
|
|
@@ -948,8 +1019,21 @@ var MixerStream = class extends Duplex {
|
|
|
948
1019
|
}
|
|
949
1020
|
return { done: false };
|
|
950
1021
|
}
|
|
1022
|
+
/**
|
|
1023
|
+
* True while a write callback is held back for the read side (see failWrite()).
|
|
1024
|
+
* @internal
|
|
1025
|
+
*/
|
|
1026
|
+
hasHeldWriteCallbacks() {
|
|
1027
|
+
return this.heldWriteCallbacks !== null && this.heldWriteCallbacks.length > 0;
|
|
1028
|
+
}
|
|
951
1029
|
// --- public half-close / reset API -----------------------------------------
|
|
952
|
-
/**
|
|
1030
|
+
/**
|
|
1031
|
+
* Sends CLOSE: "I will send no more DATA on this stream." end() is sugar
|
|
1032
|
+
* for this via _final. A write still waiting on send credit can never go
|
|
1033
|
+
* out now, so it is woken to fail with STREAM_CLOSED (reserveSendCredit)
|
|
1034
|
+
* instead of waiting on a WINDOW that, once this stream is retired, would
|
|
1035
|
+
* never reach it.
|
|
1036
|
+
*/
|
|
953
1037
|
closeWrite() {
|
|
954
1038
|
switch (this.state) {
|
|
955
1039
|
case "open":
|
|
@@ -964,13 +1048,17 @@ var MixerStream = class extends Duplex {
|
|
|
964
1048
|
}
|
|
965
1049
|
const closedNow = this.state === "closed";
|
|
966
1050
|
this.host.sendControlFrame(encodeClose(this.id));
|
|
967
|
-
|
|
1051
|
+
this.wakeSendWaiters();
|
|
1052
|
+
if (closedNow) {
|
|
1053
|
+
this.host.retireStream(this.id);
|
|
1054
|
+
this.destroyWhenDone();
|
|
1055
|
+
}
|
|
968
1056
|
}
|
|
969
1057
|
/**
|
|
970
1058
|
* Sends CLOSE (if not already sent) and stops delivering further reads. If
|
|
971
1059
|
* the peer has not yet half-closed its own send side, a plain closeWrite()
|
|
972
1060
|
* would leave it writing into a window nobody drains, so close() also
|
|
973
|
-
* RESETs with CANCEL in that case (mirrors go/wsmixer/stream.go Close()).
|
|
1061
|
+
* RESETs with CANCEL in that case (mirrors ws-mixer-go/wsmixer/stream.go Close()).
|
|
974
1062
|
*/
|
|
975
1063
|
close() {
|
|
976
1064
|
if (!this.remoteClosed) {
|
|
@@ -1009,7 +1097,8 @@ var MixerStream = class extends Duplex {
|
|
|
1009
1097
|
* MixerConn's dispatch loop). Identical effect to reset(), but also emits
|
|
1010
1098
|
* `'reset'` the same way handleReset() does for a peer-sent RESET: in both
|
|
1011
1099
|
* cases the stream's owner didn't choose this, so it needs to hear about
|
|
1012
|
-
* it.
|
|
1100
|
+
* it. Sends nothing and does not emit if the stream is already closed
|
|
1101
|
+
* (applyReset then only finishes a pending local destroy()).
|
|
1013
1102
|
* @internal
|
|
1014
1103
|
*/
|
|
1015
1104
|
abort(code, message = "") {
|
|
@@ -1017,10 +1106,10 @@ var MixerStream = class extends Duplex {
|
|
|
1017
1106
|
this.emit("reset", { code: this.resetCode, message: this.terminalError.message });
|
|
1018
1107
|
}
|
|
1019
1108
|
}
|
|
1020
|
-
/** Shared implementation behind reset()/abort(); returns false (
|
|
1109
|
+
/** Shared implementation behind reset()/abort(); returns false (nothing sent) if the stream was already closed. */
|
|
1021
1110
|
applyReset(code, message) {
|
|
1022
1111
|
if (this.state === "closed") {
|
|
1023
|
-
if (
|
|
1112
|
+
if (!this.destroyed) this.destroy();
|
|
1024
1113
|
return false;
|
|
1025
1114
|
}
|
|
1026
1115
|
const numericCode = typeof code === "string" ? parseErrorCode(code) : code;
|
|
@@ -1058,6 +1147,23 @@ var DELIVERY_QUEUE_MARGIN = 64;
|
|
|
1058
1147
|
function wireCloseCode(wsCode) {
|
|
1059
1148
|
return wsCode === 1e3 || wsCode >= 4e3 && wsCode <= 4999 ? wsCode : 4e3 + ErrorCode.INTERNAL_ERROR;
|
|
1060
1149
|
}
|
|
1150
|
+
var MAX_RESET_MESSAGE = 256;
|
|
1151
|
+
function asConnError(err) {
|
|
1152
|
+
if (err instanceof ConnError) return err;
|
|
1153
|
+
if (err instanceof WsMixerError) {
|
|
1154
|
+
return new ConnError(err.code, err.message, {
|
|
1155
|
+
streamId: err.streamId,
|
|
1156
|
+
fatal: err.fatal,
|
|
1157
|
+
wsCode: err.wsCode,
|
|
1158
|
+
closeReason: err.closeReason,
|
|
1159
|
+
cause: err
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
return new ConnError(ErrorCode.INTERNAL_ERROR, err.message, { cause: err });
|
|
1163
|
+
}
|
|
1164
|
+
function retiredStreamError(streamId) {
|
|
1165
|
+
return new StreamError(ErrorCode.STREAM_CLOSED, streamId, "DATA not sent: stream already closed");
|
|
1166
|
+
}
|
|
1061
1167
|
var TokenBucket = class {
|
|
1062
1168
|
constructor(capacity, ratePerSecond) {
|
|
1063
1169
|
this.capacity = capacity;
|
|
@@ -1094,6 +1200,23 @@ var MixerConn = class extends EventEmitter {
|
|
|
1094
1200
|
/** The `last_stream_id` from the most recently received `drain`; set only once draining. */
|
|
1095
1201
|
lastStreamId;
|
|
1096
1202
|
streams = /* @__PURE__ */ new Map();
|
|
1203
|
+
/**
|
|
1204
|
+
* Retired (out of `streams`: no frame or DATA is ever routed to them again)
|
|
1205
|
+
* but not yet destroyed -- in practice a stream CLOSE'd both ways, waiting
|
|
1206
|
+
* on its reader's 'end'. Tracked only so rejectOutstanding() still tears
|
|
1207
|
+
* them down; each leaves on its own 'close', and teardown clears the rest.
|
|
1208
|
+
* Weak by default: a stream never read never emits 'end', so never
|
|
1209
|
+
* 'close', and a strong ref would pin every such stream the app abandoned
|
|
1210
|
+
* for the connection's lifetime. Strong while the stream holds a write
|
|
1211
|
+
* callback (retireStream(), or pinRetired() when one is held after
|
|
1212
|
+
* retirement): the stream owns that callback, not the other way round, and
|
|
1213
|
+
* the callback is what the app is waiting on -- collecting the stream
|
|
1214
|
+
* would leave it unsettled forever. A retired stream's held callbacks are
|
|
1215
|
+
* only ever flushed by destroy(), so a strong entry leaves on 'close' and
|
|
1216
|
+
* is never unpinned.
|
|
1217
|
+
* Dead weak refs are pruned whenever the map is touched.
|
|
1218
|
+
*/
|
|
1219
|
+
retired = /* @__PURE__ */ new Map();
|
|
1097
1220
|
highestOpened = 0;
|
|
1098
1221
|
/**
|
|
1099
1222
|
* Minimal "ignore and count" counters (item 11 of the review: full
|
|
@@ -1135,13 +1258,14 @@ var MixerConn = class extends EventEmitter {
|
|
|
1135
1258
|
/**
|
|
1136
1259
|
* The `StreamHost` a `MixerStream` actually talks to: a plain object of
|
|
1137
1260
|
* bound closures, not `this` -- so `sendData`/`sendControlFrame`/
|
|
1138
|
-
* `retireStream` stay private implementation details of
|
|
1139
|
-
* of leaking onto its public (and `.d.ts`) surface (item 8).
|
|
1261
|
+
* `retireStream`/`pinRetired` stay private implementation details of
|
|
1262
|
+
* MixerConn instead of leaking onto its public (and `.d.ts`) surface (item 8).
|
|
1140
1263
|
*/
|
|
1141
1264
|
#streamHost = {
|
|
1142
1265
|
sendData: (streamId, chunk) => this.sendData(streamId, chunk),
|
|
1143
1266
|
sendControlFrame: (frame) => this.sendControlFrame(frame),
|
|
1144
|
-
retireStream: (streamId) => this.retireStream(streamId)
|
|
1267
|
+
retireStream: (streamId) => this.retireStream(streamId),
|
|
1268
|
+
pinRetired: (streamId) => this.pinRetired(streamId)
|
|
1145
1269
|
};
|
|
1146
1270
|
// --- write scheduler state ---
|
|
1147
1271
|
controlQueue = [];
|
|
@@ -1156,11 +1280,11 @@ var MixerConn = class extends EventEmitter {
|
|
|
1156
1280
|
// order, off one queue, so a slow handler cannot stall frame parsing. ---
|
|
1157
1281
|
deliveryQueue = [];
|
|
1158
1282
|
deliveryRunning = false;
|
|
1159
|
-
// --- stream-0 flood limit (item 4), mirrors go/wsmixer's Conn.stream0Bucket ---
|
|
1283
|
+
// --- stream-0 flood limit (item 4), mirrors ws-mixer-go/wsmixer's Conn.stream0Bucket ---
|
|
1160
1284
|
stream0Bucket = new TokenBucket(STREAM0_BURST, STREAM0_RATE_PER_SEC);
|
|
1161
1285
|
// --- keepalive state ---
|
|
1162
1286
|
nextPingId = 0;
|
|
1163
|
-
/** Watermark: every id below this has been acked (or pruned as stale) at least once. Mirrors go/wsmixer's Conn.lowestUnacked. */
|
|
1287
|
+
/** Watermark: every id below this has been acked (or pruned as stale) at least once. Mirrors ws-mixer-go/wsmixer's Conn.lowestUnacked. */
|
|
1164
1288
|
lowestUnacked = 0;
|
|
1165
1289
|
lastPongAt = 0;
|
|
1166
1290
|
outstandingPings = /* @__PURE__ */ new Map();
|
|
@@ -1334,7 +1458,7 @@ var MixerConn = class extends EventEmitter {
|
|
|
1334
1458
|
stream.handleClose();
|
|
1335
1459
|
break;
|
|
1336
1460
|
case FrameType.RESET:
|
|
1337
|
-
stream.handleReset(resetCode(frame), resetMessage(frame));
|
|
1461
|
+
stream.handleReset(resetCode(frame), truncateUtf8(resetMessage(frame), MAX_RESET_MESSAGE));
|
|
1338
1462
|
break;
|
|
1339
1463
|
}
|
|
1340
1464
|
}
|
|
@@ -1387,7 +1511,7 @@ var MixerConn = class extends EventEmitter {
|
|
|
1387
1511
|
// --- ordered async delivery (item 2) -------------------------------------
|
|
1388
1512
|
/**
|
|
1389
1513
|
* Queues one `'stream'`/`'app'`/`'drain'` event for in-order, async
|
|
1390
|
-
* delivery, mirroring go/wsmixer's deliveryLoop: the read/dispatch path
|
|
1514
|
+
* delivery, mirroring ws-mixer-go/wsmixer's deliveryLoop: the read/dispatch path
|
|
1391
1515
|
* above never blocks on application code, but all three still fire in
|
|
1392
1516
|
* wire order, off a single loop, one at a time. Handlers registered via
|
|
1393
1517
|
* `on('stream'|'app'|'drain', ...)` must not block for long -- a handler
|
|
@@ -1459,9 +1583,13 @@ var MixerConn = class extends EventEmitter {
|
|
|
1459
1583
|
* caught here, counted (stats().handlerErrors) and surfaced via a guarded
|
|
1460
1584
|
* `'handlerError'` emit -- never an unhandled rejection, and never fatal
|
|
1461
1585
|
* to delivery: the next queued stream/app/drain event still runs.
|
|
1586
|
+
*
|
|
1587
|
+
* Iterates rawListeners(), not listeners(): for a once() registration that
|
|
1588
|
+
* is the self-removing wrapper, so it detaches on first delivery (it
|
|
1589
|
+
* returns and throws exactly what the wrapped listener does).
|
|
1462
1590
|
*/
|
|
1463
1591
|
async emitOrdered(event, arg) {
|
|
1464
|
-
for (const listener of this.
|
|
1592
|
+
for (const listener of this.rawListeners(event)) {
|
|
1465
1593
|
try {
|
|
1466
1594
|
const result = listener.call(this, arg);
|
|
1467
1595
|
if (result instanceof Promise) await result;
|
|
@@ -1519,6 +1647,10 @@ var MixerConn = class extends EventEmitter {
|
|
|
1519
1647
|
reject(new ConnError(ErrorCode.INTERNAL_ERROR, "connection closed"));
|
|
1520
1648
|
return;
|
|
1521
1649
|
}
|
|
1650
|
+
if (!this.streams.has(streamId)) {
|
|
1651
|
+
reject(retiredStreamError(streamId));
|
|
1652
|
+
return;
|
|
1653
|
+
}
|
|
1522
1654
|
let q = this.outbox.get(streamId);
|
|
1523
1655
|
if (!q) {
|
|
1524
1656
|
q = [];
|
|
@@ -1528,12 +1660,47 @@ var MixerConn = class extends EventEmitter {
|
|
|
1528
1660
|
this.markReady(streamId);
|
|
1529
1661
|
});
|
|
1530
1662
|
}
|
|
1531
|
-
/**
|
|
1663
|
+
/**
|
|
1664
|
+
* Backs `streamHost.retireStream`: retire a fully-closed stream's id. DATA
|
|
1665
|
+
* still queued for it is rejected, never dropped (a dropped item's write
|
|
1666
|
+
* callback would hang forever) -- with the stream's own sendDone() error
|
|
1667
|
+
* (its RESET's, or STREAM_CLOSED after its CLOSE). A stream not yet
|
|
1668
|
+
* destroyed moves to `retired` until its 'close' -- strongly if it holds
|
|
1669
|
+
* a write callback, else weakly (see `retired`).
|
|
1670
|
+
*/
|
|
1532
1671
|
retireStream(streamId) {
|
|
1672
|
+
const stream = this.streams.get(streamId);
|
|
1533
1673
|
this.streams.delete(streamId);
|
|
1674
|
+
if (stream && !stream.destroyed) {
|
|
1675
|
+
this.pruneRetired();
|
|
1676
|
+
this.retired.set(streamId, stream.hasHeldWriteCallbacks() ? stream : new WeakRef(stream));
|
|
1677
|
+
stream.once("close", () => {
|
|
1678
|
+
this.retired.delete(streamId);
|
|
1679
|
+
this.pruneRetired();
|
|
1680
|
+
});
|
|
1681
|
+
}
|
|
1682
|
+
const q = this.outbox.get(streamId);
|
|
1534
1683
|
this.outbox.delete(streamId);
|
|
1684
|
+
if (q) {
|
|
1685
|
+
const err = stream?.sendDone().error ?? retiredStreamError(streamId);
|
|
1686
|
+
for (const item of q) item.reject(err);
|
|
1687
|
+
}
|
|
1535
1688
|
if (this.streams.size === 0) this.notifyIdle();
|
|
1536
1689
|
}
|
|
1690
|
+
/** Backs `streamHost.pinRetired`: a retired stream now holding a write callback is held strongly until its 'close'. */
|
|
1691
|
+
pinRetired(streamId) {
|
|
1692
|
+
const entry = this.retired.get(streamId);
|
|
1693
|
+
if (entry instanceof WeakRef) {
|
|
1694
|
+
const stream = entry.deref();
|
|
1695
|
+
if (stream) this.retired.set(streamId, stream);
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
/** Drops weak `retired` entries whose stream has been garbage-collected. */
|
|
1699
|
+
pruneRetired() {
|
|
1700
|
+
for (const [id, entry] of this.retired) {
|
|
1701
|
+
if (entry instanceof WeakRef && entry.deref() === void 0) this.retired.delete(id);
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1537
1704
|
/** Resolves every pending close()'s wait for the stream table to empty, instead of polling. */
|
|
1538
1705
|
notifyIdle() {
|
|
1539
1706
|
const waiters = this.idleWaiters;
|
|
@@ -1572,12 +1739,14 @@ var MixerConn = class extends EventEmitter {
|
|
|
1572
1739
|
if (!q || q.length === 0) continue;
|
|
1573
1740
|
const item = q.shift();
|
|
1574
1741
|
const stream = this.streams.get(id);
|
|
1575
|
-
if (stream) {
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1742
|
+
if (!stream) {
|
|
1743
|
+
item.reject(retiredStreamError(id));
|
|
1744
|
+
continue;
|
|
1745
|
+
}
|
|
1746
|
+
const { done, error } = stream.sendDone();
|
|
1747
|
+
if (done) {
|
|
1748
|
+
item.reject(error);
|
|
1749
|
+
continue;
|
|
1581
1750
|
}
|
|
1582
1751
|
if (q.length > 0) this.markReady(id);
|
|
1583
1752
|
return { streamId: id, item };
|
|
@@ -1627,20 +1796,20 @@ var MixerConn = class extends EventEmitter {
|
|
|
1627
1796
|
writeRaw(frame) {
|
|
1628
1797
|
return new Promise((resolve, reject) => {
|
|
1629
1798
|
if (this.closed) {
|
|
1630
|
-
reject(new
|
|
1799
|
+
reject(new ConnError(ErrorCode.INTERNAL_ERROR, "connection closed"));
|
|
1631
1800
|
return;
|
|
1632
1801
|
}
|
|
1633
1802
|
try {
|
|
1634
1803
|
this.ws.send(frame, (err) => {
|
|
1635
1804
|
if (err) {
|
|
1636
|
-
reject(err);
|
|
1805
|
+
reject(asConnError(err));
|
|
1637
1806
|
return;
|
|
1638
1807
|
}
|
|
1639
1808
|
this.counters.bytesOut += frame.length;
|
|
1640
1809
|
resolve();
|
|
1641
1810
|
});
|
|
1642
1811
|
} catch (e) {
|
|
1643
|
-
reject(e);
|
|
1812
|
+
reject(asConnError(e instanceof Error ? e : new Error(String(e))));
|
|
1644
1813
|
}
|
|
1645
1814
|
});
|
|
1646
1815
|
}
|
|
@@ -1668,7 +1837,7 @@ var MixerConn = class extends EventEmitter {
|
|
|
1668
1837
|
});
|
|
1669
1838
|
}
|
|
1670
1839
|
/**
|
|
1671
|
-
* Pong watermark scheme, mirroring go/wsmixer/dispatch.go's handlePong:
|
|
1840
|
+
* Pong watermark scheme, mirroring ws-mixer-go/wsmixer/dispatch.go's handlePong:
|
|
1672
1841
|
* every id below lowestUnacked has been acked (or pruned as stale) at
|
|
1673
1842
|
* least once, and no id >= nextPingId has ever been sent.
|
|
1674
1843
|
* - id >= nextPingId: never sent -> PROTOCOL_ERROR, connection-fatal.
|
|
@@ -1774,7 +1943,7 @@ var MixerConn = class extends EventEmitter {
|
|
|
1774
1943
|
/**
|
|
1775
1944
|
* The peer sent `error{code,message}`: record it and close with
|
|
1776
1945
|
* `4000 + code` immediately, without waiting for the peer to do anything
|
|
1777
|
-
* else -- mirrors go/wsmixer/dispatch.go's handlePeerError. `error` is
|
|
1946
|
+
* else -- mirrors ws-mixer-go/wsmixer/dispatch.go's handlePeerError. `error` is
|
|
1778
1947
|
* always the last message on the wire (WIRE.md section 2.7), so
|
|
1779
1948
|
* there is nothing left to negotiate.
|
|
1780
1949
|
*/
|
|
@@ -1889,19 +2058,26 @@ var MixerConn = class extends EventEmitter {
|
|
|
1889
2058
|
this.jitterTimer = null;
|
|
1890
2059
|
}
|
|
1891
2060
|
rejectOutstanding(err, streamErrorFactory) {
|
|
1892
|
-
|
|
1893
|
-
for (const item of q) item.reject(err);
|
|
1894
|
-
}
|
|
1895
|
-
this.outbox.clear();
|
|
2061
|
+
const connErr2 = asConnError(err);
|
|
1896
2062
|
for (const item of this.controlQueue.splice(0, this.controlQueue.length)) {
|
|
1897
|
-
item.reject(
|
|
2063
|
+
item.reject(connErr2);
|
|
1898
2064
|
}
|
|
1899
|
-
|
|
1900
|
-
for (const
|
|
1901
|
-
stream.
|
|
1902
|
-
|
|
1903
|
-
);
|
|
2065
|
+
const retired = [];
|
|
2066
|
+
for (const entry of this.retired.values()) {
|
|
2067
|
+
const stream = entry instanceof WeakRef ? entry.deref() : entry;
|
|
2068
|
+
if (stream) retired.push(stream);
|
|
1904
2069
|
}
|
|
2070
|
+
for (const stream of [...this.streams.values(), ...retired]) {
|
|
2071
|
+
if (stream.destroyed) continue;
|
|
2072
|
+
stream.terminateNoThrow(streamErrorFactory ? streamErrorFactory(stream.id) : connErr2);
|
|
2073
|
+
}
|
|
2074
|
+
this.retired.clear();
|
|
2075
|
+
for (const [id, q] of this.outbox) {
|
|
2076
|
+
const streamErr = this.streams.get(id)?.sendDone().error ?? connErr2;
|
|
2077
|
+
for (const item of q) item.reject(streamErr);
|
|
2078
|
+
}
|
|
2079
|
+
this.outbox.clear();
|
|
2080
|
+
this.wake();
|
|
1905
2081
|
}
|
|
1906
2082
|
isDraining() {
|
|
1907
2083
|
return this.draining;
|
|
@@ -1913,7 +2089,7 @@ var MixerConn = class extends EventEmitter {
|
|
|
1913
2089
|
|
|
1914
2090
|
// src/client.ts
|
|
1915
2091
|
var SUBPROTOCOL = "ws-mixer.v1";
|
|
1916
|
-
var SDK_VERSION = "0.
|
|
2092
|
+
var SDK_VERSION = "0.7.0";
|
|
1917
2093
|
async function resolveToken(token) {
|
|
1918
2094
|
return typeof token === "function" ? await token() : token;
|
|
1919
2095
|
}
|