fractor 0.1.6 → 0.1.7

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 (172) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +227 -102
  3. data/README.adoc +113 -1940
  4. data/docs/.lycheeignore +16 -0
  5. data/docs/Gemfile +24 -0
  6. data/docs/README.md +157 -0
  7. data/docs/_config.yml +151 -0
  8. data/docs/_features/error-handling.adoc +1192 -0
  9. data/docs/_features/index.adoc +80 -0
  10. data/docs/_features/monitoring.adoc +589 -0
  11. data/docs/_features/signal-handling.adoc +202 -0
  12. data/docs/_features/workflows.adoc +1235 -0
  13. data/docs/_guides/continuous-mode.adoc +736 -0
  14. data/docs/_guides/cookbook.adoc +1133 -0
  15. data/docs/_guides/index.adoc +55 -0
  16. data/docs/_guides/pipeline-mode.adoc +730 -0
  17. data/docs/_guides/troubleshooting.adoc +358 -0
  18. data/docs/_pages/architecture.adoc +1390 -0
  19. data/docs/_pages/core-concepts.adoc +1392 -0
  20. data/docs/_pages/design-principles.adoc +862 -0
  21. data/docs/_pages/getting-started.adoc +290 -0
  22. data/docs/_pages/installation.adoc +143 -0
  23. data/docs/_reference/api.adoc +1080 -0
  24. data/docs/_reference/error-reporting.adoc +670 -0
  25. data/docs/_reference/examples.adoc +181 -0
  26. data/docs/_reference/index.adoc +96 -0
  27. data/docs/_reference/troubleshooting.adoc +862 -0
  28. data/docs/_tutorials/complex-workflows.adoc +1022 -0
  29. data/docs/_tutorials/data-processing-pipeline.adoc +740 -0
  30. data/docs/_tutorials/first-application.adoc +384 -0
  31. data/docs/_tutorials/index.adoc +48 -0
  32. data/docs/_tutorials/long-running-services.adoc +931 -0
  33. data/docs/assets/images/favicon-16.png +0 -0
  34. data/docs/assets/images/favicon-32.png +0 -0
  35. data/docs/assets/images/favicon-48.png +0 -0
  36. data/docs/assets/images/favicon.ico +0 -0
  37. data/docs/assets/images/favicon.png +0 -0
  38. data/docs/assets/images/favicon.svg +45 -0
  39. data/docs/assets/images/fractor-icon.svg +49 -0
  40. data/docs/assets/images/fractor-logo.svg +61 -0
  41. data/docs/index.adoc +131 -0
  42. data/docs/lychee.toml +39 -0
  43. data/examples/api_aggregator/README.adoc +627 -0
  44. data/examples/api_aggregator/api_aggregator.rb +376 -0
  45. data/examples/auto_detection/README.adoc +407 -29
  46. data/examples/continuous_chat_common/message_protocol.rb +1 -1
  47. data/examples/error_reporting.rb +207 -0
  48. data/examples/file_processor/README.adoc +170 -0
  49. data/examples/file_processor/file_processor.rb +615 -0
  50. data/examples/file_processor/sample_files/invalid.csv +1 -0
  51. data/examples/file_processor/sample_files/orders.xml +24 -0
  52. data/examples/file_processor/sample_files/products.json +23 -0
  53. data/examples/file_processor/sample_files/users.csv +6 -0
  54. data/examples/hierarchical_hasher/README.adoc +629 -41
  55. data/examples/image_processor/README.adoc +610 -0
  56. data/examples/image_processor/image_processor.rb +349 -0
  57. data/examples/image_processor/processed_images/sample_10_processed.jpg.json +12 -0
  58. data/examples/image_processor/processed_images/sample_1_processed.jpg.json +12 -0
  59. data/examples/image_processor/processed_images/sample_2_processed.jpg.json +12 -0
  60. data/examples/image_processor/processed_images/sample_3_processed.jpg.json +12 -0
  61. data/examples/image_processor/processed_images/sample_4_processed.jpg.json +12 -0
  62. data/examples/image_processor/processed_images/sample_5_processed.jpg.json +12 -0
  63. data/examples/image_processor/processed_images/sample_6_processed.jpg.json +12 -0
  64. data/examples/image_processor/processed_images/sample_7_processed.jpg.json +12 -0
  65. data/examples/image_processor/processed_images/sample_8_processed.jpg.json +12 -0
  66. data/examples/image_processor/processed_images/sample_9_processed.jpg.json +12 -0
  67. data/examples/image_processor/test_images/sample_1.png +1 -0
  68. data/examples/image_processor/test_images/sample_10.png +1 -0
  69. data/examples/image_processor/test_images/sample_2.png +1 -0
  70. data/examples/image_processor/test_images/sample_3.png +1 -0
  71. data/examples/image_processor/test_images/sample_4.png +1 -0
  72. data/examples/image_processor/test_images/sample_5.png +1 -0
  73. data/examples/image_processor/test_images/sample_6.png +1 -0
  74. data/examples/image_processor/test_images/sample_7.png +1 -0
  75. data/examples/image_processor/test_images/sample_8.png +1 -0
  76. data/examples/image_processor/test_images/sample_9.png +1 -0
  77. data/examples/log_analyzer/README.adoc +662 -0
  78. data/examples/log_analyzer/log_analyzer.rb +579 -0
  79. data/examples/log_analyzer/sample_logs/apache.log +20 -0
  80. data/examples/log_analyzer/sample_logs/json.log +15 -0
  81. data/examples/log_analyzer/sample_logs/nginx.log +15 -0
  82. data/examples/log_analyzer/sample_logs/rails.log +29 -0
  83. data/examples/multi_work_type/README.adoc +576 -26
  84. data/examples/performance_monitoring.rb +120 -0
  85. data/examples/pipeline_processing/README.adoc +740 -26
  86. data/examples/pipeline_processing/pipeline_processing.rb +2 -2
  87. data/examples/priority_work_example.rb +155 -0
  88. data/examples/producer_subscriber/README.adoc +889 -46
  89. data/examples/scatter_gather/README.adoc +829 -27
  90. data/examples/simple/README.adoc +347 -0
  91. data/examples/specialized_workers/README.adoc +622 -26
  92. data/examples/specialized_workers/specialized_workers.rb +44 -8
  93. data/examples/stream_processor/README.adoc +206 -0
  94. data/examples/stream_processor/stream_processor.rb +284 -0
  95. data/examples/web_scraper/README.adoc +625 -0
  96. data/examples/web_scraper/web_scraper.rb +285 -0
  97. data/examples/workflow/README.adoc +406 -0
  98. data/examples/workflow/circuit_breaker/README.adoc +360 -0
  99. data/examples/workflow/circuit_breaker/circuit_breaker_workflow.rb +225 -0
  100. data/examples/workflow/conditional/README.adoc +483 -0
  101. data/examples/workflow/conditional/conditional_workflow.rb +215 -0
  102. data/examples/workflow/dead_letter_queue/README.adoc +374 -0
  103. data/examples/workflow/dead_letter_queue/dead_letter_queue_workflow.rb +217 -0
  104. data/examples/workflow/fan_out/README.adoc +381 -0
  105. data/examples/workflow/fan_out/fan_out_workflow.rb +202 -0
  106. data/examples/workflow/retry/README.adoc +248 -0
  107. data/examples/workflow/retry/retry_workflow.rb +195 -0
  108. data/examples/workflow/simple_linear/README.adoc +267 -0
  109. data/examples/workflow/simple_linear/simple_linear_workflow.rb +175 -0
  110. data/examples/workflow/simplified/README.adoc +329 -0
  111. data/examples/workflow/simplified/simplified_workflow.rb +222 -0
  112. data/exe/fractor +10 -0
  113. data/lib/fractor/cli.rb +288 -0
  114. data/lib/fractor/configuration.rb +307 -0
  115. data/lib/fractor/continuous_server.rb +60 -65
  116. data/lib/fractor/error_formatter.rb +72 -0
  117. data/lib/fractor/error_report_generator.rb +152 -0
  118. data/lib/fractor/error_reporter.rb +244 -0
  119. data/lib/fractor/error_statistics.rb +147 -0
  120. data/lib/fractor/execution_tracer.rb +162 -0
  121. data/lib/fractor/logger.rb +230 -0
  122. data/lib/fractor/main_loop_handler.rb +406 -0
  123. data/lib/fractor/main_loop_handler3.rb +135 -0
  124. data/lib/fractor/main_loop_handler4.rb +299 -0
  125. data/lib/fractor/performance_metrics_collector.rb +181 -0
  126. data/lib/fractor/performance_monitor.rb +215 -0
  127. data/lib/fractor/performance_report_generator.rb +202 -0
  128. data/lib/fractor/priority_work.rb +93 -0
  129. data/lib/fractor/priority_work_queue.rb +189 -0
  130. data/lib/fractor/result_aggregator.rb +32 -0
  131. data/lib/fractor/shutdown_handler.rb +168 -0
  132. data/lib/fractor/signal_handler.rb +80 -0
  133. data/lib/fractor/supervisor.rb +382 -269
  134. data/lib/fractor/supervisor_logger.rb +88 -0
  135. data/lib/fractor/version.rb +1 -1
  136. data/lib/fractor/work.rb +12 -0
  137. data/lib/fractor/work_distribution_manager.rb +151 -0
  138. data/lib/fractor/work_queue.rb +20 -0
  139. data/lib/fractor/work_result.rb +181 -9
  140. data/lib/fractor/worker.rb +73 -0
  141. data/lib/fractor/workflow/builder.rb +210 -0
  142. data/lib/fractor/workflow/chain_builder.rb +169 -0
  143. data/lib/fractor/workflow/circuit_breaker.rb +183 -0
  144. data/lib/fractor/workflow/circuit_breaker_orchestrator.rb +208 -0
  145. data/lib/fractor/workflow/circuit_breaker_registry.rb +112 -0
  146. data/lib/fractor/workflow/dead_letter_queue.rb +334 -0
  147. data/lib/fractor/workflow/execution_hooks.rb +39 -0
  148. data/lib/fractor/workflow/execution_strategy.rb +225 -0
  149. data/lib/fractor/workflow/execution_trace.rb +134 -0
  150. data/lib/fractor/workflow/helpers.rb +191 -0
  151. data/lib/fractor/workflow/job.rb +290 -0
  152. data/lib/fractor/workflow/job_dependency_validator.rb +120 -0
  153. data/lib/fractor/workflow/logger.rb +110 -0
  154. data/lib/fractor/workflow/pre_execution_context.rb +193 -0
  155. data/lib/fractor/workflow/retry_config.rb +156 -0
  156. data/lib/fractor/workflow/retry_orchestrator.rb +184 -0
  157. data/lib/fractor/workflow/retry_strategy.rb +93 -0
  158. data/lib/fractor/workflow/structured_logger.rb +30 -0
  159. data/lib/fractor/workflow/type_compatibility_validator.rb +222 -0
  160. data/lib/fractor/workflow/visualizer.rb +211 -0
  161. data/lib/fractor/workflow/workflow_context.rb +132 -0
  162. data/lib/fractor/workflow/workflow_executor.rb +669 -0
  163. data/lib/fractor/workflow/workflow_result.rb +55 -0
  164. data/lib/fractor/workflow/workflow_validator.rb +295 -0
  165. data/lib/fractor/workflow.rb +333 -0
  166. data/lib/fractor/wrapped_ractor.rb +66 -101
  167. data/lib/fractor/wrapped_ractor3.rb +161 -0
  168. data/lib/fractor/wrapped_ractor4.rb +242 -0
  169. data/lib/fractor.rb +92 -4
  170. metadata +179 -6
  171. data/tests/sample.rb.bak +0 -309
  172. data/tests/sample_working.rb.bak +0 -209
