cable_ready 4.5.0 → 5.0.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 (78) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +2 -376
  3. data/Gemfile +4 -1
  4. data/Gemfile.lock +146 -144
  5. data/README.md +54 -20
  6. data/Rakefile +8 -8
  7. data/app/assets/javascripts/cable_ready.js +1269 -0
  8. data/app/assets/javascripts/cable_ready.umd.js +1190 -0
  9. data/app/channels/cable_ready/stream.rb +14 -0
  10. data/app/helpers/cable_ready/view_helper.rb +58 -0
  11. data/app/jobs/cable_ready/broadcast_job.rb +15 -0
  12. data/app/models/concerns/cable_ready/updatable/collection_updatable_callbacks.rb +21 -0
  13. data/app/models/concerns/cable_ready/updatable/collections_registry.rb +59 -0
  14. data/app/models/concerns/cable_ready/updatable/memory_cache_debounce_adapter.rb +24 -0
  15. data/app/models/concerns/cable_ready/updatable/model_updatable_callbacks.rb +33 -0
  16. data/app/models/concerns/cable_ready/updatable.rb +211 -0
  17. data/app/models/concerns/extend_has_many.rb +15 -0
  18. data/bin/standardize +1 -1
  19. data/cable_ready.gemspec +20 -6
  20. data/lib/cable_ready/broadcaster.rb +4 -3
  21. data/lib/cable_ready/cable_car.rb +19 -0
  22. data/lib/cable_ready/channel.rb +29 -31
  23. data/lib/cable_ready/channels.rb +4 -5
  24. data/lib/cable_ready/compoundable.rb +11 -0
  25. data/lib/cable_ready/config.rb +28 -1
  26. data/lib/cable_ready/engine.rb +59 -0
  27. data/lib/cable_ready/identifiable.rb +48 -0
  28. data/lib/cable_ready/importmap.rb +4 -0
  29. data/lib/cable_ready/installer.rb +224 -0
  30. data/lib/cable_ready/operation_builder.rb +80 -0
  31. data/lib/cable_ready/sanity_checker.rb +63 -0
  32. data/lib/cable_ready/stream_identifier.rb +13 -0
  33. data/lib/cable_ready/version.rb +1 -1
  34. data/lib/cable_ready.rb +23 -10
  35. data/lib/cable_ready_helper.rb +13 -0
  36. data/lib/generators/cable_ready/channel_generator.rb +110 -0
  37. data/lib/generators/cable_ready/templates/app/javascript/channels/consumer.js.tt +6 -0
  38. data/lib/generators/cable_ready/templates/app/javascript/channels/index.js.esbuild.tt +4 -0
  39. data/lib/generators/cable_ready/templates/app/javascript/channels/index.js.importmap.tt +2 -0
  40. data/lib/generators/cable_ready/templates/app/javascript/channels/index.js.shakapacker.tt +5 -0
  41. data/lib/generators/cable_ready/templates/app/javascript/channels/index.js.vite.tt +1 -0
  42. data/lib/generators/cable_ready/templates/app/javascript/channels/index.js.webpacker.tt +5 -0
  43. data/lib/generators/cable_ready/templates/app/javascript/config/cable_ready.js.tt +4 -0
  44. data/lib/generators/cable_ready/templates/app/javascript/config/index.js.tt +1 -0
  45. data/lib/generators/cable_ready/templates/app/javascript/config/mrujs.js.tt +9 -0
  46. data/lib/generators/cable_ready/templates/app/javascript/controllers/%file_name%_controller.js.tt +38 -0
  47. data/lib/generators/cable_ready/templates/app/javascript/controllers/application.js.tt +11 -0
  48. data/lib/generators/cable_ready/templates/app/javascript/controllers/index.js.esbuild.tt +7 -0
  49. data/lib/generators/cable_ready/templates/app/javascript/controllers/index.js.importmap.tt +5 -0
  50. data/lib/generators/cable_ready/templates/app/javascript/controllers/index.js.shakapacker.tt +5 -0
  51. data/lib/generators/cable_ready/templates/app/javascript/controllers/index.js.vite.tt +5 -0
  52. data/lib/generators/cable_ready/templates/app/javascript/controllers/index.js.webpacker.tt +5 -0
  53. data/lib/generators/cable_ready/templates/config/initializers/cable_ready.rb +27 -0
  54. data/lib/generators/cable_ready/templates/esbuild.config.mjs.tt +94 -0
  55. data/lib/install/action_cable.rb +144 -0
  56. data/lib/install/broadcaster.rb +109 -0
  57. data/lib/install/bundle.rb +54 -0
  58. data/lib/install/compression.rb +51 -0
  59. data/lib/install/config.rb +39 -0
  60. data/lib/install/development.rb +34 -0
  61. data/lib/install/esbuild.rb +101 -0
  62. data/lib/install/importmap.rb +96 -0
  63. data/lib/install/initializers.rb +15 -0
  64. data/lib/install/mrujs.rb +121 -0
  65. data/lib/install/npm_packages.rb +13 -0
  66. data/lib/install/shakapacker.rb +65 -0
  67. data/lib/install/spring.rb +54 -0
  68. data/lib/install/updatable.rb +34 -0
  69. data/lib/install/vite.rb +66 -0
  70. data/lib/install/webpacker.rb +93 -0
  71. data/lib/install/yarn.rb +56 -0
  72. data/lib/tasks/cable_ready/cable_ready.rake +247 -0
  73. data/package.json +42 -13
  74. data/rollup.config.mjs +57 -0
  75. data/web-test-runner.config.mjs +12 -0
  76. data/yarn.lock +3252 -327
  77. metadata +138 -9
  78. data/tags +0 -62
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CableReady::ChannelGenerator < Rails::Generators::NamedBase
4
+ source_root File.expand_path("templates", __dir__)
5
+
6
+ class_option :stream_from, type: :string
7
+ class_option :stream_for, type: :string
8
+ class_option :stimulus, type: :boolean
9
+
10
+ def destroy_not_supported
11
+ if behavior == :revoke
12
+ puts "Sorry, we don't support destroying generated channels.\nDelete the Action Cable channel class, as well as any corresponding JavaScript classes."
13
+ exit
14
+ end
15
+ end
16
+
17
+ def check_options
18
+ if options.key?(:stream_from) && options.key?(:stream_for)
19
+ puts "Can't specify --stream-from and --stream-for at the same time"
20
+ exit
21
+ end
22
+ end
23
+
24
+ def create_channel
25
+ generate "channel", file_name, "--skip"
26
+ end
27
+
28
+ def enhance_channels
29
+ @entrypoint = [
30
+ "app/javascript",
31
+ "app/frontend"
32
+ ].find { |path| File.exist?(Rails.root.join(path)) } || "app/javascript"
33
+ puts "Where do JavaScript files live in your app? Our best guess is: \e[1m#{@entrypoint}\e[22m 🤔"
34
+ puts "Press enter to accept this, or type a different path."
35
+ print "> "
36
+ input = Rails.env.test? ? "tmp/app/javascript" : $stdin.gets.chomp
37
+ @entrypoint = input unless input.blank?
38
+ @js_channel = "#{@entrypoint}/channels/#{file_name}_channel.js"
39
+
40
+ if using_broadcast_to?
41
+ if using_stimulus?
42
+ template("#{@entrypoint}/controllers/%file_name%_controller.js")
43
+ Rails.root.join(@js_channel).delete
44
+ else
45
+ gsub_file "app/channels/#{file_name}_channel.rb", /# stream_from.*\n/, "stream_for #{resource}.find(params[:id])\n", verbose: false
46
+ gsub_file @js_channel, /"#{resource}Channel"/, verbose: false do
47
+ <<-JS
48
+
49
+ {
50
+ channel: "#{resource}Channel",
51
+ id: 1
52
+ }
53
+ JS
54
+ end
55
+ doctor_javascript_channel_class
56
+ puts "\nDon't forget to update the id in the channel subscription: #{@js_channel}\nIt's currently set to 1; you'll want to change that to a dynamic value based on something in your DOM."
57
+ end
58
+ else
59
+ gsub_file "app/channels/#{file_name}_channel.rb", /# stream_from.*\n/, "stream_from \"#{identifier}\"\n", verbose: false
60
+ doctor_javascript_channel_class
61
+ end
62
+ end
63
+
64
+ private
65
+
66
+ def doctor_javascript_channel_class
67
+ prepend_to_file @js_channel, "import CableReady from 'cable_ready'\n", verbose: false
68
+ inject_into_file @js_channel, after: "// Called when there's incoming data on the websocket for this channel\n", verbose: false do
69
+ <<-JS
70
+ if (data.cableReady) CableReady.perform(data.operations)
71
+ JS
72
+ end
73
+ end
74
+
75
+ def option_given?
76
+ options.key?(:stream_from) || options.key?(:stream_for)
77
+ end
78
+
79
+ def using_broadcast_to?
80
+ @using_broadcast_to ||= option_given? ? options.key?(:stream_for) : yes?("Are you streaming to a resource using broadcast_to? (y/N)")
81
+ end
82
+
83
+ def using_stimulus?
84
+ @using_stimulus ||= options.fetch(:stimulus) {
85
+ yes?("Are you going to use a Stimulus controller to subscribe to this channel? (y/N)")
86
+ }
87
+ end
88
+
89
+ def resource
90
+ return @resource if @resource
91
+
92
+ stream_for = options.fetch(:stream_for) {
93
+ ask("Which resource are you streaming for?", default: class_name)
94
+ }
95
+
96
+ stream_for = file_name if stream_for == "stream_for"
97
+ @resource = stream_for.camelize
98
+ end
99
+
100
+ def identifier
101
+ return @identifier if @identifier
102
+
103
+ stream_from = options.fetch(:stream_from) {
104
+ ask("What is the stream identifier that goes into stream_from?", default: file_name)
105
+ }
106
+
107
+ stream_from = file_name if stream_from == "stream_from"
108
+ @identifier = stream_from.underscore
109
+ end
110
+ end
@@ -0,0 +1,6 @@
1
+ // Action Cable provides the framework to deal with WebSockets in Rails.
2
+ // You can generate new channels where WebSocket features live using the `bin/rails generate channel` command.
3
+
4
+ import { createConsumer } from '@rails/actioncable'
5
+
6
+ export default createConsumer()
@@ -0,0 +1,4 @@
1
+ // Load all the channels within this directory and all subdirectories.
2
+ // Channel files must be named *_channel.js.
3
+
4
+ import './**/*_channel.js'
@@ -0,0 +1,2 @@
1
+ // app/channels/index.js
2
+ // Importmaps don't require anything here
@@ -0,0 +1,5 @@
1
+ // Load all the channels within this directory and all subdirectories.
2
+ // Channel files must be named *_channel.js.
3
+
4
+ const channels = require.context('.', true, /_channel\.js$/)
5
+ channels.keys().forEach(channels)
@@ -0,0 +1 @@
1
+ const channels = import.meta.globEager('./**/*_channel.js')
@@ -0,0 +1,5 @@
1
+ // Load all the channels within this directory and all subdirectories.
2
+ // Channel files must be named *_channel.js.
3
+
4
+ const channels = require.context('.', true, /_channel\.js$/)
5
+ channels.keys().forEach(channels)
@@ -0,0 +1,4 @@
1
+ import consumer from '../channels/consumer'
2
+ import CableReady from 'cable_ready'
3
+
4
+ CableReady.initialize({ consumer })
@@ -0,0 +1 @@
1
+ import './cable_ready'
@@ -0,0 +1,9 @@
1
+ import CableReady from "cable_ready"
2
+ import mrujs from "mrujs"
3
+ import { CableCar } from "mrujs/plugins"
4
+
5
+ mrujs.start({
6
+ plugins: [
7
+ new CableCar(CableReady)
8
+ ]
9
+ })
@@ -0,0 +1,38 @@
1
+ import { Controller } from '@hotwired/stimulus'
2
+ import CableReady from 'cable_ready'
3
+
4
+ export default class extends Controller {
5
+ static values = { id: Number }
6
+
7
+ connect () {
8
+ if (this.preview) return
9
+ if (this.application.consumer) {
10
+ this.channel = this.application.consumer.subscriptions.create(
11
+ {
12
+ channel: '<%= class_name %>Channel',
13
+ id: this.idValue
14
+ },
15
+ {
16
+ received (data) {
17
+ if (data.cableReady) CableReady.perform(data.operations)
18
+ }
19
+ }
20
+ )
21
+ } else {
22
+ console.error(
23
+ `The "<%= class_name.underscore.dasherize %>" Stimulus controller requires an Action Cable consumer.\nPlease set 'application.consumer = consumer' in your application.js.`
24
+ )
25
+ }
26
+ }
27
+
28
+ disconnect () {
29
+ this.channel.unsubscribe()
30
+ }
31
+
32
+ get preview () {
33
+ return (
34
+ document.documentElement.hasAttribute('data-turbolinks-preview') ||
35
+ document.documentElement.hasAttribute('data-turbo-preview')
36
+ )
37
+ }
38
+ }
@@ -0,0 +1,11 @@
1
+ import { Application } from "@hotwired/stimulus"
2
+ import consumer from "../channels/consumer"
3
+
4
+ const application = Application.start()
5
+
6
+ // Configure Stimulus development experience
7
+ application.debug = false
8
+ application.consumer = consumer
9
+ window.Stimulus = application
10
+
11
+ export { application }
@@ -0,0 +1,7 @@
1
+ import { application } from "./application"
2
+
3
+ import controllers from "./**/*_controller.js"
4
+
5
+ controllers.forEach((controller) => {
6
+ application.register(controller.name, controller.module.default)
7
+ })
@@ -0,0 +1,5 @@
1
+ import { application } from "./application"
2
+
3
+ // Eager load all controllers defined in the import map under controllers/**/*_controller
4
+ import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading"
5
+ eagerLoadControllersFrom("controllers", application)
@@ -0,0 +1,5 @@
1
+ import { application } from "./application"
2
+ import { definitionsFromContext } from "@hotwired/stimulus-webpack-helpers"
3
+
4
+ const controllers = definitionsFromContext(require.context("controllers", true, /_controller\.js$/))
5
+ application.load(controllers)
@@ -0,0 +1,5 @@
1
+ import { application } from "./application"
2
+ import { registerControllers } from "stimulus-vite-helpers"
3
+
4
+ const controllers = import.meta.globEager("./**/*_controller.js");
5
+ registerControllers(application, controllers)
@@ -0,0 +1,5 @@
1
+ import { application } from "./application"
2
+ import { definitionsFromContext } from "@hotwired/stimulus-webpack-helpers"
3
+
4
+ const controllers = definitionsFromContext(require.context("controllers", true, /_controller\.js$/))
5
+ application.load(controllers)
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ CableReady.configure do |config|
4
+ # Enable/disable exiting / warning when the sanity checks fail options:
5
+ # `:exit` or `:warn` or `:ignore`
6
+ #
7
+ # config.on_failed_sanity_checks = :exit
8
+
9
+ # Enable/disable assets compilation
10
+ # `true` or `false`
11
+ #
12
+ # config.precompile_assets = true
13
+
14
+ # Define your own custom operations
15
+ # https://cableready.stimulusreflex.com/customization#custom-operations
16
+ #
17
+ # config.add_operation_name :jazz_hands
18
+
19
+ # Change the default Active Job queue used for broadcast_later and broadcast_later_to
20
+ #
21
+ # config.broadcast_job_queue = :default
22
+
23
+ # Specify a default debounce time for CableReady::Updatable callbacks
24
+ # Doing so is a best practice to avoid heavy ActionCable traffic
25
+ #
26
+ # config.updatable_debounce_time = 0.1.seconds
27
+ end
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Esbuild is configured with 3 modes:
4
+ //
5
+ // `yarn build` - Build JavaScript and exit
6
+ // `yarn build --watch` - Rebuild JavaScript on change
7
+ // `yarn build --reload` - Reloads page when views, JavaScript, or stylesheets change
8
+ //
9
+ // Minify is enabled when "RAILS_ENV=production"
10
+ // Sourcemaps are enabled in non-production environments
11
+
12
+ import * as esbuild from "esbuild"
13
+ import path from "path"
14
+ import rails from "esbuild-rails"
15
+ import chokidar from "chokidar"
16
+ import http from "http"
17
+ import { setTimeout } from "timers/promises"
18
+
19
+ const clients = []
20
+
21
+ const entryPoints = [
22
+ "application.js"
23
+ ]
24
+
25
+ const watchDirectories = [
26
+ "./app/javascript/**/*.js",
27
+ "./app/views/**/*.html.erb",
28
+ "./app/assets/builds/**/*.css", // Wait for cssbundling changes
29
+ ]
30
+
31
+ const config = {
32
+ absWorkingDir: path.join(process.cwd(), "app/javascript"),
33
+ bundle: true,
34
+ entryPoints: entryPoints,
35
+ minify: process.env.RAILS_ENV == "production",
36
+ outdir: path.join(process.cwd(), "app/assets/builds"),
37
+ plugins: [rails()],
38
+ sourcemap: process.env.RAILS_ENV != "production"
39
+ }
40
+
41
+ async function buildAndReload() {
42
+ // Foreman & Overmind assign a separate PORT for each process
43
+ const port = parseInt(process.env.PORT)
44
+ const context = await esbuild.context({
45
+ ...config,
46
+ banner: {
47
+ js: ` (() => new EventSource("http://localhost:${port}").onmessage = () => location.reload())();`,
48
+ }
49
+ })
50
+
51
+ // Reload uses an HTTP server as an even stream to reload the browser
52
+ http.createServer((req, res) => {
53
+ return clients.push(
54
+ res.writeHead(200, {
55
+ "Content-Type": "text/event-stream",
56
+ "Cache-Control": "no-cache",
57
+ "Access-Control-Allow-Origin": "*",
58
+ Connection: "keep-alive",
59
+ })
60
+ )
61
+ }).listen(port)
62
+
63
+ await context.rebuild()
64
+ console.log("[reload] initial build succeeded")
65
+
66
+ let ready = false
67
+ chokidar.watch(watchDirectories).on("ready", () => {
68
+ console.log("[reload] ready")
69
+ ready = true
70
+ }).on("all", async (event, path) => {
71
+ if (ready === false) return
72
+
73
+ if (path.includes("javascript")) {
74
+ try {
75
+ await setTimeout(20)
76
+ await context.rebuild()
77
+ console.log("[reload] build succeeded")
78
+ } catch (error) {
79
+ console.error("[reload] build failed", error)
80
+ }
81
+ }
82
+ clients.forEach((res) => res.write("data: update\n\n"))
83
+ clients.length = 0
84
+ })
85
+ }
86
+
87
+ if (process.argv.includes("--reload")) {
88
+ buildAndReload()
89
+ } else if (process.argv.includes("--watch")) {
90
+ let context = await esbuild.context({...config, logLevel: 'info'})
91
+ context.watch()
92
+ } else {
93
+ esbuild.build(config)
94
+ }
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cable_ready/installer"
4
+
5
+ # verify that Action Cable is installed
6
+ if defined?(ActionCable::Engine)
7
+ say "✅ ActionCable::Engine is loaded and in scope"
8
+ else
9
+ halt "ActionCable::Engine is not loaded, please add or uncomment `require \"action_cable/engine\"` to your `config/application.rb`"
10
+ return
11
+ end
12
+
13
+ return if pack_path_missing?
14
+
15
+ # verify that the Action Cable pubsub config is created
16
+ cable_config = Rails.root.join("config/cable.yml")
17
+
18
+ if cable_config.exist?
19
+ say "✅ config/cable.yml is present"
20
+ else
21
+ inside "config" do
22
+ template "cable.yml"
23
+ end
24
+ end
25
+
26
+ # verify that the Action Cable pubsub is set to use redis in development
27
+ yaml = YAML.safe_load(cable_config.read)
28
+ app_name = Rails.application.class.module_parent.name.underscore
29
+
30
+ if yaml["development"]["adapter"] == "redis"
31
+ say "✅ config/cable.yml is configured to use the redis adapter in development"
32
+ elsif yaml["development"]["adapter"] == "async"
33
+ yaml["development"] = {
34
+ "adapter" => "redis",
35
+ "url" => "<%= ENV.fetch(\"REDIS_URL\") { \"redis://localhost:6379/1\" } %>",
36
+ "channel_prefix" => "#{app_name}_development"
37
+ }
38
+ backup(cable_config) do
39
+ cable_config.write(yaml.to_yaml)
40
+ end
41
+ say "✅ config/cable.yml was updated to use the redis adapter in development"
42
+ else
43
+ say "🤷 config/cable.yml should use the redis adapter - or something like it - in development. You have something else specified, and we trust that you know what you're doing."
44
+ end
45
+
46
+ if Rails::VERSION::MAJOR >= 7
47
+ add_gem "redis@~> 5"
48
+ else
49
+ add_gem "redis@~> 4"
50
+ end
51
+
52
+ # install action-cable-redis-backport gem if using Action Cable < 7.1
53
+ unless ActionCable::VERSION::MAJOR >= 7 && ActionCable::VERSION::MINOR >= 1
54
+ if !gemfile.match?(/gem ['"]action-cable-redis-backport['"]/)
55
+ add_gem "action-cable-redis-backport@~> 1"
56
+ end
57
+ end
58
+
59
+ # verify that the Action Cable channels folder and consumer class is available
60
+ step_path = "/app/javascript/channels/"
61
+ channels_path = Rails.root.join(entrypoint, "channels")
62
+ consumer_src = fetch(step_path, "consumer.js.tt")
63
+ consumer_path = channels_path / "consumer.js"
64
+ index_src = fetch(step_path, "index.js.#{bundler}.tt")
65
+ index_path = channels_path / "index.js"
66
+ friendly_index_path = index_path.relative_path_from(Rails.root).to_s
67
+
68
+ empty_directory channels_path unless channels_path.exist?
69
+
70
+ copy_file(consumer_src, consumer_path) unless consumer_path.exist?
71
+
72
+ if index_path.exist?
73
+ if index_path.read == index_src.read
74
+ say "✅ #{friendly_index_path} is present"
75
+ else
76
+ backup(index_path) do
77
+ copy_file(index_src, index_path, verbose: false)
78
+ end
79
+ say "✅ #{friendly_index_path} has been created"
80
+ end
81
+ else
82
+ copy_file(index_src, index_path)
83
+ end
84
+
85
+ # import Action Cable channels into application pack
86
+ channels_pattern = /import ['"](\.\.\/|\.\/)?channels['"]/
87
+ channels_commented_pattern = /\s*\/\/\s*#{channels_pattern}/
88
+ channel_import = "import \"#{prefix}channels\"\n"
89
+
90
+ if pack.match?(channels_pattern)
91
+ if pack.match?(channels_commented_pattern)
92
+ proceed = if options.key? "uncomment"
93
+ options["uncomment"]
94
+ else
95
+ !no?("✨ Action Cable seems to be commented out in your application.js. Do you want to uncomment it? (Y/n)")
96
+ end
97
+
98
+ if proceed
99
+ # uncomment_lines only works with Ruby comments 🙄
100
+ lines = pack_path.readlines
101
+ matches = lines.select { |line| line =~ channels_commented_pattern }
102
+ lines[lines.index(matches.last).to_i] = channel_import
103
+ pack_path.write lines.join
104
+ say "✅ channels imported in #{friendly_pack_path}"
105
+ else
106
+ say "🤷 your Action Cable channels are not being imported in your application.js. We trust that you have a reason for this."
107
+ end
108
+ else
109
+ say "✅ channels imported in #{friendly_pack_path}"
110
+ end
111
+ else
112
+ lines = pack_path.readlines
113
+ matches = lines.select { |line| line =~ /^import / }
114
+ lines.insert lines.index(matches.last).to_i + 1, channel_import
115
+ pack_path.write lines.join
116
+ say "✅ channels imported in #{friendly_pack_path}"
117
+ end
118
+
119
+ # create working copy of Action Cable initializer in tmp
120
+ if action_cable_initializer_path.exist?
121
+ FileUtils.cp(action_cable_initializer_path, action_cable_initializer_working_path)
122
+ else
123
+ # create Action Cable initializer if it doesn't already exist
124
+ create_file(action_cable_initializer_working_path, verbose: false) do
125
+ <<~RUBY
126
+ # frozen_string_literal: true
127
+
128
+ RUBY
129
+ end
130
+ say "✅ Action Cable initializer created"
131
+ end
132
+
133
+ # silence notoriously chatty Action Cable logs
134
+ if !action_cable_initializer_working_path.read.match?(/^[^#]*ActionCable.server.config.logger/)
135
+ append_file(action_cable_initializer_working_path, verbose: false) do
136
+ <<~RUBY
137
+ ActionCable.server.config.logger = Logger.new(nil)
138
+
139
+ RUBY
140
+ end
141
+ say "✅ Action Cable logger silenced for performance and legibility"
142
+ end
143
+
144
+ complete_step :action_cable
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cable_ready/installer"
4
+
5
+ proceed = if options.key? "broadcaster"
6
+ options["broadcaster"]
7
+ else
8
+ !no?("✨ Make CableReady::Broadcaster available to channels, controllers, jobs and models? (Y/n)")
9
+ end
10
+
11
+ unless proceed
12
+ complete_step :broadcaster
13
+
14
+ puts "⏩ Skipping."
15
+ return
16
+ end
17
+
18
+ # include CableReady::Broadcaster in Action Cable Channel classes
19
+ channel_path = Rails.root.join("app/channels/application_cable/channel.rb")
20
+ if channel_path.exist?
21
+ lines = channel_path.readlines
22
+ if !lines.index { |line| line =~ /^\s*include CableReady::Broadcaster/ }
23
+ backup(channel_path) do
24
+ index = lines.index { |line| line.include?("class Channel < ActionCable::Channel::Base") }
25
+ lines.insert index + 1, " include CableReady::Broadcaster\n"
26
+ channel_path.write lines.join
27
+ end
28
+
29
+ puts "✅ include CableReady::Broadcaster in Action Cable channels"
30
+ else
31
+ puts "⏩ already included CableReady::Broadcaster in Action Cable channels. Skipping"
32
+ end
33
+ end
34
+
35
+ # include CableReady::Broadcaster in Action Controller classes
36
+ controller_path = Rails.root.join("app/controllers/application_controller.rb")
37
+ if controller_path.exist?
38
+ lines = controller_path.readlines
39
+ if !lines.index { |line| line =~ /^\s*include CableReady::Broadcaster/ }
40
+ backup(controller_path) do
41
+ index = lines.index { |line| line.include?("class ApplicationController < ActionController::Base") }
42
+ lines.insert index + 1, " include CableReady::Broadcaster\n"
43
+ controller_path.write lines.join
44
+ end
45
+
46
+ puts "✅ include CableReady::Broadcaster in Action Controller classes"
47
+ else
48
+ puts "⏩ already included CableReady::Broadcaster in Action Controller classes. Skipping"
49
+ end
50
+ end
51
+
52
+ # include CableReady::Broadcaster in Active Job classes, if present
53
+ if defined?(ActiveJob)
54
+ job_path = Rails.root.join("app/jobs/application_job.rb")
55
+ if job_path.exist?
56
+ lines = job_path.readlines
57
+ if !lines.index { |line| line =~ /^\s*include CableReady::Broadcaster/ }
58
+ backup(job_path) do
59
+ index = lines.index { |line| line.include?("class ApplicationJob < ActiveJob::Base") }
60
+ lines.insert index + 1, " include CableReady::Broadcaster\n"
61
+ job_path.write lines.join
62
+ end
63
+
64
+ puts "✅ include CableReady::Broadcaster in Active Job classes"
65
+ else
66
+ puts "⏩ already included CableReady::Broadcaster in Active Job classes. Skipping"
67
+ end
68
+ end
69
+ else
70
+ puts "⏩ Active Job not available. Skipping."
71
+ end
72
+
73
+ # include CableReady::Broadcaster in StateMachines, if present
74
+ if defined?(StateMachines)
75
+ lines = action_cable_initializer_working_path.read
76
+ if !lines.include?("StateMachines::Machine.prepend(CableReady::Broadcaster)")
77
+ inject_into_file action_cable_initializer_working_path, after: "CableReady.configure do |config|\n", verbose: false do
78
+ <<-RUBY
79
+
80
+ StateMachines::Machine.prepend(CableReady::Broadcaster)
81
+
82
+ RUBY
83
+ end
84
+
85
+ puts "✅ prepend CableReady::Broadcaster into StateMachines::Machine"
86
+ else
87
+ puts "⏩ already prepended CableReady::Broadcaster into StateMachines::Machine. Skipping"
88
+ end
89
+ else
90
+ puts "⏩ StateMachines not available. Skipping."
91
+ end
92
+
93
+ # include CableReady::Broadcaster in Active Record model classes
94
+ if Rails.root.join(application_record_path).exist?
95
+ lines = application_record_path.readlines
96
+ if !lines.index { |line| line =~ /^\s*include CableReady::Broadcaster/ }
97
+ backup(application_record_path) do
98
+ index = lines.index { |line| line.include?("class ApplicationRecord < ActiveRecord::Base") }
99
+ lines.insert index + 1, " include CableReady::Broadcaster\n"
100
+ application_record_path.write lines.join
101
+ end
102
+
103
+ puts "✅ include CableReady::Broadcaster in Active Record model classes"
104
+ else
105
+ puts "⏩ already included CableReady::Broadcaster in Active Record model classes. Skipping"
106
+ end
107
+ end
108
+
109
+ complete_step :broadcaster