Class: ConvertSdk::EventManager

Inherits:
Object
  • Object
show all
Defined in:
lib/convert_sdk/event_manager.rb

Overview

Synchronous, thread-safe pub/sub engine for SDK lifecycle events.

EventManager is the single emission point for the SDK's lifecycle signals. Consumers subscribe with #on using the cross-SDK-consistent event names (SystemEvents); the SDK's internal stages fire those events with #fire as wiring lands in later stories (Client ready in 2.5, config.updated per refresh in 2.7, bucketing in 2.11/4.1, conversion in 4.3, api.queue.released in 4.2). This story delivers the engine only.

Event names are a wire-parity surface (FR57)

Event names are byte-identical to the JS SDK's SystemEvents strings. A SystemEvents constant is its wire string (e.g. SystemEvents::READY == "ready"), so on(SystemEvents::READY) and on("ready") register under the SAME string key. Names are normalized to their string form (+#to_s+) before they touch the registry.

Synchronous firing

Events fire synchronously, in registration order, at each lifecycle stage — no event thread, no queue. A slow listener slows the SDK (documented, JS parity). The firing path never raises into its caller: a listener that raises is caught and logged, and the remaining listeners still run.

Deferred replay for late subscribers

Some events (READY, CONVERSION in JS) fire with deferred: true. The first deferred emission of an event records its {payload, err} so a listener that subscribes after the event already happened is replayed the stored value the moment it registers. This lets late subscribers observe a one-shot lifecycle signal they would otherwise have missed.

Thread safety

The listener registry and the deferred store are both guarded by @listeners_mutex. Registration mutates the registry inside the lock. Firing takes a dup snapshot of the listener list inside the lock, then iterates that snapshot OUTSIDE the lock — so a listener body (which runs unlocked) may itself call #on to register a new listener without deadlocking. The newly added listener is not invoked by the in-flight fire (it was not in the snapshot); it participates in subsequent fires.

Instance Method Summary collapse

Constructor Details

#initialize(log_manager:) ⇒ EventManager

Returns a new instance of EventManager.

Parameters:

  • log_manager (LogManager)

    sink for contained listener failures and unknown-event debug traces.



48
49
50
51
52
53
54
55
56
57
# File 'lib/convert_sdk/event_manager.rb', line 48

def initialize(log_manager:)
  @log_manager = log_manager
  # event name (String) => Array<Proc> of listeners, registration-ordered.
  @listeners = {}
  # event name (String) => { payload:, err: } recorded by the first
  # deferred fire, replayed to late subscribers.
  @deferred = {}
  # Thread safety: guarded by @listeners_mutex (both @listeners and @deferred).
  @listeners_mutex = Thread::Mutex.new
end

Instance Method Details

#fire(event, payload = nil, err = nil, deferred: false) ⇒ void

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This method returns an undefined value.

Emit an event to all currently registered listeners. Internal API.

Parameters:

  • event (String)

    a SystemEvents value or matching string.

  • payload (Object, nil) (defaults to: nil)

    delivered as the listener's first argument.

  • err (Object, nil) (defaults to: nil)

    delivered as the listener's second argument (+nil+ on normal emission).

  • deferred (Boolean) (defaults to: false)

    when true, the first such emission of this event is recorded for replay to late subscribers (see class docs).



95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/convert_sdk/event_manager.rb', line 95

def fire(event, payload = nil, err = nil, deferred: false)
  key = event.to_s
  snapshot = @listeners_mutex.synchronize do
    @deferred[key] ||= { payload: payload, err: err } if deferred
    @listeners[key]&.dup
  end

  if snapshot.nil? || snapshot.empty?
    @log_manager.debug("EventManager#fire: no listeners for '#{key}'")
    return
  end

  # Iterate the snapshot OUTSIDE the lock — listener bodies run unlocked and
  # may re-register without deadlock.
  snapshot.each { |listener| invoke(key, listener, payload, err) }
end

#on(event) {|payload, err| ... } ⇒ self

Subscribe to an event. Public API.

Accepts a SystemEvents constant (which IS its string value) or any matching string; the name is normalized to its string form so both spellings register under one key. If the event was previously fired with deferred: true, the listener is invoked immediately with the stored payload/err (deferred replay).

Parameters:

  • event (String)

    a SystemEvents value or matching string.

Yield Parameters:

  • payload (Object, nil)

    the emitted payload.

  • err (Object, nil)

    the emitted error, or nil on normal emission. Single-parameter blocks work — extra args are ignored.

Returns:

  • (self)


72
73
74
75
76
77
78
79
80
81
82
83
# File 'lib/convert_sdk/event_manager.rb', line 72

def on(event, &listener)
  return self if listener.nil?

  key = event.to_s
  deferred = @listeners_mutex.synchronize do
    (@listeners[key] ||= []) << listener
    @deferred[key]
  end
  # Replay outside the lock so the listener body may itself call #on.
  invoke(key, listener, deferred[:payload], deferred[:err]) if deferred
  self
end