Transport patterns
Internal transport components: the pipe writer that streams and repairs appends, delivery through the codec, the buffer that replays history before live messages, and the watcher that reports discontinuities.
A transport is one channel and one codec. These are the parts inside it, and how they coordinate the codec, the channel and your application code.
Pipe writer
pipe() reads a source of events one at a time, encodes each one, and gives the messages to a pipe writer. The writer keeps a key table for the life of the pipe. A codec names streams by its own keys, such as a message id or a tool call id, because it cannot know the serial Ably returns for a publish:
publishunder a key publishes the message and remembers its serial.appendto a key that is not live publishes the message, waits for its serial and remembers it, so a stream's first delta opens the message.appendto a live key appends.updatereplaces the content of the message under a live key, andendsforgets a key.
The writer sends appends without waiting for them, so a response streams at the speed of the connection rather than one round trip per token. It waits for a stream's pending appends before it sends the message that ends the stream. Ably rolls up the appends a connection publishes within a short window into one delivery, and the wait keeps the end message from being delivered ahead of the stream's last text.
If an append fails, the writer carries on. When the stream ends, it repairs the message with one update that carries the full content, before the end message. History then holds the whole message, and a live client reads the missing text as the update's new part. If the repair fails, the pipe rejects with PipeFailed.
On every exit, cancellation included, the pipe flushes and repairs what it wrote and releases the source before it settles, so a cancelled pipe leaves whole messages on the channel.
Delivery
The first subscribe() registers the transport's channel listener and then attaches the channel, so no message can arrive before something receives it. Every inbound message goes through the codec:
- Each event the codec decodes becomes one delivery, with the event and the Ably message.
- A message the codec has nothing for, such as another publisher's message, a message the codec has already seen, or a deleted message, becomes one delivery with
eventset toundefined. - A message whose decode throws becomes one delivery with
eventset toundefined, and the error goes to the transport'serrorevent asSessionMessageProcessingFailed.
The application always sees the raw message and decides what to do with it. Every handler receives every delivery. A handler that throws is logged, and the other handlers still receive the delivery.
History replay
A subscription with history gets its own buffer. From the moment it subscribes, the transport puts that handler's live deliveries in the buffer instead of delivering them. It then reads history backwards from the channel's attach point, through the same codec, and gives the newest page to the replay function.
When the replay function returns, the transport sorts its deliveries by their message's creation serial, which is the order history holds them in, and delivers them. Deliveries of the same message keep the order the codec decoded them in. The transport then releases the buffer in arrival order, and the handler receives live deliveries from then on. If the replay fails, the transport still releases the buffer, so the handler goes live with a gap, and replayed rejects.
Replays run one at a time on a transport, linked behind each other. Unsubscribing during a replay stops it between pages and drops the buffer.
The history read uses the attach point as its starting place, so it accounts for every message between the history it reads and the live messages the channel delivers after the attach. A later subscription, on a channel that attached earlier, reads from that earlier attach point, so it can miss the messages in between.
Channel continuity
A continuity watcher listens to the channel's state. When the channel enters FAILED, SUSPENDED or DETACHED, or attaches again with resumed set to false, messages may have been missed, and the transport emits a discontinuity event. The application recovers by subscribing again with fromSerial from the serial of the last message it received.
The handler still receives the full text of a stream in flight during the gap. The first message Ably delivers after the channel attaches again carries the full content so far, and the decoder core passes on only the part the handler has not received.
Close
close() stops every pipe in flight, removes the channel listener, and detaches the channel if subscribe() or history() attached it. Each pipe flushes and repairs what it wrote before it rejects with OperationCancelled. The detach is best effort: a failure is logged and ignored. The Ably client stays connected, because your application owns it.
Related pages
- Wire protocol: the fields the transport writes on the channel.
- Codec architecture: how the decoder core reads appends, updates and history.
- Transport API reference: the methods these parts sit behind.