Engine Application

Socket Handlers

Three transport handlers deliver conversations. All of them do the same three things — derive a PipelineConfiguration, feed elements into PipelineService, serialize the output back — and differ only in framing and in how the pipeline key and the caller identity are obtained.

The externally visible protocol is documented in Pipeline Socket Communication and Wire Formats. This page covers the implementation.

Handler Registration

EngineConfiguration maps the WebSocket paths through a SimpleUrlHandlerMapping:

Path Handler

/socket/pipeline, /socket/pipeline/**

PipelineWebSocketHandler

/socket/twilio/**

TwilioWebSocketHandler

Upgrades use the Reactor Netty strategy with a 1 MB maximum frame size. The Socket.IO server is a separate listener on port 9011, started by PipelineSocketService.

PipelineWebSocketHandler

engine/socket/PipelineWebSocketHandler.kt.

  • The pipeline key is the remainder of the request path after /socket/pipeline/; configuration parameters come from the query string and the handshake headers.

  • Text frames are deserialized with ElementMapper using the connection’s inputFormat.

  • Binary frames are deserialized as MessagePack when inputFormat is MSGPACK; otherwise the payload is taken as raw audio and wrapped in a Binary element. This is what lets a client stream audio without base64 overhead.

  • Output is written as a binary frame for MSGPACK and as a text frame otherwise, with the connection’s protocolVersion applied.

  • AbortedException and SocketException on the input stream are treated as a closed connection and complete the stream rather than propagating — a client disappearing is normal, not an error.

  • Authentication is taken from the reactive security context populated by the security filters during the handshake.

TwilioWebSocketHandler

engine/socket/TwilioWebSocketHandler.kt.

  • The key is the last path segment, with - translated to : so a colon need not appear in the stream URL.

  • Messages are Twilio Media Streams JSON, deserialized into the sealed InputMessage hierarchy in engine/dto/twilio/ (discriminated on the event property); responses are OutputMessage subtypes.

  • The pipeline configuration is not read from the URL but constructed from the start event: μ-law encoding, the announced sample rate for input and output, continuous input mode, speech processing and caching disabled. The customParameters of the stream supply the caller number, locale, time zone, tracking ref and the first input.

  • The caller’s phone number becomes the identity: the handler synthesizes a UsernamePasswordAuthenticationToken from the qualified username derived from it, so a returning caller resolves to the same user.

  • Output mapping: a transcript command becomes a clear message (barge-in), an audio-end command becomes a mark, speech items are converted from 16-bit PCM to μ-law and sent as media, and an exit command becomes the Listening mark unless the session is ending.

  • The conversation is closed only after Twilio echoes back the final SessionEnd mark, so the caller hears the closing utterance.

See Twilio Telephony Integration for the protocol details and TwilioCallService for the outbound direction.

PipelineSocketService

engine/service/PipelineSocketService.kt — the Socket.IO side, on the server bean declared by EngineConfiguration (port 9011, 1 MB frame and content limits, worker threads from PipelineProperties.socketIOWorkerThreads).

  • One namespace, /socket/io/pipeline, with connect and disconnect listeners and two event listeners: text for string payloads and msgpack for binary payloads. The event names are the lower-cased ElementFormat values.

  • On connect, the configuration is built from the handshake query parameters (key, sessionId and the rest) and headers, and a per-client session holding a Sinks.Many<Element> is stored in a concurrent map keyed by the socket ID. This sink is what makes the conversation independent of individual events.

  • Authentication is resolved from the handshake: Basic credentials, an Authorization: Bearer header, or an authorization query parameter carrying a raw JWT, decoded with the reactive JWT decoder and mapped through AuthorityMapper.

  • Incoming payloads are deserialized per the session’s inputFormat and emitted into the sink. The literal text #end completes the sink instead of being forwarded, which ends the session gracefully.

  • A deserialization or emit failure sends an error element back to the client without necessarily disconnecting it.

  • Output is sent back on the same event name — bytes for MSGPACK, text otherwise — and the socket is disconnected once the output stream completes or a fatal error is emitted.

  • The number of entries in the session map backs the activeSessions gauge described in Observability.

Shared Serialization

All handlers use ElementMapper from common, which is the single place implementing the three wire formats: the #-prefixed text protocol, JSON with a type discriminator, and MessagePack with a wrapper object. Adding an element type therefore only requires touching the type hierarchy and, if it needs a text form, the mapper.