Module: Mbeditor::PresenceRegistry
- Defined in:
- app/services/mbeditor/presence_registry.rb
Overview
Who is currently connected for collaborative editing. Thread-safe and in-memory, modelled on CollaborationDocStore (Mutex + monotonic clock).
This exists because the clients cannot maintain an accurate roster between themselves. The original protocol relayed one "here"/"leave" event per participant and every client merged those into its own roster, which meant a single dropped or missed message — a peer's browser killed outright, a relay failure, or simply reloading your own page at the moment someone else closed theirs — left a participant in your roster permanently, with no expiry to reclaim it. That is not cosmetic: the roster gates collaboration, so one ghost kept persistent undo disabled and external on-disk changes suppressed for the rest of the session, for a peer who was not there.
The server already knows exactly who is subscribed — Action Cable calls
unsubscribed on a clean close and on its own connection timeout — so it is
the only party that can answer this correctly. Every broadcast now carries the
complete roster and clients replace theirs wholesale, so a dropped message
costs one stale interval rather than a permanent phantom. No TTL sweep: an
entry outlives its connection only if the process dies, which takes the
registry with it.
Constant Summary collapse
- RELAYED_FIELDS =
Fields a participant publishes about itself. Anything else in the payload is dropped — this is relayed to every other client, so the shape is a boundary.
seqis the sender's own heartbeat counter, echoed back so they can tell which broadcast their message caused — every participant's heartbeat now rebroadcasts everyone, so "my entry came back" alone does not mean "my message round-tripped", and timing against it inflates the measurement.seedis a stable number derived from the sender's persisted identity. It exists so a colour clash resolves to the same winner on both sides and stays resolved across a reload; it is deliberately a hash rather than the id itself. %w[name colour current_file rtt seq seed].freeze
Class Method Summary collapse
- .record(client_id, attrs, now: monotonic) ⇒ Object
- .remove(client_id) ⇒ Object
- .reset! ⇒ Object
-
.roster(now: monotonic) ⇒ Object
The full roster, with each entry's idle time resolved against a monotonic clock here rather than compared across machines — no clock skew to correct.
Class Method Details
.record(client_id, attrs, now: monotonic) ⇒ Object
41 42 43 44 45 46 47 48 49 50 |
# File 'app/services/mbeditor/presence_registry.rb', line 41 def record(client_id, attrs, now: monotonic) id = client_id.to_s return if id.empty? entry = attrs.slice(*RELAYED_FIELDS) MUTEX.synchronize do participants[id] = entry.merge("client_id" => id, "last_seen" => now) end nil end |
.remove(client_id) ⇒ Object
52 53 54 55 56 |
# File 'app/services/mbeditor/presence_registry.rb', line 52 def remove(client_id) id = client_id.to_s MUTEX.synchronize { participants.delete(id) } nil end |
.reset! ⇒ Object
68 69 70 71 |
# File 'app/services/mbeditor/presence_registry.rb', line 68 def reset! MUTEX.synchronize { @participants = {} } nil end |
.roster(now: monotonic) ⇒ Object
The full roster, with each entry's idle time resolved against a monotonic clock here rather than compared across machines — no clock skew to correct.
60 61 62 63 64 65 66 |
# File 'app/services/mbeditor/presence_registry.rb', line 60 def roster(now: monotonic) MUTEX.synchronize do participants.transform_values do |entry| entry.except("last_seen").merge("idle" => (now - entry["last_seen"]).round) end end end |