ruby_reactor 0.6.0 → 0.7.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.
Files changed (81) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/demo-app-e2e-verify/SKILL.md +226 -0
  3. data/.claude/skills/speckit-demo-tests/SKILL.md +144 -0
  4. data/.release-please-manifest.json +1 -1
  5. data/.specify/feature.json +1 -1
  6. data/.specify/memory/constitution.md +79 -12
  7. data/.specify/templates/tasks-template.md +7 -0
  8. data/CHANGELOG.md +125 -1
  9. data/CLAUDE.md +5 -0
  10. data/README.md +155 -25
  11. data/lib/ruby_reactor/adapters/active_job/router.rb +21 -6
  12. data/lib/ruby_reactor/adapters/active_job/step_worker.rb +20 -0
  13. data/lib/ruby_reactor/adapters/active_job/worker.rb +6 -1
  14. data/lib/ruby_reactor/adapters/sidekiq/router.rb +21 -6
  15. data/lib/ruby_reactor/adapters/sidekiq/step_worker.rb +17 -0
  16. data/lib/ruby_reactor/adapters/sidekiq/worker.rb +5 -2
  17. data/lib/ruby_reactor/async_waiter.rb +113 -0
  18. data/lib/ruby_reactor/configuration.rb +30 -1
  19. data/lib/ruby_reactor/context.rb +28 -0
  20. data/lib/ruby_reactor/context_serializer.rb +15 -0
  21. data/lib/ruby_reactor/dsl/async_macros.rb +176 -0
  22. data/lib/ruby_reactor/dsl/async_reactor_builder.rb +66 -0
  23. data/lib/ruby_reactor/dsl/compose_builder.rb +23 -4
  24. data/lib/ruby_reactor/dsl/interrupt_builder.rb +3 -2
  25. data/lib/ruby_reactor/dsl/interrupt_step_config.rb +7 -1
  26. data/lib/ruby_reactor/dsl/lockable.rb +3 -3
  27. data/lib/ruby_reactor/dsl/map_builder.rb +1 -2
  28. data/lib/ruby_reactor/dsl/reactor.rb +29 -10
  29. data/lib/ruby_reactor/dsl/step_builder.rb +30 -11
  30. data/lib/ruby_reactor/dsl/template_helpers.rb +11 -3
  31. data/lib/ruby_reactor/error/async_result_pending.rb +21 -0
  32. data/lib/ruby_reactor/error/async_wait_timeout_error.rb +10 -0
  33. data/lib/ruby_reactor/error/deprecated_dsl_error.rb +11 -0
  34. data/lib/ruby_reactor/executor/async_step_dispatch.rb +110 -0
  35. data/lib/ruby_reactor/executor/compensation_manager.rb +58 -45
  36. data/lib/ruby_reactor/executor/ordered_lock_support.rb +11 -11
  37. data/lib/ruby_reactor/executor/result_handler.rb +47 -11
  38. data/lib/ruby_reactor/executor/retry_manager.rb +13 -6
  39. data/lib/ruby_reactor/executor/step_executor.rb +77 -24
  40. data/lib/ruby_reactor/executor.rb +179 -23
  41. data/lib/ruby_reactor/lock.rb +19 -0
  42. data/lib/ruby_reactor/map/element_executor.rb +13 -2
  43. data/lib/ruby_reactor/map/helpers.rb +10 -8
  44. data/lib/ruby_reactor/map/result_enumerator.rb +7 -1
  45. data/lib/ruby_reactor/map/result_summary.rb +63 -0
  46. data/lib/ruby_reactor/map/sweeper.rb +1 -1
  47. data/lib/ruby_reactor/open_telemetry.rb +8 -5
  48. data/lib/ruby_reactor/ordered_lock.rb +3 -3
  49. data/lib/ruby_reactor/reactor.rb +25 -2
  50. data/lib/ruby_reactor/rspec/matchers.rb +61 -11
  51. data/lib/ruby_reactor/rspec/sidekiq_helpers.rb +2 -1
  52. data/lib/ruby_reactor/rspec/step_executor_patch.rb +2 -2
  53. data/lib/ruby_reactor/rspec/test_subject.rb +74 -19
  54. data/lib/ruby_reactor/semaphore.rb +10 -0
  55. data/lib/ruby_reactor/step/async_reactor_step.rb +207 -0
  56. data/lib/ruby_reactor/step/compose_step.rb +1 -1
  57. data/lib/ruby_reactor/step/map_step.rb +6 -2
  58. data/lib/ruby_reactor/step.rb +10 -4
  59. data/lib/ruby_reactor/step_signals.rb +33 -0
  60. data/lib/ruby_reactor/step_sweeper.rb +72 -0
  61. data/lib/ruby_reactor/step_worker.rb +260 -0
  62. data/lib/ruby_reactor/storage/adapter.rb +26 -1
  63. data/lib/ruby_reactor/storage/redis_adapter.rb +4 -71
  64. data/lib/ruby_reactor/storage/redis_locking.rb +7 -0
  65. data/lib/ruby_reactor/storage/redis_pub_sub.rb +31 -0
  66. data/lib/ruby_reactor/storage/redis_reactor_scan.rb +116 -0
  67. data/lib/ruby_reactor/storage/redis_step_results.rb +49 -0
  68. data/lib/ruby_reactor/sweeper.rb +7 -1
  69. data/lib/ruby_reactor/sweeper_job.rb +1 -0
  70. data/lib/ruby_reactor/template/result.rb +151 -5
  71. data/lib/ruby_reactor/version.rb +1 -1
  72. data/lib/ruby_reactor/web/api.rb +117 -22
  73. data/lib/ruby_reactor/web/public/assets/index-BQvIWPdx.css +1 -0
  74. data/lib/ruby_reactor/web/public/assets/index-Dw4KV4QY.js +22 -0
  75. data/lib/ruby_reactor/web/public/index.html +2 -2
  76. data/lib/ruby_reactor/worker.rb +53 -3
  77. data/lib/ruby_reactor.rb +80 -10
  78. data/specs/active_job.md +1 -1
  79. metadata +23 -3
  80. data/lib/ruby_reactor/web/public/assets/index-CCnNVQy5.css +0 -1
  81. data/lib/ruby_reactor/web/public/assets/index-D7IBZvos.js +0 -21
@@ -5,8 +5,8 @@
5
5
  <link rel="icon" type="image/svg+xml" href="./vite.svg" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
7
  <title>ui</title>
8
- <script type="module" crossorigin src="./assets/index-D7IBZvos.js"></script>
9
- <link rel="stylesheet" crossorigin href="./assets/index-CCnNVQy5.css">
8
+ <script type="module" crossorigin src="./assets/index-Dw4KV4QY.js"></script>
9
+ <link rel="stylesheet" crossorigin href="./assets/index-BQvIWPdx.css">
10
10
  </head>
11
11
  <body>
12
12
  <div id="root"></div>
@@ -8,6 +8,36 @@ module RubyReactor
8
8
  # `Adapters::ActiveJob::Compat` on ActiveJob::Base) — nothing here references
9
9
  # a specific backend.
10
10
  module Worker
11
+ TERMINAL_STATUSES = %w[completed failed cancelled skipped].freeze
12
+
13
+ # Last line of observability when a job burns its whole retry budget on an
14
+ # infrastructure failure and the backend then discards it (Sidekiq runs
15
+ # with `dead: false`): without this, the context would stay "running"
16
+ # forever with zero surface anywhere, and every reader would wait out its
17
+ # full timeout. Called from the backends' retries-exhausted hooks with the
18
+ # job's own args. Best-effort — never raises back into the backend.
19
+ def self.record_retries_exhausted(args, exception)
20
+ context_id, reactor_class_name = args
21
+ return unless context_id
22
+
23
+ reactor_class_name ||= RubyReactor.reactor_storage_name(nil)
24
+ storage = RubyReactor.configuration.storage_adapter
25
+ data = storage.retrieve_context(context_id, reactor_class_name)
26
+ return if data.nil? || TERMINAL_STATUSES.include?((data["status"] || data[:status]).to_s)
27
+
28
+ data["status"] = "failed"
29
+ data["failure_reason"] = {
30
+ "message" => "job retries exhausted: #{exception.class.name}: #{exception.message}",
31
+ "exception_class" => exception.class.name
32
+ }
33
+ storage.store_context(context_id, JSON.generate(data), reactor_class_name)
34
+ storage.publish(RubyReactor.async_reactor_channel(context_id), "failed")
35
+ rescue StandardError => e
36
+ RubyReactor.configuration.logger.error(
37
+ "RubyReactor: could not record retries-exhausted failure for #{context_id}: #{e.class.name}: #{e.message}"
38
+ )
39
+ end
40
+
11
41
  # Identity-only payload: storage is the source of truth. Rehydrate the live
12
42
  # context from storage by id, then resume. A nil read means the context was
13
43
  # swept, expired, or already terminal-and-collected — nothing to resume.
@@ -62,7 +92,8 @@ module RubyReactor
62
92
  rescue RubyReactor::Lock::AcquisitionError,
63
93
  RubyReactor::Semaphore::AcquisitionError,
64
94
  RubyReactor::RateLimit::ExceededError,
