hibiki_rails 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +252 -0
- data/app/assets/javascripts/hibiki.js +231 -0
- data/config/importmap.rb +7 -0
- data/lib/generators/hibiki/rails/generator_helpers.rb +62 -0
- data/lib/generators/hibiki/rails/install/install_generator.rb +112 -0
- data/lib/generators/hibiki/rails/install/templates/channel.rb.tt +8 -0
- data/lib/generators/hibiki/rails/install/templates/connection.rb.tt +8 -0
- data/lib/generators/hibiki/rails/install/templates/hibiki_controller.js.tt +7 -0
- data/lib/generators/hibiki/rails/island/island_generator.rb +63 -0
- data/lib/generators/hibiki/rails/island/templates/channel.rb.tt +23 -0
- data/lib/generators/hibiki/rails/island/templates/display.html.erb.tt +5 -0
- data/lib/generators/hibiki/rails/island/templates/island.html.erb.tt +14 -0
- data/lib/generators/hibiki/rails/phlex/phlex_generator.rb +73 -0
- data/lib/generators/hibiki/rails/phlex/templates/channel.rb.tt +19 -0
- data/lib/generators/hibiki/rails/phlex/templates/component.rb.tt +29 -0
- data/lib/generators/hibiki/rails/phlex/templates/island_component.rb.tt +17 -0
- data/lib/generators/hibiki/rails/stimulus/stimulus_generator.rb +78 -0
- data/lib/generators/hibiki/rails/stimulus/templates/channel.rb.tt +23 -0
- data/lib/generators/hibiki/rails/stimulus/templates/controller.js.tt +16 -0
- data/lib/generators/hibiki/rails/stimulus/templates/display.html.erb.tt +5 -0
- data/lib/generators/hibiki/rails/stimulus/templates/island.html.erb.tt +11 -0
- data/lib/hibiki/rails/broadcasts.rb +52 -0
- data/lib/hibiki/rails/channel.rb +113 -0
- data/lib/hibiki/rails/debounce.rb +41 -0
- data/lib/hibiki/rails/engine.rb +34 -0
- data/lib/hibiki/rails/graph_actor.rb +60 -0
- data/lib/hibiki/rails/helpers.rb +50 -0
- data/lib/hibiki/rails/registry.rb +50 -0
- data/lib/hibiki/rails/version.rb +7 -0
- data/lib/hibiki/rails.rb +28 -0
- data/lib/hibiki_rails.rb +4 -0
- metadata +133 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hibiki
|
|
4
|
+
module Rails
|
|
5
|
+
# Funnels all access to one connection's signal graph through a single
|
|
6
|
+
# worker thread. ActionCable dispatches incoming commands on a thread
|
|
7
|
+
# pool with no per-channel ordering guarantee, while hibiki's threading
|
|
8
|
+
# model is confinement without locks — so the graph is built, mutated,
|
|
9
|
+
# and disposed on exactly one thread, and cable threads only ever
|
|
10
|
+
# enqueue closures.
|
|
11
|
+
#
|
|
12
|
+
# Thread-per-graph is deliberate while the gem incubates; a pooled
|
|
13
|
+
# executor with per-graph ordering can replace it behind
|
|
14
|
+
# Channel#build_graph_actor once real numbers ask for one.
|
|
15
|
+
class GraphActor
|
|
16
|
+
def initialize(name: "hibiki-graph", on_error: Hibiki::Rails.default_error_reporter)
|
|
17
|
+
@on_error = on_error
|
|
18
|
+
@queue = Queue.new
|
|
19
|
+
@thread = Thread.new { work }
|
|
20
|
+
@thread.name = name # keep <= 15 chars: Linux truncates pthread names
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Enqueue a closure for the worker. Returns false (rather than
|
|
24
|
+
# raising) once stopped: an action can race teardown, and a cable
|
|
25
|
+
# thread must never blow up because the user just navigated away.
|
|
26
|
+
def post(&job)
|
|
27
|
+
@queue << job
|
|
28
|
+
true
|
|
29
|
+
rescue ClosedQueueError
|
|
30
|
+
false
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Queue#close, not clear: already-posted jobs — in particular the
|
|
34
|
+
# root dispose posted from unsubscribed — drain before the worker
|
|
35
|
+
# exits. Idempotent. `wait: true` joins the worker; specs use it as
|
|
36
|
+
# a deterministic drain barrier.
|
|
37
|
+
def stop(wait: false)
|
|
38
|
+
@queue.close
|
|
39
|
+
@thread.join if wait
|
|
40
|
+
nil
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def stopped? = @queue.closed?
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
# Per-job rescue keeps the worker alive: one bad action must not take
|
|
48
|
+
# the whole graph down. StandardError only — an Interrupt should.
|
|
49
|
+
def work
|
|
50
|
+
while (job = @queue.pop)
|
|
51
|
+
begin
|
|
52
|
+
job.call
|
|
53
|
+
rescue StandardError => e
|
|
54
|
+
@on_error.call(e)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Hibiki
|
|
6
|
+
module Rails
|
|
7
|
+
# Opt-in helpers that stamp the packaged client's attribute protocol
|
|
8
|
+
# (interpreted by the vendored hibiki.js Stimulus controller). Strictly
|
|
9
|
+
# opt-in, mirroring Hibiki::DSL: include it where you want the bare
|
|
10
|
+
# names — ApplicationHelper for ERB views, individual Phlex components —
|
|
11
|
+
# the gem never includes it for you.
|
|
12
|
+
#
|
|
13
|
+
# div(**hibiki_island(CounterChannel, cid:)) do
|
|
14
|
+
# button(**on(:increment)) { "+" }
|
|
15
|
+
# button(**on(:toggle, with: { index: 3 })) { "toggle" }
|
|
16
|
+
# input(name: "step", **on(:set_step, event: :change))
|
|
17
|
+
# form(**on(:add, event: :submit)) { ... }
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# Both helpers return a `{ data: { ... } }` hash: splat it into Phlex
|
|
21
|
+
# element methods or Rails tag helpers (`tag.div(**hibiki_island(...))`).
|
|
22
|
+
# When the element needs other attributes on the same `data:` key, merge
|
|
23
|
+
# the hashes yourself (Phlex's `mix` does this).
|
|
24
|
+
#
|
|
25
|
+
# The emitted attribute names are a private contract between these
|
|
26
|
+
# helpers and the gem's JS — they version together; don't hand-write
|
|
27
|
+
# them in app code.
|
|
28
|
+
module Helpers
|
|
29
|
+
# The island root: one channel subscription per island, identified by
|
|
30
|
+
# a per-page-load cid (each tab is its own graph). `channel` is the
|
|
31
|
+
# channel class or its name as a string.
|
|
32
|
+
def hibiki_island(channel, cid:)
|
|
33
|
+
channel_name = channel.is_a?(Class) ? channel.name : channel.to_s
|
|
34
|
+
{ data: { controller: "hibiki", hibiki_channel_value: channel_name,
|
|
35
|
+
hibiki_cid_value: cid } }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Forward a DOM event on this element as a channel action. `event:`
|
|
39
|
+
# picks the DOM event (:click, :change, :submit); `with:` is a hash
|
|
40
|
+
# sent as the action's payload. The client adds event-derived data on
|
|
41
|
+
# top: a changed control contributes `{ name => value }`, a submitted
|
|
42
|
+
# form contributes its FormData (and is reset after the perform).
|
|
43
|
+
def on(action, event: :click, with: nil)
|
|
44
|
+
data = { hibiki_on: "#{event}->#{action}" }
|
|
45
|
+
data[:hibiki_with] = JSON.generate(with) unless with.nil?
|
|
46
|
+
{ data: }
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hibiki
|
|
4
|
+
module Rails
|
|
5
|
+
# Tracks every live hibiki channel in the process so the Rails
|
|
6
|
+
# reloader can tear them down: a long-lived graph holds blocks bound
|
|
7
|
+
# to the class versions it was built from, so surviving a code reload
|
|
8
|
+
# would mean effects running stale code forever.
|
|
9
|
+
#
|
|
10
|
+
# NOTE: the core's no-mutex rule is about the GRAPH (confinement, see
|
|
11
|
+
# hibiki's docs-md/threading-model.md). This is glue infrastructure
|
|
12
|
+
# touched from many cable threads; a lock is the right tool here.
|
|
13
|
+
class Registry
|
|
14
|
+
def initialize
|
|
15
|
+
@lock = Mutex.new
|
|
16
|
+
@channels = Set.new
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def register(channel)
|
|
20
|
+
@lock.synchronize { @channels << channel }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def unregister(channel)
|
|
24
|
+
@lock.synchronize { @channels.delete(channel) }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def size
|
|
28
|
+
@lock.synchronize { @channels.size }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Close each distinct cable connection, which routes every one of
|
|
32
|
+
# its channels through the normal unsubscribed path — dispose root,
|
|
33
|
+
# stop actor, unregister — so teardown has exactly ONE code path and
|
|
34
|
+
# idempotency comes free. Clients auto-reconnect, resubscribe, and
|
|
35
|
+
# #build_graph reruns on fresh class versions; graph state resets,
|
|
36
|
+
# which is inherent to a remount (LiveView does the same).
|
|
37
|
+
def dispose_all
|
|
38
|
+
channels = @lock.synchronize { @channels.to_a }
|
|
39
|
+
channels.map(&:connection).uniq.each(&:close)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
@registry = Registry.new
|
|
44
|
+
|
|
45
|
+
class << self
|
|
46
|
+
# Process-wide registry of live hibiki channels.
|
|
47
|
+
attr_reader :registry
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
data/lib/hibiki/rails.rb
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# NOTE: for everything under lib/hibiki/rails/ — inside this namespace a bare
|
|
4
|
+
# `Rails` constant resolves to Hibiki::Rails, not the framework. Always
|
|
5
|
+
# write `::Rails.` when you mean the framework.
|
|
6
|
+
module Hibiki
|
|
7
|
+
# Rails glue for hibiki: connection-scoped signal graphs over ActionCable,
|
|
8
|
+
# pushing re-rendered HTML through Turbo Streams.
|
|
9
|
+
module Rails
|
|
10
|
+
# Default per-job error sink for GraphActor: the Rails error reporter,
|
|
11
|
+
# so a raising action shows up wherever the app already sends errors.
|
|
12
|
+
# This is the outer layer of the funnel — a flush error first goes to
|
|
13
|
+
# an app-set Hibiki.error_handler if there is one, and only re-raises
|
|
14
|
+
# into the actor's per-job rescue when there isn't.
|
|
15
|
+
def self.default_error_reporter
|
|
16
|
+
->(error) { ::Rails.error.report(error, handled: true, source: "hibiki_rails") }
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
require_relative "rails/version"
|
|
22
|
+
require_relative "rails/registry"
|
|
23
|
+
require_relative "rails/graph_actor"
|
|
24
|
+
require_relative "rails/debounce"
|
|
25
|
+
require_relative "rails/broadcasts"
|
|
26
|
+
require_relative "rails/channel"
|
|
27
|
+
require_relative "rails/helpers"
|
|
28
|
+
require_relative "rails/engine"
|
data/lib/hibiki_rails.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: hibiki_rails
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- planetaska
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: actioncable
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '7.1'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '7.1'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: hibiki
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0.1'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0.1'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: railties
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '7.1'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '7.1'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: turbo-rails
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '2.0'
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '2.0'
|
|
68
|
+
description: 'Run a hibiki signal graph per ActionCable connection: build it in subscribed,
|
|
69
|
+
mutate it from channel actions (batched, confined to one worker thread), and let
|
|
70
|
+
effects push re-rendered partials to the page through Turbo Streams.'
|
|
71
|
+
email:
|
|
72
|
+
- planetaska@gmail.com
|
|
73
|
+
executables: []
|
|
74
|
+
extensions: []
|
|
75
|
+
extra_rdoc_files: []
|
|
76
|
+
files:
|
|
77
|
+
- README.md
|
|
78
|
+
- app/assets/javascripts/hibiki.js
|
|
79
|
+
- config/importmap.rb
|
|
80
|
+
- lib/generators/hibiki/rails/generator_helpers.rb
|
|
81
|
+
- lib/generators/hibiki/rails/install/install_generator.rb
|
|
82
|
+
- lib/generators/hibiki/rails/install/templates/channel.rb.tt
|
|
83
|
+
- lib/generators/hibiki/rails/install/templates/connection.rb.tt
|
|
84
|
+
- lib/generators/hibiki/rails/install/templates/hibiki_controller.js.tt
|
|
85
|
+
- lib/generators/hibiki/rails/island/island_generator.rb
|
|
86
|
+
- lib/generators/hibiki/rails/island/templates/channel.rb.tt
|
|
87
|
+
- lib/generators/hibiki/rails/island/templates/display.html.erb.tt
|
|
88
|
+
- lib/generators/hibiki/rails/island/templates/island.html.erb.tt
|
|
89
|
+
- lib/generators/hibiki/rails/phlex/phlex_generator.rb
|
|
90
|
+
- lib/generators/hibiki/rails/phlex/templates/channel.rb.tt
|
|
91
|
+
- lib/generators/hibiki/rails/phlex/templates/component.rb.tt
|
|
92
|
+
- lib/generators/hibiki/rails/phlex/templates/island_component.rb.tt
|
|
93
|
+
- lib/generators/hibiki/rails/stimulus/stimulus_generator.rb
|
|
94
|
+
- lib/generators/hibiki/rails/stimulus/templates/channel.rb.tt
|
|
95
|
+
- lib/generators/hibiki/rails/stimulus/templates/controller.js.tt
|
|
96
|
+
- lib/generators/hibiki/rails/stimulus/templates/display.html.erb.tt
|
|
97
|
+
- lib/generators/hibiki/rails/stimulus/templates/island.html.erb.tt
|
|
98
|
+
- lib/hibiki/rails.rb
|
|
99
|
+
- lib/hibiki/rails/broadcasts.rb
|
|
100
|
+
- lib/hibiki/rails/channel.rb
|
|
101
|
+
- lib/hibiki/rails/debounce.rb
|
|
102
|
+
- lib/hibiki/rails/engine.rb
|
|
103
|
+
- lib/hibiki/rails/graph_actor.rb
|
|
104
|
+
- lib/hibiki/rails/helpers.rb
|
|
105
|
+
- lib/hibiki/rails/registry.rb
|
|
106
|
+
- lib/hibiki/rails/version.rb
|
|
107
|
+
- lib/hibiki_rails.rb
|
|
108
|
+
homepage: https://github.com/planetaska/hibiki-rails
|
|
109
|
+
licenses:
|
|
110
|
+
- MIT
|
|
111
|
+
metadata:
|
|
112
|
+
homepage_uri: https://github.com/planetaska/hibiki-rails
|
|
113
|
+
source_code_uri: https://github.com/planetaska/hibiki-rails
|
|
114
|
+
rubygems_mfa_required: 'true'
|
|
115
|
+
rdoc_options: []
|
|
116
|
+
require_paths:
|
|
117
|
+
- lib
|
|
118
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
119
|
+
requirements:
|
|
120
|
+
- - ">="
|
|
121
|
+
- !ruby/object:Gem::Version
|
|
122
|
+
version: '3.4'
|
|
123
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
124
|
+
requirements:
|
|
125
|
+
- - ">="
|
|
126
|
+
- !ruby/object:Gem::Version
|
|
127
|
+
version: '0'
|
|
128
|
+
requirements: []
|
|
129
|
+
rubygems_version: 4.0.16
|
|
130
|
+
specification_version: 4
|
|
131
|
+
summary: 'Rails glue for hibiki: connection-scoped signal graphs over ActionCable
|
|
132
|
+
+ Turbo Streams.'
|
|
133
|
+
test_files: []
|