pulse_zero 0.3.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 (30) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +91 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +281 -0
  5. data/lib/generators/pulse_zero/install/install_generator.rb +186 -0
  6. data/lib/generators/pulse_zero/install/templates/backend/app/channels/application_cable/channel.rb.tt +6 -0
  7. data/lib/generators/pulse_zero/install/templates/backend/app/channels/application_cable/connection.rb.tt +59 -0
  8. data/lib/generators/pulse_zero/install/templates/backend/app/channels/pulse/channel.rb.tt +15 -0
  9. data/lib/generators/pulse_zero/install/templates/backend/app/controllers/concerns/pulse/request_id_tracking.rb.tt +17 -0
  10. data/lib/generators/pulse_zero/install/templates/backend/app/jobs/pulse/broadcast_job.rb.tt +28 -0
  11. data/lib/generators/pulse_zero/install/templates/backend/app/models/concerns/pulse/broadcastable.rb.tt +85 -0
  12. data/lib/generators/pulse_zero/install/templates/backend/app/models/current.rb.tt +9 -0
  13. data/lib/generators/pulse_zero/install/templates/backend/config/initializers/pulse.rb.tt +43 -0
  14. data/lib/generators/pulse_zero/install/templates/backend/lib/pulse/engine.rb.tt +43 -0
  15. data/lib/generators/pulse_zero/install/templates/backend/lib/pulse/streams/broadcasts.rb.tt +80 -0
  16. data/lib/generators/pulse_zero/install/templates/backend/lib/pulse/streams/stream_name.rb.tt +34 -0
  17. data/lib/generators/pulse_zero/install/templates/backend/lib/pulse/thread_debouncer.rb.tt +31 -0
  18. data/lib/generators/pulse_zero/install/templates/backend/lib/pulse.rb.tt +38 -0
  19. data/lib/generators/pulse_zero/install/templates/docs/PULSE_USAGE.md.tt +532 -0
  20. data/lib/generators/pulse_zero/install/templates/frontend/hooks/use-pulse.ts.tt +66 -0
  21. data/lib/generators/pulse_zero/install/templates/frontend/hooks/use-visibility-refresh.ts.tt +61 -0
  22. data/lib/generators/pulse_zero/install/templates/frontend/lib/pulse-connection.ts.tt +169 -0
  23. data/lib/generators/pulse_zero/install/templates/frontend/lib/pulse-recovery-strategy.ts.tt +156 -0
  24. data/lib/generators/pulse_zero/install/templates/frontend/lib/pulse-visibility-manager.ts.tt +143 -0
  25. data/lib/generators/pulse_zero/install/templates/frontend/lib/pulse.ts.tt +130 -0
  26. data/lib/pulse_zero/engine.rb +10 -0
  27. data/lib/pulse_zero/version.rb +5 -0
  28. data/lib/pulse_zero.rb +13 -0
  29. data/pulse_zero.gemspec +35 -0
  30. metadata +109 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: bc39a93099909f808b4bde3b5cfb4826be34d8feb9c1c71eb5e301e9527b2679