65
- RubyReactor::OrderedLock::WaitError => e
95
+ RubyReactor::OrderedLock::WaitError,
96
+ RubyReactor::Error::AsyncResultPending => e
66
97
  # Snooze on expected concurrency, rate, or ordering contention.
67
98
  # OrderedLock::WaitError carries a poison-pill-derived retry hint,
68
99
  # consumed by compute_snooze_delay below. We avoid the framework's native
@@ -89,7 +120,16 @@ module RubyReactor
89
120
  rescue NameError
90
121
  # If not found, try to find it in the current namespace
91
122
  # This is a fallback for test environments
92
- context.reactor_class = reactor_class_name.constantize if reactor_class_name.respond_to?(:constantize)
123
+ begin
124
+ context.reactor_class = reactor_class_name.constantize if reactor_class_name.respond_to?(:constantize)
125
+ rescue NameError
126
+ # Leave reactor_class nil: the caller's guard fails the context with
127
+ # a durable record. Letting this second NameError escape would burn
128
+ # the job's whole retry budget on an error retries can never fix,
129
+ # then vanish — leaving the context "running" forever and any reader
130
+ # waiting out its full timeout.
131
+ nil
132
+ end
93
133
  end
94
134
  end
95
135
 
@@ -107,8 +147,12 @@ module RubyReactor
107
147
  # duplicate of the *same* execution may wait arbitrarily long for the
108
148
  # live original to finish (e.g. a sweeper re-enqueue racing a slow but
109
149
  # alive worker). Capping it would fail a legitimately-waiting duplicate.
150
+ # A parked async wait is likewise uncapped HERE: its bound is
151
+ # `async_park_timeout`, enforced against `dispatched_at` at the wait
152
+ # site — counting snoozes would double-bound it with the wrong unit.
110
153
  capped = !(error.is_a?(RubyReactor::OrderedLock::WaitError) ||
111
- error.is_a?(RubyReactor::Lock::ContextLockContention))
154
+ error.is_a?(RubyReactor::Lock::ContextLockContention) ||
155
+ error.is_a?(RubyReactor::Error::AsyncResultPending))
112
156
 
113
157
  if capped && max != :infinity && snooze_count >= max
114
158
  escalate_snooze(context, snooze_count, error)
@@ -203,6 +247,12 @@ module RubyReactor
203
247
  payload,
204
248
  reactor_class_name
205
249
  )
250
+ # Written first, signalled second — wake any reader blocked (or parked)
251
+ # on this execution so it fails fast with the real cause instead of
252
+ # waiting out its timeout.
253
+ RubyReactor.configuration.storage_adapter.publish(
254
+ RubyReactor.async_reactor_channel(context_id), "failed"
255
+ )
206
256
  rescue StandardError => e
207
257
  # Don't let a persistence failure mask the original deserialization error.
208
258
  RubyReactor.configuration.logger.error(
data/lib/ruby_reactor.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require "zeitwerk"
4
4
  require "pathname"
5
5
  require "securerandom"
6
+ require "time"
6
7
  require_relative "ruby_reactor/registry"
7
8
  require_relative "ruby_reactor/utils/code_extractor"
8
9
  require_relative "ruby_reactor/dsl/lockable" # Add this
@@ -58,6 +59,10 @@ module RubyReactor
58
59
  false
59
60
  end
60
61
 
62
+ def halted?
63
+ false
64
+ end
65
+
61
66
  def to_h
62
67
  { success: true, value: @value }
63
68
  end
@@ -69,7 +74,7 @@ module RubyReactor
69
74
  # already been claimed. The executor short-circuits before any step
70
75
  # runs.
71
76
  #
72
- # 2. Explicitly, by returning `RubyReactor.Skipped(reason: "...")` from a
77
+ # 2. Explicitly, by returning `RubyReactor.Halt(reason: "...")` from a
73
78
  # step's `run` block. The reactor halts immediately — no further steps,
74
79
  # and crucially **no compensation** of already-completed steps. Use this
75
80
  # when a step discovers that the rest of the workflow is not needed
@@ -77,8 +82,10 @@ module RubyReactor
77
82
  # partial progress is still correct to keep.
78
83
  #
79
84
  # Subclass of Success so callers that only check `success?` continue to work;
80
- # `skipped?` distinguishes it.
81
- class Skipped < Success
85
+ # `halted?` distinguishes it. Deliberately does NOT define `skipped?` — a
86
+ # migration site that still asks a halt whether it was skipped should raise
87
+ # NoMethodError, not silently read false.
88
+ class Halt < Success
82
89
  attr_reader :reason, :period_key, :step_name
83
90
 
84
91
  def initialize(reason: nil, period_key: nil, step_name: nil)
@@ -88,6 +95,37 @@ module RubyReactor
88
95
  @step_name = step_name
89
96
  end
90
97
 
98
+ def halted?
99
+ true
100
+ end
101
+
102
+ undef_method :skipped?
103
+ end
104
+
105
+ # Marks a single step as skipped while the reactor continues. Behaves
106
+ # exactly like Success — the value flows to dependants via `result(:step)` —
107
+ # except `skipped?` is true and the step is not enrolled for rollback
108
+ # (nothing happened, so there is nothing to undo).
109
+ class Skipped < Success
110
+ # Sentinel distinguishing "no value argument given" (the old Halt call
111
+ # shape reused this class's name) from an explicit `Skipped(nil)`.
112
+ UNSET = Object.new.freeze
113
+ private_constant :UNSET
114
+
115
+ attr_reader :reason, :step_name
116
+
117
+ def initialize(value = UNSET, reason: nil, step_name: nil)
118
+ if value.equal?(UNSET) && !reason.nil?
119
+ raise ArgumentError,
120
+ "RubyReactor::Skipped now marks a single step as skipped and continues. " \
121
+ "The clean halt you want is RubyReactor.Halt(reason: ...) / halt!(reason: ...)."
122
+ end
123
+
124
+ super(value.equal?(UNSET) ? nil : value)
125
+ @reason = reason
126
+ @step_name = step_name
127
+ end
128
+
91
129
  def skipped?
92
130
  true
93
131
  end
@@ -100,8 +138,10 @@ module RubyReactor
100
138
  # rubocop:disable Metrics/ParameterLists, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
101
139
  def initialize(error, retryable: nil, step_name: nil, inputs: {}, backtrace: nil, redact_inputs: [],
102
140
  reactor_name: nil, step_arguments: {}, exception_class: nil,
103
- file_path: nil, line_number: nil, code_snippet: nil, invalid_payload: false, validation_errors: nil)
141
+ file_path: nil, line_number: nil, code_snippet: nil, invalid_payload: false, validation_errors: nil,
142
+ **opts)
104
143
  # rubocop:enable Metrics/ParameterLists, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
144
+ retryable = opts[:retry] if opts.key?(:retry) # `retry:` wins over `retryable:` when both are given
105
145
  @error = error
106
146
 
107
147
  # Handle case where error is a serialized hash (e.g. from async failure propagation)
@@ -157,6 +197,10 @@ module RubyReactor
157
197
  false
158
198
  end
159
199
 
200
+ def halted?
201
+ false
202
+ end
203
+
160
204
  def invalid_payload?
161
205
  @invalid_payload
162
206
  end
@@ -292,8 +336,10 @@ module RubyReactor
292
336
  end
293
337
  end
294
338
 
295
- # Async result for background job execution
296
- class AsyncResult
339
+ # Sentinel returned when a step's work is handed off to a worker job and is
340
+ # not yet resolved — produced by `background`, `async_step`, `async_reactor`,
341
+ # and map's async element dispatch alike.
342
+ class DispatchResult
297
343
  attr_reader :job_id, :intermediate_results, :execution_id
298
344
 
299
345
  def initialize(job_id:, intermediate_results: {}, execution_id: nil)
@@ -324,10 +370,16 @@ module RubyReactor
324
370
  Failure.new(error, **kwargs)
325
371
  end
326
372
 
327
- # Build a `Skipped` result. Return one from a step's `run` block to halt the
373
+ # Build a `Halt` result. Return one from a step's `run` block to halt the
328
374
  # reactor cleanly without triggering compensation of previous steps.
329
- def self.Skipped(reason: nil, **kwargs)
330
- Skipped.new(reason: reason, **kwargs)
375
+ def self.Halt(reason: nil, **kwargs)
376
+ Halt.new(reason: reason, **kwargs)
377
+ end
378
+
379
+ # Build a `Skipped` result. Return one from a step's `run` block to mark
380
+ # that single step skipped while the reactor continues.
381
+ def self.Skipped(...)
382
+ Skipped.new(...)
331
383
  end
332
384
 
333
385
  def self.configure
@@ -348,6 +400,23 @@ module RubyReactor
348
400
  # (A truly anonymous class can't be reconstituted by name in another process,
349
401
  # so cross-process resume of one is inherently unsupported; this only keeps
350
402
  # the keys self-consistent within a process — e.g. inline tests.)
403
+ # Completion-signal channels for the notified wait. One place, because
404
+ # the publishing side and the waiting side must agree exactly and they live in
405
+ # different files (StepWorker / Executor vs Template::Result).
406
+ def self.async_step_channel(context_id, step_name)
407
+ "rr:done:#{context_id}:#{step_name}"
408
+ end
409
+
410
+ def self.async_reactor_channel(execution_id)
411
+ "rr:done:#{execution_id}"
412
+ end
413
+
414
+ # Liveness lock for one dispatched `async_step`. Held by StepWorker for the
415
+ # life of the unit, so StepSweeper can tell a slow unit from a lost job.
416
+ def self.async_step_lock_key(context_id, step_name)
417
+ "async_step:#{context_id}:#{step_name}"
418
+ end
419
+
351
420
  def self.reactor_storage_name(reactor_class)
