ruby_reactor 0.4.1 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.release-please-manifest.json +1 -1
- data/.rubocop.yml +1 -0
- data/CHANGELOG.md +14 -0
- data/README.md +153 -18
- data/lib/ruby_reactor/configuration.rb +13 -1
- data/lib/ruby_reactor/context.rb +2 -1
- data/lib/ruby_reactor/context_serializer.rb +1 -3
- data/lib/ruby_reactor/dsl/interrupt_builder.rb +18 -2
- data/lib/ruby_reactor/dsl/lockable.rb +19 -28
- data/lib/ruby_reactor/dsl/reactor.rb +44 -9
- data/lib/ruby_reactor/dsl/step_builder.rb +25 -39
- data/lib/ruby_reactor/dsl/validation_helpers.rb +34 -0
- data/lib/ruby_reactor/error/input_validation_error.rb +4 -0
- data/lib/ruby_reactor/executor/compensation_manager.rb +75 -47
- data/lib/ruby_reactor/executor/result_handler.rb +35 -8
- data/lib/ruby_reactor/executor/retry_manager.rb +15 -5
- data/lib/ruby_reactor/executor/step_executor.rb +46 -23
- data/lib/ruby_reactor/executor.rb +188 -49
- data/lib/ruby_reactor/map/collector.rb +4 -4
- data/lib/ruby_reactor/map/element_executor.rb +15 -1
- data/lib/ruby_reactor/map/helpers.rb +17 -4
- data/lib/ruby_reactor/middleware.rb +13 -0
- data/lib/ruby_reactor/middleware_runner.rb +29 -0
- data/lib/ruby_reactor/open_telemetry.rb +647 -0
- data/lib/ruby_reactor/rate_limit.rb +28 -0
- data/lib/ruby_reactor/rate_limit_registry.rb +51 -0
- data/lib/ruby_reactor/reactor.rb +1 -0
- data/lib/ruby_reactor/rspec/test_subject.rb +0 -1
- data/lib/ruby_reactor/sidekiq_adapter.rb +7 -21
- data/lib/ruby_reactor/sidekiq_workers/worker.rb +4 -0
- data/lib/ruby_reactor/step/map_step.rb +25 -33
- data/lib/ruby_reactor/validation/base.rb +4 -1
- data/lib/ruby_reactor/validation/input_validator.rb +4 -2
- data/lib/ruby_reactor/validation/schema_builder.rb +82 -0
- data/lib/ruby_reactor/version.rb +1 -1
- data/lib/ruby_reactor/web/coordination_serializer.rb +12 -18
- data/teley/Dockerfile +60 -0
- metadata +6 -3
- data/lib/ruby_reactor/map/execution.rb +0 -101
- data/lib/ruby_reactor/sidekiq_workers/map_execution_worker.rb +0 -15
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyReactor
|
|
4
|
+
# Global registry of named rate limits, configured once via
|
|
5
|
+
# `RubyReactor.configure`. Lets multiple reactors share a single quota for an
|
|
6
|
+
# external service (e.g. all Stripe-calling reactors throttle against one
|
|
7
|
+
# `:stripe` bucket).
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# RubyReactor.configure do |config|
|
|
11
|
+
# config.rate_limits.register(:stripe, limit: 3, period: :second)
|
|
12
|
+
# config.rate_limits.register(:twilio, limits: { second: 10, minute: 100 })
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
# class ChargeReactor < RubyReactor::Reactor
|
|
16
|
+
# with_rate_limit(:stripe)
|
|
17
|
+
# end
|
|
18
|
+
class RateLimitRegistry
|
|
19
|
+
# Raised when a reactor references a rate-limit name that was never
|
|
20
|
+
# registered. This is a configuration error, so it propagates out of
|
|
21
|
+
# `execute` rather than being swallowed into a step failure result.
|
|
22
|
+
class UnknownLimitError < StandardError; end
|
|
23
|
+
|
|
24
|
+
def initialize
|
|
25
|
+
@limits = {}
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Register a named rate limit. Same window args as the inline DSL form:
|
|
29
|
+
# a single window (`limit:` + `period:`) or layered windows (`limits:`).
|
|
30
|
+
def register(name, limit: nil, period: nil, limits: nil)
|
|
31
|
+
@limits[name.to_sym] = RubyReactor::RateLimit.normalize_specs(
|
|
32
|
+
limit: limit, period: period, limits: limits
|
|
33
|
+
)
|
|
34
|
+
self
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Return the normalized spec array for a registered name, or raise if the
|
|
38
|
+
# name was never registered (resolved lazily at execute time, so the error
|
|
39
|
+
# surfaces with a clear message instead of a nil dereference).
|
|
40
|
+
def fetch(name)
|
|
41
|
+
@limits.fetch(name.to_sym) do
|
|
42
|
+
raise UnknownLimitError, "Unknown rate limit #{name.inspect}. " \
|
|
43
|
+
"Register it with config.rate_limits.register(#{name.inspect}, ...)."
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def registered?(name)
|
|
48
|
+
@limits.key?(name.to_sym)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
data/lib/ruby_reactor/reactor.rb
CHANGED
|
@@ -105,6 +105,7 @@ module RubyReactor
|
|
|
105
105
|
if self.class.async? && !@context.inline_async_execution
|
|
106
106
|
# For async reactors, queue a job for the whole reactor
|
|
107
107
|
@context.status = :running
|
|
108
|
+
Executor.middlewares_for(self.class).on(:before_async_enqueue, @context)
|
|
108
109
|
save_context
|
|
109
110
|
|
|
110
111
|
serialized_context = ContextSerializer.serialize(@context)
|
|
@@ -41,8 +41,8 @@ module RubyReactor
|
|
|
41
41
|
|
|
42
42
|
def self.perform_map_element_in(delay, map_id:, element_id:, index:, serialized_inputs:, reactor_class_info:,
|
|
43
43
|
strict_ordering:, parent_context_id:, parent_reactor_class_name:, step_name:,
|
|
44
|
-
batch_size: nil, serialized_context: nil)
|
|
45
|
-
RubyReactor::SidekiqWorkers::MapElementWorker.perform_in(
|
|
44
|
+
batch_size: nil, serialized_context: nil, fail_fast: nil)
|
|
45
|
+
job_id = RubyReactor::SidekiqWorkers::MapElementWorker.perform_in(
|
|
46
46
|
delay,
|
|
47
47
|
{
|
|
48
48
|
"map_id" => map_id,
|
|
@@ -55,9 +55,13 @@ module RubyReactor
|
|
|
55
55
|
"parent_reactor_class_name" => parent_reactor_class_name,
|
|
56
56
|
"step_name" => step_name,
|
|
57
57
|
"batch_size" => batch_size,
|
|
58
|
-
"serialized_context" => serialized_context
|
|
58
|
+
"serialized_context" => serialized_context,
|
|
59
|
+
"fail_fast" => fail_fast
|
|
59
60
|
}
|
|
60
61
|
)
|
|
62
|
+
# Return an AsyncResult so RetryManager#handle_async_retry recognises the
|
|
63
|
+
# element was successfully requeued and yields a RetryQueuedResult.
|
|
64
|
+
RubyReactor::AsyncResult.new(job_id: job_id)
|
|
61
65
|
end
|
|
62
66
|
# rubocop:enable Metrics/ParameterLists
|
|
63
67
|
|
|
@@ -78,23 +82,5 @@ module RubyReactor
|
|
|
78
82
|
end
|
|
79
83
|
|
|
80
84
|
# rubocop:enable Metrics/ParameterLists
|
|
81
|
-
# rubocop:disable Metrics/ParameterLists
|
|
82
|
-
def self.perform_map_execution_async(map_id:, serialized_inputs:, reactor_class_info:, strict_ordering:,
|
|
83
|
-
parent_context_id:, parent_reactor_class_name:, step_name:, fail_fast: nil)
|
|
84
|
-
# rubocop:enable Metrics/ParameterLists
|
|
85
|
-
job_id = RubyReactor::SidekiqWorkers::MapExecutionWorker.perform_async(
|
|
86
|
-
{
|
|
87
|
-
"map_id" => map_id,
|
|
88
|
-
"serialized_inputs" => serialized_inputs,
|
|
89
|
-
"reactor_class_info" => reactor_class_info,
|
|
90
|
-
"strict_ordering" => strict_ordering,
|
|
91
|
-
"parent_context_id" => parent_context_id,
|
|
92
|
-
"parent_reactor_class_name" => parent_reactor_class_name,
|
|
93
|
-
"step_name" => step_name,
|
|
94
|
-
"fail_fast" => fail_fast
|
|
95
|
-
}
|
|
96
|
-
)
|
|
97
|
-
RubyReactor::AsyncResult.new(job_id: job_id)
|
|
98
|
-
end
|
|
99
85
|
end
|
|
100
86
|
end
|
|
@@ -60,6 +60,10 @@ module RubyReactor
|
|
|
60
60
|
# budget or appear as an error in dashboards. After the configured
|
|
61
61
|
# cap is reached we escalate by marking the reactor as failed.
|
|
62
62
|
handle_snooze(serialized_context, reactor_class_name, context, snooze_count, e)
|
|
63
|
+
rescue RubyReactor::RateLimitRegistry::UnknownLimitError => e
|
|
64
|
+
# Permanent configuration error — snoozing or retrying the same job
|
|
65
|
+
# will keep failing. Mark the context failed immediately.
|
|
66
|
+
escalate_snooze(context, snooze_count, e)
|
|
63
67
|
end
|
|
64
68
|
end
|
|
65
69
|
|
|
@@ -183,31 +183,35 @@ module RubyReactor
|
|
|
183
183
|
)
|
|
184
184
|
end
|
|
185
185
|
|
|
186
|
-
def dispatch_async_map(map_id, arguments, context,
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
186
|
+
def dispatch_async_map(map_id, arguments, context, _reactor_class_info, step_name)
|
|
187
|
+
# Every async map runs through the per-element Dispatcher path. When no
|
|
188
|
+
# batch_size is given we default to the full source size (one fan-out
|
|
189
|
+
# batch), so there is a single execution path: each element runs in its
|
|
190
|
+
# own worker, with the map counter/collector tracking completion. This
|
|
191
|
+
# lets elements with async steps or async retries hand off correctly
|
|
192
|
+
# instead of being forced to run synchronously in a single worker.
|
|
193
|
+
batch_size = arguments[:batch_size] || arguments[:source].size
|
|
194
|
+
|
|
195
|
+
RubyReactor::Map::Dispatcher.perform(
|
|
196
|
+
map_id: map_id,
|
|
197
|
+
parent_context_id: context.context_id,
|
|
198
|
+
parent_reactor_class_name: context.reactor_class.name,
|
|
199
|
+
source: arguments[:source],
|
|
200
|
+
batch_size: batch_size,
|
|
201
|
+
step_name: step_name,
|
|
202
|
+
argument_mappings: arguments[:argument_mappings],
|
|
203
|
+
strict_ordering: arguments[:strict_ordering],
|
|
204
|
+
mapped_reactor_class: arguments[:mapped_reactor_class],
|
|
205
|
+
fail_fast: arguments[:fail_fast].nil? || arguments[:fail_fast]
|
|
206
|
+
)
|
|
207
|
+
queue_collector(map_id, context, step_name, arguments[:strict_ordering])
|
|
208
|
+
"map:#{map_id}"
|
|
207
209
|
end
|
|
208
210
|
|
|
209
211
|
def prepare_async_execution(context, map_id, count)
|
|
210
212
|
storage = RubyReactor.configuration.storage_adapter
|
|
213
|
+
middlewares = context.middlewares || Executor.middlewares_for(context.reactor_class)
|
|
214
|
+
middlewares.on(:before_async_enqueue, context)
|
|
211
215
|
serialized_context = ContextSerializer.serialize(context)
|
|
212
216
|
storage.store_context(context.context_id, serialized_context, context.reactor_class.name)
|
|
213
217
|
storage.set_map_counter(map_id, count, context.reactor_class.name)
|
|
@@ -268,18 +272,6 @@ module RubyReactor
|
|
|
268
272
|
strict_ordering: strict_ordering, timeout: 3600
|
|
269
273
|
)
|
|
270
274
|
end
|
|
271
|
-
|
|
272
|
-
def queue_single_worker(map_id:, arguments:, context:, reactor_class_info:, step_name:)
|
|
273
|
-
inputs = { source: arguments[:source], mappings: arguments[:argument_mappings] || {} }
|
|
274
|
-
serialized_inputs = ContextSerializer.serialize_value(inputs)
|
|
275
|
-
|
|
276
|
-
RubyReactor.configuration.async_router.perform_map_execution_async(
|
|
277
|
-
map_id: map_id, serialized_inputs: serialized_inputs,
|
|
278
|
-
reactor_class_info: reactor_class_info, strict_ordering: arguments[:strict_ordering],
|
|
279
|
-
parent_context_id: context.context_id, parent_reactor_class_name: context.reactor_class.name,
|
|
280
|
-
step_name: step_name.to_s, fail_fast: arguments[:fail_fast].nil? || arguments[:fail_fast]
|
|
281
|
-
)
|
|
282
|
-
end
|
|
283
275
|
end
|
|
284
276
|
end
|
|
285
277
|
end
|
|
@@ -19,7 +19,10 @@ module RubyReactor
|
|
|
19
19
|
|
|
20
20
|
def failure(errors)
|
|
21
21
|
error = RubyReactor::Error::InputValidationError.new(errors)
|
|
22
|
-
|
|
22
|
+
# Carry the structured field errors on the Failure itself so every
|
|
23
|
+
# validation failure shape (reactor inputs via `run`, step args, step
|
|
24
|
+
# output) exposes `validation_errors` uniformly.
|
|
25
|
+
RubyReactor.Failure(error, validation_errors: errors)
|
|
23
26
|
end
|
|
24
27
|
end
|
|
25
28
|
end
|
|
@@ -5,14 +5,16 @@ require "dry-validation"
|
|
|
5
5
|
module RubyReactor
|
|
6
6
|
module Validation
|
|
7
7
|
class InputValidator < Base
|
|
8
|
-
attr_reader :schema
|
|
8
|
+
attr_reader :schema, :wrap_key
|
|
9
9
|
|
|
10
|
-
def initialize(schema)
|
|
10
|
+
def initialize(schema, wrap_key: nil)
|
|
11
11
|
super()
|
|
12
12
|
@schema = schema
|
|
13
|
+
@wrap_key = wrap_key
|
|
13
14
|
end
|
|
14
15
|
|
|
15
16
|
def call(data)
|
|
17
|
+
data = { @wrap_key => data } if @wrap_key
|
|
16
18
|
result = schema.call(data)
|
|
17
19
|
|
|
18
20
|
if result.success?
|
|
@@ -12,6 +12,88 @@ module RubyReactor
|
|
|
12
12
|
def self.build_contract_from_block(&block)
|
|
13
13
|
Class.new(Dry::Validation::Contract, &block).new
|
|
14
14
|
end
|
|
15
|
+
|
|
16
|
+
# Form 1 / 1b — a single named value with an optional type and predicates.
|
|
17
|
+
#
|
|
18
|
+
# build_inline(:age, :integer, false, { gteq?: 18 })
|
|
19
|
+
# => required(:age).filled(:integer, gteq?: 18)
|
|
20
|
+
# build_inline(:user, User, false, {})
|
|
21
|
+
# => required(:user).filled(type?: User)
|
|
22
|
+
# build_inline(:bio, :string, true, { max_size?: 100 })
|
|
23
|
+
# => optional(:bio).maybe(:string, max_size?: 100)
|
|
24
|
+
def self.build_inline(name, type, optional, predicates)
|
|
25
|
+
rules = [[name, type, optional, predicates]]
|
|
26
|
+
Dry::Schema.Params do
|
|
27
|
+
SchemaBuilder.apply_inline_rules(self, rules)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Form 2 — the block receives the macro already bound to the name and
|
|
32
|
+
# optionality (`required(name)` / `optional(name)`). Because it is invoked
|
|
33
|
+
# as a block argument (not via instance_eval) the caller's `self` and
|
|
34
|
+
# lexical scope (class constants, helper methods) stay reachable.
|
|
35
|
+
def self.build_macro(name, optional, &block)
|
|
36
|
+
macro_method = optional ? :optional : :required
|
|
37
|
+
Dry::Schema.Params do
|
|
38
|
+
block.call(public_send(macro_method, name))
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Compose per-argument inline rules with an optional `validate_args`
|
|
43
|
+
# input (block or pre-built schema). Block/inline rules are applied first;
|
|
44
|
+
# a pre-built schema is merged last so its rules win.
|
|
45
|
+
def self.build_args(inline_rules, validate_input)
|
|
46
|
+
block = validate_input.is_a?(Proc) ? validate_input : nil
|
|
47
|
+
prebuilt = !validate_input.nil? && block.nil? ? schema_for(validate_input) : nil
|
|
48
|
+
|
|
49
|
+
composed = build_args_base(inline_rules, block)
|
|
50
|
+
|
|
51
|
+
if prebuilt && composed
|
|
52
|
+
composed.merge(prebuilt)
|
|
53
|
+
elsif prebuilt
|
|
54
|
+
prebuilt
|
|
55
|
+
else
|
|
56
|
+
composed
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def self.build_args_base(inline_rules, block)
|
|
61
|
+
return Dry::Schema.Params(&block) if inline_rules.empty? && block
|
|
62
|
+
return nil if inline_rules.empty?
|
|
63
|
+
|
|
64
|
+
Dry::Schema.Params do
|
|
65
|
+
SchemaBuilder.apply_inline_rules(self, inline_rules)
|
|
66
|
+
instance_exec(&block) if block
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Apply a list of inline rules ([name, type, optional, predicates]) onto a
|
|
71
|
+
# dry-schema DSL context.
|
|
72
|
+
def self.apply_inline_rules(dsl, rules)
|
|
73
|
+
rules.each do |name, type, optional, predicates|
|
|
74
|
+
macro_method = optional ? :optional : :required
|
|
75
|
+
rule_method = optional ? :maybe : :filled
|
|
76
|
+
positional = type.is_a?(Symbol) ? [type] : []
|
|
77
|
+
kwargs = type.is_a?(Module) ? { type?: type, **predicates } : predicates
|
|
78
|
+
|
|
79
|
+
macro = dsl.public_send(macro_method, name)
|
|
80
|
+
if kwargs.empty?
|
|
81
|
+
macro.public_send(rule_method, *positional)
|
|
82
|
+
else
|
|
83
|
+
macro.public_send(rule_method, *positional, **kwargs)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Unwrap an InputValidator into its underlying dry-schema; pass dry-schemas
|
|
89
|
+
# through unchanged.
|
|
90
|
+
def self.schema_for(schema_or_validator)
|
|
91
|
+
if schema_or_validator.is_a?(RubyReactor::Validation::InputValidator)
|
|
92
|
+
schema_or_validator.schema
|
|
93
|
+
else
|
|
94
|
+
schema_or_validator
|
|
95
|
+
end
|
|
96
|
+
end
|
|
15
97
|
end
|
|
16
98
|
end
|
|
17
99
|
end
|
data/lib/ruby_reactor/version.rb
CHANGED
|
@@ -122,34 +122,28 @@ module RubyReactor
|
|
|
122
122
|
end
|
|
123
123
|
|
|
124
124
|
{
|
|
125
|
-
configured: {
|
|
126
|
-
limits: config[:limits].map do |window|
|
|
127
|
-
{
|
|
128
|
-
name: window[:name],
|
|
129
|
-
limit: window[:limit],
|
|
130
|
-
period_seconds: window[:period_seconds]
|
|
131
|
-
}
|
|
132
|
-
end
|
|
133
|
-
},
|
|
125
|
+
configured: { limits: map_limits(config[:limits]) },
|
|
134
126
|
key: key,
|
|
135
127
|
state: windows
|
|
136
128
|
}
|
|
137
129
|
rescue StandardError => e
|
|
138
130
|
{
|
|
139
|
-
configured: {
|
|
140
|
-
limits: config[:limits]&.map do |window|
|
|
141
|
-
{
|
|
142
|
-
name: window[:name],
|
|
143
|
-
limit: window[:limit],
|
|
144
|
-
period_seconds: window[:period_seconds]
|
|
145
|
-
}
|
|
146
|
-
end
|
|
147
|
-
},
|
|
131
|
+
configured: { limits: map_limits(config[:limits]) },
|
|
148
132
|
key: nil,
|
|
149
133
|
key_error: e.message
|
|
150
134
|
}
|
|
151
135
|
end
|
|
152
136
|
|
|
137
|
+
def map_limits(limits)
|
|
138
|
+
Array(limits).map do |window|
|
|
139
|
+
{
|
|
140
|
+
name: window[:name],
|
|
141
|
+
limit: window[:limit],
|
|
142
|
+
period_seconds: window[:period_seconds]
|
|
143
|
+
}
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
153
147
|
def build_period(config, inputs, adapter)
|
|
154
148
|
key = resolve_key(config[:key_proc], inputs)
|
|
155
149
|
every = config[:every]
|
data/teley/Dockerfile
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Dockerfile for teley
|
|
2
|
+
FROM node:24-slim
|
|
3
|
+
|
|
4
|
+
WORKDIR /app
|
|
5
|
+
|
|
6
|
+
# Install git and other build deps
|
|
7
|
+
RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/*
|
|
8
|
+
|
|
9
|
+
# Clone teley repo
|
|
10
|
+
RUN git clone https://github.com/logaretm/teley.git .
|
|
11
|
+
|
|
12
|
+
# Allow native builds for dependencies (like esbuild) via workspace config
|
|
13
|
+
RUN echo "dangerouslyAllowAllBuilds: true" > pnpm-workspace.yaml
|
|
14
|
+
|
|
15
|
+
# Patch workers/src/durable-object.ts to support default_token
|
|
16
|
+
RUN node -e ' \
|
|
17
|
+
const fs = require("fs"); \
|
|
18
|
+
const file = "workers/src/durable-object.ts"; \
|
|
19
|
+
let content = fs.readFileSync(file, "utf8"); \
|
|
20
|
+
content = content.replace("const token = url.searchParams.get(\x27token\x27);", "const token = url.searchParams.get(\x27token\x27) || \x27default_token\x27;"); \
|
|
21
|
+
fs.writeFileSync(file, content, "utf8"); \
|
|
22
|
+
'
|
|
23
|
+
|
|
24
|
+
# Patch app/pages/live/[roomId].vue to support default_token fallback
|
|
25
|
+
RUN node -e ' \
|
|
26
|
+
const fs = require("fs"); \
|
|
27
|
+
const file = "app/pages/live/[roomId].vue"; \
|
|
28
|
+
let content = fs.readFileSync(file, "utf8"); \
|
|
29
|
+
content = content.replace("const token = (route.query.token as string) || \x27\x27;", "const token = (route.query.token as string) || \x27default_token\x27;"); \
|
|
30
|
+
fs.writeFileSync(file, content, "utf8"); \
|
|
31
|
+
'
|
|
32
|
+
|
|
33
|
+
# Patch app/composables/useSession.ts to default to ruby_reactor_session and default_token
|
|
34
|
+
RUN node -e ' \
|
|
35
|
+
const fs = require("fs"); \
|
|
36
|
+
const file = "app/composables/useSession.ts"; \
|
|
37
|
+
let content = fs.readFileSync(file, "utf8"); \
|
|
38
|
+
content = content.replace(/const initialize = async \(\): Promise<void> => {[\s\S]*?};/, "const initialize = async (): Promise<void> => {\n if (initialized.value) return;\n roomId.value = \x27ruby_reactor_session\x27;\n receiveToken.value = \x27default_token\x27;\n isNewSession.value = false;\n initialized.value = true;\n };"); \
|
|
39
|
+
fs.writeFileSync(file, content, "utf8"); \
|
|
40
|
+
'
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# Enable corepack and install pnpm
|
|
44
|
+
RUN corepack enable && corepack prepare pnpm@latest --activate
|
|
45
|
+
|
|
46
|
+
# Install dependencies
|
|
47
|
+
RUN pnpm install --frozen-lockfile
|
|
48
|
+
|
|
49
|
+
# Build the Nuxt static files
|
|
50
|
+
RUN pnpm build
|
|
51
|
+
|
|
52
|
+
# Expose port
|
|
53
|
+
EXPOSE 3000
|
|
54
|
+
|
|
55
|
+
ENV HOST=0.0.0.0
|
|
56
|
+
ENV PORT=3000
|
|
57
|
+
|
|
58
|
+
# Start Cloudflare Pages local dev server via wrangler
|
|
59
|
+
WORKDIR /app/workers
|
|
60
|
+
CMD ["npx", "wrangler", "dev", "--ip", "0.0.0.0", "--port", "3000"]
|
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.
|
|
4
|
+
version: 0.5.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Artur
|
|
@@ -133,12 +133,15 @@ files:
|
|
|
133
133
|
- lib/ruby_reactor/map/collector.rb
|
|
134
134
|
- lib/ruby_reactor/map/dispatcher.rb
|
|
135
135
|
- lib/ruby_reactor/map/element_executor.rb
|
|
136
|
-
- lib/ruby_reactor/map/execution.rb
|
|
137
136
|
- lib/ruby_reactor/map/helpers.rb
|
|
138
137
|
- lib/ruby_reactor/map/result_enumerator.rb
|
|
139
138
|
- lib/ruby_reactor/max_retries_exhausted_failure.rb
|
|
139
|
+
- lib/ruby_reactor/middleware.rb
|
|
140
|
+
- lib/ruby_reactor/middleware_runner.rb
|
|
141
|
+
- lib/ruby_reactor/open_telemetry.rb
|
|
140
142
|
- lib/ruby_reactor/period.rb
|
|
141
143
|
- lib/ruby_reactor/rate_limit.rb
|
|
144
|
+
- lib/ruby_reactor/rate_limit_registry.rb
|
|
142
145
|
- lib/ruby_reactor/reactor.rb
|
|
143
146
|
- lib/ruby_reactor/registry.rb
|
|
144
147
|
- lib/ruby_reactor/retry_context.rb
|
|
@@ -152,7 +155,6 @@ files:
|
|
|
152
155
|
- lib/ruby_reactor/sidekiq_adapter.rb
|
|
153
156
|
- lib/ruby_reactor/sidekiq_workers/map_collector_worker.rb
|
|
154
157
|
- lib/ruby_reactor/sidekiq_workers/map_element_worker.rb
|
|
155
|
-
- lib/ruby_reactor/sidekiq_workers/map_execution_worker.rb
|
|
156
158
|
- lib/ruby_reactor/sidekiq_workers/worker.rb
|
|
157
159
|
- lib/ruby_reactor/step.rb
|
|
158
160
|
- lib/ruby_reactor/step/compose_step.rb
|
|
@@ -184,6 +186,7 @@ files:
|
|
|
184
186
|
- llms-full.txt
|
|
185
187
|
- llms.txt
|
|
186
188
|
- sig/ruby_reactor.rbs
|
|
189
|
+
- teley/Dockerfile
|
|
187
190
|
homepage: https://github.com/arturictus/ruby_reactor
|
|
188
191
|
licenses: []
|
|
189
192
|
metadata:
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module RubyReactor
|
|
4
|
-
module Map
|
|
5
|
-
class Execution
|
|
6
|
-
extend Helpers
|
|
7
|
-
|
|
8
|
-
def self.perform(arguments)
|
|
9
|
-
arguments = arguments.transform_keys(&:to_sym)
|
|
10
|
-
storage = RubyReactor.configuration.storage_adapter
|
|
11
|
-
|
|
12
|
-
parent_context = load_parent_context_from_storage(
|
|
13
|
-
arguments[:parent_context_id], arguments[:parent_reactor_class_name], storage
|
|
14
|
-
)
|
|
15
|
-
reactor_class = resolve_reactor_class(arguments[:reactor_class_info])
|
|
16
|
-
inputs = ContextSerializer.deserialize_value(arguments[:serialized_inputs])
|
|
17
|
-
|
|
18
|
-
results = execute_all_elements(
|
|
19
|
-
source: inputs[:source], mappings: inputs[:mappings],
|
|
20
|
-
reactor_class: reactor_class, parent_context: parent_context,
|
|
21
|
-
storage_options: {
|
|
22
|
-
map_id: arguments[:map_id], storage: storage,
|
|
23
|
-
parent_reactor_class_name: arguments[:parent_reactor_class_name],
|
|
24
|
-
strict_ordering: arguments[:strict_ordering],
|
|
25
|
-
fail_fast: arguments[:fail_fast]
|
|
26
|
-
}
|
|
27
|
-
)
|
|
28
|
-
|
|
29
|
-
finalize_execution(results, parent_context, arguments[:step_name], arguments[:parent_reactor_class_name],
|
|
30
|
-
storage)
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def self.execute_all_elements(source:, mappings:, reactor_class:, parent_context:, storage_options:)
|
|
34
|
-
# rubocop:disable Metrics/BlockLength
|
|
35
|
-
source.map.with_index do |element, index|
|
|
36
|
-
if storage_options[:fail_fast]
|
|
37
|
-
failed_context_id = storage_options[:storage].retrieve_map_failed_context_id(
|
|
38
|
-
storage_options[:map_id], storage_options[:parent_reactor_class_name]
|
|
39
|
-
)
|
|
40
|
-
next if failed_context_id
|
|
41
|
-
end
|
|
42
|
-
element_inputs = build_element_inputs(mappings, parent_context, element)
|
|
43
|
-
|
|
44
|
-
# Manually create and link context to ensure parent_context_id is set
|
|
45
|
-
child_context = RubyReactor::Context.new(element_inputs, reactor_class)
|
|
46
|
-
link_contexts(child_context, parent_context)
|
|
47
|
-
|
|
48
|
-
# Ensure we store the element context linkage
|
|
49
|
-
storage_options[:storage].store_map_element_context_id(
|
|
50
|
-
storage_options[:map_id], child_context.context_id, storage_options[:parent_reactor_class_name]
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
# Set map metadata for failure handling
|
|
54
|
-
metadata = {
|
|
55
|
-
map_id: storage_options[:map_id],
|
|
56
|
-
parent_reactor_class_name: storage_options[:parent_reactor_class_name],
|
|
57
|
-
index: index
|
|
58
|
-
}
|
|
59
|
-
child_context.map_metadata = metadata
|
|
60
|
-
|
|
61
|
-
executor = RubyReactor::Executor.new(reactor_class, {}, child_context)
|
|
62
|
-
executor.execute
|
|
63
|
-
result = executor.result
|
|
64
|
-
|
|
65
|
-
store_result(result, index, storage_options)
|
|
66
|
-
|
|
67
|
-
if result.failure? && storage_options[:fail_fast]
|
|
68
|
-
storage_options[:storage].store_map_failed_context_id(
|
|
69
|
-
storage_options[:map_id], child_context.context_id, storage_options[:parent_reactor_class_name]
|
|
70
|
-
)
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
result
|
|
74
|
-
end.compact
|
|
75
|
-
# rubocop:enable Metrics/BlockLength
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
def self.link_contexts(child_context, parent_context)
|
|
79
|
-
child_context.parent_context = parent_context
|
|
80
|
-
child_context.root_context = parent_context.root_context || parent_context
|
|
81
|
-
child_context.inline_async_execution = parent_context.inline_async_execution
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
def self.store_result(result, index, options)
|
|
85
|
-
value = result.success? ? result.value : { _error: result.error }
|
|
86
|
-
options[:storage].store_map_result(
|
|
87
|
-
options[:map_id], index, ContextSerializer.serialize_value(value), options[:parent_reactor_class_name],
|
|
88
|
-
strict_ordering: options[:strict_ordering]
|
|
89
|
-
)
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
def self.finalize_execution(results, parent_context, step_name, parent_reactor_class_name, storage)
|
|
93
|
-
step_config = Object.const_get(parent_reactor_class_name).steps[step_name.to_sym]
|
|
94
|
-
final_result = apply_collect_block(results, step_config)
|
|
95
|
-
resume_parent_execution(parent_context, step_name, final_result, storage)
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
private_class_method :execute_all_elements, :store_result, :finalize_execution
|
|
99
|
-
end
|
|
100
|
-
end
|
|
101
|
-
end
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "sidekiq"
|
|
4
|
-
|
|
5
|
-
module RubyReactor
|
|
6
|
-
module SidekiqWorkers
|
|
7
|
-
class MapExecutionWorker
|
|
8
|
-
include ::Sidekiq::Worker
|
|
9
|
-
|
|
10
|
-
def perform(arguments)
|
|
11
|
-
RubyReactor::Map::Execution.perform(arguments)
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
end
|
|
15
|
-
end
|