Files
once-campfire/app/models/response_cache.rb
T
GPT on behalf of DHH ac73267b07 Reuse authorized read pages without stale presentation or CSRF masks
Transfer the C completed-response cache lesson into Rails, keeping authentication, room checks and cookies per request. A persistent read-only SQLite observer detects local and foreign commits and rejects racing admission. Whole-page misses render fresh to avoid stale nested fragments; message ETags reflect token-neutral presentation.
2026-10-07 20:02:20 +02:00

90 lines
2.1 KiB
Ruby

require "sqlite3"
# A read-only observer sees commits from every writer, including other runtimes.
# PRAGMA data_version can only be compared on the same persistent connection.
class ResponseCache
MAX_ENTRY_BYTES = 1.megabyte
MAX_KEY_BYTES = 2.kilobytes
def self.instance
@instance ||= new
end
def initialize
@mutex = Mutex.new
@entries = {}
@bytes = 0
end
def budget
[ ENV.fetch("CAMPFIRE_RESPONSE_CACHE_MB", "64").to_i, 0 ].max.megabytes
end
def version
@mutex.synchronize { current_version }
rescue SQLite3::Exception
clear
nil
end
def read(key, version)
@mutex.synchronize do
@entries[key]&.first if current_version == version
end
rescue SQLite3::Exception
clear
nil
end
def write(key, version, entry)
size = key.bytesize + entry[:body].bytesize + entry[:headers].sum { |name, value| name.bytesize + value.bytesize } + 256
return if key.bytesize > MAX_KEY_BYTES || size > [ budget, MAX_ENTRY_BYTES ].min
@mutex.synchronize do
return unless current_version == version
return if @entries.key?(key)
while @entries.any? && @bytes + size > budget
_, (_, removed_size) = @entries.shift
@bytes -= removed_size
end
@entries[key] = [ entry, size ]
@bytes += size
end
rescue SQLite3::Exception
clear
end
def clear
@mutex.synchronize do
@entries.clear
@bytes = 0
@observer&.close
@observer = @database = @version = nil
end
end
private
def current_version
database = File.expand_path(ActiveRecord::Base.connection_db_config.database)
if @database != database || !@observer
@entries.clear
@bytes = 0
@observer&.close
@observer = nil
@observer = SQLite3::Database.new(database, readonly: true)
@database = database
@namespace = SecureRandom.hex(16)
@version = nil
end
version = @observer.get_first_value("PRAGMA data_version")
if @version != version
@entries.clear
@bytes = 0
@version = version
end
[ @database, @namespace, @version ]
end
end