ori-rb 0.4

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 (84) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +8 -0
  3. data/.ruby-version +1 -0
  4. data/LICENSE +9 -0
  5. data/README.md +444 -0
  6. data/Rakefile +17 -0
  7. data/docs/images/example_boundary.png +0 -0
  8. data/docs/images/example_boundary_cancellation.png +0 -0
  9. data/docs/images/example_channel.png +0 -0
  10. data/docs/images/example_mutex.png +0 -0
  11. data/docs/images/example_promise.png +0 -0
  12. data/docs/images/example_semaphore.png +0 -0
  13. data/docs/images/example_trace.png +0 -0
  14. data/docs/images/example_trace_tag.png +0 -0
  15. data/lib/ori/channel.rb +148 -0
  16. data/lib/ori/lazy.rb +163 -0
  17. data/lib/ori/mutex.rb +9 -0
  18. data/lib/ori/out/index.html +146 -0
  19. data/lib/ori/out/script.js +3 -0
  20. data/lib/ori/promise.rb +39 -0
  21. data/lib/ori/reentrant_semaphore.rb +68 -0
  22. data/lib/ori/scope.rb +620 -0
  23. data/lib/ori/select.rb +35 -0
  24. data/lib/ori/selectable.rb +9 -0
  25. data/lib/ori/semaphore.rb +49 -0
  26. data/lib/ori/task.rb +78 -0
  27. data/lib/ori/timeout.rb +16 -0
  28. data/lib/ori/tracer.rb +335 -0
  29. data/lib/ori/version.rb +5 -0
  30. data/lib/ori.rb +68 -0
  31. data/mise-tasks/test +15 -0
  32. data/mise.toml +40 -0
  33. data/sorbet/config +8 -0
  34. data/sorbet/rbi/gems/.gitattributes +1 -0
  35. data/sorbet/rbi/gems/ast@2.4.3.rbi +585 -0
  36. data/sorbet/rbi/gems/benchmark@0.4.1.rbi +619 -0
  37. data/sorbet/rbi/gems/date@3.4.1.rbi +75 -0
  38. data/sorbet/rbi/gems/erb@5.1.1.rbi +845 -0
  39. data/sorbet/rbi/gems/erubi@1.13.1.rbi +155 -0
  40. data/sorbet/rbi/gems/io-console@0.8.1.rbi +9 -0
  41. data/sorbet/rbi/gems/json@2.15.1.rbi +2101 -0
  42. data/sorbet/rbi/gems/language_server-protocol@3.17.0.5.rbi +9 -0
  43. data/sorbet/rbi/gems/lint_roller@1.1.0.rbi +240 -0
  44. data/sorbet/rbi/gems/logger@1.7.0.rbi +963 -0
  45. data/sorbet/rbi/gems/minitest@5.26.0.rbi +2234 -0
  46. data/sorbet/rbi/gems/netrc@0.11.0.rbi +159 -0
  47. data/sorbet/rbi/gems/nio4r@2.7.4.rbi +293 -0
  48. data/sorbet/rbi/gems/parallel@1.27.0.rbi +291 -0
  49. data/sorbet/rbi/gems/parser@3.3.9.0.rbi +5535 -0
  50. data/sorbet/rbi/gems/pp@0.6.3.rbi +376 -0
  51. data/sorbet/rbi/gems/prettyprint@0.2.0.rbi +477 -0
  52. data/sorbet/rbi/gems/prism@1.5.2.rbi +42056 -0
  53. data/sorbet/rbi/gems/psych@5.2.6.rbi +2469 -0
  54. data/sorbet/rbi/gems/racc@1.8.1.rbi +160 -0
  55. data/sorbet/rbi/gems/rainbow@3.1.1.rbi +403 -0
  56. data/sorbet/rbi/gems/rake@13.3.0.rbi +3036 -0
  57. data/sorbet/rbi/gems/rbi@0.3.7.rbi +7115 -0
  58. data/sorbet/rbi/gems/rbs@3.9.5.rbi +6978 -0
  59. data/sorbet/rbi/gems/rdoc@6.15.0.rbi +12777 -0
  60. data/sorbet/rbi/gems/regexp_parser@2.11.3.rbi +3845 -0
  61. data/sorbet/rbi/gems/reline@0.6.2.rbi +9 -0
  62. data/sorbet/rbi/gems/rexml@3.4.4.rbi +5285 -0
  63. data/sorbet/rbi/gems/rubocop-ast@1.47.1.rbi +7780 -0
  64. data/sorbet/rbi/gems/rubocop-shopify@2.17.1.rbi +9 -0
  65. data/sorbet/rbi/gems/rubocop-sorbet@0.11.0.rbi +2506 -0
  66. data/sorbet/rbi/gems/rubocop@1.81.1.rbi +63489 -0
  67. data/sorbet/rbi/gems/ruby-progressbar@1.13.0.rbi +1318 -0
  68. data/sorbet/rbi/gems/spoom@1.6.3.rbi +6985 -0
  69. data/sorbet/rbi/gems/stringio@3.1.7.rbi +9 -0
  70. data/sorbet/rbi/gems/tapioca@0.16.11.rbi +3628 -0
  71. data/sorbet/rbi/gems/thor@1.4.0.rbi +4399 -0
  72. data/sorbet/rbi/gems/tsort@0.2.0.rbi +393 -0
  73. data/sorbet/rbi/gems/unicode-display_width@3.2.0.rbi +132 -0
  74. data/sorbet/rbi/gems/unicode-emoji@4.1.0.rbi +251 -0
  75. data/sorbet/rbi/gems/vernier@1.8.1-96ce5c739bfe6a18d2f4393f4219a1bf48674b87.rbi +904 -0
  76. data/sorbet/rbi/gems/yard-sorbet@0.9.0.rbi +435 -0
  77. data/sorbet/rbi/gems/yard@0.9.37.rbi +18379 -0
  78. data/sorbet/rbi/gems/zeitwerk@2.7.3.rbi +1429 -0
  79. data/sorbet/shims/fiber.rbi +21 -0
  80. data/sorbet/shims/io.rbi +8 -0
  81. data/sorbet/shims/random.rbi +9 -0
  82. data/sorbet/shims/rdoc.rbi +3 -0
  83. data/sorbet/tapioca/require.rb +7 -0
  84. metadata +169 -0
