Create messages, search entries and unread state atomically

Apply the Laravel implementation’s MessageWriter architecture to Rails creation: save native Action Text and attachment metadata, insert the search entry and update unread memberships in the message transaction. Keep notification jobs after commit and preserve Room#receive and existing serialized jobs.

Cover native HTML/mentions, attachment-only search, derived-write failures, outer rollback and SQL commit ordering. Updates and destruction retain their current indexing behavior.
This commit is contained in:
GPT on behalf of DHH
2026-10-08 13:22:14 +02:00
parent e9f8b7117e
commit 7331d3a946
4 changed files with 154 additions and 15 deletions
+9 -1
View File
@@ -9,7 +9,9 @@ class Message < ApplicationRecord
has_rich_text :body
before_create -> { self.client_message_id ||= Random.uuid } # Bots don't care
after_create_commit -> { room.receive(self) }
# Run after Action Text and Active Storage autosave, while creation is still atomic.
after_save :record_creation, if: :previously_new_record?
after_create_commit -> { room.push_later(self) }
scope :ordered, -> { order(:created_at) }
scope :with_creator, -> { preload(creator: :avatar_attachment) }
@@ -45,4 +47,10 @@ class Message < ApplicationRecord
Sound.find_by_name match[:name]
end
end
private
def record_creation
create_in_index
room.unread_memberships(self)
end
end
-1
View File
@@ -2,7 +2,6 @@ module Message::Searchable
extend ActiveSupport::Concern
included do
after_create_commit :create_in_index
before_update -> { @attachment_replaced = attachment_changes.key?("attachment") }
after_update_commit :update_in_index, if: :indexed_text_changed?
after_destroy_commit :remove_from_index
+13 -13
View File
@@ -78,6 +78,19 @@ class Room < ApplicationRecord
push_later(message)
end
# Rewriting every member on every message is most of what posting to a large room writes,
# so members who are unread already stay as they are. Directs keep touching them all: a
# direct's sidebar row is cached by membership and shows the room's recency.
def unread_memberships(message)
recipients = memberships.visible.disconnected.where.not(user: message.creator)
recipients = recipients.where(unread_at: nil) unless direct?
recipients.update_all(unread_at: message.created_at, updated_at: Time.current)
end
def push_later(message)
Room::PushMessageJob.perform_later(self, message)
end
def open?
is_a?(Rooms::Open)
end
@@ -103,17 +116,4 @@ class Room < ApplicationRecord
errors.add :type, "can't be changed for a direct room"
end
end
# Rewriting every member on every message is most of what posting to a large room writes,
# so members who are unread already stay as they are. Directs keep touching them all: a
# direct's sidebar row is cached by membership and shows the room's recency.
def unread_memberships(message)
recipients = memberships.visible.disconnected.where.not(user: message.creator)
recipients = recipients.where(unread_at: nil) unless direct?
recipients.update_all(unread_at: message.created_at, updated_at: Time.current)
end
def push_later(message)
Room::PushMessageJob.perform_later(self, message)
end
end