352
421
  return "AnonymousReactor" if reactor_class.nil?
353
422
 
@@ -407,7 +476,8 @@ module RubyReactor
407
476
  limit ||= configuration.sweeper_limit
408
477
  {
409
478
  reactors: Sweeper.run_once(limit: limit),
410
- maps: Map::Sweeper.run_once(limit: limit)
479
+ maps: Map::Sweeper.run_once(limit: limit),
480
+ async_steps: StepSweeper.run_once(limit: limit)
411
481
  }
412
482
  end
413
483
 
data/specs/active_job.md CHANGED
@@ -29,7 +29,7 @@ The **enqueue side** is already behind a seam:
29
29
  ([sidekiq_adapter.rb](../lib/ruby_reactor/sidekiq_adapter.rb)):
30
30
  `perform_async`, `perform_in`, `perform_map_element_async`,
31
31
  `perform_map_element_in`, `perform_map_collection_async`. All return
32
- `RubyReactor::AsyncResult`.
32
+ `RubyReactor::DispatchResult`.
33
33
  - An adapter for any other queueing backend just needs to implement that
34
34
  same 5-method contract and assign it to `config.async_router`. **This part
35
35
  needs no rework.**
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby_reactor
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.7.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Artur
@@ -88,12 +88,14 @@ executables: []
88
88
  extensions: []
89
89
  extra_rdoc_files: []
90
90
  files:
91
+ - ".claude/skills/demo-app-e2e-verify/SKILL.md"
91
92
  - ".claude/skills/speckit-agent-context-update/SKILL.md"
92
93
  - ".claude/skills/speckit-analyze/SKILL.md"
93
94
  - ".claude/skills/speckit-checklist/SKILL.md"
94
95
  - ".claude/skills/speckit-clarify/SKILL.md"
95
96
  - ".claude/skills/speckit-constitution/SKILL.md"
96
97
  - ".claude/skills/speckit-converge/SKILL.md"
98
+ - ".claude/skills/speckit-demo-tests/SKILL.md"
97
99
  - ".claude/skills/speckit-implement/SKILL.md"
98
100
  - ".claude/skills/speckit-plan/SKILL.md"
99
101
  - ".claude/skills/speckit-specify/SKILL.md"
@@ -131,6 +133,7 @@ files:
131
133
  - ".specify/workflows/workflow-registry.json"
132
134
  - ".tool-versions"
133
135
  - CHANGELOG.md
136
+ - CLAUDE.md
134
137
  - CODE_OF_CONDUCT.md
135
138
  - README.md
136
139
  - Rakefile
@@ -139,17 +142,22 @@ files:
139
142
  - lib/ruby_reactor/adapters/active_job/map_collector_worker.rb
140
143
  - lib/ruby_reactor/adapters/active_job/map_element_worker.rb
141
144
  - lib/ruby_reactor/adapters/active_job/router.rb
145
+ - lib/ruby_reactor/adapters/active_job/step_worker.rb
142
146
  - lib/ruby_reactor/adapters/active_job/sweeper_worker.rb
143
147
  - lib/ruby_reactor/adapters/active_job/worker.rb
144
148
  - lib/ruby_reactor/adapters/sidekiq/map_collector_worker.rb
145
149
  - lib/ruby_reactor/adapters/sidekiq/map_element_worker.rb
146
150
  - lib/ruby_reactor/adapters/sidekiq/router.rb
151
+ - lib/ruby_reactor/adapters/sidekiq/step_worker.rb
147
152
  - lib/ruby_reactor/adapters/sidekiq/sweeper_worker.rb
148
153
  - lib/ruby_reactor/adapters/sidekiq/worker.rb
154
+ - lib/ruby_reactor/async_waiter.rb
149
155
  - lib/ruby_reactor/configuration.rb
150
156
  - lib/ruby_reactor/context.rb
151
157
  - lib/ruby_reactor/context_serializer.rb
152
158
  - lib/ruby_reactor/dependency_graph.rb
159
+ - lib/ruby_reactor/dsl/async_macros.rb
160
+ - lib/ruby_reactor/dsl/async_reactor_builder.rb
153
161
  - lib/ruby_reactor/dsl/compose_builder.rb
154
162
  - lib/ruby_reactor/dsl/interrupt_builder.rb
155
163
  - lib/ruby_reactor/dsl/interrupt_step_config.rb
@@ -159,10 +167,13 @@ files:
159
167
  - lib/ruby_reactor/dsl/step_builder.rb
160
168
  - lib/ruby_reactor/dsl/template_helpers.rb
161
169
  - lib/ruby_reactor/dsl/validation_helpers.rb
170
+ - lib/ruby_reactor/error/async_result_pending.rb
171
+ - lib/ruby_reactor/error/async_wait_timeout_error.rb
162
172
  - lib/ruby_reactor/error/base.rb
163
173
  - lib/ruby_reactor/error/compensation_error.rb
164
174
  - lib/ruby_reactor/error/context_too_large_error.rb
165
175
  - lib/ruby_reactor/error/dependency_error.rb
176
+ - lib/ruby_reactor/error/deprecated_dsl_error.rb
166
177
  - lib/ruby_reactor/error/deserialization_error.rb
167
178
  - lib/ruby_reactor/error/input_validation_error.rb
168
179
  - lib/ruby_reactor/error/schema_version_error.rb
@@ -170,6 +181,7 @@ files:
170
181
  - lib/ruby_reactor/error/undo_error.rb
171
182
  - lib/ruby_reactor/error/validation_error.rb
172
183
  - lib/ruby_reactor/executor.rb
184
+ - lib/ruby_reactor/executor/async_step_dispatch.rb
173
185
  - lib/ruby_reactor/executor/compensation_manager.rb
174
186
  - lib/ruby_reactor/executor/graph_manager.rb
175
187
  - lib/ruby_reactor/executor/input_validator.rb
@@ -184,6 +196,7 @@ files:
184
196
  - lib/ruby_reactor/map/element_executor.rb
185
197
  - lib/ruby_reactor/map/helpers.rb
186
198
  - lib/ruby_reactor/map/result_enumerator.rb
199
+ - lib/ruby_reactor/map/result_summary.rb
187
200
  - lib/ruby_reactor/map/sweeper.rb
188
201
  - lib/ruby_reactor/max_retries_exhausted_failure.rb
189
202
  - lib/ruby_reactor/middleware.rb
@@ -208,13 +221,20 @@ files:
208
221
  - lib/ruby_reactor/rspec/test_subject.rb
209
222
  - lib/ruby_reactor/semaphore.rb
210
223
  - lib/ruby_reactor/step.rb
224
+ - lib/ruby_reactor/step/async_reactor_step.rb
211
225
  - lib/ruby_reactor/step/compose_step.rb
212
226
  - lib/ruby_reactor/step/map_step.rb
227
+ - lib/ruby_reactor/step_signals.rb
228
+ - lib/ruby_reactor/step_sweeper.rb
229
+ - lib/ruby_reactor/step_worker.rb
213
230
  - lib/ruby_reactor/storage/adapter.rb
214
231
  - lib/ruby_reactor/storage/configuration.rb
215
232
  - lib/ruby_reactor/storage/redis_adapter.rb
216
233
  - lib/ruby_reactor/storage/redis_locking.rb
217
234
  - lib/ruby_reactor/storage/redis_ordered_locking.rb
235
+ - lib/ruby_reactor/storage/redis_pub_sub.rb
236
+ - lib/ruby_reactor/storage/redis_reactor_scan.rb
237
+ - lib/ruby_reactor/storage/redis_step_results.rb
218
238
  - lib/ruby_reactor/sweeper.rb
219
239
  - lib/ruby_reactor/sweeper_job.rb
220
240
  - lib/ruby_reactor/template/base.rb
@@ -233,8 +253,8 @@ files:
233
253
  - lib/ruby_reactor/web/application.rb
234
254
  - lib/ruby_reactor/web/config.ru
235
255
  - lib/ruby_reactor/web/coordination_serializer.rb
236
- - lib/ruby_reactor/web/public/assets/index-CCnNVQy5.css
237
- - lib/ruby_reactor/web/public/assets/index-D7IBZvos.js
256
+ - lib/ruby_reactor/web/public/assets/index-BQvIWPdx.css
257
+ - lib/ruby_reactor/web/public/assets/index-Dw4KV4QY.js
238
258
  - lib/ruby_reactor/web/public/index.html
239
259
  - lib/ruby_reactor/web/public/vite.svg
240
260
  - lib/ruby_reactor/worker.rb
