ruby_reactor 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
@@ -183,31 +183,35 @@ module RubyReactor
183
183
  )
184
184
  end
185
185
 
186
- def dispatch_async_map(map_id, arguments, context, reactor_class_info, step_name)
187
- if arguments[:batch_size]
188
- # Use new Dispatcher with Backpressure
189
- RubyReactor::Map::Dispatcher.perform(
190
- map_id: map_id,
191
- parent_context_id: context.context_id,
192
- parent_reactor_class_name: context.reactor_class.name,
193
- source: arguments[:source],
194
- batch_size: arguments[:batch_size],
195
- step_name: step_name,
196
- argument_mappings: arguments[:argument_mappings],
197
- strict_ordering: arguments[:strict_ordering],
198
- mapped_reactor_class: arguments[:mapped_reactor_class],
199
- fail_fast: arguments[:fail_fast].nil? || arguments[:fail_fast]
200
- )
201
- queue_collector(map_id, context, step_name, arguments[:strict_ordering])
202
- "map:#{map_id}"
203
- else
204
- queue_single_worker(map_id: map_id, arguments: arguments, context: context,
205
- reactor_class_info: reactor_class_info, step_name: step_name)
206
- end
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyReactor
4
- VERSION = "0.4.1"
4
+ VERSION = "0.5.0"
5
5
  end
@@ -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.1
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Artur
@@ -133,10 +133,12 @@ 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
142
144
  - lib/ruby_reactor/reactor.rb
@@ -152,7 +154,6 @@ files:
152
154
  - lib/ruby_reactor/sidekiq_adapter.rb
153
155
  - lib/ruby_reactor/sidekiq_workers/map_collector_worker.rb
154
156
  - lib/ruby_reactor/sidekiq_workers/map_element_worker.rb
155
- - lib/ruby_reactor/sidekiq_workers/map_execution_worker.rb
156
157
  - lib/ruby_reactor/sidekiq_workers/worker.rb
157
158
  - lib/ruby_reactor/step.rb
158
159
  - lib/ruby_reactor/step/compose_step.rb
@@ -184,6 +185,7 @@ files:
184
185
  - llms-full.txt
185
186
  - llms.txt
186
187
  - sig/ruby_reactor.rbs
188
+ - teley/Dockerfile
187
189
  homepage: https://github.com/arturictus/ruby_reactor
188
190
  licenses: []
189
191
  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