flow_chat 0.9.0 → 0.10.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 +4 -4
- data/.github/workflows/pages.yml +43 -0
- data/CHANGELOG.md +26 -0
- data/README.md +166 -295
- data/Rakefile +12 -1
- data/SECURITY.md +1 -1
- data/docs/architecture.md +52 -479
- data/docs/async-background-processing.md +31 -265
- data/docs/configuration.md +106 -613
- data/docs/factory-pattern.md +27 -324
- data/docs/gateway-context-variables.md +140 -143
- data/docs/gateway-development.md +86 -650
- data/docs/getting-started.md +40 -379
- data/docs/instrumentation.md +88 -279
- data/docs/platforms/instagram.md +278 -0
- data/docs/platforms/messenger.md +205 -0
- data/docs/platforms/telegram.md +47 -951
- data/docs/platforms/ussd.md +38 -653
- data/docs/platforms/whatsapp.md +73 -1321
- data/docs/superpowers/plans/2026-07-09-inbound-media-support.md +732 -0
- data/docs/superpowers/plans/2026-07-09-inbound-media-support.md.tasks.json +58 -0
- data/docs/superpowers/plans/2026-08-10-messenger-instagram.md +4064 -0
- data/docs/superpowers/plans/2026-08-10-messenger-instagram.md.tasks.json +226 -0
- data/docs/superpowers/plans/2026-08-16-unified-choice-resolution.md +972 -0
- data/docs/superpowers/plans/2026-08-16-unified-choice-resolution.md.tasks.json +88 -0
- data/docs/superpowers/specs/2026-07-09-inbound-media-support-design.md +195 -0
- data/docs/superpowers/specs/2026-08-10-messenger-instagram-design.md +391 -0
- data/docs/testing.md +26 -297
- data/examples/http_controller.rb +12 -10
- data/examples/intercom_configuration_example.rb +19 -24
- data/examples/intercom_controller.rb +8 -20
- data/examples/multi_tenant_whatsapp_controller.rb +61 -166
- data/examples/simulator_controller.rb +0 -1
- data/examples/ussd_controller.rb +86 -158
- data/examples/whatsapp_controller.rb +16 -15
- data/examples/whatsapp_media_examples.rb +27 -79
- data/lib/flow_chat/app.rb +129 -11
- data/lib/flow_chat/choice_titles.rb +95 -0
- data/lib/flow_chat/config.rb +124 -1
- data/lib/flow_chat/delivery_error.rb +9 -0
- data/lib/flow_chat/http/configuration_error.rb +9 -0
- data/lib/flow_chat/http/gateway/simple.rb +24 -4
- data/lib/flow_chat/http/middleware/choice_mapper.rb +94 -0
- data/lib/flow_chat/input.rb +86 -0
- data/lib/flow_chat/instagram/client.rb +32 -0
- data/lib/flow_chat/instagram/configuration.rb +147 -0
- data/lib/flow_chat/instagram/configuration_error.rb +7 -0
- data/lib/flow_chat/instagram/gateway/send_api.rb +63 -0
- data/lib/flow_chat/instagram/middleware/choice_mapper.rb +22 -0
- data/lib/flow_chat/instagram/renderer.rb +23 -0
- data/lib/flow_chat/instrumentation/metrics_collector.rb +6 -1
- data/lib/flow_chat/instrumentation.rb +160 -1
- data/lib/flow_chat/intercom/client.rb +34 -28
- data/lib/flow_chat/intercom/configuration.rb +2 -49
- data/lib/flow_chat/intercom/configuration_error.rb +9 -0
- data/lib/flow_chat/intercom/gateway/intercom_api.rb +81 -57
- data/lib/flow_chat/intercom/middleware/choice_mapper.rb +101 -0
- data/lib/flow_chat/intercom/renderer.rb +57 -5
- data/lib/flow_chat/media.rb +121 -0
- data/lib/flow_chat/messenger/client.rb +264 -0
- data/lib/flow_chat/messenger/configuration.rb +103 -0
- data/lib/flow_chat/messenger/configuration_error.rb +9 -0
- data/lib/flow_chat/messenger/gateway/send_api.rb +42 -0
- data/lib/flow_chat/messenger/middleware/choice_mapper.rb +185 -0
- data/lib/flow_chat/messenger/renderer.rb +150 -0
- data/lib/flow_chat/meta/challenge.rb +24 -0
- data/lib/flow_chat/meta/choice_ladder.rb +37 -0
- data/lib/flow_chat/meta/configuration_error.rb +7 -0
- data/lib/flow_chat/meta/gateway_identity.rb +38 -0
- data/lib/flow_chat/meta/messaging_gateway.rb +468 -0
- data/lib/flow_chat/meta/signature.rb +30 -0
- data/lib/flow_chat/meta/signature_validation.rb +66 -0
- data/lib/flow_chat/meta/webhook_verification.rb +43 -0
- data/lib/flow_chat/named_configuration.rb +65 -0
- data/lib/flow_chat/prompt.rb +13 -16
- data/lib/flow_chat/renderers/markdown_support.rb +109 -0
- data/lib/flow_chat/security.rb +76 -0
- data/lib/flow_chat/session/middleware.rb +11 -2
- data/lib/flow_chat/simulator/controller.rb +31 -15
- data/lib/flow_chat/simulator/views/simulator.html.erb +184 -20
- data/lib/flow_chat/telegram/client.rb +47 -4
- data/lib/flow_chat/telegram/configuration.rb +2 -42
- data/lib/flow_chat/telegram/configuration_error.rb +9 -0
- data/lib/flow_chat/telegram/gateway/bot_api.rb +46 -28
- data/lib/flow_chat/telegram/middleware/choice_mapper.rb +77 -16
- data/lib/flow_chat/telegram/renderer.rb +10 -2
- data/lib/flow_chat/text_truncator.rb +75 -0
- data/lib/flow_chat/ussd/middleware/choice_mapper.rb +10 -0
- data/lib/flow_chat/version.rb +1 -1
- data/lib/flow_chat/whatsapp/client.rb +14 -7
- data/lib/flow_chat/whatsapp/configuration.rb +12 -51
- data/lib/flow_chat/whatsapp/configuration_error.rb +9 -0
- data/lib/flow_chat/whatsapp/gateway/cloud_api.rb +254 -203
- data/lib/flow_chat/whatsapp/middleware/choice_mapper.rb +144 -57
- data/lib/flow_chat/whatsapp/renderer.rb +121 -60
- data/lib/flow_chat.rb +0 -10
- data/site/.nojekyll +0 -0
- data/site/.og-card.html +89 -0
- data/site/favicon.svg +6 -0
- data/site/index.html +209 -0
- data/site/og.png +0 -0
- metadata +51 -3
- data/lib/flow_chat/whatsapp/id_generator.rb +0 -124
|
@@ -1,298 +1,64 @@
|
|
|
1
|
-
# Async
|
|
1
|
+
# Async and background processing
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A webhook has a response deadline. If a flow does slow work (calling the platform API, hitting your own services) the webhook can time out and the platform will retry, sometimes delivering the same message twice. Async processing acknowledges the webhook immediately and runs the flow in a background job.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
This works on gateways with an outbound API (WhatsApp, Telegram, HTTP, Intercom), because the job can send the reply through that API afterward. USSD cannot use it: its protocol requires the answer in the webhook response itself.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
## Enabling it
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Call `use_async` in the processor. It has two forms.
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
# config/initializers/flow_chat.rb
|
|
13
|
-
FlowChat::Factory.register :whatsapp do |controller|
|
|
14
|
-
processor = FlowChat::Processor.new(controller) do |config|
|
|
15
|
-
config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi
|
|
16
|
-
config.use_session_store FlowChat::Session::CacheSessionStore
|
|
17
|
-
config.use_session_config(boundaries: [:flow])
|
|
18
|
-
config.use_async(factory: :whatsapp) # Self-referencing for async
|
|
19
|
-
end
|
|
20
|
-
processor.run(WhatsAppFlow, :start)
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
# app/controllers/webhooks_controller.rb
|
|
24
|
-
class WebhooksController < ApplicationController
|
|
25
|
-
def whatsapp
|
|
26
|
-
FlowChat::Factory.execute(:whatsapp, controller: self)
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
**Benefits:**
|
|
32
|
-
- No custom job class needed
|
|
33
|
-
- Configuration defined once in initializer
|
|
34
|
-
- Works seamlessly in both webhook and background contexts
|
|
35
|
-
|
|
36
|
-
See [Factory Pattern Documentation](factory-pattern.md) for more details.
|
|
37
|
-
|
|
38
|
-
### Option 2: Custom Job Class
|
|
39
|
-
|
|
40
|
-
For advanced use cases, create a custom job class:
|
|
11
|
+
Use a factory, and let FlowChat provide the job class:
|
|
41
12
|
|
|
42
13
|
```ruby
|
|
43
|
-
|
|
44
|
-
class MyFlowJob < FlowChat::AsyncJob
|
|
45
|
-
def execute(controller, **job_params)
|
|
46
|
-
# Access job params passed from use_async
|
|
47
|
-
deployment_id = job_params[:deployment_id]
|
|
48
|
-
|
|
49
|
-
# Build and run your processor
|
|
50
|
-
processor = FlowChat::Processor.new(controller) do |config|
|
|
51
|
-
config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi
|
|
52
|
-
config.use_session_store FlowChat::Session::CacheSessionStore
|
|
53
|
-
config.use_session_config(boundaries: [:flow])
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
processor.run MyFlow, :start
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
# app/controllers/webhooks_controller.rb
|
|
61
|
-
class WebhooksController < ApplicationController
|
|
62
|
-
def whatsapp
|
|
63
|
-
processor = FlowChat::Processor.new(self) do |config|
|
|
64
|
-
config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi
|
|
65
|
-
config.use_session_store FlowChat::Session::CacheSessionStore
|
|
66
|
-
config.use_async MyFlowJob, deployment_id: params[:deployment_id]
|
|
67
|
-
end
|
|
68
|
-
processor.run(MyFlow, :start)
|
|
69
|
-
end
|
|
70
|
-
end
|
|
14
|
+
config.use_async(factory: :whatsapp)
|
|
71
15
|
```
|
|
72
16
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
### Session Store
|
|
76
|
-
|
|
77
|
-
Use a centralized cache store e.g. `CacheSessionStore` for async processing:
|
|
17
|
+
Or use your own ActiveJob subclass, with optional job params:
|
|
78
18
|
|
|
79
19
|
```ruby
|
|
80
|
-
config.
|
|
20
|
+
config.use_async(MyFlowJob, deployment_id: 123)
|
|
81
21
|
```
|
|
82
22
|
|
|
83
|
-
|
|
23
|
+
The `factory:` form is the common one and needs no custom job class. See [factory-pattern.md](factory-pattern.md).
|
|
84
24
|
|
|
85
|
-
|
|
86
|
-
# config/application.rb
|
|
87
|
-
config.active_job.queue_adapter = :sidekiq # or :resque, :delayed_job
|
|
88
|
-
```
|
|
25
|
+
## How it works
|
|
89
26
|
|
|
90
|
-
|
|
27
|
+
The `GatewayAsyncSupport` concern, mixed into every async-capable gateway, does the detection and enqueueing.
|
|
91
28
|
|
|
92
|
-
|
|
29
|
+
1. On a real webhook, the gateway calls `should_enqueue_async?`. It returns true when async is enabled, the gateway supports it (`async_supported?`), and the request is not already running in the background.
|
|
30
|
+
2. If so, the gateway serializes the request (params, method, headers, host, path, body, remote ip) and enqueues the job with `perform_later(request_context: ..., **job_params)`, then returns an immediate acknowledgement to the platform.
|
|
31
|
+
3. The job reconstructs a `FlowChat::BackgroundController` from the serialized request. It quacks like a Rails controller (its `render` and `head` are no-ops, its `request` is a `BackgroundRequest` rebuilt from the serialized data), so the same gateway code runs against it.
|
|
32
|
+
4. Running in the background, `should_enqueue_async?` now returns false (the controller is a `BackgroundController`), so the gateway processes the flow inline and sends the reply through the platform API.
|
|
93
33
|
|
|
94
|
-
|
|
95
|
-
class MyFlowJob < FlowChat::AsyncJob
|
|
96
|
-
queue_as :support_flows
|
|
34
|
+
The same gateway and the same flow run in both passes. The only difference is who calls them: the webhook the first time, the job the second.
|
|
97
35
|
|
|
98
|
-
|
|
99
|
-
# ...
|
|
100
|
-
end
|
|
101
|
-
end
|
|
102
|
-
```
|
|
36
|
+
## Custom jobs
|
|
103
37
|
|
|
104
|
-
|
|
38
|
+
`FlowChat::AsyncJob` is the base class. It handles reconstructing the controller in `perform`; you implement `execute(controller, **job_params)`:
|
|
105
39
|
|
|
106
40
|
```ruby
|
|
107
|
-
# config/sidekiq.yml
|
|
108
|
-
:queues:
|
|
109
|
-
- [critical, 2]
|
|
110
|
-
- [support_flows, 1]
|
|
111
|
-
- [default, 1]
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
## Passing Parameters to Jobs
|
|
115
|
-
|
|
116
|
-
### Job Params
|
|
117
|
-
|
|
118
|
-
When using custom job classes, you can pass additional parameters via `use_async`:
|
|
119
|
-
|
|
120
|
-
```ruby
|
|
121
|
-
# In controller
|
|
122
|
-
config.use_async(MyFlowJob, deployment_id: 123, flow_name: "SupportFlow")
|
|
123
|
-
|
|
124
|
-
# In job execute method
|
|
125
41
|
class MyFlowJob < FlowChat::AsyncJob
|
|
126
42
|
def execute(controller, **job_params)
|
|
127
|
-
deployment_id = job_params[:deployment_id]
|
|
128
|
-
flow_name = job_params[:flow_name] # => "SupportFlow"
|
|
129
|
-
|
|
130
|
-
# Use params for business logic
|
|
43
|
+
deployment_id = job_params[:deployment_id]
|
|
131
44
|
processor = FlowChat::Processor.new(controller) do |config|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
end
|
|
136
|
-
end
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
**Note:** Request params (from `controller.params`) are automatically available in the background job via the reconstructed `controller.params`. Job params are for additional data specific to job execution logic.
|
|
140
|
-
|
|
141
|
-
### Factory with GenericAsyncJob
|
|
142
|
-
|
|
143
|
-
When using `factory:` param, additional params are ignored by `GenericAsyncJob` but can be used by the factory:
|
|
144
|
-
|
|
145
|
-
```ruby
|
|
146
|
-
config.use_async(factory: :whatsapp, extra_data: "value")
|
|
147
|
-
# extra_data is passed to perform but not used by GenericAsyncJob
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
## Gateway Support
|
|
151
|
-
|
|
152
|
-
| Gateway | Async Support | Nil Response Handling |
|
|
153
|
-
|---------|--------------|----------------------|
|
|
154
|
-
| WhatsApp Cloud API | ✅ | ✅ Returns silently |
|
|
155
|
-
| Intercom API | ✅ | ✅ Returns silently |
|
|
156
|
-
| HTTP Simple | ✅ | ✅ Returns `{type: :skip}` JSON |
|
|
157
|
-
| USSD Nalo | ❌ (synchronous protocol) | ❌ Requires immediate response |
|
|
158
|
-
|
|
159
|
-
### Middleware Nil Response Handling
|
|
160
|
-
|
|
161
|
-
Async-capable gateways support middleware that returns `nil` instead of a response tuple. This is useful for middleware that handles responses directly (e.g., `AgentHandoffMiddleware`):
|
|
162
|
-
|
|
163
|
-
```ruby
|
|
164
|
-
class AgentHandoffMiddleware
|
|
165
|
-
def call(context)
|
|
166
|
-
if should_handoff_to_agent?
|
|
167
|
-
# Middleware handles response directly
|
|
168
|
-
send_to_agent(context)
|
|
169
|
-
return nil # Signal that response was handled
|
|
45
|
+
config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi
|
|
46
|
+
config.use_session_store FlowChat::Session::CacheSessionStore
|
|
47
|
+
config.use_async(MyFlowJob, deployment_id: deployment_id)
|
|
170
48
|
end
|
|
171
|
-
|
|
172
|
-
@app.call(context) # Continue to next middleware
|
|
49
|
+
processor.run(WhatsAppFlow, :start)
|
|
173
50
|
end
|
|
174
51
|
end
|
|
175
52
|
```
|
|
176
53
|
|
|
177
|
-
|
|
178
|
-
- **HTTP Simple**: Returns JSON response `{type: :skip, session_id, user_id, timestamp}`
|
|
179
|
-
- **WhatsApp/Intercom**: Returns silently (message already sent by middleware)
|
|
180
|
-
- **USSD**: Not supported - synchronous protocol requires immediate response
|
|
181
|
-
|
|
182
|
-
**Note**: USSD cannot support middleware that returns nil because the USSD protocol requires an immediate synchronous response to every request.
|
|
183
|
-
|
|
184
|
-
## How It Works
|
|
185
|
-
|
|
186
|
-
### Execution Paths
|
|
187
|
-
|
|
188
|
-
FlowChat's async system automatically routes requests through one of three paths:
|
|
189
|
-
|
|
190
|
-
#### 1. Webhook → Async Job (async enabled, webhook context)
|
|
191
|
-
|
|
192
|
-
```
|
|
193
|
-
1. Webhook request arrives at controller
|
|
194
|
-
2. Processor.run() called with use_async configured
|
|
195
|
-
3. Gateway checks: should_enqueue_async?
|
|
196
|
-
- Not in background? ✅
|
|
197
|
-
- Async enabled? ✅
|
|
198
|
-
- Gateway supports async? ✅
|
|
199
|
-
4. Gateway serializes request context:
|
|
200
|
-
- params (session_id, user_id, input, etc.)
|
|
201
|
-
- method (POST/GET)
|
|
202
|
-
- headers (Content-Type, User-Agent)
|
|
203
|
-
5. Gateway enqueues job: JobClass.perform_later(request_context: {...})
|
|
204
|
-
6. Gateway returns immediately (< 100ms)
|
|
205
|
-
7. Background worker picks up job
|
|
206
|
-
8. AsyncJob.perform creates BackgroundController from request_context
|
|
207
|
-
9. User's execute() called with controller
|
|
208
|
-
10. Flow processes normally with reconstructed request
|
|
209
|
-
```
|
|
210
|
-
|
|
211
|
-
#### 2. Background Job → Inline (async enabled, background context)
|
|
212
|
-
|
|
213
|
-
```
|
|
214
|
-
1. Background job executes
|
|
215
|
-
2. AsyncJob.perform creates BackgroundController
|
|
216
|
-
3. User's execute() creates new processor
|
|
217
|
-
4. Processor.run() called
|
|
218
|
-
5. Gateway checks: should_enqueue_async?
|
|
219
|
-
- Not in background? ❌ (BackgroundController detected)
|
|
220
|
-
- Async enabled? ✅
|
|
221
|
-
- Gateway supports async? ✅
|
|
222
|
-
6. Gateway processes inline (prevents double-enqueueing)
|
|
223
|
-
7. Flow executes, calls controller.render (no-op in background)
|
|
224
|
-
8. Job completes
|
|
225
|
-
```
|
|
226
|
-
|
|
227
|
-
#### 3. Webhook → Inline (async not configured)
|
|
228
|
-
|
|
229
|
-
```
|
|
230
|
-
1. Webhook request arrives
|
|
231
|
-
2. Processor.run() called without use_async
|
|
232
|
-
3. Gateway checks: should_enqueue_async?
|
|
233
|
-
- Not in background? ✅
|
|
234
|
-
- Async enabled? ❌
|
|
235
|
-
- Gateway supports async? ✅
|
|
236
|
-
4. Gateway processes inline
|
|
237
|
-
5. Flow executes
|
|
238
|
-
6. Response returned to webhook provider
|
|
239
|
-
```
|
|
240
|
-
|
|
241
|
-
### Key Components
|
|
54
|
+
The job params you pass to `use_async` are forwarded to `execute`, so the background pass can rebuild the same processor. `FlowChat::GenericAsyncJob` is exactly this pattern wrapped around a factory, which is why the `factory:` form needs no job class of your own.
|
|
242
55
|
|
|
243
|
-
|
|
244
|
-
- Base class users inherit from
|
|
245
|
-
- `perform(request_context:, **job_params)` entry point
|
|
246
|
-
- Creates `BackgroundController` from serialized request
|
|
247
|
-
- Calls user's `execute(controller, **job_params)` method
|
|
248
|
-
|
|
249
|
-
**GenericAsyncJob**
|
|
250
|
-
- Built-in job that uses Factory pattern
|
|
251
|
-
- Automatically used when `use_async(factory: :name)` is called
|
|
252
|
-
- Executes registered factory in background context
|
|
253
|
-
- Validates factory is registered before execution
|
|
254
|
-
|
|
255
|
-
**BackgroundController**
|
|
256
|
-
- Duck-types as Rails controller
|
|
257
|
-
- Provides `request`, `params`, `render`, `head` interface
|
|
258
|
-
- `params` delegates to `request.params` (mimics Rails controller)
|
|
259
|
-
- `render` and `head` are no-ops in background context
|
|
260
|
-
- `is_a?(FlowChat::BackgroundController)` returns true for detection
|
|
261
|
-
|
|
262
|
-
**BackgroundRequest**
|
|
263
|
-
- Reconstructs full Rails request interface from serialized data
|
|
264
|
-
- Provides `params`, `method`, `headers`, `host`, `path`, `remote_ip` accessors
|
|
265
|
-
- HTTP method predicates: `post?`, `get?`, `head?`
|
|
266
|
-
- Rails compatibility methods: `request_method`, `user_agent`, `ssl?`
|
|
267
|
-
- Request body support: `body` returns object with `read()` and `rewind()` methods
|
|
268
|
-
- Body content is serialized from webhook and reconstructed in background
|
|
269
|
-
- Returns empty hash for `cookies` (not available in background context)
|
|
270
|
-
|
|
271
|
-
**GatewayAsyncSupport**
|
|
272
|
-
- Concern mixed into all gateways
|
|
273
|
-
- `should_enqueue_async?` decision logic
|
|
274
|
-
- `in_background?` detection via `BackgroundController`
|
|
275
|
-
- `enqueue_async_job` serializes and enqueues
|
|
276
|
-
- `async_supported?` override for gateways like USSD
|
|
277
|
-
|
|
278
|
-
### Decision Logic
|
|
279
|
-
|
|
280
|
-
The gateway's `should_enqueue_async?` method checks three conditions:
|
|
281
|
-
|
|
282
|
-
```ruby
|
|
283
|
-
def should_enqueue_async?
|
|
284
|
-
processor = @context["processor"]
|
|
285
|
-
|
|
286
|
-
!in_background? && # Prevent double-enqueueing
|
|
287
|
-
processor&.async_enabled? && # User opted in
|
|
288
|
-
async_supported? # Gateway allows it
|
|
289
|
-
end
|
|
290
|
-
```
|
|
56
|
+
## ActiveJob is optional
|
|
291
57
|
|
|
292
|
-
|
|
58
|
+
`FlowChat::AsyncJob` subclasses `ActiveJob::Base` when ActiveJob is available. Without ActiveJob, FlowChat falls back to a plain class, so requiring the gem does not fail; you supply the queueing yourself in that case.
|
|
293
59
|
|
|
294
|
-
|
|
295
|
-
- Background jobs never enqueue another job
|
|
296
|
-
- Users explicitly opt in per processor
|
|
297
|
-
- Synchronous protocols (USSD) always process inline
|
|
60
|
+
## Related
|
|
298
61
|
|
|
62
|
+
- [Factory pattern](factory-pattern.md)
|
|
63
|
+
- [Configuration](configuration.md#async-processing)
|
|
64
|
+
- [Architecture](architecture.md)
|