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.
Files changed (33) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +252 -0
  3. data/app/assets/javascripts/hibiki.js +231 -0
  4. data/config/importmap.rb +7 -0
  5. data/lib/generators/hibiki/rails/generator_helpers.rb +62 -0
  6. data/lib/generators/hibiki/rails/install/install_generator.rb +112 -0
  7. data/lib/generators/hibiki/rails/install/templates/channel.rb.tt +8 -0
  8. data/lib/generators/hibiki/rails/install/templates/connection.rb.tt +8 -0
  9. data/lib/generators/hibiki/rails/install/templates/hibiki_controller.js.tt +7 -0
  10. data/lib/generators/hibiki/rails/island/island_generator.rb +63 -0
  11. data/lib/generators/hibiki/rails/island/templates/channel.rb.tt +23 -0
  12. data/lib/generators/hibiki/rails/island/templates/display.html.erb.tt +5 -0
  13. data/lib/generators/hibiki/rails/island/templates/island.html.erb.tt +14 -0
  14. data/lib/generators/hibiki/rails/phlex/phlex_generator.rb +73 -0
  15. data/lib/generators/hibiki/rails/phlex/templates/channel.rb.tt +19 -0
  16. data/lib/generators/hibiki/rails/phlex/templates/component.rb.tt +29 -0
  17. data/lib/generators/hibiki/rails/phlex/templates/island_component.rb.tt +17 -0
  18. data/lib/generators/hibiki/rails/stimulus/stimulus_generator.rb +78 -0
  19. data/lib/generators/hibiki/rails/stimulus/templates/channel.rb.tt +23 -0
  20. data/lib/generators/hibiki/rails/stimulus/templates/controller.js.tt +16 -0
  21. data/lib/generators/hibiki/rails/stimulus/templates/display.html.erb.tt +5 -0
  22. data/lib/generators/hibiki/rails/stimulus/templates/island.html.erb.tt +11 -0
  23. data/lib/hibiki/rails/broadcasts.rb +52 -0
  24. data/lib/hibiki/rails/channel.rb +113 -0
  25. data/lib/hibiki/rails/debounce.rb +41 -0
  26. data/lib/hibiki/rails/engine.rb +34 -0
  27. data/lib/hibiki/rails/graph_actor.rb +60 -0
  28. data/lib/hibiki/rails/helpers.rb +50 -0
  29. data/lib/hibiki/rails/registry.rb +50 -0
  30. data/lib/hibiki/rails/version.rb +7 -0
  31. data/lib/hibiki/rails.rb +28 -0
  32. data/lib/hibiki_rails.rb +4 -0
  33. 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
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hibiki
4
+ module Rails
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -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"
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "hibiki"
4
+ require "hibiki/rails"
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: []