4
+ data.tar.gz: dba7ce1f61b85c4cef989e84c1b6c3c7354cdcad8eff69572e141a9d39640fef
5
+ SHA512:
6
+ metadata.gz: 496b91b569c09f96f1aad6844811f73e206a5ccdecaa7c9d02aef428f03386c2c0661a85a0b4e12a67416294794810e0da91c73d42d584b29c1f903a9aa61f6e
7
+ data.tar.gz: 860817c4a47cc4e66ff484df3ef3f148ac31699bc7d67dd34804f7e0c972617740e4e85dea7773d5ad77270d0a890c333356f79631b023c5e20b68a73f6fe5e7
data/CHANGELOG.md ADDED
@@ -0,0 +1,91 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.3.0] - 2025-01-26
11
+
12
+ ### Changed
13
+ - Moved all frontend library files from `app/frontend/lib/` to `app/frontend/lib/pulse/` subdirectory
14
+ - Updated all import paths in hooks to reference the new location
15
+ - This provides better organization and avoids naming conflicts with other libraries
16
+
17
+ ### Fixed
18
+ - Updated documentation to reflect new import paths
19
+
20
+ ## [0.2.2] - 2025-01-26
21
+
22
+ ### Fixed
23
+ - Fixed WebSocket subscription parameter name in pulse.ts (changed from `signed_stream_name` to `"signed-stream-name"`)
24
+ - This fixes the issue where broadcasts were sent but not received by the frontend
25
+
26
+ ## [0.2.1] - 2025-01-26
27
+
28
+ ### Fixed
29
+ - Fixed "uninitialized constant Pulse" error on Rails startup
30
+ - Generator now properly sets up autoload paths for lib directory
31
+ - Made initializer defensive by checking if Pulse is defined
32
+ - Removed conditional engine loading from pulse.rb template
33
+ - Added setup_autoload_paths step to generator execution flow
34
+ - Fixed ApplicationCable connection to not assume authentication is configured
35
+ - Default connection now accepts all connections with guest identifiers
36
+
37
+ ### Changed
38
+ - ApplicationCable::Connection template now provides safe default that accepts all connections
39
+ - Added comprehensive authentication examples (Devise, Session, JWT) in documentation
40
+ - Generator now warns about authentication configuration requirement
41
+ - Improved documentation with authentication setup section
42
+
43
+ ## [0.2.0] - 2025-01-26
44
+
45
+ ### Changed
46
+ - Complete rewrite of templates to match actual implementation
47
+ - Use `mattr_accessor :config` instead of thread-local variables
48
+ - Use `Pulse.config` for all configuration
49
+ - Match exact broadcast API with keyword arguments
50
+ - BroadcastJob now accepts named parameters (streamables:, event:, payload:, request_id:)
51
+ - ThreadDebouncer uses instance-based approach with `.for(key)`
52
+ - StreamName module uses `extend self` pattern
53
+ - Channel uses hyphenated parameter names ("signed-stream-name")
54
+ - Documentation shows both direct broadcasting and DSL approaches
55
+
56
+ ### Fixed
57
+ - Exact match with production code patterns
58
+ - Proper configuration in initializer using Pulse.config
59
+ - Correct parameter passing to broadcast methods
60
+
61
+ ## [0.1.1] - 2025-01-26
62
+
63
+ ### Fixed
64
+ - Create `app/frontend/types/index.ts` if it doesn't exist instead of failing
65
+ - Prevent duplicate injection of `pulse_request_id` in Current model
66
+ - Prevent duplicate inclusion of `Pulse::RequestIdTracking` in ApplicationController
67
+
68
+ ## [0.1.0] - 2025-01-26
69
+
70
+ ### Added
71
+ - Initial release of Pulse Zero
72
+ - Rails generator for installing real-time broadcasting system
73
+ - Backend components:
74
+ - Core Pulse module with stream verification
75
+ - Rails Engine for isolation
76
+ - Broadcasting system with CRUD events
77
+ - Model concern with `broadcasts_to` DSL
78
+ - ActionCable channel for WebSocket subscriptions
79
+ - Background job for async broadcasting
80
+ - Request ID tracking for correlation
81
+ - Frontend components (TypeScript):
82
+ - Subscription manager
83
+ - Connection monitor with exponential backoff
84
+ - Recovery strategy for browser tab suspension
85
+ - Visibility manager for tab focus handling
86
+ - React hooks: `usePulse` and `useVisibilityRefresh`
87
+ - Comprehensive documentation
88
+ - Test helpers and examples
89
+
90
+ [Unreleased]: https://github.com/yourusername/pulse_zero/compare/v0.1.0...HEAD
91
+ [0.1.0]: https://github.com/yourusername/pulse_zero/releases/tag/v0.1.0
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Pulse Zero Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,281 @@
1
+ # Pulse Zero
2
+
3
+ Real-time broadcasting generator for Rails + Inertia.js applications. Generate a complete WebSocket-based real-time system with zero runtime dependencies.
4
+
5
+ ## What is Pulse Zero?
6
+
7
+ Pulse Zero generates a complete real-time broadcasting system directly into your Rails application. Unlike traditional gems, all code is copied into your project, giving you full ownership and the ability to customize everything.
8
+
9
+ Features:
10
+ - 🚀 WebSocket broadcasting via ActionCable
11
+ - 🔒 Secure signed streams
12
+ - 📱 Browser tab suspension handling
13
+ - 🔄 Automatic reconnection with exponential backoff
14
+ - 📦 TypeScript support for Inertia + React
15
+ - 🎯 Zero runtime dependencies
16
+
17
+ ## Installation
18
+
19
+ Add this gem to your application's Gemfile:
20
+
21
+ ```ruby
22
+ group :development do
23
+ gem 'pulse_zero'
24
+ end
25
+ ```
26
+
27
+ Then run:
28
+
29
+ ```bash
30
+ bundle install
31
+ rails generate pulse_zero:install
32
+ ```
33
+
34
+ ## What Gets Generated?
35
+
36
+ ### Backend (Ruby)
37
+ - `lib/pulse/` - Core broadcasting system
38
+ - `app/models/concerns/pulse/broadcastable.rb` - Model broadcasting DSL
39
+ - `app/controllers/concerns/pulse/request_id_tracking.rb` - Request tracking
40
+ - `app/channels/pulse/channel.rb` - WebSocket channel
41
+ - `app/jobs/pulse/broadcast_job.rb` - Async broadcasting
42
+ - `config/initializers/pulse.rb` - Configuration
43
+
44
+ ### Frontend (TypeScript)
45
+ - `app/frontend/lib/pulse.ts` - Subscription manager
46
+ - `app/frontend/lib/pulse-connection.ts` - Connection monitoring
47
+ - `app/frontend/lib/pulse-recovery-strategy.ts` - Recovery logic
48
+ - `app/frontend/lib/pulse-visibility-manager.ts` - Tab visibility handling
49
+ - `app/frontend/hooks/use-pulse.ts` - React subscription hook
50
+ - `app/frontend/hooks/use-visibility-refresh.ts` - Tab refresh hook
51
+
52
+ ## Quick Start
53
+
54
+ ### 1. Enable Broadcasting on a Model
55
+
56
+ ```ruby
57
+ class Post < ApplicationRecord
58
+ include Pulse::Broadcastable
59
+
60
+ # Broadcast to account-scoped channel
61
+ broadcasts_to ->(post) { [post.account, "posts"] }
62
+
63
+ # Or broadcast to a simple channel
64
+ broadcasts "posts"
65
+ end
66
+ ```
67
+
68
+ ### 2. Pass Stream Token to Frontend
69
+
70
+ ```ruby
71
+ class PostsController < ApplicationController
72
+ def index
73
+ @posts = Current.account.posts
74
+ @pulse_stream = Pulse::Streams::StreamName
75
+ .signed_stream_name([Current.account, "posts"])
76
+ end
77
+ end
78
+ ```
79
+
80
+ ### 3. Subscribe in React Component
81
+
82
+ ```tsx
83
+ import { usePulse } from '@/hooks/use-pulse'
84
+ import { useVisibilityRefresh } from '@/hooks/use-visibility-refresh'
85
+ import { router } from '@inertiajs/react'
86
+
87
+ export default function Posts({ posts, pulseStream }) {
88
+ // Handle tab visibility
89
+ useVisibilityRefresh(30, () => {
90
+ router.reload({ only: ['posts'] })
91
+ })
92
+
93
+ // Subscribe to real-time updates
94
+ usePulse(pulseStream, (message) => {
95
+ switch (message.event) {
96
+ case 'created':
97
+ case 'updated':
98
+ case 'deleted':
99
+ router.reload({ only: ['posts'] })
100
+ break
101
+ }
102
+ })
103
+
104
+ return <PostsList posts={posts} />
105
+ }
106
+ ```
107
+
108
+ ## Broadcasting Events
109
+
110
+ Pulse broadcasts four types of events:
111
+
112
+ ### `created` - When a record is created
113
+ ```json
114
+ {
115
+ "event": "created",
116
+ "payload": { "id": 123, "content": "New post" },
117
+ "requestId": "uuid-123",
118
+ "at": 1234567890.123
119
+ }
120
+ ```
121
+
122
+ ### `updated` - When a record is updated
123
+ ```json
124
+ {
125
+ "event": "updated",
126
+ "payload": { "id": 123, "content": "Updated post" },
127
+ "requestId": "uuid-456",
128
+ "at": 1234567891.456
129
+ }
130
+ ```
131
+
132
+ ### `deleted` - When a record is destroyed
133
+ ```json
134
+ {
135
+ "event": "deleted",
136
+ "payload": { "id": 123 },
137
+ "requestId": "uuid-789",
138
+ "at": 1234567892.789
139
+ }
140
+ ```
141
+
142
+ ### `refresh` - Force a full refresh
143
+ ```json
144
+ {
145
+ "event": "refresh",
146
+ "payload": {},
147
+ "requestId": "uuid-012",
148
+ "at": 1234567893.012
149
+ }
150
+ ```
151
+
152
+ ## Advanced Usage
153
+
154
+ ### Manual Broadcasting
155
+
156
+ ```ruby
157
+ # Broadcast with custom payload
158
+ post.broadcast_updated_to(
159
+ [Current.account, "posts"],
160
+ payload: { id: post.id, featured: true }
161
+ )
162
+
163
+ # Async broadcasting
164
+ post.broadcast_updated_later_to([Current.account, "posts"])
165
+ ```
166
+
167
+ ### Suppress Broadcasts During Bulk Operations
168
+
169
+ ```ruby
170
+ Post.suppressing_pulse_broadcasts do
171
+ Post.where(account: account).update_all(featured: true)
172
+ end
173
+
174
+ # Then send one refresh broadcast
175
+ Post.new.broadcast_refresh_to([account, "posts"])
176
+ ```
177
+
178
+ ### Custom Serialization
179
+
180
+ ```ruby
181
+ # config/initializers/pulse.rb
182
+ Rails.application.configure do
183
+ config.pulse.serializer = ->(record) {
184
+ case record
185
+ when Post
186
+ record.as_json(only: [:id, :title, :state])
187
+ else
188
+ record.as_json
189
+ end
190
+ }
191
+ end
192
+ ```
193
+
194
+ ## Configuration
195
+
196
+ ```ruby
197
+ # config/initializers/pulse.rb
198
+ Rails.application.configure do
199
+ # Debounce window in milliseconds (default: 300)
200
+ config.pulse.debounce_ms = 300
201
+
202
+ # Background job queue (default: :default)
203
+ config.pulse.queue_name = :low
204
+
205
+ # Custom serializer
206
+ config.pulse.serializer = ->(record) { record.as_json }
207
+ end
208
+ ```
209
+
210
+ ## Browser Tab Handling
211
+
212
+ Pulse includes sophisticated handling for browser tab suspension:
213
+
214
+ - **Quick switches (<30s)**: Just ensures connection is alive
215
+ - **Medium absence (30s-5min)**: Reconnects and syncs data
216
+ - **Long absence (>5min)**: Full page refresh for consistency
217
+
218
+ Platform-aware thresholds:
219
+ - Desktop Chrome/Firefox: 30 seconds
220
+ - Safari/Mobile: 15 seconds (more aggressive)
221
+
222
+ ## Testing
223
+
224
+ ```ruby
225
+ # In your test files
226
+ test "broadcasts on update" do
227
+ post = posts(:one)
228
+
229
+ assert_broadcast_on([post.account, "posts"]) do
230
+ post.update!(title: "New Title")
231
+ end
232
+ end
233
+
234
+ # Suppress broadcasts in tests
235
+ Post.suppressing_pulse_broadcasts do
236
+ # Your test code
237
+ end
238
+ ```
239
+
240
+ ## Debugging
241
+
242
+ Enable debug logging:
243
+
244
+ ```javascript
245
+ // In browser console
246
+ localStorage.setItem('PULSE_DEBUG', 'true')
247
+ ```
248
+
249
+ Check connection health:
250
+
251
+ ```javascript
252
+ import { getPulseMonitorStats } from '@/lib/pulse-connection'
253
+
254
+ const stats = getPulseMonitorStats()
255
+ console.log(stats)
256
+ ```
257
+
258
+ ## Requirements
259
+
260
+ - Rails 7.0+
261
+ - ActionCable
262
+ - Inertia.js
263
+ - React (Vue/Svelte support coming soon)
264
+ - TypeScript
265
+
266
+ ## Philosophy
267
+
268
+ Pulse Zero follows the same philosophy as [authentication-zero](https://github.com/lazaronixon/authentication-zero):
269
+
270
+ - **Own your code**: All code is generated into your project
271
+ - **No runtime dependencies**: The gem is only needed during generation
272
+ - **Customizable**: Modify any generated code to fit your needs
273
+ - **Production-ready**: Includes battle-tested patterns from real applications
274
+
275
+ ## Contributing
276
+
277
+ Bug reports and pull requests are welcome on GitHub.
278
+
279
+ ## License
280
+
281
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,186 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "rails/generators/base"
5
+
6
+ module PulseZero
7
+ module Generators
8
+ class InstallGenerator < Rails::Generators::Base
9
+ source_root File.expand_path("templates", __dir__)
10
+
11
+ def check_prerequisites
12
+ unless defined?(ActionCable)
13
+ say "ActionCable is required. Installing...", :yellow
14
+ generate "action_cable:install"
15
+ end
16
+
17
+ return if File.exist?("app/frontend")
18
+
19
+ say "This generator is designed for Inertia.js applications with app/frontend directory", :red
20
+ exit 1
21
+ end
22
+
23
+ def setup_action_cable
24
+ # Generate base channel classes if missing
25
+ unless File.exist?("app/channels/application_cable/connection.rb")
26
+ template "backend/app/channels/application_cable/connection.rb.tt",
27
+ "app/channels/application_cable/connection.rb"
28
+ end
29
+
30
+ unless File.exist?("app/channels/application_cable/channel.rb")
31
+ template "backend/app/channels/application_cable/channel.rb.tt",
32
+ "app/channels/application_cable/channel.rb"
33
+ end
34
+
35
+ # Add route if missing
36
+ route 'mount ActionCable.server => "/cable"' unless action_cable_route_exists?
37
+ end
38
+
39
+ def copy_backend_files
40
+ # Core library files
41
+ template "backend/lib/pulse.rb.tt", "lib/pulse.rb"
42
+ template "backend/lib/pulse/engine.rb.tt", "lib/pulse/engine.rb"
43
+ template "backend/lib/pulse/streams/broadcasts.rb.tt", "lib/pulse/streams/broadcasts.rb"
44
+ template "backend/lib/pulse/streams/stream_name.rb.tt", "lib/pulse/streams/stream_name.rb"
45
+ template "backend/lib/pulse/thread_debouncer.rb.tt", "lib/pulse/thread_debouncer.rb"
46
+
47
+ # Application files
48
+ template "backend/app/channels/pulse/channel.rb.tt", "app/channels/pulse/channel.rb"
49
+ template "backend/app/controllers/concerns/pulse/request_id_tracking.rb.tt",
50
+ "app/controllers/concerns/pulse/request_id_tracking.rb"
51
+ template "backend/app/models/concerns/pulse/broadcastable.rb.tt",
52
+ "app/models/concerns/pulse/broadcastable.rb"
53
+ template "backend/app/jobs/pulse/broadcast_job.rb.tt", "app/jobs/pulse/broadcast_job.rb"
54
+
55
+ # Configuration
56
+ template "backend/config/initializers/pulse.rb.tt", "config/initializers/pulse.rb"
57
+ end
58
+
59
+ def copy_frontend_files
60
+ # TypeScript files in lib/pulse/
61
+ template "frontend/lib/pulse.ts.tt", "app/frontend/lib/pulse/pulse.ts"
62
+ template "frontend/lib/pulse-connection.ts.tt", "app/frontend/lib/pulse/pulse-connection.ts"
63
+ template "frontend/lib/pulse-recovery-strategy.ts.tt", "app/frontend/lib/pulse/pulse-recovery-strategy.ts"
64
+ template "frontend/lib/pulse-visibility-manager.ts.tt", "app/frontend/lib/pulse/pulse-visibility-manager.ts"
65
+
66
+ # React hooks
67
+ template "frontend/hooks/use-pulse.ts.tt", "app/frontend/hooks/use-pulse.ts"
68
+ template "frontend/hooks/use-visibility-refresh.ts.tt", "app/frontend/hooks/use-visibility-refresh.ts"
69
+
70
+ # Create or append to types/index.ts
71
+ if File.exist?("app/frontend/types/index.ts")
72
+ append_to_file "app/frontend/types/index.ts", pulse_types_content
73
+ else
74
+ create_file "app/frontend/types/index.ts", pulse_types_content
75
+ end
76
+ end
77
+
78
+ def setup_current_model
79
+ if File.exist?("app/models/current.rb")
80
+ # Check if pulse_request_id is already defined
81
+ current_content = File.read("app/models/current.rb")
82
+ unless current_content.include?("pulse_request_id")
83
+ inject_into_class "app/models/current.rb", "Current" do
84
+ " attribute :pulse_request_id\n"
85
+ end
86
+ end
87
+ else
88
+ template "backend/app/models/current.rb.tt", "app/models/current.rb"
89
+ end
90
+ end
91
+
92
+ def setup_autoload_paths
93
+ add_pulse_to_autoload_paths
94
+ end
95
+
96
+ def install_npm_dependencies
97
+ return unless File.exist?("package.json")
98
+
99
+ say "Installing @rails/actioncable...", :green
100
+ run "npm install @rails/actioncable"
101
+ end
102
+
103
+ def add_pulse_to_autoload_paths
104
+ # Add lib to autoload paths
105
+ application_file = "config/application.rb"
106
+ application_content = File.read(application_file)
107
+
108
+ # Add to autoload_lib if not already there
109
+ return if application_content.include?("config.autoload_lib")
110
+
111
+ inject_into_file application_file, after: /class Application < Rails::Application\n/ do
112
+ <<-RUBY
113
+ # Autoload lib directory
114
+ config.autoload_lib(ignore: %w[assets tasks])
115
+
116
+ RUBY
117
+ end
118
+ end
119
+
120
+ def setup_application_controller
121
+ # Check if already included
122
+ controller_content = File.read("app/controllers/application_controller.rb")
123
+ return if controller_content.include?("Pulse::RequestIdTracking")
124
+
125
+ inject_into_class "app/controllers/application_controller.rb",
126
+ "ApplicationController" do
127
+ " include Pulse::RequestIdTracking\n"
128
+ end
129
+ end
130
+
131
+ def create_documentation
132
+ template "docs/PULSE_USAGE.md.tt", "docs/PULSE_USAGE.md"
133
+
134
+ say "\n✅ Pulse real-time broadcasting has been installed!", :green
135
+ say "\n⚠️ IMPORTANT: Configure authentication!", :yellow
136
+ say "The default ApplicationCable connection accepts all connections."
137
+ say "Edit app/channels/application_cable/connection.rb to add your authentication logic."
138
+ say "\nNext steps:", :yellow
139
+ say "1. Configure authentication in app/channels/application_cable/connection.rb"
140
+ say "2. Read docs/PULSE_USAGE.md for complete setup instructions"
141
+ say "3. Add 'include Pulse::Broadcastable' to models that need broadcasting"
142
+ say "4. Use 'usePulse' hook in your React components"
143
+ say "\nExample:", :blue
144
+ say <<~EXAMPLE
145
+ # In your model:
146
+ class Post < ApplicationRecord
147
+ include Pulse::Broadcastable
148
+ broadcasts_to ->(post) { [post.account, "posts"] }
149
+ end
150
+
151
+ # In your controller:
152
+ @pulse_stream = Pulse::Streams::StreamName.signed_stream_name([Current.account, "posts"])
153
+
154
+ # In your React component:
155
+ usePulse(pulseStream, (message) => {
156
+ router.reload({ only: ['posts'] })
157
+ })
158
+ EXAMPLE
159
+ end
160
+
161
+ private
162
+
163
+ def action_cable_route_exists?
164
+ routes_file = File.read("config/routes.rb")
165
+ routes_file.include?("ActionCable.server") || routes_file.include?("action_cable")
166
+ end
167
+
168
+ def pulse_types_content
169
+ <<~TYPES
170
+
171
+ // Pulse Types
172
+ export interface PulseMessage {
173
+ event: 'created' | 'updated' | 'deleted' | 'refresh'
174
+ payload: any
175
+ requestId?: string
176
+ at: number
177
+ }
178
+
179
+ export interface PulseSubscription {
180
+ unsubscribe: () => void
181
+ }
182
+ TYPES
183
+ end
184
+ end
185
+ end
186
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ApplicationCable
4
+ class Channel < ActionCable::Channel::Base
5
+ end
6
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ApplicationCable
4
+ class Connection < ActionCable::Connection::Base
5
+ identified_by :current_user
6
+
7
+ def connect
8
+ self.current_user = find_verified_user
9
+ end
10
+
11
+ private
12
+
13
+ def find_verified_user
14
+ # Default implementation that accepts all connections
15
+ # Replace this with your authentication logic
16
+
17
+ # Example 1: Allow all connections (development/testing)
18
+ # Return a simple identifier for the connection
19
+ "guest_#{SecureRandom.hex(8)}"
20
+
21
+ # Example 2: Devise authentication (if using Devise)
22
+ # if verified_user = env["warden"]&.user
23
+ # verified_user
24
+ # else
25
+ # reject_unauthorized_connection
26
+ # end
27
+
28
+ # Example 3: Session-based authentication
29
+ # if session[:user_id] && verified_user = User.find_by(id: session[:user_id])
30
+ # verified_user
31
+ # else
32
+ # reject_unauthorized_connection
33
+ # end
34
+
35
+ # Example 4: JWT token authentication
36
+ # if verified_user = User.find_by(id: decoded_jwt_user_id)
37
+ # verified_user
38
+ # else
39
+ # reject_unauthorized_connection
40
+ # end
41
+ end
42
+
43
+ # Helper method for JWT authentication (uncomment if needed)
44
+ # def decoded_jwt_user_id
45
+ # token = request.params[:token] || request.headers["Authorization"]&.split(" ")&.last
46
+ # return unless token
47
+ #
48
+ # decoded = JWT.decode(token, Rails.application.secret_key_base, true, algorithm: "HS256")
49
+ # decoded.first["user_id"]
50
+ # rescue JWT::DecodeError
51
+ # nil
52
+ # end
53
+
54
+ # Helper method for session access (uncomment if needed)
55
+ # def session
56
+ # cookies.encrypted[Rails.application.config.session_options[:key]]
57
+ # end
58
+ end
59
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Pulse::Channel < ApplicationCable::Channel
4
+ include Pulse::Streams::StreamName::ClassMethods
5
+
6
+ def subscribed
7
+ signed_name = params["signed-stream-name"]
8
+ if signed_name && verified_stream_name_from_params
9
+ # Stream from the signed name (same as what broadcaster uses)
10
+ stream_from signed_name
11
+ else
12
+ reject
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pulse
4
+ module RequestIdTracking
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ before_action :set_pulse_request_id
9
+ end
10
+
11
+ private
12
+
13
+ def set_pulse_request_id
14
+ Current.pulse_request_id = request.uuid if defined?(Current) && Current.respond_to?(:pulse_request_id=)
15
+ end
16
+ end
17
+ end