falcon-rails 0.2.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 34ee82aedb10194c91447a3ea58db73ba79da8ebbe6621cc4a9148f5517bdeb6
4
+ data.tar.gz: 14d659677ee4e20bddfc2b1107277b01c883aecf6a843eaca4ff2799bf58976b
5
+ SHA512:
6
+ metadata.gz: 6e8ad267e81745e93527f25511a56775ff6cf3c8b38ef99553fdd2679de87c8b2d090788f34b48ebca0ed0a20f8a881fa97026466ac62ffdc99399618ed5fc2a
7
+ data.tar.gz: 150e56abe7e16e42d56421a5b4c860d1da3d5e653615ee2dcb7b20e94fc213e3c1010596959593c4a57b7830fb5c95a6e1d503adb8699a1052dd605356a8465e
checksums.yaml.gz.sig ADDED
Binary file
@@ -0,0 +1,43 @@
1
+ # Getting Started
2
+
3
+ This guide explains how to get started using Falcon to host your Rails application.
4
+
5
+ ## Installation
6
+
7
+ In your existing Rails application:
8
+
9
+ ```bash
10
+ bundle add falcon-rails
11
+ ```
12
+
13
+ You might also like to remove your existing web server, such as Puma, and replace it with Falcon:
14
+
15
+ ```bash
16
+ bundle remove puma
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ Falcon is an Async-compatible web server that can be used with Rails. It provides high concurrency and low latency for web applications.
22
+
23
+ To use Falcon in development, first install the development TLS certificates:
24
+
25
+ ```
26
+ > bundle exec bake localhost:install
27
+ ```
28
+
29
+ Then, you can start your Rails application with Falcon:
30
+
31
+ ```bash
32
+ > bundle exec falcon serve
33
+ ```
34
+
35
+ You can access your application at `https://localhost:9292`.
36
+
37
+ ## Integrations
38
+
39
+ Consult the other integration guides for more information on using Falcon with popular libraries and frameworks.
40
+
41
+ ### Example
42
+
43
+ Many of the integrations are demonstrated in the example application available at: https://github.com/socketry/falcon-rails-examples
@@ -0,0 +1,200 @@
1
+ # HTTP Streaming
2
+
3
+ This guide explains how to implement HTTP response streaming with Falcon and Rails.
4
+
5
+ ## What is HTTP Streaming?
6
+
7
+ HTTP streaming allows you to send data to the client progressively over a single HTTP connection using chunked transfer encoding. Unlike Server-Sent Events, HTTP streaming gives you complete control over the data format and doesn't require specific protocols.
8
+
9
+ **When to use HTTP streaming:**
10
+ - Progress indicators for long-running tasks.
11
+ - Live log streaming.
12
+ - Large file generation (CSV, JSON exports).
13
+ - Real-time data feeds with custom formats.
14
+ - Streaming API responses.
15
+
16
+ **When NOT to use HTTP streaming:**
17
+ - When you need persistent connections (use SSE or WebSockets instead).
18
+ - For simple real-time updates (SSE is easier).
19
+
20
+ ## Basic Implementation
21
+
22
+ ### Server-Side: Rails Controller
23
+
24
+ Create a controller action that streams data using `Rack::Response`:
25
+
26
+ ```ruby
27
+ class StreamingController < ApplicationController
28
+ def index
29
+ # Render the page with streaming JavaScript
30
+ end
31
+
32
+ def stream
33
+ body = proc do |stream|
34
+ 10.downto(1) do |i|
35
+ stream.write "#{i} bottles of beer on the wall\n"
36
+ sleep 1
37
+ stream.write "#{i} bottles of beer\n"
38
+ sleep 1
39
+ stream.write "Take one down, pass it around\n"
40
+ sleep 1
41
+ stream.write "#{i - 1} bottles of beer on the wall\n"
42
+ sleep 1
43
+ end
44
+ end
45
+
46
+ self.response = Rack::Response[200, {"content-type" => "text/plain"}, body]
47
+ end
48
+ end
49
+ ```
50
+
51
+ **Key Points:**
52
+ - Use `Rack::Response` with a callable body for streaming.
53
+ - `content-type` can be `text/plain`, `application/json`, or any format you need.
54
+ - Each `stream.write` sends data immediately to the client.
55
+ - No special formatting required (unlike SSE's `data: ` prefix).
56
+
57
+ ### Client-Side: Fetch API with ReadableStream
58
+
59
+ Create an HTML page that consumes the HTTP stream:
60
+
61
+ ```html
62
+ <button id="startStream" class="button">🚀 Start Streaming Demo</button>
63
+ <button id="stopStream" class="button" disabled>⏹️ Stop Stream</button>
64
+ <div id="streamOutput" class="terminal"></div>
65
+
66
+ <script>
67
+ let streamController = null;
68
+ let streamReader = null;
69
+
70
+ document.getElementById('startStream').addEventListener('click', function() {
71
+ const output = document.getElementById('streamOutput');
72
+ const startBtn = document.getElementById('startStream');
73
+ const stopBtn = document.getElementById('stopStream');
74
+
75
+ // Clear previous output
76
+ output.innerHTML = '<div class="terminal-status">🔄 Starting stream...</div>';
77
+
78
+ // Create abort controller for stopping the stream
79
+ streamController = new AbortController();
80
+
81
+ // Start streaming
82
+ fetch('/streaming/stream', { signal: streamController.signal })
83
+ .then(response => {
84
+ if (!response.ok) throw new Error('Network response was not ok');
85
+
86
+ streamReader = response.body.getReader();
87
+ const decoder = new TextDecoder();
88
+
89
+ function readStream() {
90
+ streamReader.read().then(({ done, value }) => {
91
+ if (done) {
92
+ output.innerHTML += '<div class="terminal-complete">✅ Stream completed!</div>';
93
+ startBtn.disabled = false;
94
+ stopBtn.disabled = true;
95
+ return;
96
+ }
97
+
98
+ const text = decoder.decode(value, { stream: true });
99
+ const lines = text.split('\n');
100
+
101
+ lines.forEach(line => {
102
+ if (line.trim()) {
103
+ const lineDiv = document.createElement('div');
104
+ lineDiv.className = 'terminal-line';
105
+ lineDiv.textContent = line;
106
+ output.appendChild(lineDiv);
107
+ output.scrollTop = output.scrollHeight;
108
+ }
109
+ });
110
+
111
+ readStream();
112
+ });
113
+ }
114
+
115
+ readStream();
116
+ });
117
+ });
118
+
119
+ document.getElementById('stopStream').addEventListener('click', function() {
120
+ if (streamController) {
121
+ streamController.abort();
122
+ }
123
+ });
124
+ </script>
125
+ ```
126
+
127
+ **Key Points:**
128
+ - Use `fetch()` with `AbortController` for cancellation support.
129
+ - Get a `ReadableStream` reader with `response.body.getReader()`.
130
+ - Use `TextDecoder` to convert binary data to text.
131
+ - Handle the stream chunks manually in the `readStream()` function.
132
+
133
+ ### Routing Configuration
134
+
135
+ Add routes to your `config/routes.rb`:
136
+
137
+ ```ruby
138
+ Rails.application.routes.draw do
139
+ # Streaming Example:
140
+ get 'streaming/index' # Page with streaming JavaScript
141
+ get 'streaming/stream' # HTTP streaming endpoint
142
+ end
143
+ ```
144
+
145
+ ## Advanced Patterns
146
+
147
+ ### Streaming NDJSON Data
148
+
149
+ Server-side:
150
+ ```ruby
151
+ def stream
152
+ body = proc do |stream|
153
+ User.find_each(batch_size: 100) do |user|
154
+ # Each line is a complete JSON object
155
+ stream.write "#{user.to_json}\n"
156
+ end
157
+ end
158
+
159
+ self.response = Rack::Response[200, {"content-type" => "application/x-ndjson"}, body]
160
+ end
161
+ ```
162
+
163
+ Client-side:
164
+ ```javascript
165
+ fetch('/streaming/users')
166
+ .then(response => {
167
+ const reader = response.body.getReader();
168
+ const decoder = new TextDecoder();
169
+ let buffer = '';
170
+
171
+ function readStream() {
172
+ reader.read().then(({ done, value }) => {
173
+ if (done) {
174
+ console.log('All users loaded');
175
+ return;
176
+ }
177
+
178
+ buffer += decoder.decode(value, { stream: true });
179
+ const lines = buffer.split('\n');
180
+ buffer = lines.pop(); // Keep incomplete line in buffer
181
+
182
+ lines.forEach(line => {
183
+ if (line.trim()) {
184
+ try {
185
+ const user = JSON.parse(line);
186
+ console.log('User loaded:', user);
187
+ displayUser(user);
188
+ } catch (e) {
189
+ console.error('Invalid JSON:', line);
190
+ }
191
+ }
192
+ });
193
+
194
+ readStream();
195
+ });
196
+ }
197
+
198
+ readStream();
199
+ });
200
+ ```
@@ -0,0 +1,78 @@
1
+ # Job Processing
2
+
3
+ This guide explains how to implement background job processing with Falcon and Rails using the `async-job` gem.
4
+
5
+ ## What is Async::Job?
6
+
7
+ `Async::Job` is a framework for creating background jobs that run asynchronously without blocking web requests. It integrates seamlessly with Rails' ActiveJob, allowing you to offload long-running tasks to background workers. The Rails integration uses a specific gem to provide this functionality, called `async-job-adapter-active_job`.
8
+
9
+ **When to use async jobs:**
10
+ - Long-running tasks (data processing, file uploads).
11
+ - Email sending and external API calls.
12
+ - Scheduled tasks and periodic jobs.
13
+ - Heavy computations that would slow down web responses.
14
+
15
+ **When NOT to use async jobs:**
16
+ - Simple operations that complete quickly.
17
+ - Tasks that need immediate user feedback.
18
+
19
+ ## Basic Implementation
20
+
21
+ ### Server-Side: Job Class
22
+
23
+ Create a job class that inherits from `ApplicationJob`:
24
+
25
+ ```ruby
26
+ class MyJob < ApplicationJob
27
+ # Specify the queue adapter per-job rather than globally:
28
+ # queue_adapter :async_job
29
+
30
+ queue_as "default"
31
+
32
+ def perform
33
+ # ... work ...
34
+ end
35
+ end
36
+ ```
37
+
38
+ **Key Points:**
39
+ - Use `queue_as` to specify which queue this job should use.
40
+ - The `perform` method contains your background work.
41
+
42
+ ### Configuration
43
+
44
+ Configure async-job queues in `config/initializers/async_job.rb`:
45
+
46
+ ```ruby
47
+ require 'async/job'
48
+ require 'async/job/processor/aggregate'
49
+ require 'async/job/processor/redis'
50
+ require 'async/job/processor/inline'
51
+
52
+ Rails.application.configure do
53
+ config.async_job.define_queue "default" do
54
+ # Double-buffers incoming jobs to avoid submission latency:
55
+ enqueue Async::Job::Processor::Aggregate
56
+ dequeue Async::Job::Processor::Redis
57
+ end
58
+
59
+ config.async_job.define_queue "local" do
60
+ dequeue Async::Job::Processor::Inline
61
+ end
62
+ end
63
+ ```
64
+
65
+ **Key Points:**
66
+ - `default` queue uses Redis for persistent job storage.
67
+ - `local` queue processes jobs inline, but in a background Async task - higher throughput but lower robustness.
68
+ - Different processors can be used for different queue behaviors.
69
+
70
+ Configure the default adapter in `config/application.rb`:
71
+
72
+ ```ruby
73
+ # ... in the application configuration:
74
+ config.active_job.queue_adapter = :async_job
75
+ ```
76
+
77
+ **Key Points:**
78
+ - This sets `Async::Job` as the default queue adapter. You can instead specify this per-job for incremental migration.
@@ -0,0 +1,259 @@
1
+ # Real-Time Views
2
+
3
+ This guide explains how to implement real-time interfaces with `Live::View`.
4
+
5
+ ## What is `Live::View`?
6
+
7
+ `Live::View` enables real-time, interactive web interfaces using WebSocket connections. It allows you to update the DOM in real-time without JavaScript, making it perfect for dynamic content that changes frequently.
8
+
9
+ **When to use `Live::View`:**
10
+ - Real-time dashboards and status displays.
11
+ - Interactive forms with live validation.
12
+ - Live updating content (clocks, counters, progress bars).
13
+ - Simple games and interactive applications.
14
+
15
+ **When NOT to use `Live::View`:**
16
+ - Static content that doesn't change.
17
+ - Complex client-side interactions requiring heavy JavaScript.
18
+
19
+ ## Setup
20
+
21
+ ### Import Maps Configuration
22
+
23
+ Use `bin/importmap` to install the required JavaScript packages:
24
+
25
+ ```bash
26
+ > bin/importmap pin @socketry/live
27
+ Pinning "@socketry/live" to vendor/javascript/@socketry/live.js via download from https://ga.jspm.io/npm:@socketry/live@0.14.0/Live.js
28
+ Pinning "morphdom" to vendor/javascript/morphdom.js via download from https://ga.jspm.io/npm:morphdom@2.7.7/dist/morphdom-esm.js
29
+ ```
30
+
31
+ ### JavaScript Setup
32
+
33
+ Create `app/javascript/live.js` to initialize `Live::View`:
34
+
35
+ ```javascript
36
+ import {Live} from "@socketry/live"
37
+ window.live = Live.start()
38
+ ```
39
+
40
+ Pin your local `live.js` file in `config/importmap.rb`:
41
+
42
+ ```ruby
43
+ pin "live"
44
+ ```
45
+
46
+ **Key Points:**
47
+ - Import maps handle the `@socketry/live` package automatically.
48
+ - The `live.js` file starts the `Live::View` client connection.
49
+ - `pin "live"` makes your local `live.js` file importable in view templates.
50
+ - `window.live` makes the Live connection globally available.
51
+
52
+ ## Basic Implementation: Live Clock
53
+
54
+ ### Live::View Class
55
+
56
+ Create a `Live::View` class that handles the real-time logic:
57
+
58
+ ```ruby
59
+ require 'live'
60
+
61
+ class ClockTag < Live::View
62
+ def initialize(...)
63
+ super
64
+ end
65
+
66
+ def bind(page)
67
+ @task ||= start_clock
68
+ end
69
+
70
+ def close
71
+ if task = @task
72
+ @task = nil
73
+ task.stop
74
+ end
75
+ end
76
+
77
+ def start_clock
78
+ Async do
79
+ while true
80
+ sleep 1
81
+ self.update!
82
+ end
83
+ end
84
+ end
85
+
86
+ def forward_event(name)
87
+ "event.preventDefault(); live.forwardEvent(#{JSON.dump(@id)}, event, {name: #{name.inspect}})"
88
+ end
89
+
90
+ def render(builder)
91
+ builder.tag(:div, class: "clock-container") do
92
+ builder.tag(:h2) {builder.text("Live Clock")}
93
+ builder.tag(:div, id: "clock", class: "clock-display") do
94
+ builder.text(Time.now.strftime("%H:%M:%S"))
95
+ end
96
+ end
97
+ end
98
+ end
99
+ ```
100
+
101
+ **Key Points:**
102
+ - Inherit from `Live::View` to get real-time capabilities.
103
+ - Use `Async` blocks for background tasks.
104
+ - Use `self.update!` to queue a full re-render.
105
+ - Define `forward_event` method to handle user interactions.
106
+ - The `render` method defines the initial HTML structure.
107
+
108
+ ### Controller
109
+
110
+ Create a controller to handle the Live::View connection:
111
+
112
+ ```ruby
113
+ require 'async/websocket/adapters/rails'
114
+
115
+ class ClockController < ApplicationController
116
+ RESOLVER = Live::Resolver.allow(ClockTag)
117
+
118
+ def index
119
+ @tag = ClockTag.new('clock')
120
+ end
121
+
122
+ skip_before_action :verify_authenticity_token, only: :live
123
+
124
+ def live
125
+ self.response = Async::WebSocket::Adapters::Rails.open(request) do |connection|
126
+ Live::Page.new(RESOLVER).run(connection)
127
+ end
128
+ end
129
+ end
130
+ ```
131
+
132
+ **Key Points:**
133
+ - Use `Live::Resolver.allow` to whitelist your `Live::View` sub-classes.
134
+ - Skip CSRF verification for the WebSocket endpoint.
135
+ - Use `Async::WebSocket::Adapters::Rails.open` for WebSocket handling.
136
+ - `Live::Page.new(RESOLVER).run(connection)` handles the `Live::View` protocol.
137
+
138
+ ### View Template
139
+
140
+ Create a view template that renders the Live::View:
141
+
142
+ ```html
143
+ <h1>⏰ Live Clock Example</h1>
144
+ <p>This clock updates every second using Live::View.</p>
145
+
146
+ <%= javascript_import_module_tag "live" %>
147
+
148
+ <div class="clock-wrapper">
149
+ <%= raw @tag.to_html %>
150
+ </div>
151
+
152
+ <style>
153
+ .clock-container {
154
+ text-align: center;
155
+ padding: 2rem;
156
+ border: 2px solid #ddd;
157
+ border-radius: 8px;
158
+ margin: 2rem 0;
159
+ }
160
+
161
+ .clock-display {
162
+ font-size: 3rem;
163
+ font-family: 'Monaco', 'Consolas', monospace;
164
+ color: #333;
165
+ background: #f0f0f0;
166
+ padding: 1rem;
167
+ border-radius: 4px;
168
+ margin-top: 1rem;
169
+ }
170
+ </style>
171
+ ```
172
+
173
+ **Key Points:**
174
+ - Use `<%= javascript_import_module_tag "live" %>` to load Live::View on specific pages.
175
+ - Use `raw @tag.to_html` to render the Live::View component.
176
+ - The Live::View will automatically connect via WebSocket.
177
+ - This loads Live::View only on pages that need it, not globally.
178
+
179
+ ### Routing Configuration
180
+
181
+ Add routes to your `config/routes.rb`:
182
+
183
+ ```ruby
184
+ Rails.application.routes.draw do
185
+ # Live Clock Example:
186
+ get "clock/index"
187
+ connect "clock/live"
188
+ end
189
+ ```
190
+
191
+ **Key Points:**
192
+ - Use the Rails 8 `connect` helper for WebSocket routes.
193
+ - The `live` action handles the WebSocket connection.
194
+
195
+ ## Interactive Example: Counter with Buttons
196
+
197
+ ### Live::View Class with User Interaction
198
+
199
+ ```ruby
200
+ class CounterTag < Live::View
201
+ def initialize(count: 0)
202
+ super
203
+
204
+ # @data is persisted on the tag in `data-` attributes.
205
+ @data["count"] = @data.fetch("count", count).to_i
206
+ end
207
+
208
+ def handle(event)
209
+ case event[:type]
210
+ when "click"
211
+ case event.dig(:detail, :name)
212
+ when "increment"
213
+ @data["count"] += 1
214
+ when "decrement"
215
+ @data["count"] -= 1
216
+ end
217
+
218
+ update_counter
219
+ end
220
+ end
221
+
222
+ def update_counter
223
+ self.replace("#counter") do |builder|
224
+ builder.tag(:div, id: "counter", class: "counter-display") do
225
+ builder.text(@data["count"].to_s)
226
+ end
227
+ end
228
+ end
229
+
230
+ def forward_event(name)
231
+ "event.preventDefault(); live.forwardEvent(#{JSON.dump(@id)}, event, {name: #{name.inspect}})"
232
+ end
233
+
234
+ def render(builder)
235
+ builder.tag(:div, class: "counter-container") do
236
+ builder.tag(:h2) { builder.text("Live Counter") }
237
+
238
+ builder.tag(:div, id: "counter", class: "counter-display") do
239
+ builder.text(@count.to_s)
240
+ end
241
+
242
+ builder.tag(:div, class: "counter-buttons") do
243
+ builder.tag(:button, onclick: forward_event("decrement")) do
244
+ builder.text("-")
245
+ end
246
+ builder.tag(:button, onclick: forward_event("increment")) do
247
+ builder.text("+")
248
+ end
249
+ end
250
+ end
251
+ end
252
+ end
253
+ ```
254
+
255
+ **Key Points:**
256
+ - Define `forward_event` (or `forward_click`, etc) methods to generate JavaScript for event handling.
257
+ - `live.forwardEvent(...)` on the client side invokes `handle(event)` on the server side.
258
+ - Update the DOM in response to user actions.
259
+ - Maintain persistent state in `@data`.
@@ -0,0 +1,149 @@
1
+ # Server-Sent Events
2
+
3
+ This guide explains how to implement Server-Sent Events with Falcon and Rails.
4
+
5
+ ## What are Server-Sent Events?
6
+
7
+ Server-Sent Events (SSE) provide a way to push real-time updates from the server to the client over a single HTTP connection. Unlike WebSockets, SSE is unidirectional (server-to-client only) and uses the standard HTTP protocol.
8
+
9
+ **When to use SSE:**
10
+ - Live dashboards and monitoring systems.
11
+ - Real-time notifications.
12
+ - Progress indicators for long-running tasks.
13
+ - Live feeds (news, social media updates).
14
+ - Database change notifications.
15
+
16
+ **When NOT to use SSE:**
17
+ - When you need bidirectional communication (use WebSockets instead).
18
+ - When you need binary data transmission.
19
+
20
+ ## Basic Implementation
21
+
22
+ ### Server-Side: Rails Controller
23
+
24
+ Create a controller action that streams events using `Rack::Response`:
25
+
26
+ ```ruby
27
+ class SseController < ApplicationController
28
+ def index
29
+ # Render the page with EventSource JavaScript
30
+ end
31
+
32
+ EVENT_STREAM_HEADERS = {
33
+ 'content-type' => 'text/event-stream',
34
+ 'cache-control' => 'no-cache',
35
+ 'connection' => 'keep-alive'
36
+ }
37
+
38
+ def events
39
+ body = proc do |stream|
40
+ while true
41
+ # Send timestamped data:
42
+ stream.write("data: #{Time.now}\n\n")
43
+ sleep 1
44
+ end
45
+ end
46
+
47
+ self.response = Rack::Response[200, EVENT_STREAM_HEADERS.dup, body]
48
+ end
49
+ end
50
+ ```
51
+
52
+ **Key Points:**
53
+ - `content-type: text/event-stream` is required for SSE.
54
+ - Each message must end with `\n\n` (two newlines).
55
+ - The `data: ` prefix is required for the message content.
56
+ - Use a callable body for streaming responses.
57
+
58
+ ### Client-Side: JavaScript EventSource
59
+
60
+ Create an HTML page that consumes the SSE stream:
61
+
62
+ ```html
63
+ <div id="status" class="status warning">🔄 Connecting to event stream...</div>
64
+ <div id="history" class="terminal"></div>
65
+
66
+ <script>
67
+ var eventSource = new EventSource("events");
68
+ var messageCount = 0;
69
+
70
+ eventSource.addEventListener("open", function(event) {
71
+ document.getElementById("status").innerHTML = "✅ Connected to event stream";
72
+ document.getElementById("status").className = "status success";
73
+ });
74
+
75
+ eventSource.addEventListener("message", function(event) {
76
+ messageCount++;
77
+ var container = document.createElement("div");
78
+ container.className = "terminal-line";
79
+ container.innerHTML = `<span class="event-count">#${messageCount}</span> <span class="event-data">${event.data}</span>`;
80
+
81
+ var history = document.querySelector("#history");
82
+ history.appendChild(container);
83
+ history.scrollTop = history.scrollHeight;
84
+
85
+ // Keep only last 20 messages to prevent memory issues
86
+ if (history.children.length > 20) {
87
+ history.removeChild(history.firstChild);
88
+ }
89
+ });
90
+
91
+ eventSource.addEventListener("error", function(event) {
92
+ document.getElementById("status").innerHTML = "❌ Connection error - attempting to reconnect...";
93
+ document.getElementById("status").className = "status error";
94
+ });
95
+ </script>
96
+ ```
97
+
98
+ **Key Points:**
99
+ - `EventSource` automatically handles connection and reconnection.
100
+ - Listen for `open`, `message`, and `error` events.
101
+ - `event.data` contains the message content.
102
+ - Implement UI feedback for connection status.
103
+
104
+ ### Routing Configuration
105
+
106
+ Add routes to your `config/routes.rb`:
107
+
108
+ ```ruby
109
+ Rails.application.routes.draw do
110
+ # SSE Example:
111
+ get 'sse/index' # Page with EventSource JavaScript
112
+ get 'sse/events' # SSE endpoint
113
+ end
114
+ ```
115
+
116
+ ## Advanced Patterns
117
+
118
+ ### Sending Custom Event Types
119
+
120
+ By default, `event: message` is assumed for each record sent on the stream. However, it's possible to specify different kinds of events:
121
+
122
+ ```ruby
123
+ def events
124
+ body = proc do |stream|
125
+ # Send different event types
126
+ stream.write("event: user_joined\n")
127
+ stream.write("data: #{user.to_json}\n\n")
128
+
129
+ stream.write("event: message\n")
130
+ stream.write("data: #{message.to_json}\n\n")
131
+ end
132
+
133
+ self.response = Rack::Response[200, EVENT_STREAM_HEADERS.dup, body]
134
+ end
135
+ ```
136
+
137
+ On the client, you need to register multiple event listeners:
138
+
139
+ ```javascript
140
+ eventSource.addEventListener("user_joined", function(event) {
141
+ var user = JSON.parse(event.data);
142
+ // Handle user joined event
143
+ });
144
+
145
+ eventSource.addEventListener("message", function(event) {
146
+ var message = JSON.parse(event.data);
147
+ // Handle message event
148
+ });
149
+ ```
@@ -0,0 +1,133 @@
1
+ # WebSockets
2
+
3
+ This guide explains how to implement WebSocket connections with Falcon and Rails.
4
+
5
+ ## What are WebSockets?
6
+
7
+ WebSockets provide full-duplex communication between client and server over a single persistent connection. Unlike HTTP streaming or SSE, WebSockets allow both client and server to send messages at any time.
8
+
9
+ **When to use WebSockets:**
10
+ - Real-time chat applications.
11
+ - Interactive games and collaborative tools.
12
+ - Live dashboards with user interaction.
13
+ - Real-time notifications with user actions.
14
+
15
+ **When NOT to use WebSockets:**
16
+ - Simple server-to-client updates (use SSE instead).
17
+ - Request/response patterns (use regular HTTP).
18
+
19
+ ## Basic Implementation
20
+
21
+ ### Server-Side: Rails Controller
22
+
23
+ Create a controller that handles WebSocket connections:
24
+
25
+ ```ruby
26
+ require 'async/websocket/adapters/rails'
27
+
28
+ class ChatController < ApplicationController
29
+ def index
30
+ # Render the page with WebSocket JavaScript
31
+ end
32
+
33
+ skip_before_action :verify_authenticity_token, only: :connect
34
+
35
+ def connect
36
+ self.response = Async::WebSocket::Adapters::Rails.open(request) do |connection|
37
+ Sync do
38
+ # Perpetually read incoming messages from client:
39
+ while message = connection.read
40
+ # Echo message back to client:
41
+ response_data = { text: "Echo: #{JSON.parse(message.buffer)['text']}" }
42
+ connection.send_text(response_data.to_json)
43
+ connection.flush
44
+ end
45
+ rescue Protocol::WebSocket::ClosedError
46
+ # Connection closed by client.
47
+ end
48
+ end
49
+ end
50
+ end
51
+ ```
52
+
53
+ **Key Points:**
54
+ - Use `Async::WebSocket::Adapters::Rails.open` for WebSocket handling.
55
+ - Skip CSRF token verification for WebSocket endpoints.
56
+ - Use `connection.read` to receive messages from clients.
57
+ - Use `connection.send_text` and `connection.flush` to send messages.
58
+
59
+ ### Client-Side: WebSocket JavaScript
60
+
61
+ Create JavaScript that connects to the WebSocket endpoint:
62
+
63
+ ```html
64
+ <section id="response"></section>
65
+ <section class="input">
66
+ <input id="chat" disabled="true" placeholder="Type your message and press Enter..." />
67
+ </section>
68
+
69
+ <script>
70
+ function connectToChatServer(url) {
71
+ console.log("WebSocket Connecting...", url);
72
+ var server = new WebSocket(url.href);
73
+
74
+ server.onopen = function(event) {
75
+ console.log("WebSocket Connected:", server);
76
+ chat.disabled = false;
77
+
78
+ chat.onkeypress = function(event) {
79
+ if (event.keyCode == 13) {
80
+ server.send(JSON.stringify({text: chat.value}));
81
+ chat.value = "";
82
+ }
83
+ }
84
+ };
85
+
86
+ server.onmessage = function(event) {
87
+ console.log("WebSocket Message:", event);
88
+
89
+ var message = JSON.parse(event.data);
90
+
91
+ var pre = document.createElement('pre');
92
+ pre.innerText = message.text;
93
+
94
+ response.appendChild(pre);
95
+ };
96
+
97
+ server.onerror = function(event) {
98
+ console.log("WebSocket Error:", event);
99
+ chat.disabled = true;
100
+ server.close();
101
+ };
102
+
103
+ server.onclose = function(event) {
104
+ console.log("WebSocket Close:", event);
105
+
106
+ setTimeout(function() {
107
+ connectToChatServer(url);
108
+ }, 1000);
109
+ };
110
+ }
111
+
112
+ var url = new URL('/chat/connect', window.location.href);
113
+ url.protocol = url.protocol.replace('http', 'ws');
114
+ connectToChatServer(url);
115
+ </script>
116
+ ```
117
+
118
+ **Key Points:**
119
+ - Convert HTTP URL to WebSocket URL by replacing protocol.
120
+ - Handle `onopen`, `onmessage`, `onerror`, and `onclose` events.
121
+ - Use `JSON.stringify` and `JSON.parse` for structured messages.
122
+ - Implement automatic reconnection on close.
123
+
124
+ ### Routing Configuration
125
+
126
+ Add routes to your `config/routes.rb`:
127
+
128
+ ```ruby
129
+ Rails.application.routes.draw do
130
+ get "chat/index"
131
+ connect "chat/connect"
132
+ end
133
+ ```
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @namespace
4
+ module Falcon
5
+ # @namespace
6
+ module Rails
7
+ VERSION = "0.2.0"
8
+ end
9
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025, by Samuel Williams.
5
+
6
+ require_relative "rails/version"
7
+
8
+ require "falcon"
9
+
10
+ # Load all the dependencies:
11
+ require "async/cable"
12
+ require "async/job/adapter/active_job"
13
+ require "async/websocket"
14
+ require "console/adapter/rails"
15
+ require "live"
data/license.md ADDED
@@ -0,0 +1,21 @@
1
+ # MIT License
2
+
3
+ Copyright, 2025, by Samuel Williams.
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 all
13
+ 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 THE
21
+ SOFTWARE.
data/readme.md ADDED
@@ -0,0 +1,54 @@
1
+ # Async::Rails
2
+
3
+ Provides integration support for Async and Rails, allowing you to use Async's concurrency model within a Rails application.
4
+
5
+ [![Development Status](https://github.com/socketry/falcon-rails/workflows/Test/badge.svg)](https://github.com/socketry/falcon-rails/actions?workflow=Test)
6
+
7
+ ## Usage
8
+
9
+ Please see the [project documentation](https://socketry.github.io/falcon-rails/) for more details.
10
+
11
+ - [Getting Started](https://socketry.github.io/falcon-rails/guides/getting-started/index) - This guide explains how to get started using Falcon to host your Rails application.
12
+
13
+ - [Job Processing](https://socketry.github.io/falcon-rails/guides/job-processing/index) - This guide explains how to implement background job processing with Falcon and Rails using the `async-job` gem.
14
+
15
+ - [HTTP Streaming](https://socketry.github.io/falcon-rails/guides/http-streaming/index) - This guide explains how to implement HTTP response streaming with Falcon and Rails.
16
+
17
+ - [Server-Sent Events](https://socketry.github.io/falcon-rails/guides/server-sent-events/index) - This guide explains how to implement Server-Sent Events with Falcon and Rails.
18
+
19
+ - [WebSockets](https://socketry.github.io/falcon-rails/guides/websockets/index) - This guide explains how to implement WebSocket connections with Falcon and Rails.
20
+
21
+ - [Real-Time Views](https://socketry.github.io/falcon-rails/guides/real-time-views/index) - This guide explains how to implement real-time interfaces with `Live::View`.
22
+
23
+ ## Releases
24
+
25
+ Please see the [project releases](https://socketry.github.io/falcon-rails/releases/index) for all releases.
26
+
27
+ ### v0.1.0
28
+
29
+ ## See Also
30
+
31
+ - [Async](https://github.com/socketry/async) - The core library for asynchronous programming in Ruby.
32
+ - [Async::Cable](https://github.com/socketry/async-cable) - Async-compatible ActionCable implementation.
33
+ - [Async::Job](https://github.com/socketry/async-job) - Background job processing with ActiveJob adapter.
34
+ - [Console::Adapter::Rails](https://github.com/socketry/console-adapter-rails) - Rails logging adapter for the console gem.
35
+ - [Falcon](https://github.com/socketry/falcon) - High-performance web server for Ruby.
36
+ - [Live](https://github.com/socketry/live) - Real-time server components for interactive web applications.
37
+
38
+ ## Contributing
39
+
40
+ We welcome contributions to this project.
41
+
42
+ 1. Fork it.
43
+ 2. Create your feature branch (`git checkout -b my-new-feature`).
44
+ 3. Commit your changes (`git commit -am 'Add some feature'`).
45
+ 4. Push to the branch (`git push origin my-new-feature`).
46
+ 5. Create new Pull Request.
47
+
48
+ ### Developer Certificate of Origin
49
+
50
+ In order to protect users of this project, we require all contributors to comply with the [Developer Certificate of Origin](https://developercertificate.org/). This ensures that all contributions are properly licensed and attributed.
51
+
52
+ ### Community Guidelines
53
+
54
+ This project is best served by a collaborative and respectful environment. Treat each other professionally, respect differing viewpoints, and engage constructively. Harassment, discrimination, or harmful behavior is not tolerated. Communicate clearly, listen actively, and support one another. If any issues arise, please inform the project maintainers.
data/releases.md ADDED
@@ -0,0 +1,3 @@
1
+ # Releases
2
+
3
+ ## v0.1.0
data.tar.gz.sig ADDED
Binary file
metadata ADDED
@@ -0,0 +1,177 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: falcon-rails
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Samuel Williams
8
+ bindir: bin
9
+ cert_chain:
10
+ - |
11
+ -----BEGIN CERTIFICATE-----
12
+ MIIE2DCCA0CgAwIBAgIBATANBgkqhkiG9w0BAQsFADBhMRgwFgYDVQQDDA9zYW11
13
+ ZWwud2lsbGlhbXMxHTAbBgoJkiaJk/IsZAEZFg1vcmlvbnRyYW5zZmVyMRIwEAYK
14
+ CZImiZPyLGQBGRYCY28xEjAQBgoJkiaJk/IsZAEZFgJuejAeFw0yMjA4MDYwNDUz
15
+ MjRaFw0zMjA4MDMwNDUzMjRaMGExGDAWBgNVBAMMD3NhbXVlbC53aWxsaWFtczEd
16
+ MBsGCgmSJomT8ixkARkWDW9yaW9udHJhbnNmZXIxEjAQBgoJkiaJk/IsZAEZFgJj
17
+ bzESMBAGCgmSJomT8ixkARkWAm56MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIB
18
+ igKCAYEAomvSopQXQ24+9DBB6I6jxRI2auu3VVb4nOjmmHq7XWM4u3HL+pni63X2
19
+ 9qZdoq9xt7H+RPbwL28LDpDNflYQXoOhoVhQ37Pjn9YDjl8/4/9xa9+NUpl9XDIW
20
+ sGkaOY0eqsQm1pEWkHJr3zn/fxoKPZPfaJOglovdxf7dgsHz67Xgd/ka+Wo1YqoE
21
+ e5AUKRwUuvaUaumAKgPH+4E4oiLXI4T1Ff5Q7xxv6yXvHuYtlMHhYfgNn8iiW8WN
22
+ XibYXPNP7NtieSQqwR/xM6IRSoyXKuS+ZNGDPUUGk8RoiV/xvVN4LrVm9upSc0ss
23
+ RZ6qwOQmXCo/lLcDUxJAgG95cPw//sI00tZan75VgsGzSWAOdjQpFM0l4dxvKwHn
24
+ tUeT3ZsAgt0JnGqNm2Bkz81kG4A2hSyFZTFA8vZGhp+hz+8Q573tAR89y9YJBdYM
25
+ zp0FM4zwMNEUwgfRzv1tEVVUEXmoFCyhzonUUw4nE4CFu/sE3ffhjKcXcY//qiSW
26
+ xm4erY3XAgMBAAGjgZowgZcwCQYDVR0TBAIwADALBgNVHQ8EBAMCBLAwHQYDVR0O
27
+ BBYEFO9t7XWuFf2SKLmuijgqR4sGDlRsMC4GA1UdEQQnMCWBI3NhbXVlbC53aWxs
28
+ aWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MC4GA1UdEgQnMCWBI3NhbXVlbC53aWxs
29
+ aWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MA0GCSqGSIb3DQEBCwUAA4IBgQB5sxkE
30
+ cBsSYwK6fYpM+hA5B5yZY2+L0Z+27jF1pWGgbhPH8/FjjBLVn+VFok3CDpRqwXCl
31
+ xCO40JEkKdznNy2avOMra6PFiQyOE74kCtv7P+Fdc+FhgqI5lMon6tt9rNeXmnW/
32
+ c1NaMRdxy999hmRGzUSFjozcCwxpy/LwabxtdXwXgSay4mQ32EDjqR1TixS1+smp
33
+ 8C/NCWgpIfzpHGJsjvmH2wAfKtTTqB9CVKLCWEnCHyCaRVuKkrKjqhYCdmMBqCws
34
+ JkxfQWC+jBVeG9ZtPhQgZpfhvh+6hMhraUYRQ6XGyvBqEUe+yo6DKIT3MtGE2+CP
35
+ eX9i9ZWBydWb8/rvmwmX2kkcBbX0hZS1rcR593hGc61JR6lvkGYQ2MYskBveyaxt
36
+ Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
37
+ voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
38
+ -----END CERTIFICATE-----
39
+ date: 1980-01-02 00:00:00.000000000 Z
40
+ dependencies:
41
+ - !ruby/object:Gem::Dependency
42
+ name: async-cable
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: async-job-adapter-active_job
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: async-websocket
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: console-adapter-rails
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: falcon
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: live
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: rails
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '8.0'
132
+ type: :runtime
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '8.0'
139
+ executables: []
140
+ extensions: []
141
+ extra_rdoc_files: []
142
+ files:
143
+ - context/getting-started.md
144
+ - context/http-streaming.md
145
+ - context/job-processing.md
146
+ - context/real-time-views.md
147
+ - context/server-sent-events.md
148
+ - context/websockets.md
149
+ - lib/falcon/rails.rb
150
+ - lib/falcon/rails/version.rb
151
+ - license.md
152
+ - readme.md
153
+ - releases.md
154
+ homepage: https://github.com/socketry/falcon-rails
155
+ licenses:
156
+ - MIT
157
+ metadata:
158
+ documentation_uri: https://socketry.github.io/falcon-rails/
159
+ source_code_uri: https://github.com/socketry/falcon-rails.git
160
+ rdoc_options: []
161
+ require_paths:
162
+ - lib
163
+ required_ruby_version: !ruby/object:Gem::Requirement
164
+ requirements:
165
+ - - ">="
166
+ - !ruby/object:Gem::Version
167
+ version: '3.2'
168
+ required_rubygems_version: !ruby/object:Gem::Requirement
169
+ requirements:
170
+ - - ">="
171
+ - !ruby/object:Gem::Version
172
+ version: '0'
173
+ requirements: []
174
+ rubygems_version: 3.6.7
175
+ specification_version: 4
176
+ summary: Easy Falcon and Rails integration.
177
+ test_files: []
metadata.gz.sig ADDED
Binary file