cmdx 1.1.0 → 1.1.1
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/.cursor/prompts/docs.md +9 -0
- data/.cursor/prompts/rspec.md +13 -12
- data/.cursor/prompts/yardoc.md +11 -6
- data/CHANGELOG.md +13 -2
- data/README.md +1 -0
- data/docs/ai_prompts.md +269 -195
- data/docs/basics/call.md +124 -58
- data/docs/basics/chain.md +190 -160
- data/docs/basics/context.md +242 -154
- data/docs/basics/setup.md +302 -32
- data/docs/callbacks.md +390 -94
- data/docs/configuration.md +181 -65
- data/docs/deprecation.md +245 -0
- data/docs/getting_started.md +161 -39
- data/docs/internationalization.md +590 -70
- data/docs/interruptions/exceptions.md +135 -118
- data/docs/interruptions/faults.md +150 -125
- data/docs/interruptions/halt.md +134 -80
- data/docs/logging.md +181 -118
- data/docs/middlewares.md +150 -377
- data/docs/outcomes/result.md +140 -112
- data/docs/outcomes/states.md +134 -99
- data/docs/outcomes/statuses.md +204 -146
- data/docs/parameters/coercions.md +232 -281
- data/docs/parameters/defaults.md +224 -169
- data/docs/parameters/definitions.md +289 -141
- data/docs/parameters/namespacing.md +250 -161
- data/docs/parameters/validations.md +260 -133
- data/docs/testing.md +191 -197
- data/docs/workflows.md +143 -98
- data/lib/cmdx/callback.rb +23 -19
- data/lib/cmdx/callback_registry.rb +1 -3
- data/lib/cmdx/chain_inspector.rb +23 -23
- data/lib/cmdx/chain_serializer.rb +38 -19
- data/lib/cmdx/coercion.rb +20 -12
- data/lib/cmdx/coercion_registry.rb +51 -32
- data/lib/cmdx/configuration.rb +84 -31
- data/lib/cmdx/context.rb +32 -21
- data/lib/cmdx/core_ext/hash.rb +13 -13
- data/lib/cmdx/core_ext/module.rb +1 -1
- data/lib/cmdx/core_ext/object.rb +12 -12
- data/lib/cmdx/correlator.rb +60 -39
- data/lib/cmdx/errors.rb +105 -131
- data/lib/cmdx/fault.rb +66 -45
- data/lib/cmdx/immutator.rb +20 -21
- data/lib/cmdx/lazy_struct.rb +78 -70
- data/lib/cmdx/log_formatters/json.rb +1 -1
- data/lib/cmdx/log_formatters/key_value.rb +1 -1
- data/lib/cmdx/log_formatters/line.rb +1 -1
- data/lib/cmdx/log_formatters/logstash.rb +1 -1
- data/lib/cmdx/log_formatters/pretty_json.rb +1 -1
- data/lib/cmdx/log_formatters/pretty_key_value.rb +1 -1
- data/lib/cmdx/log_formatters/pretty_line.rb +1 -1
- data/lib/cmdx/log_formatters/raw.rb +2 -2
- data/lib/cmdx/logger.rb +19 -14
- data/lib/cmdx/logger_ansi.rb +33 -17
- data/lib/cmdx/logger_serializer.rb +85 -24
- data/lib/cmdx/middleware.rb +39 -21
- data/lib/cmdx/middleware_registry.rb +4 -3
- data/lib/cmdx/parameter.rb +151 -89
- data/lib/cmdx/parameter_inspector.rb +34 -21
- data/lib/cmdx/parameter_registry.rb +36 -30
- data/lib/cmdx/parameter_serializer.rb +21 -14
- data/lib/cmdx/result.rb +136 -135
- data/lib/cmdx/result_ansi.rb +31 -17
- data/lib/cmdx/result_inspector.rb +32 -27
- data/lib/cmdx/result_logger.rb +23 -14
- data/lib/cmdx/result_serializer.rb +65 -27
- data/lib/cmdx/task.rb +234 -113
- data/lib/cmdx/task_deprecator.rb +22 -25
- data/lib/cmdx/task_processor.rb +89 -88
- data/lib/cmdx/task_serializer.rb +27 -14
- data/lib/cmdx/utils/monotonic_runtime.rb +2 -4
- data/lib/cmdx/validator.rb +25 -16
- data/lib/cmdx/validator_registry.rb +53 -31
- data/lib/cmdx/validators/exclusion.rb +1 -1
- data/lib/cmdx/validators/format.rb +2 -2
- data/lib/cmdx/validators/inclusion.rb +2 -2
- data/lib/cmdx/validators/length.rb +2 -2
- data/lib/cmdx/validators/numeric.rb +3 -3
- data/lib/cmdx/validators/presence.rb +2 -2
- data/lib/cmdx/version.rb +1 -1
- data/lib/cmdx/workflow.rb +54 -33
- data/lib/generators/cmdx/task_generator.rb +6 -6
- data/lib/generators/cmdx/workflow_generator.rb +6 -6
- metadata +3 -1
data/docs/basics/chain.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Basics - Chain
|
2
2
|
|
3
|
-
|
3
|
+
Chains automatically group related task executions within a thread, providing unified tracking, correlation, and execution context management. Each thread maintains its own chain through thread-local storage, eliminating the need for manual coordination.
|
4
4
|
|
5
5
|
## Table of Contents
|
6
6
|
|
@@ -12,266 +12,296 @@ A chain represents a collection of related task executions that share a common e
|
|
12
12
|
- [Correlation ID Integration](#correlation-id-integration)
|
13
13
|
- [State Delegation](#state-delegation)
|
14
14
|
- [Serialization and Logging](#serialization-and-logging)
|
15
|
+
- [Error Handling](#error-handling)
|
15
16
|
|
16
17
|
## TLDR
|
17
18
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
19
|
+
```ruby
|
20
|
+
# Automatic chain creation per thread
|
21
|
+
result = ProcessOrderTask.call(order_id: 123)
|
22
|
+
result.chain.id # Unique chain ID
|
23
|
+
result.chain.results.size # All tasks in this chain
|
24
|
+
|
25
|
+
# Access current thread's chain
|
26
|
+
CMDx::Chain.current # Current chain or nil
|
27
|
+
CMDx::Chain.clear # Clear thread's chain
|
28
|
+
|
29
|
+
# Subtasks automatically inherit chain
|
30
|
+
class ProcessOrderTask < CMDx::Task
|
31
|
+
def call
|
32
|
+
# These inherit the same chain automatically
|
33
|
+
ValidateOrderTask.call!(order_id: order_id)
|
34
|
+
ChargePaymentTask.call!(order_id: order_id)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
```
|
23
38
|
|
24
39
|
## Thread-Local Chain Management
|
25
40
|
|
26
|
-
|
41
|
+
> [!NOTE]
|
42
|
+
> Each thread maintains its own chain context through thread-local storage, providing automatic isolation without manual coordination.
|
27
43
|
|
28
44
|
```ruby
|
29
|
-
#
|
45
|
+
# Thread A
|
30
46
|
Thread.new do
|
31
47
|
result = ProcessOrderTask.call(order_id: 123)
|
32
|
-
result.chain.id #
|
48
|
+
result.chain.id # "018c2b95-b764-7615-a924-cc5b910ed1e5"
|
33
49
|
end
|
34
50
|
|
51
|
+
# Thread B (completely separate chain)
|
35
52
|
Thread.new do
|
36
53
|
result = ProcessOrderTask.call(order_id: 456)
|
37
|
-
result.chain.id #
|
54
|
+
result.chain.id # "018c2b95-c821-7892-b156-dd7c921fe2a3"
|
38
55
|
end
|
39
56
|
|
40
|
-
# Access
|
41
|
-
CMDx::Chain.current #
|
42
|
-
CMDx::Chain.clear #
|
57
|
+
# Access current thread's chain
|
58
|
+
CMDx::Chain.current # Returns current chain or nil
|
59
|
+
CMDx::Chain.clear # Clears current thread's chain
|
43
60
|
```
|
44
61
|
|
45
62
|
## Automatic Chain Creation
|
46
63
|
|
47
|
-
Every task execution automatically creates or joins
|
64
|
+
Every task execution automatically creates or joins the current thread's chain:
|
48
65
|
|
49
66
|
```ruby
|
50
|
-
#
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
#
|
56
|
-
result2 =
|
57
|
-
result2.chain.id ==
|
58
|
-
result2.chain.results.size
|
67
|
+
# First task creates new chain
|
68
|
+
result1 = ProcessOrderTask.call(order_id: 123)
|
69
|
+
result1.chain.id # "018c2b95-b764-7615-a924-cc5b910ed1e5"
|
70
|
+
result1.chain.results.size # 1
|
71
|
+
|
72
|
+
# Second task joins existing chain
|
73
|
+
result2 = SendEmailTask.call(to: "user@example.com")
|
74
|
+
result2.chain.id == result1.chain.id # true
|
75
|
+
result2.chain.results.size # 2
|
76
|
+
|
77
|
+
# Both results reference the same chain
|
78
|
+
result1.chain.results == result2.chain.results # true
|
59
79
|
```
|
60
80
|
|
61
81
|
## Chain Inheritance
|
62
82
|
|
63
|
-
|
83
|
+
> [!IMPORTANT]
|
84
|
+
> When tasks call subtasks within the same thread, all executions automatically inherit the current chain, creating a unified execution trail.
|
64
85
|
|
65
86
|
```ruby
|
66
|
-
class
|
87
|
+
class ProcessOrderTask < CMDx::Task
|
67
88
|
def call
|
68
89
|
context.order = Order.find(order_id)
|
69
90
|
|
70
|
-
# Subtasks automatically inherit
|
71
|
-
|
72
|
-
|
91
|
+
# Subtasks automatically inherit current chain
|
92
|
+
ValidateOrderTask.call!(order_id: order_id)
|
93
|
+
ChargePaymentTask.call!(order_id: order_id)
|
94
|
+
SendConfirmationTask.call!(order_id: order_id)
|
73
95
|
end
|
74
96
|
end
|
75
97
|
|
76
|
-
result =
|
98
|
+
result = ProcessOrderTask.call(order_id: 123)
|
77
99
|
chain = result.chain
|
78
100
|
|
79
|
-
# All
|
80
|
-
chain.results.size
|
101
|
+
# All tasks share the same chain
|
102
|
+
chain.results.size # 4 (main task + 3 subtasks)
|
81
103
|
chain.results.map(&:task).map(&:class)
|
82
|
-
|
104
|
+
# [ProcessOrderTask, ValidateOrderTask, ChargePaymentTask, SendConfirmationTask]
|
83
105
|
```
|
84
106
|
|
85
|
-
> [!NOTE]
|
86
|
-
> Tasks automatically inherit the current thread's chain, creating a unified execution trail for debugging and monitoring purposes without any manual chain management.
|
87
|
-
|
88
107
|
## Chain Structure and Metadata
|
89
108
|
|
90
|
-
Chains provide comprehensive execution information:
|
109
|
+
Chains provide comprehensive execution information with state delegation:
|
91
110
|
|
92
111
|
```ruby
|
93
|
-
result =
|
112
|
+
result = ProcessOrderTask.call(order_id: 123)
|
94
113
|
chain = result.chain
|
95
114
|
|
96
115
|
# Chain identification
|
97
|
-
chain.id
|
98
|
-
chain.results
|
99
|
-
|
100
|
-
#
|
101
|
-
chain.state
|
102
|
-
chain.status
|
103
|
-
chain.outcome
|
104
|
-
chain.runtime
|
116
|
+
chain.id # "018c2b95-b764-7615-a924-cc5b910ed1e5"
|
117
|
+
chain.results # Array of all results in execution order
|
118
|
+
|
119
|
+
# State delegation (from first/outer-most result)
|
120
|
+
chain.state # "complete"
|
121
|
+
chain.status # "success"
|
122
|
+
chain.outcome # "success"
|
123
|
+
chain.runtime # 1.2 (total execution time)
|
124
|
+
|
125
|
+
# Access individual results
|
126
|
+
chain.results.each_with_index do |result, index|
|
127
|
+
puts "#{index}: #{result.task.class} - #{result.status}"
|
128
|
+
end
|
105
129
|
```
|
106
130
|
|
107
131
|
## Correlation ID Integration
|
108
132
|
|
109
|
-
|
110
|
-
|
111
|
-
### Custom Chain IDs
|
112
|
-
|
113
|
-
You can specify custom chain IDs for specific correlation contexts:
|
114
|
-
|
115
|
-
```ruby
|
116
|
-
# Create a chain with custom ID
|
117
|
-
chain = CMDx::Chain.new(id: "user-session-123")
|
118
|
-
CMDx::Chain.current = chain
|
119
|
-
|
120
|
-
result = ProcessUserOrderTask.call(order_id: 123)
|
121
|
-
result.chain.id #=> "user-session-123"
|
122
|
-
```
|
133
|
+
> [!TIP]
|
134
|
+
> Chain IDs serve as correlation identifiers, enabling request tracing across distributed systems and complex workflows.
|
123
135
|
|
124
|
-
### Automatic Correlation
|
136
|
+
### Automatic Correlation
|
125
137
|
|
126
|
-
Chains
|
138
|
+
Chains integrate with the correlation system using hierarchical precedence:
|
127
139
|
|
128
140
|
```ruby
|
129
141
|
# 1. Existing chain ID takes precedence
|
130
|
-
CMDx::Chain.current = CMDx::Chain.new(id: "
|
131
|
-
result =
|
132
|
-
result.chain.id
|
142
|
+
CMDx::Chain.current = CMDx::Chain.new(id: "request-123")
|
143
|
+
result = ProcessOrderTask.call(order_id: 456)
|
144
|
+
result.chain.id # "request-123"
|
133
145
|
|
134
|
-
# 2. Thread-local correlation
|
146
|
+
# 2. Thread-local correlation used if no chain exists
|
135
147
|
CMDx::Chain.clear
|
136
|
-
CMDx::Correlator.id = "
|
137
|
-
result =
|
138
|
-
result.chain.id
|
148
|
+
CMDx::Correlator.id = "session-456"
|
149
|
+
result = ProcessOrderTask.call(order_id: 789)
|
150
|
+
result.chain.id # "session-456"
|
139
151
|
|
140
152
|
# 3. Generated UUID when no correlation exists
|
141
153
|
CMDx::Correlator.clear
|
142
|
-
result =
|
143
|
-
result.chain.id
|
154
|
+
result = ProcessOrderTask.call(order_id: 101)
|
155
|
+
result.chain.id # "018c2b95-b764-7615-a924-cc5b910ed1e5" (generated)
|
144
156
|
```
|
145
157
|
|
146
|
-
###
|
147
|
-
|
148
|
-
When tasks call subtasks within the same thread, correlation IDs automatically propagate:
|
158
|
+
### Custom Chain IDs
|
149
159
|
|
150
160
|
```ruby
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
# Subtasks inherit the same correlation ID automatically
|
156
|
-
SendOrderConfirmationTask.call(order_id: order_id)
|
157
|
-
NotifyWarehousePartnersTask.call(order_id: order_id)
|
158
|
-
end
|
159
|
-
end
|
160
|
-
|
161
|
-
# Set correlation for this execution context
|
162
|
-
CMDx::Chain.current = CMDx::Chain.new(id: "user-order-correlation-123")
|
161
|
+
# Create chain with specific correlation ID
|
162
|
+
chain = CMDx::Chain.new(id: "api-request-789")
|
163
|
+
CMDx::Chain.current = chain
|
163
164
|
|
164
|
-
result =
|
165
|
-
chain
|
165
|
+
result = ProcessApiRequestTask.call(data: payload)
|
166
|
+
result.chain.id # "api-request-789"
|
166
167
|
|
167
|
-
# All
|
168
|
-
chain.id
|
169
|
-
chain.results.all? { |r| r.chain.id == "user-order-correlation-123" } #=> true
|
168
|
+
# All subtasks inherit the same correlation ID
|
169
|
+
result.chain.results.all? { |r| r.chain.id == "api-request-789" } # true
|
170
170
|
```
|
171
171
|
|
172
172
|
### Correlation Context Management
|
173
173
|
|
174
|
-
Use correlation blocks to manage correlation scope:
|
175
|
-
|
176
174
|
```ruby
|
177
|
-
#
|
178
|
-
CMDx::Correlator.use("
|
179
|
-
result =
|
180
|
-
result.chain.id
|
175
|
+
# Scoped correlation context
|
176
|
+
CMDx::Correlator.use("user-session-123") do
|
177
|
+
result = ProcessUserActionTask.call(action: "purchase")
|
178
|
+
result.chain.id # "user-session-123"
|
181
179
|
|
182
|
-
# Nested
|
183
|
-
AuditLogTask.call(
|
180
|
+
# Nested operations inherit correlation
|
181
|
+
AuditLogTask.call(event: "purchase_completed")
|
184
182
|
end
|
185
183
|
|
186
|
-
# Outside
|
187
|
-
result =
|
188
|
-
result.chain.id
|
184
|
+
# Outside block, correlation context restored
|
185
|
+
result = OtherTask.call
|
186
|
+
result.chain.id # Different correlation ID
|
189
187
|
```
|
190
188
|
|
191
|
-
|
189
|
+
## State Delegation
|
192
190
|
|
193
|
-
|
191
|
+
> [!WARNING]
|
192
|
+
> Chain state always reflects the first (outer-most) task result, not individual subtask outcomes. Subtasks maintain their own success/failure states.
|
194
193
|
|
195
194
|
```ruby
|
196
195
|
class ProcessOrderTask < CMDx::Task
|
197
|
-
# Apply correlate middleware globally or per-task
|
198
|
-
use :middleware, CMDx::Middlewares::Correlate
|
199
|
-
|
200
196
|
def call
|
201
|
-
#
|
202
|
-
#
|
197
|
+
ValidateOrderTask.call!(order_id: order_id) # Success
|
198
|
+
ChargePaymentTask.call!(order_id: order_id) # Failure
|
203
199
|
end
|
204
200
|
end
|
205
|
-
```
|
206
201
|
|
207
|
-
|
208
|
-
|
202
|
+
result = ProcessOrderTask.call(order_id: 123)
|
203
|
+
chain = result.chain
|
209
204
|
|
210
|
-
|
211
|
-
|
205
|
+
# Chain delegates to main task (first result)
|
206
|
+
chain.status # "failed" (ProcessOrderTask failed due to subtask)
|
207
|
+
chain.state # "interrupted"
|
212
208
|
|
213
|
-
|
209
|
+
# Individual results maintain their own state
|
210
|
+
chain.results[0].status # "failed" (ProcessOrderTask - main)
|
211
|
+
chain.results[1].status # "success" (ValidateOrderTask)
|
212
|
+
chain.results[2].status # "failed" (ChargePaymentTask)
|
213
|
+
```
|
214
214
|
|
215
|
-
|
215
|
+
## Serialization and Logging
|
216
216
|
|
217
|
-
|
218
|
-
class ProcessOrderTask < CMDx::Task
|
219
|
-
def call
|
220
|
-
ValidateOrderDataTask.call!(order_id: order_id) # Success
|
221
|
-
ProcessOrderPaymentTask.call!(order_id: order_id) # Failed
|
222
|
-
end
|
223
|
-
end
|
217
|
+
Chains provide comprehensive serialization for monitoring and debugging:
|
224
218
|
|
225
|
-
|
219
|
+
```ruby
|
220
|
+
result = ProcessOrderTask.call(order_id: 123)
|
226
221
|
chain = result.chain
|
227
222
|
|
228
|
-
#
|
229
|
-
chain.
|
230
|
-
|
223
|
+
# Structured data representation
|
224
|
+
chain.to_h
|
225
|
+
# {
|
226
|
+
# id: "018c2b95-b764-7615-a924-cc5b910ed1e5",
|
227
|
+
# state: "complete",
|
228
|
+
# status: "success",
|
229
|
+
# outcome: "success",
|
230
|
+
# runtime: 0.8,
|
231
|
+
# results: [
|
232
|
+
# { class: "ProcessOrderTask", state: "complete", status: "success", ... },
|
233
|
+
# { class: "ValidateOrderTask", state: "complete", status: "success", ... },
|
234
|
+
# { class: "ChargePaymentTask", state: "complete", status: "success", ... }
|
235
|
+
# ]
|
236
|
+
# }
|
237
|
+
|
238
|
+
# Human-readable execution summary
|
239
|
+
puts chain.to_s
|
240
|
+
# chain: 018c2b95-b764-7615-a924-cc5b910ed1e5
|
241
|
+
# ================================================
|
242
|
+
#
|
243
|
+
# ProcessOrderTask: index=0 state=complete status=success runtime=0.8
|
244
|
+
# ValidateOrderTask: index=1 state=complete status=success runtime=0.1
|
245
|
+
# ChargePaymentTask: index=2 state=complete status=success runtime=0.5
|
246
|
+
#
|
247
|
+
# ================================================
|
248
|
+
# state: complete | status: success | outcome: success | runtime: 0.8
|
249
|
+
```
|
250
|
+
|
251
|
+
## Error Handling
|
252
|
+
|
253
|
+
### Chain Access Patterns
|
231
254
|
|
232
|
-
|
233
|
-
chain
|
234
|
-
|
235
|
-
|
255
|
+
```ruby
|
256
|
+
# Safe chain access
|
257
|
+
result = ProcessOrderTask.call(order_id: 123)
|
258
|
+
|
259
|
+
if result.chain
|
260
|
+
correlation_id = result.chain.id
|
261
|
+
execution_count = result.chain.results.size
|
262
|
+
else
|
263
|
+
# Handle missing chain (shouldn't happen in normal execution)
|
264
|
+
correlation_id = "unknown"
|
265
|
+
end
|
236
266
|
```
|
237
267
|
|
268
|
+
### Thread Safety
|
269
|
+
|
238
270
|
> [!IMPORTANT]
|
239
|
-
> Chain
|
271
|
+
> Chain operations are thread-safe within individual threads but chains should not be shared across threads. Each thread maintains its own isolated chain context.
|
240
272
|
|
241
|
-
|
273
|
+
```ruby
|
274
|
+
# Safe: Each thread has its own chain
|
275
|
+
threads = 3.times.map do |i|
|
276
|
+
Thread.new do
|
277
|
+
result = ProcessOrderTask.call(order_id: 100 + i)
|
278
|
+
result.chain.id # Unique per thread
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
# Collect results safely
|
283
|
+
chain_ids = threads.map(&:value)
|
284
|
+
chain_ids.uniq.size # 3 (all different)
|
285
|
+
```
|
242
286
|
|
243
|
-
|
287
|
+
### Chain State Validation
|
244
288
|
|
245
289
|
```ruby
|
246
|
-
result =
|
290
|
+
result = ProcessOrderTask.call(order_id: 123)
|
247
291
|
chain = result.chain
|
248
292
|
|
249
|
-
#
|
250
|
-
chain.
|
251
|
-
|
252
|
-
#
|
253
|
-
|
254
|
-
|
255
|
-
#
|
256
|
-
|
257
|
-
|
258
|
-
#
|
259
|
-
|
260
|
-
|
261
|
-
# ]
|
262
|
-
# }
|
263
|
-
|
264
|
-
# Human-readable summary
|
265
|
-
puts chain.to_s
|
266
|
-
# chain: 018c2b95-b764-7615-a924-cc5b910ed1e5
|
267
|
-
# ================================================
|
268
|
-
#
|
269
|
-
# ProcessUserOrderTask: index=0 state=complete status=success ...
|
270
|
-
# SendOrderConfirmationTask: index=1 state=complete status=success ...
|
271
|
-
# NotifyWarehousePartnersTask: index=2 state=complete status=success ...
|
272
|
-
#
|
273
|
-
# ================================================
|
274
|
-
# state: complete | status: success | outcome: success | runtime: 0.5
|
293
|
+
# Validate chain integrity
|
294
|
+
case chain.state
|
295
|
+
when "complete"
|
296
|
+
# All tasks finished normally
|
297
|
+
process_successful_chain(chain)
|
298
|
+
when "interrupted"
|
299
|
+
# Task was halted or failed
|
300
|
+
handle_chain_interruption(chain)
|
301
|
+
else
|
302
|
+
# Unexpected state
|
303
|
+
log_chain_anomaly(chain)
|
304
|
+
end
|
275
305
|
```
|
276
306
|
|
277
307
|
---
|