@@ -1 +0,0 @@
1
- @import"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap";@layer properties{@supports ((-webkit-hyphens:none) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1;--tw-space-y-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial}}}@layer theme{:root,:host{--font-sans:"Inter",system-ui,sans-serif;--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-red-300:oklch(80.8% .114 19.571);--color-red-400:oklch(70.4% .191 22.216);--color-red-500:oklch(63.7% .237 25.331);--color-red-950:oklch(25.8% .092 26.042);--color-orange-400:oklch(75% .183 55.934);--color-orange-500:oklch(70.5% .213 47.604);--color-amber-400:oklch(82.8% .189 84.429);--color-amber-500:oklch(76.9% .188 70.08);--color-emerald-400:oklch(76.5% .177 163.223);--color-emerald-500:oklch(69.6% .17 162.48);--color-emerald-600:oklch(59.6% .145 163.225);--color-teal-400:oklch(77.7% .152 181.912);--color-teal-500:oklch(70.4% .14 182.503);--color-cyan-400:oklch(78.9% .154 211.53);--color-sky-400:oklch(74.6% .16 232.661);--color-sky-500:oklch(68.5% .169 237.323);--color-indigo-300:oklch(78.5% .115 274.713);--color-indigo-400:oklch(67.3% .182 276.935);--color-indigo-500:#6366f1;--color-indigo-600:oklch(51.1% .262 276.966);--color-violet-400:oklch(70.2% .183 293.541);--color-violet-500:oklch(60.6% .25 292.717);--color-rose-400:oklch(71.2% .194 13.428);--color-rose-500:oklch(64.5% .246 16.439);--color-slate-50:oklch(98.4% .003 247.858);--color-slate-200:oklch(92.9% .013 255.508);--color-slate-300:oklch(86.9% .022 252.894);--color-slate-400:oklch(70.4% .04 256.788);--color-slate-500:oklch(55.4% .046 257.417);--color-slate-600:oklch(44.6% .043 257.281);--color-slate-700:oklch(37.2% .044 257.287);--color-slate-800:#1e293b;--color-slate-900:#0f172a;--color-slate-950:#020617;--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-xs:20rem;--container-7xl:80rem;--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-2xl:1.5rem;--text-2xl--line-height:calc(2/1.5);--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-tight:-.025em;--tracking-wide:.025em;--tracking-wider:.05em;--tracking-widest:.1em;--leading-tight:1.25;--leading-relaxed:1.625;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--animate-pulse:pulse 2s cubic-bezier(.4,0,.6,1)infinite;--blur-sm:8px;--blur-md:12px;--blur-xl:24px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::-moz-placeholder{opacity:1}::placeholder{opacity:1}@supports (not (-webkit-appearance:-apple-pay-button)) or (contain-intrinsic-size:1px){::-moz-placeholder{color:currentColor}::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::-moz-placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){-webkit-appearance:button;-moz-appearance:button;appearance:button}::file-selector-button{-webkit-appearance:button;-moz-appearance:button;appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.pointer-events-none{pointer-events:none}.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.absolute{position:absolute}.relative{position:relative}.static{position:static}.sticky{position:sticky}.top-0{top:calc(var(--spacing)*0)}.top-1\/2{top:50%}.-left-2{left:calc(var(--spacing)*-2)}.left-0{left:calc(var(--spacing)*0)}.left-3{left:calc(var(--spacing)*3)}.isolate{isolation:isolate}.z-50{z-index:50}.container{width:100%}@media(min-width:40rem){.container{max-width:40rem}}@media(min-width:48rem){.container{max-width:48rem}}@media(min-width:64rem){.container{max-width:64rem}}@media(min-width:80rem){.container{max-width:80rem}}@media(min-width:96rem){.container{max-width:96rem}}.m-0{margin:calc(var(--spacing)*0)}.-mx-4{margin-inline:calc(var(--spacing)*-4)}.mx-auto{margin-inline:auto}.mt-0\.5{margin-top:calc(var(--spacing)*.5)}.mt-1{margin-top:calc(var(--spacing)*1)}.mt-1\.5{margin-top:calc(var(--spacing)*1.5)}.mt-2{margin-top:calc(var(--spacing)*2)}.mt-3{margin-top:calc(var(--spacing)*3)}.mb-1\.5{margin-bottom:calc(var(--spacing)*1.5)}.mb-2{margin-bottom:calc(var(--spacing)*2)}.mb-3{margin-bottom:calc(var(--spacing)*3)}.mb-4{margin-bottom:calc(var(--spacing)*4)}.ml-3{margin-left:calc(var(--spacing)*3)}.ml-auto{margin-left:auto}.block{display:block}.flex{display:flex}.grid{display:grid}.inline-flex{display:inline-flex}.\!h-2{height:calc(var(--spacing)*2)!important}.h-0{height:calc(var(--spacing)*0)}.h-1\.5{height:calc(var(--spacing)*1.5)}.h-3{height:calc(var(--spacing)*3)}.h-4{height:calc(var(--spacing)*4)}.h-5{height:calc(var(--spacing)*5)}.h-6{height:calc(var(--spacing)*6)}.h-16{height:calc(var(--spacing)*16)}.h-24{height:calc(var(--spacing)*24)}.h-40{height:calc(var(--spacing)*40)}.h-\[calc\(100vh-8rem\)\]{height:calc(100vh - 8rem)}.h-full{height:100%}.h-px{height:1px}.max-h-\[300px\]{max-height:300px}.min-h-0{min-height:calc(var(--spacing)*0)}.min-h-\[500px\]{min-height:500px}.min-h-screen{min-height:100vh}.\!w-2{width:calc(var(--spacing)*2)!important}.w-1{width:calc(var(--spacing)*1)}.w-3{width:calc(var(--spacing)*3)}.w-4{width:calc(var(--spacing)*4)}.w-5{width:calc(var(--spacing)*5)}.w-6{width:calc(var(--spacing)*6)}.w-10{width:calc(var(--spacing)*10)}.w-full{width:100%}.max-w-7xl{max-width:var(--container-7xl)}.max-w-full{max-width:100%}.max-w-xs{max-width:var(--container-xs)}.min-w-0{min-width:calc(var(--spacing)*0)}.min-w-\[150px\]{min-width:150px}.flex-1{flex:1}.shrink-0{flex-shrink:0}.-translate-y-1\/2{--tw-translate-y: -50% ;translate:var(--tw-translate-x)var(--tw-translate-y)}.scale-105{--tw-scale-x:105%;--tw-scale-y:105%;--tw-scale-z:105%;scale:var(--tw-scale-x)var(--tw-scale-y)}.animate-pulse{animation:var(--animate-pulse)}.cursor-pointer{cursor:pointer}.appearance-none{-webkit-appearance:none;-moz-appearance:none;appearance:none}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-1{gap:calc(var(--spacing)*1)}.gap-1\.5{gap:calc(var(--spacing)*1.5)}.gap-2{gap:calc(var(--spacing)*2)}.gap-3{gap:calc(var(--spacing)*3)}.gap-4{gap:calc(var(--spacing)*4)}.gap-6{gap:calc(var(--spacing)*6)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*1)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*1)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-1\.5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*1.5)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*1.5)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*2)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*2)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*3)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*3)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*4)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*4)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*6)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*6)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-8>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*8)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*8)*calc(1 - var(--tw-space-y-reverse)))}.gap-x-4{-moz-column-gap:calc(var(--spacing)*4);column-gap:calc(var(--spacing)*4)}.gap-y-1{row-gap:calc(var(--spacing)*1)}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px*var(--tw-divide-y-reverse));border-bottom-width:calc(1px*calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-slate-800\/50>:not(:last-child)){border-color:#1e293b80}@supports (color:color-mix(in lab,red,red)){:where(.divide-slate-800\/50>:not(:last-child)){border-color:color-mix(in oklab,var(--color-slate-800)50%,transparent)}}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-xl{border-radius:var(--radius-xl)}.rounded-br-lg{border-bottom-right-radius:var(--radius-lg)}.border{border-style:var(--tw-border-style);border-width:1px}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-l-2{border-left-style:var(--tw-border-style);border-left-width:2px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-amber-500\/20{border-color:#f99c0033}@supports (color:color-mix(in lab,red,red)){.border-amber-500\/20{border-color:color-mix(in oklab,var(--color-amber-500)20%,transparent)}}.border-amber-500\/50{border-color:#f99c0080}@supports (color:color-mix(in lab,red,red)){.border-amber-500\/50{border-color:color-mix(in oklab,var(--color-amber-500)50%,transparent)}}.border-emerald-500\/20{border-color:#00bb7f33}@supports (color:color-mix(in lab,red,red)){.border-emerald-500\/20{border-color:color-mix(in oklab,var(--color-emerald-500)20%,transparent)}}.border-indigo-500{border-color:var(--color-indigo-500)}.border-indigo-500\/20{border-color:#6366f133}@supports (color:color-mix(in lab,red,red)){.border-indigo-500\/20{border-color:color-mix(in oklab,var(--color-indigo-500)20%,transparent)}}.border-orange-500\/20{border-color:#fe6e0033}@supports (color:color-mix(in lab,red,red)){.border-orange-500\/20{border-color:color-mix(in oklab,var(--color-orange-500)20%,transparent)}}.border-red-400{border-color:var(--color-red-400)}.border-red-500\/10{border-color:#fb2c361a}@supports (color:color-mix(in lab,red,red)){.border-red-500\/10{border-color:color-mix(in oklab,var(--color-red-500)10%,transparent)}}.border-red-500\/20{border-color:#fb2c3633}@supports (color:color-mix(in lab,red,red)){.border-red-500\/20{border-color:color-mix(in oklab,var(--color-red-500)20%,transparent)}}.border-rose-500{border-color:var(--color-rose-500)}.border-rose-500\/20{border-color:#ff235733}@supports (color:color-mix(in lab,red,red)){.border-rose-500\/20{border-color:color-mix(in oklab,var(--color-rose-500)20%,transparent)}}.border-sky-500\/20{border-color:#00a5ef33}@supports (color:color-mix(in lab,red,red)){.border-sky-500\/20{border-color:color-mix(in oklab,var(--color-sky-500)20%,transparent)}}.border-slate-700{border-color:var(--color-slate-700)}.border-slate-800{border-color:var(--color-slate-800)}.border-slate-800\/50{border-color:#1e293b80}@supports (color:color-mix(in lab,red,red)){.border-slate-800\/50{border-color:color-mix(in oklab,var(--color-slate-800)50%,transparent)}}.border-slate-800\/60{border-color:#1e293b99}@supports (color:color-mix(in lab,red,red)){.border-slate-800\/60{border-color:color-mix(in oklab,var(--color-slate-800)60%,transparent)}}.border-teal-500{border-color:var(--color-teal-500)}.border-teal-500\/20{border-color:#00baa733}@supports (color:color-mix(in lab,red,red)){.border-teal-500\/20{border-color:color-mix(in oklab,var(--color-teal-500)20%,transparent)}}.border-white\/5{border-color:#ffffff0d}@supports (color:color-mix(in lab,red,red)){.border-white\/5{border-color:color-mix(in oklab,var(--color-white)5%,transparent)}}.\!bg-slate-500{background-color:var(--color-slate-500)!important}.\!bg-transparent{background-color:#0000!important}.bg-amber-500\/10{background-color:#f99c001a}@supports (color:color-mix(in lab,red,red)){.bg-amber-500\/10{background-color:color-mix(in oklab,var(--color-amber-500)10%,transparent)}}.bg-black\/30{background-color:#0000004d}@supports (color:color-mix(in lab,red,red)){.bg-black\/30{background-color:color-mix(in oklab,var(--color-black)30%,transparent)}}.bg-emerald-500\/5{background-color:#00bb7f0d}@supports (color:color-mix(in lab,red,red)){.bg-emerald-500\/5{background-color:color-mix(in oklab,var(--color-emerald-500)5%,transparent)}}.bg-emerald-500\/10{background-color:#00bb7f1a}@supports (color:color-mix(in lab,red,red)){.bg-emerald-500\/10{background-color:color-mix(in oklab,var(--color-emerald-500)10%,transparent)}}.bg-emerald-600{background-color:var(--color-emerald-600)}.bg-indigo-500{background-color:var(--color-indigo-500)}.bg-indigo-500\/10{background-color:#6366f11a}@supports (color:color-mix(in lab,red,red)){.bg-indigo-500\/10{background-color:color-mix(in oklab,var(--color-indigo-500)10%,transparent)}}.bg-indigo-600{background-color:var(--color-indigo-600)}.bg-orange-500\/10{background-color:#fe6e001a}@supports (color:color-mix(in lab,red,red)){.bg-orange-500\/10{background-color:color-mix(in oklab,var(--color-orange-500)10%,transparent)}}.bg-red-500\/10{background-color:#fb2c361a}@supports (color:color-mix(in lab,red,red)){.bg-red-500\/10{background-color:color-mix(in oklab,var(--color-red-500)10%,transparent)}}.bg-red-500\/15{background-color:#fb2c3626}@supports (color:color-mix(in lab,red,red)){.bg-red-500\/15{background-color:color-mix(in oklab,var(--color-red-500)15%,transparent)}}.bg-red-950\/20{background-color:#46080933}@supports (color:color-mix(in lab,red,red)){.bg-red-950\/20{background-color:color-mix(in oklab,var(--color-red-950)20%,transparent)}}.bg-rose-500\/10{background-color:#ff23571a}@supports (color:color-mix(in lab,red,red)){.bg-rose-500\/10{background-color:color-mix(in oklab,var(--color-rose-500)10%,transparent)}}.bg-sky-500\/10{background-color:#00a5ef1a}@supports (color:color-mix(in lab,red,red)){.bg-sky-500\/10{background-color:color-mix(in oklab,var(--color-sky-500)10%,transparent)}}.bg-slate-700\/50{background-color:#31415880}@supports (color:color-mix(in lab,red,red)){.bg-slate-700\/50{background-color:color-mix(in oklab,var(--color-slate-700)50%,transparent)}}.bg-slate-800{background-color:var(--color-slate-800)}.bg-slate-900{background-color:var(--color-slate-900)}.bg-slate-900\/30{background-color:#0f172a4d}@supports (color:color-mix(in lab,red,red)){.bg-slate-900\/30{background-color:color-mix(in oklab,var(--color-slate-900)30%,transparent)}}.bg-slate-900\/40{background-color:#0f172a66}@supports (color:color-mix(in lab,red,red)){.bg-slate-900\/40{background-color:color-mix(in oklab,var(--color-slate-900)40%,transparent)}}.bg-slate-900\/50{background-color:#0f172a80}@supports (color:color-mix(in lab,red,red)){.bg-slate-900\/50{background-color:color-mix(in oklab,var(--color-slate-900)50%,transparent)}}.bg-slate-900\/60{background-color:#0f172a99}@supports (color:color-mix(in lab,red,red)){.bg-slate-900\/60{background-color:color-mix(in oklab,var(--color-slate-900)60%,transparent)}}.bg-slate-900\/80{background-color:#0f172acc}@supports (color:color-mix(in lab,red,red)){.bg-slate-900\/80{background-color:color-mix(in oklab,var(--color-slate-900)80%,transparent)}}.bg-slate-950{background-color:var(--color-slate-950)}.bg-slate-950\/50{background-color:#02061780}@supports (color:color-mix(in lab,red,red)){.bg-slate-950\/50{background-color:color-mix(in oklab,var(--color-slate-950)50%,transparent)}}.bg-teal-500\/10{background-color:#00baa71a}@supports (color:color-mix(in lab,red,red)){.bg-teal-500\/10{background-color:color-mix(in oklab,var(--color-teal-500)10%,transparent)}}.bg-transparent{background-color:#0000}.bg-violet-500\/70{background-color:#8d54ffb3}@supports (color:color-mix(in lab,red,red)){.bg-violet-500\/70{background-color:color-mix(in oklab,var(--color-violet-500)70%,transparent)}}.bg-white\/10{background-color:#ffffff1a}@supports (color:color-mix(in lab,red,red)){.bg-white\/10{background-color:color-mix(in oklab,var(--color-white)10%,transparent)}}.fill-slate-400{fill:var(--color-slate-400)}.\!p-0{padding:calc(var(--spacing)*0)!important}.p-1{padding:calc(var(--spacing)*1)}.p-1\.5{padding:calc(var(--spacing)*1.5)}.p-2{padding:calc(var(--spacing)*2)}.p-3{padding:calc(var(--spacing)*3)}.p-4{padding:calc(var(--spacing)*4)}.p-6{padding:calc(var(--spacing)*6)}.p-8{padding:calc(var(--spacing)*8)}.p-12{padding:calc(var(--spacing)*12)}.px-1{padding-inline:calc(var(--spacing)*1)}.px-1\.5{padding-inline:calc(var(--spacing)*1.5)}.px-2{padding-inline:calc(var(--spacing)*2)}.px-2\.5{padding-inline:calc(var(--spacing)*2.5)}.px-3{padding-inline:calc(var(--spacing)*3)}.px-4{padding-inline:calc(var(--spacing)*4)}.px-6{padding-inline:calc(var(--spacing)*6)}.py-0\.5{padding-block:calc(var(--spacing)*.5)}.py-1{padding-block:calc(var(--spacing)*1)}.py-1\.5{padding-block:calc(var(--spacing)*1.5)}.py-2{padding-block:calc(var(--spacing)*2)}.py-3{padding-block:calc(var(--spacing)*3)}.py-4{padding-block:calc(var(--spacing)*4)}.py-8{padding-block:calc(var(--spacing)*8)}.py-24{padding-block:calc(var(--spacing)*24)}.pt-3{padding-top:calc(var(--spacing)*3)}.pr-2{padding-right:calc(var(--spacing)*2)}.pr-4{padding-right:calc(var(--spacing)*4)}.pr-8{padding-right:calc(var(--spacing)*8)}.pb-4{padding-bottom:calc(var(--spacing)*4)}.pb-6{padding-bottom:calc(var(--spacing)*6)}.pl-2{padding-left:calc(var(--spacing)*2)}.pl-3{padding-left:calc(var(--spacing)*3)}.pl-9{padding-left:calc(var(--spacing)*9)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.font-mono{font-family:var(--font-mono)}.font-sans{font-family:var(--font-sans)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.leading-6{--tw-leading:calc(var(--spacing)*6);line-height:calc(var(--spacing)*6)}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.leading-tight{--tw-leading:var(--leading-tight);line-height:var(--leading-tight)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.tracking-widest{--tw-tracking:var(--tracking-widest);letter-spacing:var(--tracking-widest)}.whitespace-pre{white-space:pre}.whitespace-pre-wrap{white-space:pre-wrap}.text-amber-400{color:var(--color-amber-400)}.text-amber-400\/80{color:#fcbb00cc}@supports (color:color-mix(in lab,red,red)){.text-amber-400\/80{color:color-mix(in oklab,var(--color-amber-400)80%,transparent)}}.text-amber-500{color:var(--color-amber-500)}.text-cyan-400{color:var(--color-cyan-400)}.text-emerald-400{color:var(--color-emerald-400)}.text-indigo-400{color:var(--color-indigo-400)}.text-orange-400{color:var(--color-orange-400)}.text-red-300{color:var(--color-red-300)}.text-red-300\/90{color:#ffa3a3e6}@supports (color:color-mix(in lab,red,red)){.text-red-300\/90{color:color-mix(in oklab,var(--color-red-300)90%,transparent)}}.text-red-400{color:var(--color-red-400)}.text-red-400\/30{color:#ff65684d}@supports (color:color-mix(in lab,red,red)){.text-red-400\/30{color:color-mix(in oklab,var(--color-red-400)30%,transparent)}}.text-red-400\/50{color:#ff656880}@supports (color:color-mix(in lab,red,red)){.text-red-400\/50{color:color-mix(in oklab,var(--color-red-400)50%,transparent)}}.text-red-400\/70{color:#ff6568b3}@supports (color:color-mix(in lab,red,red)){.text-red-400\/70{color:color-mix(in oklab,var(--color-red-400)70%,transparent)}}.text-red-400\/80{color:#ff6568cc}@supports (color:color-mix(in lab,red,red)){.text-red-400\/80{color:color-mix(in oklab,var(--color-red-400)80%,transparent)}}.text-red-500{color:var(--color-red-500)}.text-rose-400{color:var(--color-rose-400)}.text-sky-400{color:var(--color-sky-400)}.text-slate-50{color:var(--color-slate-50)}.text-slate-200{color:var(--color-slate-200)}.text-slate-300{color:var(--color-slate-300)}.text-slate-400{color:var(--color-slate-400)}.text-slate-500{color:var(--color-slate-500)}.text-slate-600{color:var(--color-slate-600)}.text-teal-400{color:var(--color-teal-400)}.text-violet-400{color:var(--color-violet-400)}.text-white{color:var(--color-white)}.capitalize{text-transform:capitalize}.uppercase{text-transform:uppercase}.italic{font-style:italic}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,)var(--tw-slashed-zero,)var(--tw-numeric-figure,)var(--tw-numeric-spacing,)var(--tw-numeric-fraction,)}.placeholder-slate-700::-moz-placeholder{color:var(--color-slate-700)}.placeholder-slate-700::placeholder{color:var(--color-slate-700)}.opacity-0{opacity:0}.opacity-50{opacity:.5}.opacity-70{opacity:.7}.opacity-75{opacity:.75}.shadow-\[0_0_10px_rgba\(14\,165\,233\,0\.15\)\]{--tw-shadow:0 0 10px var(--tw-shadow-color,#0ea5e926);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_0_10px_rgba\(20\,184\,166\,0\.15\)\]{--tw-shadow:0 0 10px var(--tw-shadow-color,#14b8a626);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_0_10px_rgba\(99\,102\,241\,0\.15\)\]{--tw-shadow:0 0 10px var(--tw-shadow-color,#6366f126);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_0_10px_rgba\(244\,63\,94\,0\.15\)\]{--tw-shadow:0 0 10px var(--tw-shadow-color,#f43f5e26);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_0_15px_rgba\(244\,63\,94\,0\.2\)\]{--tw-shadow:0 0 15px var(--tw-shadow-color,#f43f5e33);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a),0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a),0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-2{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-black\/20{--tw-shadow-color:#0003}@supports (color:color-mix(in lab,red,red)){.shadow-black\/20{--tw-shadow-color:color-mix(in oklab,color-mix(in oklab,var(--color-black)20%,transparent)var(--tw-shadow-alpha),transparent)}}.shadow-emerald-500\/20{--tw-shadow-color:#00bb7f33}@supports (color:color-mix(in lab,red,red)){.shadow-emerald-500\/20{--tw-shadow-color:color-mix(in oklab,color-mix(in oklab,var(--color-emerald-500)20%,transparent)var(--tw-shadow-alpha),transparent)}}.shadow-indigo-500\/20{--tw-shadow-color:#6366f133}@supports (color:color-mix(in lab,red,red)){.shadow-indigo-500\/20{--tw-shadow-color:color-mix(in oklab,color-mix(in oklab,var(--color-indigo-500)20%,transparent)var(--tw-shadow-alpha),transparent)}}.ring-indigo-500\/20{--tw-ring-color:#6366f133}@supports (color:color-mix(in lab,red,red)){.ring-indigo-500\/20{--tw-ring-color:color-mix(in oklab,var(--color-indigo-500)20%,transparent)}}.ring-white\/20{--tw-ring-color:#fff3}@supports (color:color-mix(in lab,red,red)){.ring-white\/20{--tw-ring-color:color-mix(in oklab,var(--color-white)20%,transparent)}}.backdrop-blur-md{--tw-backdrop-blur:blur(var(--blur-md));-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-blur-sm{--tw-backdrop-blur:blur(var(--blur-sm));-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-blur-xl{--tw-backdrop-blur:blur(var(--blur-xl));-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-300{--tw-duration:.3s;transition-duration:.3s}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}@media(hover:hover){.group-hover\:h-4:is(:where(.group):hover *){height:calc(var(--spacing)*4)}.group-hover\:bg-indigo-500\/20:is(:where(.group):hover *){background-color:#6366f133}@supports (color:color-mix(in lab,red,red)){.group-hover\:bg-indigo-500\/20:is(:where(.group):hover *){background-color:color-mix(in oklab,var(--color-indigo-500)20%,transparent)}}.group-hover\:text-indigo-300:is(:where(.group):hover *){color:var(--color-indigo-300)}.group-hover\:text-indigo-400:is(:where(.group):hover *){color:var(--color-indigo-400)}.group-hover\:opacity-100:is(:where(.group):hover *){opacity:1}}.selection\:bg-indigo-500\/30 ::-moz-selection{background-color:#6366f14d}.selection\:bg-indigo-500\/30 ::selection{background-color:#6366f14d}@supports (color:color-mix(in lab,red,red)){.selection\:bg-indigo-500\/30 ::-moz-selection{background-color:color-mix(in oklab,var(--color-indigo-500)30%,transparent)}.selection\:bg-indigo-500\/30 ::selection{background-color:color-mix(in oklab,var(--color-indigo-500)30%,transparent)}}.selection\:bg-indigo-500\/30::-moz-selection{background-color:#6366f14d}.selection\:bg-indigo-500\/30::selection{background-color:#6366f14d}@supports (color:color-mix(in lab,red,red)){.selection\:bg-indigo-500\/30::-moz-selection{background-color:color-mix(in oklab,var(--color-indigo-500)30%,transparent)}.selection\:bg-indigo-500\/30::selection{background-color:color-mix(in oklab,var(--color-indigo-500)30%,transparent)}}.placeholder\:text-slate-600::-moz-placeholder{color:var(--color-slate-600)}.placeholder\:text-slate-600::placeholder{color:var(--color-slate-600)}@media(hover:hover){.hover\:-translate-y-0\.5:hover{--tw-translate-y:calc(var(--spacing)*-.5);translate:var(--tw-translate-x)var(--tw-translate-y)}.hover\:bg-emerald-500:hover{background-color:var(--color-emerald-500)}.hover\:bg-indigo-500:hover{background-color:var(--color-indigo-500)}.hover\:bg-indigo-500\/10:hover{background-color:#6366f11a}@supports (color:color-mix(in lab,red,red)){.hover\:bg-indigo-500\/10:hover{background-color:color-mix(in oklab,var(--color-indigo-500)10%,transparent)}}.hover\:bg-slate-800:hover{background-color:var(--color-slate-800)}.hover\:bg-slate-800\/30:hover{background-color:#1e293b4d}@supports (color:color-mix(in lab,red,red)){.hover\:bg-slate-800\/30:hover{background-color:color-mix(in oklab,var(--color-slate-800)30%,transparent)}}.hover\:bg-white\/5:hover{background-color:#ffffff0d}@supports (color:color-mix(in lab,red,red)){.hover\:bg-white\/5:hover{background-color:color-mix(in oklab,var(--color-white)5%,transparent)}}.hover\:bg-white\/10:hover{background-color:#ffffff1a}@supports (color:color-mix(in lab,red,red)){.hover\:bg-white\/10:hover{background-color:color-mix(in oklab,var(--color-white)10%,transparent)}}.hover\:text-indigo-400:hover{color:var(--color-indigo-400)}.hover\:text-red-400:hover{color:var(--color-red-400)}.hover\:text-white:hover{color:var(--color-white)}}.focus\:border-emerald-500\/50:focus{border-color:#00bb7f80}@supports (color:color-mix(in lab,red,red)){.focus\:border-emerald-500\/50:focus{border-color:color-mix(in oklab,var(--color-emerald-500)50%,transparent)}}.focus\:ring-1:focus{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(1px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-emerald-500\/50:focus{--tw-ring-color:#00bb7f80}@supports (color:color-mix(in lab,red,red)){.focus\:ring-emerald-500\/50:focus{--tw-ring-color:color-mix(in oklab,var(--color-emerald-500)50%,transparent)}}.focus\:ring-indigo-500\/50:focus{--tw-ring-color:#6366f180}@supports (color:color-mix(in lab,red,red)){.focus\:ring-indigo-500\/50:focus{--tw-ring-color:color-mix(in oklab,var(--color-indigo-500)50%,transparent)}}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}@media(min-width:40rem){.sm\:w-64{width:calc(var(--spacing)*64)}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.sm\:flex-row{flex-direction:row}.sm\:items-center{align-items:center}.sm\:px-6{padding-inline:calc(var(--spacing)*6)}}@media(min-width:64rem){.lg\:col-span-2{grid-column:span 2/span 2}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:px-8{padding-inline:calc(var(--spacing)*8)}}}body{background-color:var(--color-slate-950);font-family:var(--font-sans);color:var(--color-slate-50);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@keyframes pulse{50%{opacity:.5}}.react-flow{direction:ltr;--xy-edge-stroke-default: #b1b1b7;--xy-edge-stroke-width-default: 1;--xy-edge-stroke-selected-default: #555;--xy-connectionline-stroke-default: #b1b1b7;--xy-connectionline-stroke-width-default: 1;--xy-attribution-background-color-default: rgba(255, 255, 255, .5);--xy-minimap-background-color-default: #fff;--xy-minimap-mask-background-color-default: rgba(240, 240, 240, .6);--xy-minimap-mask-stroke-color-default: transparent;--xy-minimap-mask-stroke-width-default: 1;--xy-minimap-node-background-color-default: #e2e2e2;--xy-minimap-node-stroke-color-default: transparent;--xy-minimap-node-stroke-width-default: 2;--xy-background-color-default: transparent;--xy-background-pattern-dots-color-default: #91919a;--xy-background-pattern-lines-color-default: #eee;--xy-background-pattern-cross-color-default: #e2e2e2;background-color:var(--xy-background-color, var(--xy-background-color-default));--xy-node-color-default: inherit;--xy-node-border-default: 1px solid #1a192b;--xy-node-background-color-default: #fff;--xy-node-group-background-color-default: rgba(240, 240, 240, .25);--xy-node-boxshadow-hover-default: 0 1px 4px 1px rgba(0, 0, 0, .08);--xy-node-boxshadow-selected-default: 0 0 0 .5px #1a192b;--xy-node-border-radius-default: 3px;--xy-handle-background-color-default: #1a192b;--xy-handle-border-color-default: #fff;--xy-selection-background-color-default: rgba(0, 89, 220, .08);--xy-selection-border-default: 1px dotted rgba(0, 89, 220, .8);--xy-controls-button-background-color-default: #fefefe;--xy-controls-button-background-color-hover-default: #f4f4f4;--xy-controls-button-color-default: inherit;--xy-controls-button-color-hover-default: inherit;--xy-controls-button-border-color-default: #eee;--xy-controls-box-shadow-default: 0 0 2px 1px rgba(0, 0, 0, .08);--xy-edge-label-background-color-default: #ffffff;--xy-edge-label-color-default: inherit;--xy-resize-background-color-default: #3367d9}.react-flow.dark{--xy-edge-stroke-default: #3e3e3e;--xy-edge-stroke-width-default: 1;--xy-edge-stroke-selected-default: #727272;--xy-connectionline-stroke-default: #b1b1b7;--xy-connectionline-stroke-width-default: 1;--xy-attribution-background-color-default: rgba(150, 150, 150, .25);--xy-minimap-background-color-default: #141414;--xy-minimap-mask-background-color-default: rgba(60, 60, 60, .6);--xy-minimap-mask-stroke-color-default: transparent;--xy-minimap-mask-stroke-width-default: 1;--xy-minimap-node-background-color-default: #2b2b2b;--xy-minimap-node-stroke-color-default: transparent;--xy-minimap-node-stroke-width-default: 2;--xy-background-color-default: #141414;--xy-background-pattern-dots-color-default: #777;--xy-background-pattern-lines-color-default: #777;--xy-background-pattern-cross-color-default: #777;--xy-node-color-default: #f8f8f8;--xy-node-border-default: 1px solid #3c3c3c;--xy-node-background-color-default: #1e1e1e;--xy-node-group-background-color-default: rgba(240, 240, 240, .25);--xy-node-boxshadow-hover-default: 0 1px 4px 1px rgba(255, 255, 255, .08);--xy-node-boxshadow-selected-default: 0 0 0 .5px #999;--xy-handle-background-color-default: #bebebe;--xy-handle-border-color-default: #1e1e1e;--xy-selection-background-color-default: rgba(200, 200, 220, .08);--xy-selection-border-default: 1px dotted rgba(200, 200, 220, .8);--xy-controls-button-background-color-default: #2b2b2b;--xy-controls-button-background-color-hover-default: #3e3e3e;--xy-controls-button-color-default: #f8f8f8;--xy-controls-button-color-hover-default: #fff;--xy-controls-button-border-color-default: #5b5b5b;--xy-controls-box-shadow-default: 0 0 2px 1px rgba(0, 0, 0, .08);--xy-edge-label-background-color-default: #141414;--xy-edge-label-color-default: #f8f8f8}.react-flow__background{background-color:var(--xy-background-color-props, var(--xy-background-color, var(--xy-background-color-default)));pointer-events:none;z-index:-1}.react-flow__container{position:absolute;width:100%;height:100%;top:0;left:0}.react-flow__pane{z-index:1}.react-flow__pane.draggable{cursor:grab}.react-flow__pane.dragging{cursor:grabbing}.react-flow__pane.selection{cursor:pointer}.react-flow__viewport{transform-origin:0 0;z-index:2;pointer-events:none}.react-flow__renderer{z-index:4}.react-flow__selection{z-index:6}.react-flow__nodesselection-rect:focus,.react-flow__nodesselection-rect:focus-visible{outline:none}.react-flow__edge-path{stroke:var(--xy-edge-stroke, var(--xy-edge-stroke-default));stroke-width:var(--xy-edge-stroke-width, var(--xy-edge-stroke-width-default));fill:none}.react-flow__connection-path{stroke:var(--xy-connectionline-stroke, var(--xy-connectionline-stroke-default));stroke-width:var(--xy-connectionline-stroke-width, var(--xy-connectionline-stroke-width-default));fill:none}.react-flow .react-flow__edges{position:absolute}.react-flow .react-flow__edges svg{overflow:visible;position:absolute;pointer-events:none}.react-flow__edge{pointer-events:visibleStroke}.react-flow__edge.selectable{cursor:pointer}.react-flow__edge.animated path{stroke-dasharray:5;animation:dashdraw .5s linear infinite}.react-flow__edge.animated path.react-flow__edge-interaction{stroke-dasharray:none;animation:none}.react-flow__edge.inactive{pointer-events:none}.react-flow__edge.selected,.react-flow__edge:focus,.react-flow__edge:focus-visible{outline:none}.react-flow__edge.selected .react-flow__edge-path,.react-flow__edge.selectable:focus .react-flow__edge-path,.react-flow__edge.selectable:focus-visible .react-flow__edge-path{stroke:var(--xy-edge-stroke-selected, var(--xy-edge-stroke-selected-default))}.react-flow__edge-textwrapper{pointer-events:all}.react-flow__edge .react-flow__edge-text{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.react-flow__arrowhead polyline{stroke:var(--xy-edge-stroke, var(--xy-edge-stroke-default))}.react-flow__arrowhead polyline.arrowclosed{fill:var(--xy-edge-stroke, var(--xy-edge-stroke-default))}.react-flow__connection{pointer-events:none}.react-flow__connection .animated{stroke-dasharray:5;animation:dashdraw .5s linear infinite}svg.react-flow__connectionline{z-index:1001;overflow:visible;position:absolute}.react-flow__nodes{pointer-events:none;transform-origin:0 0}.react-flow__node{position:absolute;-webkit-user-select:none;-moz-user-select:none;user-select:none;pointer-events:all;transform-origin:0 0;box-sizing:border-box;cursor:default}.react-flow__node.selectable{cursor:pointer}.react-flow__node.draggable{cursor:grab;pointer-events:all}.react-flow__node.draggable.dragging{cursor:grabbing}.react-flow__nodesselection{z-index:3;transform-origin:left top;pointer-events:none}.react-flow__nodesselection-rect{position:absolute;pointer-events:all;cursor:grab}.react-flow__handle{position:absolute;pointer-events:none;min-width:5px;min-height:5px;width:6px;height:6px;background-color:var(--xy-handle-background-color, var(--xy-handle-background-color-default));border:1px solid var(--xy-handle-border-color, var(--xy-handle-border-color-default));border-radius:100%}.react-flow__handle.connectingfrom{pointer-events:all}.react-flow__handle.connectionindicator{pointer-events:all;cursor:crosshair}.react-flow__handle-bottom{top:auto;left:50%;bottom:0;transform:translate(-50%,50%)}.react-flow__handle-top{top:0;left:50%;transform:translate(-50%,-50%)}.react-flow__handle-left{top:50%;left:0;transform:translate(-50%,-50%)}.react-flow__handle-right{top:50%;right:0;transform:translate(50%,-50%)}.react-flow__edgeupdater{cursor:move;pointer-events:all}.react-flow__pane.selection .react-flow__panel{pointer-events:none}.react-flow__panel{position:absolute;z-index:5;margin:15px}.react-flow__panel.top{top:0}.react-flow__panel.bottom{bottom:0}.react-flow__panel.top.center,.react-flow__panel.bottom.center{left:50%;transform:translate(-15px) translate(-50%)}.react-flow__panel.left{left:0}.react-flow__panel.right{right:0}.react-flow__panel.left.center,.react-flow__panel.right.center{top:50%;transform:translateY(-15px) translateY(-50%)}.react-flow__attribution{font-size:10px;background:var(--xy-attribution-background-color, var(--xy-attribution-background-color-default));padding:2px 3px;margin:0}.react-flow__attribution a{text-decoration:none;color:#999}@keyframes dashdraw{0%{stroke-dashoffset:10}}.react-flow__edgelabel-renderer{position:absolute;width:100%;height:100%;pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;left:0;top:0}.react-flow__viewport-portal{position:absolute;width:100%;height:100%;left:0;top:0;-webkit-user-select:none;-moz-user-select:none;user-select:none}.react-flow__minimap{background:var( --xy-minimap-background-color-props, var(--xy-minimap-background-color, var(--xy-minimap-background-color-default)) )}.react-flow__minimap-svg{display:block}.react-flow__minimap-mask{fill:var( --xy-minimap-mask-background-color-props, var(--xy-minimap-mask-background-color, var(--xy-minimap-mask-background-color-default)) );stroke:var( --xy-minimap-mask-stroke-color-props, var(--xy-minimap-mask-stroke-color, var(--xy-minimap-mask-stroke-color-default)) );stroke-width:var( --xy-minimap-mask-stroke-width-props, var(--xy-minimap-mask-stroke-width, var(--xy-minimap-mask-stroke-width-default)) )}.react-flow__minimap-node{fill:var( --xy-minimap-node-background-color-props, var(--xy-minimap-node-background-color, var(--xy-minimap-node-background-color-default)) );stroke:var( --xy-minimap-node-stroke-color-props, var(--xy-minimap-node-stroke-color, var(--xy-minimap-node-stroke-color-default)) );stroke-width:var( --xy-minimap-node-stroke-width-props, var(--xy-minimap-node-stroke-width, var(--xy-minimap-node-stroke-width-default)) )}.react-flow__background-pattern.dots{fill:var( --xy-background-pattern-color-props, var(--xy-background-pattern-color, var(--xy-background-pattern-dots-color-default)) )}.react-flow__background-pattern.lines{stroke:var( --xy-background-pattern-color-props, var(--xy-background-pattern-color, var(--xy-background-pattern-lines-color-default)) )}.react-flow__background-pattern.cross{stroke:var( --xy-background-pattern-color-props, var(--xy-background-pattern-color, var(--xy-background-pattern-cross-color-default)) )}.react-flow__controls{display:flex;flex-direction:column;box-shadow:var(--xy-controls-box-shadow, var(--xy-controls-box-shadow-default))}.react-flow__controls.horizontal{flex-direction:row}.react-flow__controls-button{display:flex;justify-content:center;align-items:center;height:26px;width:26px;padding:4px;border:none;background:var(--xy-controls-button-background-color, var(--xy-controls-button-background-color-default));border-bottom:1px solid var( --xy-controls-button-border-color-props, var(--xy-controls-button-border-color, var(--xy-controls-button-border-color-default)) );color:var( --xy-controls-button-color-props, var(--xy-controls-button-color, var(--xy-controls-button-color-default)) );cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none}.react-flow__controls-button svg{width:100%;max-width:12px;max-height:12px;fill:currentColor}.react-flow__edge.updating .react-flow__edge-path{stroke:#777}.react-flow__edge-text{font-size:10px}.react-flow__node.selectable:focus,.react-flow__node.selectable:focus-visible{outline:none}.react-flow__node-input,.react-flow__node-default,.react-flow__node-output,.react-flow__node-group{padding:10px;border-radius:var(--xy-node-border-radius, var(--xy-node-border-radius-default));width:150px;font-size:12px;color:var(--xy-node-color, var(--xy-node-color-default));text-align:center;border:var(--xy-node-border, var(--xy-node-border-default));background-color:var(--xy-node-background-color, var(--xy-node-background-color-default))}.react-flow__node-input.selectable:hover,.react-flow__node-default.selectable:hover,.react-flow__node-output.selectable:hover,.react-flow__node-group.selectable:hover{box-shadow:var(--xy-node-boxshadow-hover, var(--xy-node-boxshadow-hover-default))}.react-flow__node-input.selectable.selected,.react-flow__node-input.selectable:focus,.react-flow__node-input.selectable:focus-visible,.react-flow__node-default.selectable.selected,.react-flow__node-default.selectable:focus,.react-flow__node-default.selectable:focus-visible,.react-flow__node-output.selectable.selected,.react-flow__node-output.selectable:focus,.react-flow__node-output.selectable:focus-visible,.react-flow__node-group.selectable.selected,.react-flow__node-group.selectable:focus,.react-flow__node-group.selectable:focus-visible{box-shadow:var(--xy-node-boxshadow-selected, var(--xy-node-boxshadow-selected-default))}.react-flow__node-group{background-color:var(--xy-node-group-background-color, var(--xy-node-group-background-color-default))}.react-flow__nodesselection-rect,.react-flow__selection{background:var(--xy-selection-background-color, var(--xy-selection-background-color-default));border:var(--xy-selection-border, var(--xy-selection-border-default))}.react-flow__nodesselection-rect:focus,.react-flow__nodesselection-rect:focus-visible,.react-flow__selection:focus,.react-flow__selection:focus-visible{outline:none}.react-flow__controls-button:hover{background:var( --xy-controls-button-background-color-hover-props, var(--xy-controls-button-background-color-hover, var(--xy-controls-button-background-color-hover-default)) );color:var( --xy-controls-button-color-hover-props, var(--xy-controls-button-color-hover, var(--xy-controls-button-color-hover-default)) )}.react-flow__controls-button:disabled{pointer-events:none}.react-flow__controls-button:disabled svg{fill-opacity:.4}.react-flow__controls-button:last-child{border-bottom:none}.react-flow__controls.horizontal .react-flow__controls-button{border-bottom:none;border-right:1px solid var( --xy-controls-button-border-color-props, var(--xy-controls-button-border-color, var(--xy-controls-button-border-color-default)) )}.react-flow__controls.horizontal .react-flow__controls-button:last-child{border-right:none}.react-flow__resize-control{position:absolute}.react-flow__resize-control.left,.react-flow__resize-control.right{cursor:ew-resize}.react-flow__resize-control.top,.react-flow__resize-control.bottom{cursor:ns-resize}.react-flow__resize-control.top.left,.react-flow__resize-control.bottom.right{cursor:nwse-resize}.react-flow__resize-control.bottom.left,.react-flow__resize-control.top.right{cursor:nesw-resize}.react-flow__resize-control.handle{width:5px;height:5px;border:1px solid #fff;border-radius:1px;background-color:var(--xy-resize-background-color, var(--xy-resize-background-color-default));translate:-50% -50%}.react-flow__resize-control.handle.left{left:0;top:50%}.react-flow__resize-control.handle.right{left:100%;top:50%}.react-flow__resize-control.handle.top{left:50%;top:0}.react-flow__resize-control.handle.bottom{left:50%;top:100%}.react-flow__resize-control.handle.top.left,.react-flow__resize-control.handle.bottom.left{left:0}.react-flow__resize-control.handle.top.right,.react-flow__resize-control.handle.bottom.right{left:100%}.react-flow__resize-control.line{border-color:var(--xy-resize-background-color, var(--xy-resize-background-color-default));border-width:0;border-style:solid}.react-flow__resize-control.line.left,.react-flow__resize-control.line.right{width:1px;transform:translate(-50%);top:0;height:100%}.react-flow__resize-control.line.left{left:0;border-left-width:1px}.react-flow__resize-control.line.right{left:100%;border-right-width:1px}.react-flow__resize-control.line.top,.react-flow__resize-control.line.bottom{height:1px;transform:translateY(-50%);left:0;width:100%}.react-flow__resize-control.line.top{top:0;border-top-width:1px}.react-flow__resize-control.line.bottom{border-bottom-width:1px;top:100%}.react-flow__edge-textbg{fill:var(--xy-edge-label-background-color, var(--xy-edge-label-background-color-default))}.react-flow__edge-text{fill:var(--xy-edge-label-color, var(--xy-edge-label-color-default))}pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{color:#c9d1d9;background:#0d1117}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#ff7b72}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#d2a8ff}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#79c0ff}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#a5d6ff}.hljs-built_in,.hljs-symbol{color:#ffa657}.hljs-code,.hljs-comment,.hljs-formula{color:#8b949e}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#7ee787}.hljs-subst{color:#c9d1d9}.hljs-section{color:#1f6feb;font-weight:700}.hljs-bullet{color:#f2cc60}.hljs-emphasis{color:#c9d1d9;font-style:italic}.hljs-strong{color:#c9d1d9;font-weight:700}.hljs-addition{color:#aff5b4;background-color:#033a16}.hljs-deletion{color:#ffdcd7;background-color:#67060c}