@@ -0,0 +1,384 @@
1
+ ---
2
+ layout: default
3
+ title: Your First Fractor Application
4
+ nav_order: 3
5
+ ---
6
+
7
+ == Your First Fractor Application
8
+ :toc:
9
+ :toclevels: 3
10
+
11
+ This tutorial walks you through creating your first parallel processing application with Fractor from scratch. By the end, you'll understand the core components and how they work together.
12
+
13
+ === What we'll build
14
+
15
+ We'll create a simple image processor that:
16
+
17
+ * Takes a list of image filenames
18
+ * Resizes each image in parallel
19
+ * Collects the results
20
+ * Handles errors gracefully
21
+
22
+ === Prerequisites
23
+
24
+ * Ruby 3.0 or later installed
25
+ * Fractor gem installed (`gem install fractor`)
26
+ * Basic Ruby knowledge
27
+
28
+ === Step 1: Project setup
29
+
30
+ Create a new directory for your project:
31
+
32
+ [source,sh]
33
+ ----
34
+ mkdir fractor-tutorial
35
+ cd fractor-tutorial
36
+ ----
37
+
38
+ Create a new file called `image_processor.rb`:
39
+
40
+ [source,sh]
41
+ ----
42
+ touch image_processor.rb
43
+ ----
44
+
45
+ === Step 2: Define your Work class
46
+
47
+ The `Work` class represents a single unit of work. For our image processor, each work item contains information about one image to process.
48
+
49
+ Open `image_processor.rb` and add:
50
+
51
+ [source,ruby]
52
+ ----
53
+ require 'fractor'
54
+
55
+ # Represents a single image to be processed
56
+ class ImageWork < Fractor::Work
57
+ def initialize(filename, width, height)
58
+ super({
59
+ filename: filename,
60
+ width: width,
61
+ height: height
62
+ })
63
+ end
64
+
65
+ # Convenience methods to access work data
66
+ def filename
67
+ input[:filename]
68
+ end
69
+
70
+ def width
71
+ input[:width]
72
+ end
73
+
74
+ def height
75
+ input[:height]
76
+ end
77
+
78
+ def to_s
79
+ "ImageWork(#{filename}, #{width}x#{height})"
80
+ end
81
+ end
82
+ ----
83
+
84
+ **Key points:**
85
+
86
+ * Inherit from `Fractor::Work`
87
+ * Call `super()` with a hash of input data
88
+ * Add convenience methods for accessing data
89
+ * The `input` hash is accessible across Ractor boundaries
90
+
91
+ === Step 3: Define your Worker class
92
+
93
+ The `Worker` class contains the processing logic. Each worker runs in its own Ractor for true parallel execution.
94
+
95
+ Add this to `image_processor.rb`:
96
+
97
+ [source,ruby]
98
+ ----
99
+ # Processes image resize operations
100
+ class ImageWorker < Fractor::Worker
101
+ def process(work)
102
+ puts "Processing #{work.filename}..."
103
+
104
+ # Simulate image processing
105
+ # In real code, you'd use ImageMagick, mini_magick, etc.
106
+ sleep(rand(0.1..0.5)) # Simulate work
107
+
108
+ result_filename = "resized_#{work.filename}"
109
+
110
+ # Return successful result
111
+ Fractor::WorkResult.new(
112
+ result: {
113
+ original: work.filename,
114
+ resized: result_filename,
115
+ dimensions: "#{work.width}x#{work.height}"
116
+ },
117
+ work: work
118
+ )
119
+ rescue => e
120
+ # Return error result
121
+ Fractor::WorkResult.new(
122
+ error: e,
123
+ work: work,
124
+ error_code: :image_processing_failed,
125
+ error_context: {
126
+ filename: work.filename,
127
+ dimensions: "#{work.width}x#{work.height}"
128
+ }
129
+ )
130
+ end
131
+ end
132
+ ----
133
+
134
+ **Key points:**
135
+
136
+ * Inherit from `Fractor::Worker`
137
+ * Implement the `process(work)` method
138
+ * Always return a `WorkResult` (success or error)
139
+ * Use `rescue` to handle errors gracefully
140
+ * Add error context for debugging
141
+
142
+ === Step 4: Create the Supervisor
143
+
144
+ The Supervisor manages the worker pool and coordinates processing:
145
+
146
+ [source,ruby]
147
+ ----
148
+ # Create sample work items
149
+ work_items = [
150
+ ImageWork.new("photo1.jpg", 800, 600),
151
+ ImageWork.new("photo2.jpg", 1024, 768),
152
+ ImageWork.new("photo3.jpg", 1920, 1080),
153
+ ImageWork.new("photo4.jpg", 640, 480),
154
+ ImageWork.new("photo5.jpg", 1280, 720)
155
+ ]
156
+
157
+ # Create supervisor with ImageWorker pool
158
+ supervisor = Fractor::Supervisor.new(
159
+ worker_pools: [
160
+ {
161
+ worker_class: ImageWorker
162
+ # num_workers is auto-detected based on CPU cores
163
+ }
164
+ ]
165
+ )
166
+
167
+ puts "Starting image processor with #{supervisor.worker_pools.first[:num_workers]} workers"
168
+
169
+ # Add work items to process
170
+ supervisor.add_work_items(work_items)
171
+
172
+ # Run processing (blocks until complete)
173
+ supervisor.run
174
+
175
+ puts "\nProcessing complete!"
176
+ ----
177
+
178
+ === Step 5: Access results
179
+
180
+ After processing completes, access the results:
181
+
182
+ [source,ruby]
183
+ ----
184
+ # Get successful results
185
+ results = supervisor.results.results
186
+ puts "\nSuccessfully processed #{results.size} images:"
187
+ results.each do |work_result|
188
+ data = work_result.result
189
+ puts " #{data[:original]} -> #{data[:resized]} (#{data[:dimensions]})"
190
+ end
191
+
192
+ # Get errors
193
+ errors = supervisor.results.errors
194
+ if errors.any?
195
+ puts "\nErrors encountered: #{errors.size}"
196
+ errors.each do |error_result|
197
+ puts " #{error_result.work.filename}: #{error_result.error_info[:error_message]}"
198
+ end
199
+ else
200
+ puts "\nNo errors!"
201
+ end
202
+ ----
203
+
204
+ === Step 6: Run your application
205
+
206
+ Save the file and run it:
207
+
208
+ [source,sh]
209
+ ----
210
+ ruby image_processor.rb
211
+ ----
212
+
213
+ You should see output like:
214
+
215
+ [source]
216
+ ----
217
+ Starting image processor with 8 workers
218
+ Processing photo1.jpg...
219
+ Processing photo2.jpg...
220
+ Processing photo3.jpg...
221
+ Processing photo4.jpg...
222
+ Processing photo5.jpg...
223
+
224
+ Processing complete!
225
+
226
+ Successfully processed 5 images:
227
+ photo1.jpg -> resized_photo1.jpg (800x600)
228
+ photo2.jpg -> resized_photo2.jpg (1024x768)
229
+ photo3.jpg -> resized_photo3.jpg (1920x1080)
230
+ photo4.jpg -> resized_photo4.jpg (640x480)
231
+ photo5.jpg -> resized_photo5.jpg (1280x720)
232
+
233
+ No errors!
234
+ ----
235
+
236
+ === Complete code
237
+
238
+ Here's the complete `image_processor.rb`:
239
+
240
+ [source,ruby]
241
+ ----
242
+ require 'fractor'
243
+
244
+ class ImageWork < Fractor::Work
245
+ def initialize(filename, width, height)
246
+ super({
247
+ filename: filename,
248
+ width: width,
249
+ height: height
250
+ })
251
+ end
252
+
253
+ def filename
254
+ input[:filename]
255
+ end
256
+
257
+ def width
258
+ input[:width]
259
+ end
260
+
261
+ def height
262
+ input[:height]
263
+ end
264
+
265
+ def to_s
266
+ "ImageWork(#{filename}, #{width}x#{height})"
267
+ end
268
+ end
269
+
270
+ class ImageWorker < Fractor::Worker
271
+ def process(work)
272
+ puts "Processing #{work.filename}..."
273
+
274
+ sleep(rand(0.1..0.5))
275
+
276
+ result_filename = "resized_#{work.filename}"
277
+
278
+ Fractor::WorkResult.new(
279
+ result: {
280
+ original: work.filename,
281
+ resized: result_filename,
282
+ dimensions: "#{work.width}x#{work.height}"
283
+ },
284
+ work: work
285
+ )
286
+ rescue => e
287
+ Fractor::WorkResult.new(
288
+ error: e,
289
+ work: work,
290
+ error_code: :image_processing_failed,
291
+ error_context: {
292
+ filename: work.filename,
293
+ dimensions: "#{work.width}x#{work.height}"
294
+ }
295
+ )
296
+ end
297
+ end
298
+
299
+ # Create work items
300
+ work_items = [
301
+ ImageWork.new("photo1.jpg", 800, 600),
302
+ ImageWork.new("photo2.jpg", 1024, 768),
303
+ ImageWork.new("photo3.jpg", 1920, 1080),
304
+ ImageWork.new("photo4.jpg", 640, 480),
305
+ ImageWork.new("photo5.jpg", 1280, 720)
306
+ ]
307
+
308
+ # Create supervisor
309
+ supervisor = Fractor::Supervisor.new(
310
+ worker_pools: [{ worker_class: ImageWorker }]
311
+ )
312
+
313
+ puts "Starting image processor with #{supervisor.worker_pools.first[:num_workers]} workers"
314
+
315
+ # Process
316
+ supervisor.add_work_items(work_items)
317
+ supervisor.run
318
+
319
+ puts "\nProcessing complete!"
320
+
321
+ # Results
322
+ results = supervisor.results.results
323
+ puts "\nSuccessfully processed #{results.size} images:"
324
+ results.each do |work_result|
325
+ data = work_result.result
326
+ puts " #{data[:original]} -> #{data[:resized]} (#{data[:dimensions]})"
327
+ end
328
+
329
+ errors = supervisor.results.errors
330
+ if errors.any?
331
+ puts "\nErrors encountered: #{errors.size}"
332
+ errors.each do |error_result|
333
+ puts " #{error_result.work.filename}: #{error_result.error_info[:error_message]}"
334
+ end
335
+ else
336
+ puts "\nNo errors!"
337
+ end
338
+ ----
339
+
340
+ === What you learned
341
+
342
+ Congratulations! You've built your first Fractor application. You now understand:
343
+
344
+ * How to create `Work` classes to encapsulate input data
345
+ * How to create `Worker` classes with processing logic
346
+ * How to use the `Supervisor` to manage parallel execution
347
+ * How to handle both successful results and errors
348
+ * How Fractor automatically detects optimal worker count
349
+
350
+ === Next steps
351
+
352
+ Now that you understand the basics:
353
+
354
+ * Learn about link:../getting-started/[different processing modes] (Pipeline vs Continuous)
355
+ * Explore link:../../guides/core-concepts/[core concepts] in depth
356
+ * Try link:../../guides/workflows/[workflows] for complex processing graphs
357
+ * See link:../../reference/examples/[real-world examples]
358
+
359
+ === Troubleshooting
360
+
361
+ ==== "Cannot load Ractor"
362
+
363
+ Ensure you're using Ruby 3.0 or later:
364
+
365
+ [source,sh]
366
+ ----
367
+ ruby --version
368
+ ----
369
+
370
+ ==== Workers not running in parallel
371
+
372
+ Check your CPU core count. Fractor creates one worker per core by default:
373
+
374
+ [source,ruby]
375
+ ----
376
+ require 'etc'
377
+ puts "Available processors: #{Etc.nprocessors}"
378
+ ----
379
+
380
+ ==== Need more help?
381
+
382
+ * Check the link:../../guides/core-concepts/[Core Concepts guide]
383
+ * Browse link:../../reference/api/[API Reference]
384
+ * Open an issue on https://github.com/metanorma/fractor/issues[GitHub]
@@ -0,0 +1,48 @@
1
+ ---
2
+ layout: default
3
+ title: Overview
4
+ nav_order: 1
5
+ ---
6
+
7
+ == Overview
8
+
9
+ Step-by-step guides to help you get started with Fractor.
10
+
11
+ These tutorials are designed to be followed in order:
12
+
13
+ . *link:../installation/[Installation]* - Set up Fractor in your environment
14
+ . *link:first-application/[Your First Application]* - Build a simple parallel processor
15
+ . *link:getting-started/[Getting Started]* - Understand modes and patterns
16
+
17
+ === What You'll Learn
18
+
19
+ By completing these tutorials, you'll:
20
+
21
+ * Install and verify Fractor on your system
22
+ * Understand the core components (Work, Worker, Supervisor)
23
+ * Create your first parallel processing application
24
+ * Learn when to use Pipeline vs Continuous mode
25
+ * Handle errors gracefully
26
+ * Access and use results
27
+
28
+ === Prerequisites
29
+
30
+ * Ruby 3.0 or later
31
+ * Basic Ruby programming knowledge
32
+ * Understanding of parallel processing concepts (helpful but not required)
33
+
34
+ === Time Investment
35
+
36
+ * *Installation*: 5-10 minutes
37
+ * *Your First Application*: 20-30 minutes
38
+ * *Getting Started*: 15-20 minutes
39
+
40
+ *Total*: About 1 hour to complete all tutorials
41
+
42
+ === Next Steps
43
+
44
+ After completing the tutorials, continue to:
45
+
46
+ * link:../guides/[Guides] for in-depth feature documentation
47
+ * link:../reference/[Reference] for API details and patterns
48
+ * link:../reference/examples/[Examples] for real-world use cases