Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions docs/guides/rails.md
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,108 @@ 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 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 (Recommended)

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 {
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 %>
<!-- Add the Stimulus controller to the messages container -->
<div id="messages" data-controller="message-ordering">
<%= render @chat.messages %>
</div>

<%# app/views/messages/_message.html.erb %>
<%= turbo_frame_tag message,
data: {
message_ordering_target: "message",
created_at: message.created_at.iso8601
} do %>
<!-- message content -->
<% end %>
```

#### Option 2: Server-Side Ordering with AnyCable

[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.

#### Understanding the Root Cause

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

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.
Expand Down