From 183ca834ac75dfc07fce9e4b3cfebd99bfef59b0 Mon Sep 17 00:00:00 2001 From: Carmine Paolino Date: Mon, 21 Jul 2025 11:11:08 +0200 Subject: [PATCH 1/3] Add message ordering guidance to Rails integration docs - Add Stimulus controller example for client-side message reordering - Document ActionCable ordering limitations - Mention async stack and AnyCable as alternatives - Include practical implementation code Related to #282 --- docs/guides/rails.md | 94 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/docs/guides/rails.md b/docs/guides/rails.md index c62c64241..d1f040a46 100644 --- a/docs/guides/rails.md +++ b/docs/guides/rails.md @@ -492,6 +492,100 @@ This setup allows for: 2. Background processing to prevent request timeouts 3. Automatic persistence of all messages and tool calls +### Handling Message Ordering with ActionCable + +ActionCable doesn't guarantee message order when using certain adapters. If you experience messages appearing out of order (e.g., assistant responses appearing above user messages), you have several options: + +#### Option 1: Client-Side Reordering with Stimulus + +Add a Stimulus controller that maintains correct chronological order based on timestamps: + +```javascript +// app/javascript/controllers/message_ordering_controller.js +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + static targets = ["message"] + + connect() { + this.reorderMessages() + this.observeNewMessages() + } + + observeNewMessages() { + // Watch for new messages being added to the DOM + const observer = new MutationObserver((mutations) => { + let shouldReorder = false + + mutations.forEach((mutation) => { + mutation.addedNodes.forEach((node) => { + if (node.nodeType === 1 && node.matches('[data-message-ordering-target="message"]')) { + shouldReorder = true + } + }) + }) + + if (shouldReorder) { + // Small delay to ensure all attributes are set + setTimeout(() => this.reorderMessages(), 10) + } + }) + + observer.observe(this.element, { childList: true, subtree: true }) + this.observer = observer + } + + disconnect() { + if (this.observer) { + this.observer.disconnect() + } + } + + reorderMessages() { + const messages = Array.from(this.messageTargets) + + // Sort by timestamp (created_at) + messages.sort((a, b) => { + const timeA = new Date(a.dataset.createdAt).getTime() + const timeB = new Date(b.dataset.createdAt).getTime() + return timeA - timeB + }) + + // Reorder in DOM + messages.forEach((message) => { + this.element.appendChild(message) + }) + } +} +``` + +Update your views to use the controller: + +```erb +<%# app/views/chats/show.html.erb %> + +
+ <%= render @chat.messages %> +
+ +<%# app/views/messages/_message.html.erb %> +<%= turbo_frame_tag message, + data: { + message_ordering_target: "message", + created_at: message.created_at.iso8601 + } do %> + +<% end %> +``` + +#### Option 2: Use the Async Stack + +The async Ruby stack (Falcon + async-cable + async-job) may help with message ordering in single-machine deployments. See our [Async Guide]({% link guides/async.md %}) for details. Note that this approach might not guarantee ordering in all deployment scenarios, particularly in distributed systems. + +#### Option 3: Use AnyCable + +[AnyCable](https://anycable.io) provides order guarantees at the server level, eliminating the need for client-side reordering code. + ## Customizing Models Your `Chat`, `Message`, and `ToolCall` models are standard ActiveRecord models. You can add any other associations, validations, scopes, callbacks, or methods as needed for your application logic. The `acts_as` helpers provide the core persistence bridge to RubyLLM without interfering with other model behavior. From 77f61353a0b362c1d5563ef9cc9be5e739bdcdb2 Mon Sep 17 00:00:00 2001 From: Carmine Paolino Date: Wed, 30 Jul 2025 16:01:33 +0200 Subject: [PATCH 2/3] docs: update ActionCable ordering section - Clarify ActionCable has NO ordering guarantees - Explain root cause: concurrent thread processing - Emphasize client-side reordering as universal solution - Add references from ioquatix and palkan feedback --- docs/guides/rails.md | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/docs/guides/rails.md b/docs/guides/rails.md index d1f040a46..4e8757beb 100644 --- a/docs/guides/rails.md +++ b/docs/guides/rails.md @@ -494,9 +494,9 @@ This setup allows for: ### Handling Message Ordering with ActionCable -ActionCable doesn't guarantee message order when using certain adapters. If you experience messages appearing out of order (e.g., assistant responses appearing above user messages), you have several options: +ActionCable does not guarantee message order due to its concurrent processing model. Messages are distributed to worker threads that deliver them to clients concurrently, which can cause out-of-order delivery (e.g., assistant responses appearing above user messages). Here are the recommended solutions: -#### Option 1: Client-Side Reordering with Stimulus +#### Option 1: Client-Side Reordering with Stimulus (Recommended) Add a Stimulus controller that maintains correct chronological order based on timestamps: @@ -578,13 +578,20 @@ Update your views to use the controller: <% end %> ``` -#### Option 2: Use the Async Stack +#### Option 2: Server-Side Ordering with AnyCable -The async Ruby stack (Falcon + async-cable + async-job) may help with message ordering in single-machine deployments. See our [Async Guide]({% link guides/async.md %}) for details. Note that this approach might not guarantee ordering in all deployment scenarios, particularly in distributed systems. +[AnyCable](https://anycable.io) provides order guarantees at the server level through "sticky concurrency" - ensuring messages from the same stream are processed by the same worker. This eliminates the need for client-side reordering code. -#### Option 3: Use AnyCable +#### Understanding the Root Cause -[AnyCable](https://anycable.io) provides order guarantees at the server level, eliminating the need for client-side reordering code. +As confirmed by the ActionCable maintainers, ActionCable uses a threaded executor to distribute broadcast messages, so messages are delivered to connected clients concurrently. This is by design for performance reasons. + +The most reliable solution is client-side reordering with order information in the payload. For applications requiring strict ordering guarantees, consider: +- Server-sent events (SSE) for unidirectional streaming +- WebSocket libraries with ordered stream support like [Lively](https://github.com/socketry/lively/tree/main/examples/chatbot) +- AnyCable for server-side ordering guarantees + +**Note**: Some users report better behavior with the async Ruby stack (Falcon + async-cable), but this doesn't guarantee ordering and shouldn't be relied upon as a solution. ## Customizing Models From ccb53ddbe2b7d8084b52315eaa3ac3101100b08e Mon Sep 17 00:00:00 2001 From: Carmine Paolino Date: Wed, 30 Jul 2025 16:03:41 +0200 Subject: [PATCH 3/3] docs: clarify Stimulus controller is an example Add notes that the message ordering controller is an example implementation that should be tested before production use --- docs/guides/rails.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/guides/rails.md b/docs/guides/rails.md index 4e8757beb..ee3aee5a2 100644 --- a/docs/guides/rails.md +++ b/docs/guides/rails.md @@ -498,10 +498,11 @@ ActionCable does not guarantee message order due to its concurrent processing mo #### Option 1: Client-Side Reordering with Stimulus (Recommended) -Add a Stimulus controller that maintains correct chronological order based on timestamps: +Add a Stimulus controller that maintains correct chronological order based on timestamps. This example demonstrates the concept - adapt it to your specific needs: ```javascript // app/javascript/controllers/message_ordering_controller.js +// Note: This is an example implementation. Test thoroughly before production use. import { Controller } from "@hotwired/stimulus" export default class extends Controller {