data/lib/ori/task.rb ADDED
@@ -0,0 +1,78 @@
1
+ # typed: true
2
+
3
+ module Ori
4
+ class Task
5
+ include(Ori::Selectable)
6
+
7
+ EMPTY = :empty
8
+
9
+ attr_reader :fiber
10
+
11
+ def initialize(&block)
12
+ @fiber = Fiber.new(&block)
13
+ @killed = false
14
+ @value = EMPTY
15
+ end
16
+
17
+ def alive?
18
+ @fiber.alive?
19
+ end
20
+
21
+ def value
22
+ @value unless @value == EMPTY
23
+ end
24
+
25
+ def raise_error(error)
26
+ @fiber.raise(error)
27
+ end
28
+
29
+ def killed?
30
+ @killed
31
+ end
32
+
33
+ def kill
34
+ @fiber.kill
35
+ @killed = true
36
+ @value = EMPTY
37
+ end
38
+
39
+ def id
40
+ @id ||= @fiber.object_id
41
+ end
42
+
43
+ def resume
44
+ if @cancellation_error
45
+ @fiber.kill
46
+ return @cancellation_error
47
+ end
48
+
49
+ fiber_result = @fiber.resume
50
+
51
+ case fiber_result
52
+ when Ori::Channel, Ori::Promise, Ori::Semaphore, Ori::ReentrantSemaphore
53
+ fiber_result
54
+ else
55
+ return self if @fiber.alive?
56
+
57
+ @value = fiber_result
58
+ end
59
+ rescue => error
60
+ @fiber.kill
61
+ raise error
62
+ end
63
+
64
+ def await
65
+ Fiber.yield while @fiber.alive?
66
+ @value
67
+ end
68
+
69
+ def deconstruct
70
+ [await]
71
+ end
72
+
73
+ def cancel(error)
74
+ @cancellation_error = error
75
+ resume
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,16 @@
1
+ # typed: true
2
+
3
+ module Ori
4
+ class Timeout
5
+ include(Ori::Selectable)
6
+
7
+ def initialize(duration)
8
+ @duration = duration
9
+ end
10
+
11
+ def await
12
+ sleep(@duration)
13
+ true
14
+ end
15
+ end
16
+ end
data/lib/ori/tracer.rb ADDED
@@ -0,0 +1,335 @@
1
+ # typed: true
2
+
3
+ require "json"
4
+
5
+ module Ori
6
+ class Tracer
7
+ TIMELINE_WIDTH = 80
8
+
9
+ Event = Struct.new(:fiber_id, :type, :timestamp, :data, :scope_id)
10
+ ScopeEvent = Struct.new(:scope_id, :type, :timestamp, :data)
11
+
12
+ def initialize
13
+ @events = []
14
+ @scope_events = []
15
+ @start_time = nil
16
+ @fiber_names = {}
17
+ @fiber_ids = Set.new
18
+ @scope_hierarchy = {}
19
+ @fiber_scopes = {}
20
+ end
21
+
22
+ def register_scope(scope_id, parent_scope_id = nil, creating_fiber_id = nil, name: nil)
23
+ if parent_scope_id
24
+ @scope_hierarchy[parent_scope_id] ||= []
25
+ @scope_hierarchy[parent_scope_id] << scope_id
26
+ end
27
+
28
+ # Store scope names and use them as group IDs
29
+ @scope_names ||= {}
30
+ if name
31
+ @scope_names[scope_id] = "Scope #{name}"
32
+ end
33
+
34
+ # Track which fiber created this scope (if any)
35
+ @scope_creators ||= {}
36
+ @scope_creators[scope_id] = creating_fiber_id if creating_fiber_id
37
+ end
38
+
39
+ def register_fiber(fiber_id, scope_id)
40
+ @fiber_scopes[fiber_id] = scope_id
41
+ end
42
+
43
+ def record(fiber_id, type, data = nil)
44
+ return unless fiber_id
45
+
46
+ @start_time ||= current_time
47
+ @fiber_ids << fiber_id
48
+
49
+ @events << Event.new(
50
+ fiber_id,
51
+ type,
52
+ (current_time - @start_time).round(8),
53
+ data,
54
+ @fiber_scopes[fiber_id],
55
+ )
56
+ end
57
+
58
+ def record_scope(scope_id, type, data = nil)
59
+ return unless scope_id
60
+
61
+ @start_time ||= current_time
62
+
63
+ @scope_events << ScopeEvent.new(
64
+ scope_id,
65
+ type,
66
+ (current_time - @start_time).round(8),
67
+ data,
68
+ )
69
+ end
70
+
71
+ def visualize
72
+ return "No events recorded." if @events.empty?
73
+
74
+ name_width = 42
75
+ min_spacing = 1
76
+ duration = [@events.last.timestamp, 0.00000001].max
77
+
78
+ output = []
79
+ output << "Fiber Execution Timeline (#{duration.round(3)}s)"
80
+
81
+ # First pass: calculate all positions with minimum spacing
82
+ positions_by_fiber = {}
83
+ max_position = T.let(0, T.untyped)
84
+
85
+ @fiber_ids.sort.each do |fiber_id|
86
+ fiber_events = @events.select { |e| e.fiber_id == fiber_id }
87
+ next if fiber_events.empty?
88
+
89
+ # Calculate raw positions based on timestamps
90
+ positions = []
91
+ fiber_events.each_with_index do |evt, idx|
92
+ raw_pos = (evt.timestamp / duration * TIMELINE_WIDTH).floor # Use larger scale initially
93
+
94
+ if idx > 0
95
+ # Ensure minimum spacing from previous event
96
+ prev_pos = T.unsafe(positions.last) || -1
97
+ positions << [raw_pos, prev_pos + min_spacing].max
98
+ else
99
+ positions << raw_pos
100
+ end
101
+ end
102
+
103
+ positions_by_fiber[fiber_id] = positions
104
+ max_position = [max_position, T.unsafe(positions.last) || 0].max
105
+ end
106
+
107
+ # Calculate final timeline width based on max position
108
+ timeline_width = max_position + 1 # Add some padding
109
+ separator = "=" * (timeline_width + name_width + 3)
110
+ output << separator
111
+
112
+ # Second pass: render the timeline
113
+ @fiber_ids.sort.each do |fiber_id|
114
+ fiber_events = @events.select { |e| e.fiber_id == fiber_id }
115
+ next if fiber_events.empty?
116
+
117
+ fiber_name = @fiber_names[fiber_id] || "Fiber #{fiber_id}"
118
+ line = "#{fiber_name.ljust(name_width)} |"
119
+ timeline = " " * timeline_width
120
+ positions = positions_by_fiber[fiber_id]
121
+
122
+ # Render events using calculated positions
123
+ fiber_events.each_with_index do |evt, idx|
124
+ pos = positions[idx]
125
+ next_pos = positions[idx + 1]
126
+
127
+ # Choose character based on event type
128
+ char = case evt.type
129
+ when :opened, :created then "█"
130
+ when :resuming then "▶"
131
+ when :waiting_io then "~"
132
+ when :sleeping then "."
133
+ when :yielded then "╎"
134
+ when :closed, :completed then "▒"
135
+ when :cancelling then "⏹"
136
+ when :error, :cancelled then "✗"
137
+ when :awaiting then "↻"
138
+ else " "
139
+ end
140
+
141
+ timeline[pos] = char
142
+
143
+ # Fill the space until the next event if there is one
144
+ next unless next_pos
145
+
146
+ length = next_pos - pos - 1
147
+ next if length <= 0
148
+
149
+ fill_char = case evt.type
150
+ when :resuming then "═"
151
+ when :waiting_io then "~"
152
+ when :sleeping then "."
153
+ when :yielded then "-"
154
+ else " "
155
+ end
156
+
157
+ timeline[pos + 1, length] = fill_char * length
158
+ end
159
+
160
+ line << timeline << "|"
161
+ output << line
162
+ end
163
+
164
+ output << separator
165
+ output << "Legend: (█ Start) (▒ Finish) (═ Running) (~ IO-Wait) (. Sleeping) (╎ Yield) (✗ Error)"
166
+
167
+ output.join("\n")
168
+ end
169
+
170
+ def generate_timeline_data
171
+ # Get unique scope IDs
172
+ scope_ids = @fiber_scopes.values.uniq.sort
173
+
174
+ # Track nested groups for each parent
175
+ nested_groups = Hash.new { |h, k| h[k] = [] }
176
+
177
+ # First, handle scope hierarchy relationships
178
+ scope_ids.each do |scope_id|
179
+ next unless scope_id
180
+
181
+ # If scope was created by a fiber, nest it under that fiber
182
+ if @scope_creators&.[](scope_id)
183
+ creating_fiber = @scope_creators[scope_id]
184
+ group_id = "scope_#{scope_id}"
185
+ nested_groups["fiber_#{creating_fiber}"] << group_id
186
+ else
187
+ # Otherwise use normal scope hierarchy
188
+ parent_id = @scope_hierarchy.find { |_, children| children.include?(scope_id) }&.first
189
+ group_id = "scope_#{scope_id}"
190
+ parent_group = if parent_id
191
+ "scope_#{parent_id}"
192
+ else
193
+ "main"
194
+ end
195
+ nested_groups[parent_group] << group_id
196
+ end
197
+ end
198
+
199
+ # Then map remaining fibers to their parent scopes
200
+ @fiber_ids.sort.each do |fiber_id|
201
+ next if nested_groups.values.any? { |groups| groups.include?("fiber_#{fiber_id}") }
202
+
203
+ scope_id = @fiber_scopes[fiber_id]
204
+ parent_group = if scope_id
205
+ "scope_#{scope_id}"
206
+ else
207
+ "main"
208
+ end
209
+ nested_groups[parent_group] << "fiber_#{fiber_id}"
210
+ end
211
+
212
+ # Generate groups data
213
+ groups = []
214
+
215
+ # Add root scope group
216
+ groups << {
217
+ id: "main",
218
+ content: "Root Scope",
219
+ value: 1,
220
+ className: "main-scope",
221
+ nestedGroups: nested_groups["main"],
222
+ showNested: true,
223
+ }
224
+
225
+ # Add scope groups
226
+ scope_ids.each do |scope_id|
227
+ next unless scope_id
228
+
229
+ group_id = "scope_#{scope_id}"
230
+ title = @scope_names[scope_id] || "Scope #{scope_id}"
231
+
232
+ data = {
233
+ id: group_id,
234
+ order: scope_id,
235
+ content: title,
236
+ value: 2,
237
+ className: "scope",
238
+ showNested: false,
239
+ }
240
+
241
+ if nested_groups[group_id].any?
242
+ data[:nestedGroups] = nested_groups[group_id]
243
+ end
244
+
245
+ groups << data
246
+ end
247
+
248
+ # Add fiber groups (including those that create scopes)
249
+ @fiber_ids.sort.each do |fiber_id|
250
+ data = {
251
+ id: "fiber_#{fiber_id}",
252
+ content: "Fiber #{fiber_id}",
253
+ value: 3,
254
+ className: "fiber",
255
+ showNested: false,
256
+ }
257
+
258
+ if nested_groups["fiber_#{fiber_id}"].any?
259
+ data[:nestedGroups] = nested_groups["fiber_#{fiber_id}"]
260
+ end
261
+
262
+ groups << data
263
+ end
264
+
265
+ # Generate dataset from both scope and fiber events
266
+ dataset = []
267
+
268
+ # Add scope lifecycle events
269
+ @scope_events.each do |event|
270
+ group_id = if event.scope_id == "main"
271
+ "main"
272
+ else
273
+ "scope_#{event.scope_id}"
274
+ end
275
+
276
+ item = {
277
+ group: group_id,
278
+ content: event.type.to_s,
279
+ start: (event.timestamp * 1_000_000).to_i.to_s,
280
+ className: event.type.to_s,
281
+ data: event.data,
282
+ }
283
+
284
+ if event.type == :tag
285
+ item[:content] = event.data
286
+ item.delete(:data)
287
+ end
288
+
289
+ dataset << item
290
+ end
291
+
292
+ # Add fiber events
293
+ @events.each do |event|
294
+ item = {
295
+ group: "fiber_#{event.fiber_id}",
296
+ content: event.type.to_s,
297
+ start: (event.timestamp * 1_000_000).to_i.to_s,
298
+ className: event.type.to_s,
299
+ data: event.data,
300
+ }
301
+
302
+ # Add end time if the event has duration
303
+ item[:end] = (event.end_timestamp * 1_000_000).to_i.to_s if event.respond_to?(:end_timestamp)
304
+
305
+ dataset << item
306
+ end
307
+
308
+ {
309
+ groups: groups,
310
+ dataset: dataset,
311
+ }
312
+ end
313
+
314
+ def write_timeline_data(output_path)
315
+ data = generate_timeline_data
316
+
317
+ # Create JavaScript file content
318
+ js_content = <<~JAVASCRIPT
319
+ export const groups = #{data[:groups].to_json};
320
+
321
+ export const dataset = #{data[:dataset].to_json};
322
+ JAVASCRIPT
323
+
324
+ # Write to file
325
+ File.write(File.join(output_path, "index.html"), File.read(File.join(__dir__, "out", "index.html")))
326
+ File.write(File.join(output_path, "script.js"), js_content)
327
+ end
328
+
329
+ private
330
+
331
+ def current_time
332
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
333
+ end
334
+ end
335
+ end
@@ -0,0 +1,5 @@
1
+ # typed: strict
2
+
3
+ module Ori
4
+ VERSION = "0.4"
5
+ end
data/lib/ori.rb ADDED
@@ -0,0 +1,68 @@
1
+ # typed: strict
2
+
3
+ require "zeitwerk"
4
+ loader = Zeitwerk::Loader.for_gem
5
+ loader.setup
6
+
7
+ module Ori
8
+ class CancellationError < StandardError
9
+
10
+ #: Scope
11
+ attr_reader :scope
12
+
13
+ #: (Scope scope, ?String? message) -> void
14
+ def initialize(scope, message = "Scope cancelled")
15
+ @scope = scope
16
+ super(message)
17
+ end
18
+ end
19
+
20
+ class << self
21
+ #: (?name: String?, ?cancel_after: Numeric?, ?raise_after: Numeric?, ?trace: bool) { (Scope) -> void } -> Scope
22
+ def sync(name: nil, cancel_after: nil, raise_after: nil, trace: false, &block)
23
+ deadline = cancel_after || raise_after
24
+ prev_scheduler = Fiber.current_scheduler
25
+
26
+ scope = Scope.new(
27
+ prev_scheduler.is_a?(Scope) ? prev_scheduler : nil,
28
+ name,
29
+ deadline,
30
+ trace,
31
+ )
32
+
33
+ Fiber.set_scheduler(scope)
34
+
35
+ begin
36
+ if Fiber.current.blocking?
37
+ scope.fork { block.call(scope) }
38
+ else
39
+ yield(scope)
40
+ end
41
+
42
+ scope.await
43
+ scope
44
+ rescue CancellationError => error
45
+ # Re-raise if:
46
+ # 1. The error is from a different scope, or
47
+ # 2. This is our error but it's from raise_after
48
+ raise if error.scope != scope || !raise_after.nil?
49
+
50
+ scope # Return the scope even when cancelled
51
+ ensure
52
+ Fiber.set_scheduler(prev_scheduler)
53
+ end
54
+ end
55
+
56
+ # sig do
57
+ # type_parameters(:U)
58
+ # .params(resources: T::Array[T.all(T.type_parameter(:U), Ori::Selectable)])
59
+ # .returns(T.type_parameter(:U))
60
+ # end
61
+ #: [U] (Array[U & Selectable] resources) -> U
62
+ def select(resources)
63
+ Ori::Select.await(resources)
64
+ end
65
+ end
66
+
67
+ private_constant(:Scope)
68
+ end
data/mise-tasks/test ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env -S usage bash
2
+
3
+ #MISE description="Run tests"
4
+ #MISE alias="t"
5
+ #MISE depends=["typecheck"]
6
+ #USAGE arg "<file>" help="Run specific test file" default=""
7
+ #USAGE flag "-n --name <name>" help="Run tests with specific name" default=""
8
+
9
+ if [ -z "$usage_file" ]; then
10
+ bin/rake test
11
+ elif [ -z "$usage_name" ]; then
12
+ ruby -Itest "$usage_file"
13
+ else
14
+ ruby -Itest "$usage_file" --name "$usage_name"
15
+ fi
data/mise.toml ADDED
@@ -0,0 +1,40 @@
1
+ [tools]
2
+ ruby = "3.4.7"
3
+ usage = "latest"
4
+ watchexec = "latest"
5
+
6
+ [tasks.console]
7
+ description = "Start a REPL using IRB"
8
+ alias = "c"
9
+ run = "bin/console"
10
+
11
+ [tasks."bundle:install"]
12
+ description = "Install gem dependencies"
13
+ run = "bundle install"
14
+
15
+ [tasks.style]
16
+ description = "Run lint using Rubocop"
17
+ alias = "l"
18
+ run = "bin/rubocop"
19
+
20
+ [tasks.typecheck]
21
+ description = "Run typecheck using Sorbet"
22
+ alias = "tc"
23
+ run = "srb tc --enable-experimental-rbs-comments"
24
+
25
+ [tasks."rbi:update"]
26
+ description = "Update RBI files using Tapioca"
27
+ run = """
28
+ #!/usr/bin/env bash
29
+ bin/tapioca gem &&
30
+ bin/spoom srb bump --from false --to true &&
31
+ bin/spoom srb bump --from true --to strict
32
+ """
33
+
34
+ [tasks.'gem:build']
35
+ description = "Build the gem"
36
+ run = "gem build && mkdir -p pkg && mv ori-rb-* pkg/"
37
+
38
+ [tasks.'gem:publish']
39
+ description = "Publish the gem"
40
+ run = "gem push pkg/*.gem"
data/sorbet/config ADDED
@@ -0,0 +1,8 @@
1
+ .
2
+ --ignore=.dev/
3
+ --ignore=.git/
4
+ --allowed-extension=.rb
5
+ --allowed-extension=.rbi
6
+ --allowed-extension=.rake
7
+ --allowed-extension=.ru
8
+ --enable-experimental-rbs-comments
@@ -0,0 +1 @@
1
+ **/*.rbi linguist-generated=true