Module: Hibiki::Rails::Channel

Defined in:
lib/hibiki/rails/channel.rb

Overview

Include into an ActionCable channel to host one connection-scoped signal graph:

class CounterChannel < ApplicationCable::Channel
include Hibiki::Rails::Channel

def build_graph                # runs on the graph's own thread,
  @count = Hibiki::State.new(0) # inside Hibiki.root
  Hibiki::Effect.new { ... broadcast ... }
end

def increment = @count.value += 1  # actions are plain methods
end

Lifecycle: subscribed rejects without a cid param, then builds the graph via #build_graph inside Hibiki.root on a dedicated GraphActor thread; every incoming action runs on that same thread inside one Hibiki.batch (N writes per action still mean one re-run per affected effect); unsubscribed disposes the root and stops the actor.

A channel that overrides subscribed/unsubscribed itself must call super.

NOTE: everything this module adds is private (except the #perform_action override, which ActionCable already exposes) — any other public method would be picked up by Channel#action_methods and become client-invocable.

Defined Under Namespace

Modules: ClassMethods

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.included(base) ⇒ Object



33
34
35
36
# File 'lib/hibiki/rails/channel.rb', line 33

def self.included(base)
  base.extend(ClassMethods)
  base.include(Broadcasts)
end

Instance Method Details

#perform_action(data) ⇒ Object

ActionCable's single dispatch point for incoming actions. The whole action body is posted to the graph's thread and wrapped in one batch — cable threads never touch the graph. Spike-verified: batch per action is all the coalescing the replace-partial style needs.

rescue_from handlers still run (dispatch_action applies them inside the job, now on the graph thread); what they don't handle propagates to the actor's on_error — ::Rails.error by default.

The client stamps a sequence number under the reserved hbk key to get a post-batch ack back, which is what stops its pending indicator. It cannot wait for a render instead: the core's equality gate (hibiki 0.2.0) means an ordinary action — paging to the page you are already on, a search that doesn't change the query, a destroy of a row another tab already deleted — legitimately produces zero bytes, and "clear on the next render" would hang forever. delete, not []: the key must not reach the action method's data. No hbk, no ack, so a 0.3.0 page keeps working.



76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/hibiki/rails/channel.rb', line 76

def perform_action(data)
  seq = data.delete("hbk")

  # A nil actor means the subscription was rejected or already torn
  # down; drop the action rather than blow up the cable thread.
  posted = @__hibiki_actor&.post do
    Hibiki.batch { super(data) }
  ensure
    # Hibiki.batch flushes in its own ensure, so effects queued
    # before a raise have run by the time this does — the ack is
    # genuinely post-batch. GraphActor#work rescues around the whole
    # job, so without this a raising action would hang the indicator
    # in exactly the case the user most needs it to stop.
    transmit({ ack: seq }) if seq
  end

  # post yields nil (no actor) or false (queue closed): the block
  # never ran, so neither did its ensure. `dropped` is also what lets
  # the client tell "late" from "never" — nothing is coming for this
  # seq, so it settles immediately rather than waiting out the grace
  # window.
  transmit({ ack: seq, dropped: true }) if seq && !posted
end