Class: Supabase::Realtime::Presence

Inherits:
Object
  • Object
show all
Defined in:
lib/supabase/realtime/presence.rb

Overview

Tracks presence state for one channel and implements the Phoenix Presence sync algorithm. Mirrors supabase-py’s AsyncRealtimePresence: raw ‘{ key => { “metas” => [{ “phx_ref” => …, … }] } }` wire payloads are transformed to a flat `{ key => [{ “presence_ref” => …, … }, …] }` shape before being stored or emitted, so listener callbacks receive `(key, current_presences, new_presences)` with `presence_ref` keys.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializePresence

Returns a new instance of Presence.



14
15
16
17
18
19
# File 'lib/supabase/realtime/presence.rb', line 14

def initialize
  @state = {}
  @on_sync_callbacks = []
  @on_join_callbacks = []
  @on_leave_callbacks = []
end

Instance Attribute Details

#stateObject (readonly)

Returns the value of attribute state.



12
13
14
# File 'lib/supabase/realtime/presence.rb', line 12

def state
  @state
end

Class Method Details

.transform_meta(meta) ⇒ Object



98
99
100
101
102
103
104
105
106
107
# File 'lib/supabase/realtime/presence.rb', line 98

def self.transform_meta(meta)
  meta = meta.dup
  meta.delete("phx_ref_prev")
  if meta.key?("phx_ref")
    ref = meta.delete("phx_ref")
    { "presence_ref" => ref }.merge(meta)
  else
    meta
  end
end

.transform_state(state) ⇒ Object

Convert raw Phoenix wire format ‘{ key => { “metas” => […] } }` to flat `{ key => […, …] }`. Idempotent on already transformed input.



86
87
88
89
90
91
92
93
94
95
96
# File 'lib/supabase/realtime/presence.rb', line 86

def self.transform_state(state)
  new_state = {}
  (state || {}).each do |key, presences|
    new_state[key] = if presences.is_a?(Hash) && presences.key?("metas")
                       presences["metas"].map { |meta| transform_meta(meta) }
                     else
                       Array(presences).map { |meta| transform_meta(meta) }
                     end
  end
  new_state
end

Instance Method Details

#any_callbacks?Boolean

Returns:

  • (Boolean)


79
80
81
# File 'lib/supabase/realtime/presence.rb', line 79

def any_callbacks?
  [@on_sync_callbacks, @on_join_callbacks, @on_leave_callbacks].any? { |list| !list.empty? }
end

#listObject

Flat list of every presence currently tracked.



60
61
62
# File 'lib/supabase/realtime/presence.rb', line 60

def list
  @state.values.flatten
end

#on_join(&block) ⇒ Object



69
70
71
72
# File 'lib/supabase/realtime/presence.rb', line 69

def on_join(&block)
  @on_join_callbacks << block
  self
end

#on_leave(&block) ⇒ Object



74
75
76
77
# File 'lib/supabase/realtime/presence.rb', line 74

def on_leave(&block)
  @on_leave_callbacks << block
  self
end

#on_sync(&block) ⇒ Object



64
65
66
67
# File 'lib/supabase/realtime/presence.rb', line 64

def on_sync(&block)
  @on_sync_callbacks << block
  self
end

#sync_diff(raw_diff) ⇒ Object

Subsequent presence_diff messages: apply joins/leaves to the local state. Raw input is transformed before being applied.



51
52
53
54
55
56
57
# File 'lib/supabase/realtime/presence.rb', line 51

def sync_diff(raw_diff)
  joins = self.class.transform_state(raw_diff["joins"] || {})
  leaves = self.class.transform_state(raw_diff["leaves"] || {})
  sync_diff_internal(joins, leaves)
  @on_sync_callbacks.each(&:call)
  @state
end

#sync_state(raw_state) ⇒ Object

First snapshot after joining: diff against the (possibly empty) local state and apply the joins/leaves through the same code path as ‘sync_diff`.



24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# File 'lib/supabase/realtime/presence.rb', line 24

def sync_state(raw_state)
  new_state = self.class.transform_state(raw_state)
  joins = {}
  leaves = @state.reject { |k, _| new_state.key?(k) }

  new_state.each do |key, presences|
    current = @state[key] || []

    if current.any?
      current_refs = current.map { |p| p["presence_ref"] }
      new_refs = presences.map { |p| p["presence_ref"] }
      joined_presences = presences.reject { |p| current_refs.include?(p["presence_ref"]) }
      left_presences = current.reject { |p| new_refs.include?(p["presence_ref"]) }
      joins[key] = joined_presences if joined_presences.any?
      leaves[key] = left_presences if left_presences.any?
    else
      joins[key] = presences
    end
  end

  sync_diff_internal(joins, leaves)
  @on_sync_callbacks.each(&:call)
  @state
end