mirror of
https://github.com/basecamp/once-campfire.git
synced 2026-10-08 07:40:08 +09:00
7b16014f01
Room#destroy destroyed every message inside the room's own transaction, which holds SQLite's write lock until the last one: on a room with many messages, every other write in the app waited and failed. The request now takes the room away from its members and leaves the rest to Room::DestroyJob, which destroys the messages one at a time, each in its own short transaction, and then the room. An open room is closed in the request, so that someone who joins the account before the job ends isn't given it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bj8KnxpTf9sj2Ysa8aLAVa
108 lines
3.0 KiB
Ruby
108 lines
3.0 KiB
Ruby
class Room < ApplicationRecord
|
|
has_many :memberships, dependent: :delete_all do
|
|
def grant_to(users)
|
|
room = proxy_association.owner
|
|
Membership.insert_all(Array(users).collect { |user| { room_id: room.id, user_id: user.id, involvement: room.default_involvement } })
|
|
end
|
|
|
|
def revoke_from(users)
|
|
destroy_by user: users
|
|
end
|
|
|
|
def revise(granted: [], revoked: [])
|
|
transaction do
|
|
grant_to(granted) if granted.present?
|
|
revoke_from(revoked) if revoked.present?
|
|
end
|
|
end
|
|
end
|
|
|
|
has_many :users, through: :memberships
|
|
has_many :messages, dependent: :destroy
|
|
|
|
belongs_to :creator, class_name: "User", default: -> { Current.user }
|
|
|
|
validate :direct_rooms_keep_their_type, on: :update
|
|
|
|
scope :opens, -> { where(type: "Rooms::Open") }
|
|
scope :closeds, -> { where(type: "Rooms::Closed") }
|
|
scope :directs, -> { where(type: "Rooms::Direct") }
|
|
scope :without_directs, -> { where.not(type: "Rooms::Direct") }
|
|
|
|
scope :ordered, -> { order("LOWER(name)") }
|
|
|
|
class << self
|
|
def create_for(attributes, users:)
|
|
transaction do
|
|
create!(attributes).tap do |room|
|
|
room.memberships.grant_to users
|
|
end
|
|
end
|
|
end
|
|
|
|
def original
|
|
order(:created_at).first
|
|
end
|
|
end
|
|
|
|
# Takes the room away from its members at once, and leaves its messages to Room::DestroyJob. Destroying them in
|
|
# the room's own transaction held the database's write lock, and stopped every other write, until the last one.
|
|
# An open room is closed first, so that no one who joins the account before the job runs is let in.
|
|
def destroy_later
|
|
room = open? ? becomes!(Rooms::Closed) : self
|
|
|
|
transaction do
|
|
room.save!
|
|
room.memberships.delete_all
|
|
end
|
|
|
|
Room::DestroyJob.perform_later(room)
|
|
end
|
|
|
|
# Each message is destroyed in its own transaction, so other writes get through in between. Any message posted
|
|
# meanwhile goes with the room.
|
|
def destroy_one_message_at_a_time
|
|
messages.find_each(&:destroy)
|
|
destroy
|
|
end
|
|
|
|
def receive(message)
|
|
unread_memberships(message)
|
|
push_later(message)
|
|
end
|
|
|
|
def open?
|
|
is_a?(Rooms::Open)
|
|
end
|
|
|
|
def closed?
|
|
is_a?(Rooms::Closed)
|
|
end
|
|
|
|
def direct?
|
|
is_a?(Rooms::Direct)
|
|
end
|
|
|
|
def default_involvement
|
|
"mentions"
|
|
end
|
|
|
|
private
|
|
# Open and closed rooms convert into each other freely. A direct room can't become
|
|
# either: its participants agreed to a private conversation, not to one whose
|
|
# audience someone else gets to widen afterwards.
|
|
def direct_rooms_keep_their_type
|
|
if type_changed? && type_was == "Rooms::Direct"
|
|
errors.add :type, "can't be changed for a direct room"
|
|
end
|
|
end
|
|
|
|
def unread_memberships(message)
|
|
memberships.visible.disconnected.where.not(user: message.creator).update_all(unread_at: message.created_at, updated_at: Time.current)
|
|
end
|
|
|
|
def push_later(message)
|
|
Room::PushMessageJob.perform_later(self, message)
|
|
end
|
|
end
|