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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 47a1dea9f17b477436372cac90626ab10885a35116474b3e83edbf448af359f6
4
+ data.tar.gz: 652a3eaf18ac42ad31914d68cf70f0495bb17bb27e925d01169e607ea9e6526c
5
+ SHA512:
6
+ metadata.gz: 2ab76d413f0b280abdc43a5d2cabc0723b42e034ddcff07c1eeaa81fffb4299b0ff7c84116c7d3bd2f922f973a2764b94ce875d49ae6b5fe5f0ea1d08704c6d6
7
+ data.tar.gz: 935da20da1fdacb76f2c9df5c94b87b86dafdd737c50b3927da35d03c2415dbf75f433863bd13eab783b05af74cf97a8b6710d2e8069b108b04841aee3abf9de
data/.rubocop.yml ADDED
@@ -0,0 +1,8 @@
1
+ inherit_gem:
2
+ rubocop-shopify: rubocop.yml
3
+
4
+ require:
5
+ - rubocop-sorbet
6
+
7
+ Sorbet:
8
+ Enabled: true
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.4.7
data/LICENSE ADDED
@@ -0,0 +1,9 @@
1
+ Copyright 2025 Shopify Inc.
2
+
3
+ Copyright 2025 Jahfer Husain for contributions after October 14, 2025
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,444 @@
1
+ # Ori
2
+
3
+ Ori is a library for Ruby that provides a robust set of primitives for building concurrent applications. The name comes from the Japanese word 折り "ori" meaning "fold", reflecting how concurrent operations interleave.
4
+
5
+ Ori provides a set of primitives that allow you to build concurrent applications—that is, applications that interleave execution within a single thread—without blocking the entire Ruby interpreter for each task.
6
+
7
+ ## Table of Contents
8
+
9
+ - [Installation](#installation)
10
+ - [Usage](#usage)
11
+ - [Defining Boundaries](#defining-boundaries)
12
+ - [Matching](#matching)
13
+ - [Timeouts and Cancellation](#timeouts-and-cancellation)
14
+ - [Enumerables](#enumerables)
15
+ - [Debugging](#debugging)
16
+ - [Concurrency Utilities](#concurrency-utilities)
17
+ - [`Ori::Promise`](#oripromise)
18
+ - [`Ori::Channel`](#orichannel)
19
+ - [`Ori::Mutex`](#orimutex)
20
+ - [`Ori::Semaphore`](#orisemaphore)
21
+ - [`Ori::Timeout`](#oritimeout)
22
+ - [Releases](#releases)
23
+ - [License](#license)
24
+
25
+ ## Installation
26
+
27
+ ```ruby
28
+ gem "ori-rb", "~> 0.2"
29
+ ```
30
+
31
+ Then execute:
32
+
33
+ ```sh
34
+ bundle install
35
+ ```
36
+
37
+ In your Ruby code, you can then require the library:
38
+
39
+ ```ruby
40
+ require "ori"
41
+ ```
42
+
43
+ ## Usage
44
+
45
+ Ori aims to make concurrency in Ruby simple, intuitive, and easy to manage. There are only two decisions you need to make when using Ori:
46
+
47
+ 1. What code must complete _before_ other code starts?
48
+ 2. What code can run at the same time as other code?
49
+
50
+ ### Defining Boundaries
51
+
52
+ At the core of Ori is the concurrency boundary. Ori guarantees everything inside of a boundary will complete before any code after the boundary starts. Boundaries can be freely nested, allowing you to define critical sections inside of other critical sections.
53
+
54
+ To create a new concurrency boundary, call `Ori.sync` with your block of code. Once inside the boundary, you can use `Ori::Scope#fork` to define and run concurrent work. Code written inside of the boundary but outside of `Ori::Scope#fork` will run synchronously from the perspective of the boundary. `Ori::Scope#fork` will return an `Ori::Task` object, which you can use to wait for the fiber to complete, or retrieve its result.
55
+
56
+ ```ruby
57
+ Ori.sync do |scope|
58
+ # This runs in a new fiber
59
+ scope.fork do
60
+ sleep 1
61
+ puts "Hello from fiber!"
62
+ end
63
+
64
+ # This doesn't wait for the first fiber to complete
65
+ scope.fork do
66
+ sleep 0.5
67
+ puts "Another fiber here!"
68
+ end
69
+ end
70
+
71
+ # Ori.sync blocks until all fibers complete
72
+ puts "Success!"
73
+ ```
74
+
75
+ **Output:**
76
+
77
+ ```
78
+ Another fiber here!
79
+ Hello from fiber!
80
+ Success!
81
+ ```
82
+
83
+ <details>
84
+ <summary>See trace visualization</summary>
85
+
86
+ ![Trace visualization](./docs/images/example_boundary.png)
87
+
88
+ </details>
89
+
90
+ #### Matching
91
+
92
+ Ori has powerful support for matching against concurrent resources. If you have a set of blocking resources, you can use `Ori.select` in combination with Ruby's `case … in` pattern-matching to wait on the first available resource.
93
+
94
+ `Ori.select` will block until the first resource becomes available, returning that value and cancel waiting for the others. Matching against Ori's utility classes is particularly efficient because Ori can check internally if the blocking resources are available before attempting the heavier task of resuming the code.
95
+
96
+ See [Concurrency Utilities](#concurrency-utilities) for more details on these classes.
97
+
98
+ ```ruby
99
+ promise = Ori::Promise.new
100
+ mutex = Ori::Mutex.new
101
+ channel = Ori::Channel.new(1)
102
+ timeout = Ori::Timeout.new(0.1) # stop after 100ms if no resource completes
103
+
104
+ case Ori.select([promise, mutex, channel, timeout])
105
+ in Ori::Promise(value) then puts "Promise: #{value}"
106
+ in Ori::Mutex then puts "Mutex acquired!"
107
+ in Ori::Channel(value) then puts "Channel: #{value}"
108
+ in Ori::Timeout then puts "Timeout!"
109
+ end
110
+ ```
111
+
112
+ This matching syntax can also be leveraged to race multiple tasks against each other, in very compact form:
113
+
114
+ ```ruby
115
+ Ori.sync do |scope|
116
+ # Spawn 3 tasks
117
+ tasks = scope.fork_each(3.times).map { do_work }
118
+
119
+ # Wait for the first task to complete
120
+ Ori.select(tasks) => Ori::Task(value)
121
+ puts "First result: #{value}"
122
+
123
+ # Stop processing any further tasks
124
+ scope.shutdown!
125
+ end
126
+ ```
127
+
128
+ If you have multiple of the same resource, you can perform an explicit match using Ruby's pattern matching syntax:
129
+
130
+ ```ruby
131
+ promise_a = Ori::Promise.new
132
+ promise_b = Ori::Promise.new
133
+
134
+ case Ori.select([promise_a, promise_b])
135
+ in Ori::Promise(value) => p if p == promise_a
136
+ puts "Promise A: #{value}"
137
+ in Ori::Promise(value) => p if p == promise_b
138
+ puts "Promise B: #{value}"
139
+ end
140
+ ```
141
+
142
+ #### Timeouts and Cancellation
143
+
144
+ You can also use `Ori.sync` with timeouts to automatically cancel or raise after a specified duration.
145
+
146
+ When using `cancel_after: seconds`, the scope will be cancelled but the boundary will close with raising an error. With `raise_after: seconds`, a `Ori::Scope::CancellationError` will be raised from the boundary call site after the specified duration. Both options will properly clean up any internally-spawned fibers and nested scopes.
147
+
148
+ A parent scope's deadline is inherited by child scopes, and cancelling a parent scope will cancel all child scopes:
149
+
150
+ ```ruby
151
+ Ori.sync(raise_after: 5) do |scope|
152
+ # This inner scope inherits the 5 second deadline
153
+ scope.fork do
154
+ # Will raise `Ori::CancellationError` after 5 seconds
155
+ sleep(10)
156
+ end
157
+
158
+ # This inner scope has a shorter deadline
159
+ Ori.sync(cancel_after: 2) do |child_scope|
160
+ child_scope.fork do
161
+ # Will be cancelled after 2 seconds
162
+ sleep(10)
163
+ end
164
+ end
165
+ end
166
+ ```
167
+
168
+ <details>
169
+ <summary>See trace visualization</summary>
170
+
171
+ ![Trace visualization](./docs/images/example_boundary_cancellation.png)
172
+
173
+ </details>
174
+
175
+ ### Enumerables
176
+
177
+ As a convenience, `Ori::Scope` provides an `#fork_each` method that will spawn a new fiber for each item in an enumerable. This can be useful for performing concurrent operations on a collection.
178
+
179
+ The following code contains six seconds of `sleep` time, but will take only ~1 second to execute due to the interleaving of the fibers:
180
+
181
+ ```ruby
182
+ Ori.sync do |scope|
183
+ # Spawns a new fiber for each item in the array
184
+ scope.fork_each([1, 2, 3]) do |item|
185
+ puts "Processing #{item}"
186
+ sleep(1)
187
+ end
188
+
189
+ # Any Enumerable can be used
190
+ scope.fork_each(3.times) do |i|
191
+ puts "Processing #{i}"
192
+ sleep(1)
193
+ end
194
+ end
195
+ ```
196
+
197
+ ### Debugging
198
+
199
+ To help understand your program, Ori comes with several utilities to help you visualize the execution of your program, as well as being supported by the broader Ruby ecosystem.
200
+
201
+ #### Vernier
202
+
203
+ The HEAD of [jhawthorn/vernier](https://github.com/jhawthorn/vernier) supports tracking the spawning and yielding of fibers, to help analyze your concurrent program over time.
204
+
205
+ #### Plain-Text Visualization
206
+
207
+ `Ori::Scope#print_ascii_trace` will print the trace to stdout in plaintext. While useful as a quick overview, it's not interactive and the level of detail is limited.
208
+
209
+ ```ruby
210
+ closed_scope = Ori.sync { ... }
211
+ closed_scope.print_ascii_trace
212
+ ```
213
+
214
+ ```
215
+ Fiber Execution Timeline (0.001s)
216
+ ==============================================================================================
217
+ Main |▶.........↻.........................↻..................↻........................▒|
218
+ Fiber 1 |█▶═.╎------▶▒ |
219
+ Fiber 2 | █▶═══~╎--▶~╎-----------------▶══~╎▶~╎------------▶══▒ |
220
+ Fiber 3 | █▶╎--▶╎----------------------▶╎----------------▶═~╎-----------------▶══▒ |
221
+ ==============================================================================================
222
+ Legend: (█ Start) (▒ Finish) (═ Running) (~ IO-Wait) (. Sleeping) (╎ Yield) (✗ Error)
223
+ ```
224
+
225
+ #### HTML Visualization
226
+
227
+ `Ori::Scope#write_html_trace(dir)` will generate an `index.html` file in the specified directory containing a fully interactive timeline of the scope's execution.
228
+
229
+ ![Trace visualization](./docs/images/example_trace.png)
230
+
231
+ ##### Tags
232
+
233
+ `#write_html_trace` also supports use of `Ori::Scope#tag` to add custom labels to the trace.
234
+
235
+ ```ruby
236
+ closed_scope = Ori.sync do |scope|
237
+ scope.fork do
238
+ scope.tag("Going to sleep")
239
+ sleep(0.0001)
240
+ scope.tag("Woke up")
241
+ end
242
+
243
+ scope.fork do
244
+ scope.tag("Not sure what to do")
245
+ Fiber.yield
246
+ scope.tag("Finished yielding")
247
+ end
248
+
249
+ scope.tag("Finished queueing work")
250
+ end
251
+
252
+ closed_scope.write_html_trace(File.join(__dir_, "out"))
253
+ ```
254
+
255
+ ![Trace visualization](./docs/images/example_trace_tag.png)
256
+
257
+ ### Concurrency Utilities
258
+
259
+ Ori comes with several utilities to help you build concurrent applications. Keep in mind that these utilities are not thread-safe and should only be used in a concurrent context. The particular usefulness of these utilities are primarily how they interact with the scheduler, yielding control back to other fibers when blocked.
260
+
261
+ #### `Ori::Promise`
262
+
263
+ Promises represent values that may not be immediately available:
264
+
265
+ ```ruby
266
+ Ori.sync do |scope|
267
+ promise = Ori::Promise.new
268
+ scope.fork do
269
+ sleep(1)
270
+ promise.resolve("Hello from the future!")
271
+ end
272
+ # Wait for the promise to be fulfilled
273
+ result = promise.await
274
+ puts result # => "Hello from the future!"
275
+ end
276
+ ```
277
+
278
+ <details>
279
+ <summary>See trace visualization</summary>
280
+
281
+ ![Trace visualization](./docs/images/example_promise.png)
282
+
283
+ </details>
284
+
285
+ #### `Ori::Channel`
286
+
287
+ Channels provide a way to communicate between fibers by passing values between them. Channels can buffer up to a specified number of items. When the channel is full, `put`/`<<` will block until there is room:
288
+
289
+ ```ruby
290
+ Ori.sync do |scope|
291
+ channel = Ori::Channel.new(2)
292
+ # Producer
293
+ scope.fork do
294
+ # Will block after the first two puts
295
+ 5.times { |i| channel << i }
296
+ end
297
+
298
+ # Consumer
299
+ scope.fork do
300
+ 5.times { puts "Received: #{channel.take}" }
301
+ end
302
+ end
303
+ ```
304
+
305
+ <details>
306
+ <summary>See trace visualization</summary>
307
+
308
+ ![Trace visualization](./docs/images/example_channel.png)
309
+
310
+ </details>
311
+
312
+ If a channel has a capacity of `0`, it becomes a simple synchronous queue:
313
+
314
+ ```ruby
315
+ channel = Ori::Channel.new(0)
316
+ channel << 1 # Will block until `take` is called
317
+ ```
318
+
319
+ #### `Ori::Mutex`
320
+
321
+ When you need to enforce a critical section with strict ordering, use a mutex:
322
+
323
+ ```ruby
324
+ result = []
325
+ Ori.sync do |scope|
326
+ mutex = Ori::Mutex.new
327
+ counter = 0
328
+
329
+ scope.fork do
330
+ mutex.sync do
331
+ current = counter
332
+ result << [:A, :read, current]
333
+ Fiber.yield # Simulate work
334
+ counter = current + 1
335
+ result << [:A, :write, counter]
336
+ end
337
+ end
338
+
339
+ scope.fork do
340
+ mutex.sync do
341
+ current = counter
342
+ result << [:B, :read, current]
343
+ counter = current + 1
344
+ result << [:B, :write, counter]
345
+ end
346
+ end
347
+ end
348
+
349
+ result.each { |r| puts r.inspect }
350
+ ```
351
+
352
+ **Output:**
353
+
354
+ ```
355
+ [:A, :read, 0]
356
+ [:A, :write, 1]
357
+ [:B, :read, 1]
358
+ [:B, :write, 2]
359
+ ```
360
+
361
+ Without a mutex, the `counter` variable would be read and written in an interleaved manner, leading to race conditions where both fibers read `0`:
362
+
363
+ ```
364
+ [:A, :read, 0]
365
+ [:B, :read, 0]
366
+ [:B, :write, 1]
367
+ [:A, :write, 1]
368
+ ```
369
+
370
+ <details>
371
+ <summary>See trace visualization</summary>
372
+
373
+ ![Trace visualization](./docs/images/example_mutex.png)
374
+
375
+ </details>
376
+
377
+ #### `Ori::Semaphore`
378
+
379
+ Semaphors are a generalized form of mutexes that can be used to control access to _n_ limited resources:
380
+
381
+ ```ruby
382
+ Ori.sync do |scope|
383
+ # Allow up to 3 concurrent operations
384
+ semaphore = Ori::Semaphore.new(3)
385
+
386
+ 10.times do |i|
387
+ scope.fork do
388
+ semaphore.sync do
389
+ puts "Processing #{i}"
390
+ sleep(1) # Simulate work
391
+ end
392
+ end
393
+ end
394
+ end
395
+ ```
396
+
397
+ #### `Ori::Timeout`
398
+
399
+ A timeout is a special resource that will cancel after a specified duration. It's primary use case is as a resource in `Ori.select`.
400
+
401
+ ```ruby
402
+ Ori.sync do |scope|
403
+ promise = Ori::Promise.new
404
+ timeout = Ori::Timeout.new(0.1) # stop after 100ms if the promise hasn't resolved
405
+
406
+ scope.fork do
407
+ sleep(0.2)
408
+ promise.resolve("Hello from the future!")
409
+ end
410
+
411
+ case Ori.select([promise, timeout])
412
+ in Ori::Promise(value) then puts "Promise: #{value}"
413
+ in Ori::Timeout then puts "Timeout!"
414
+ end
415
+ end
416
+ ```
417
+
418
+ **Output:**
419
+
420
+ ```
421
+ Timeout!
422
+ ```
423
+
424
+ <details>
425
+ <summary>See trace visualization</summary>
426
+
427
+ ![Trace visualization](./docs/images/example_semaphore.png)
428
+
429
+ </details>
430
+
431
+ ## Releases
432
+
433
+ The procedure to publish a new release version is as follows:
434
+
435
+ - Update `lib/ori/version.rb`
436
+ - Run bundle install to bump the version of the gem in `Gemfile.lock`
437
+ - Open a pull request, review, and merge
438
+ - Review commits since the last release to identify user-facing changes that should be included in the release notes
439
+ - [Create a release on GitHub](https://github.com/jahfer/ori/releases/new) with a version number that matches `lib/ori/version.rb`
440
+ - Deploy the gem
441
+
442
+ ## License
443
+
444
+ The gem is available as open source under the terms of the MIT License.
data/Rakefile ADDED
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.ruby_opts = ["-W0", "-W:deprecated"]
8
+ t.libs << "test"
9
+ t.libs << "lib"
10
+ t.test_files = FileList["test/**/*_test.rb"]
11
+ end
12
+
13
+ require "rubocop/rake_task"
14
+
15
+ RuboCop::RakeTask.new
16
+
17
+ task default: [:test, :rubocop]
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,148 @@
1
+ # typed: true
2
+
3
+ module Ori
4
+ #: [E]
5
+ class Channel
6
+ include(Ori::Selectable)
7
+
8
+ EMPTY = "empty"
9
+
10
+ #: (Integer size) -> void
11
+ def initialize(size)
12
+ @size = size
13
+ if size.zero?
14
+ # Zero-sized channel state
15
+ @taker_waiting = false
16
+ @sender_waiting = false
17
+ @value = EMPTY
18
+ else
19
+ # Buffered channel state
20
+ @queue = UnboundedQueue.new
21
+ end
22
+ end
23
+
24
+ #: (E item) -> void
25
+ def put(item)
26
+ if @size.zero?
27
+ put_zero_sized(item)
28
+ else
29
+ put_buffered(item)
30
+ end
31
+ end
32
+ alias_method(:<<, :put)
33
+
34
+ #: () -> E
35
+ def take
36
+ if @size.zero?
37
+ take_zero_sized
38
+ else
39
+ take_buffered
40
+ end
41
+ end
42
+
43
+ #: () -> E
44
+ def peek
45
+ if @size.zero?
46
+ peek_zero_sized
47
+ else
48
+ peek_buffered
49
+ end
50
+ end
51
+
52
+ #: () -> bool
53
+ def value?
54
+ if @size.zero?
55
+ @value != EMPTY
56
+ else
57
+ @queue.peek != UnboundedQueue::EMPTY
58
+ end
59
+ end
60
+
61
+ #: () -> Channel[E]
62
+ def await
63
+ peek
64
+ self
65
+ end
66
+
67
+ #: () -> Array[E]
68
+ def deconstruct
69
+ Ori.sync { peek }
70
+ [take]
71
+ end
72
+
73
+ private
74
+
75
+ # Zero-sized channel implementation
76
+ def put_zero_sized(item)
77
+ @sender_waiting = true
78
+ begin
79
+ @value = item
80
+ Fiber.yield until @taker_waiting
81
+ ensure
82
+ @taker_waiting = false
83
+ end
84
+ end
85
+
86
+ def take_zero_sized
87
+ @taker_waiting = true
88
+ begin
89
+ Fiber.yield(self) until @value != EMPTY
90
+ @value
91
+ ensure
92
+ @value = EMPTY
93
+ @sender_waiting = false
94
+ end
95
+ end
96
+
97
+ def peek_zero_sized
98
+ Fiber.yield(self) until @sender_waiting
99
+ @value
100
+ end
101
+
102
+ # Buffered channel implementation
103
+ def put_buffered(item)
104
+ Fiber.yield until @queue.size < @size
105
+ @queue.push(item)
106
+ end
107
+
108
+ def take_buffered
109
+ Fiber.yield(self) until value?
110
+ @queue.shift
111
+ end
112
+
113
+ def peek_buffered
114
+ Fiber.yield(self) until value?
115
+ @queue.peek
116
+ end
117
+ end
118
+
119
+ # TODO: implement sliding queue, dropping queue
120
+ class UnboundedQueue
121
+ EMPTY = "empty"
122
+
123
+ def initialize
124
+ @buffer = []
125
+ end
126
+
127
+ def size
128
+ @buffer.size
129
+ end
130
+
131
+ def push(item)
132
+ @buffer << item
133
+ end
134
+
135
+ def peek
136
+ if @buffer.empty?
137
+ EMPTY
138
+ else
139
+ @buffer.first
140
+ end
141
+ end
142
+
143
+ def shift
144
+ @buffer.shift
145
+ end
146
+ end
147
+ private_constant(:UnboundedQueue)
148
+ end