standard-procedure-plumbing 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 45b26855ceaf54673b25fe4ca63a5e52ab302f585f7653731f444b312dec233f
4
- data.tar.gz: 936af082c395b80018878f9a53842505473f2d9978c97fafc88aea48102153e4
3
+ metadata.gz: 71e621c375dc0a17f928884eee2718fc7ed89a2f113374e90a5e6532914d71ea
4
+ data.tar.gz: 6632624c924bdee49e9dc4b273da36792916c434d12b867d874bcd1ddb880b8b
5
5
  SHA512:
6
- metadata.gz: 1cce27319493e32407381bb3623734a5bcd87bd1fa986c83f6d8790afb05376c144f994adfd5e094dd9377ad11ee6bdef3d2348ec279d9f378706d52da49362c
7
- data.tar.gz: 6594ec35d9753979414b79c524a27d3c033bf96739ec750a0774c1cbb0d7b6dc97ec7e64245026f93f42c8083cce1da91fa772cbcecc61edecba7e931e085c10
6
+ metadata.gz: aa02b3cc5688ebeed1ec98f22142077d9c26e952bc34772d9395d6d120f2a5103efc67a5cc0e3f7508c937f6392a294f3d6088d8adca9d7415cf81cb2e7f28e6
7
+ data.tar.gz: d2a164087e839fc3cd48d9b0c22cee9765541a388e4ffc48f5f614faa4690b69d7aa17a6bfc6d77d03ce2ed91e2af49cb8a02fa98e094117194414d7dbd49470
data/CHANGELOG.md CHANGED
@@ -1,3 +1,13 @@
1
+ ## [0.2.0] - 2024-08-14
2
+
3
+ - Added optional Dry::Validation support
4
+ - Use Async for fiber-based pipes
5
+
6
+ ## [0.1.2] - 2024-08-14
7
+
8
+ - Removed dependencies
9
+ - Removed Ractor-based concurrent pipe (as I don't trust it yet)
10
+
1
11
  ## [0.1.1] - 2024-08-14
2
12
 
3
13
  - Tidied up the code
data/README.md CHANGED
@@ -95,13 +95,19 @@ end
95
95
  # => "two"
96
96
  ```
97
97
 
98
- There is also [Plumbing::Concurrent::Pipe](/lib/plumbing/concurrent/pipe.rb) that uses a Ractor to dispatch events. Use with caution as Ractors are still experimental, plus they have strict conditions about the data that can be passed across Ractor boundaries.
99
-
100
98
  ## Plumbing::Chain - a chain of operations that occur in sequence
101
99
 
102
100
  Define a sequence of operations that proceed in order, passing their output from one operation as the input to another.
103
101
 
104
- You can define pre-conditions (which validate the inputs supplied) or post-conditions (which validate the output).
102
+ Use `perform` to define a step that takes some input and returns a different output.
103
+ Use `execute` to define a step that takes some input and returns that same input.
104
+ Use `embed` to define a step that uses another `Plumbing::Chain` class to generate the output.
105
+
106
+ If you have [dry-validation](https://dry-rb.org/gems/dry-validation/1.10/) installed, you can validate your input using a `Dry::Validation::Contract`.
107
+
108
+ If you don't want to use dry-validation, you can instead define a `pre_condition` - there's nothing to stop you defining a contract as well as pre_conditions (with the contract being verified first).
109
+
110
+ You can also verify that the output generated is as expected by defining a `post_condition`.
105
111
 
106
112
  ### Usage:
107
113
 
@@ -109,11 +115,12 @@ You can define pre-conditions (which validate the inputs supplied) or post-condi
109
115
  require "plumbing"
110
116
  class BuildSequence < Plumbing::Chain
111
117
  pre_condition :must_be_an_array do |input|
118
+ # you could replace this with a `validate` definition (using a Dry::Validation::Contract) if you prefer
112
119
  input.is_a? Array
113
120
  end
114
121
 
115
122
  post_condition :must_have_three_elements do |output|
116
- # yes, this is a stupid post-condition but it shows how you can ensure your outputs are valid
123
+ # yes, this is a stupid post-condition but 🤷🏾‍♂️
117
124
  output.length == 3
118
125
  end
119
126
 
@@ -0,0 +1 @@
1
+ 18d6d3138d2879c1672ab60b1534a6bc382e99a67adb4f8eff9c60ffb96b78b148bffac54ff1f7e78a2da1fc36d000041806b3a73993416c62279ef3ca09beba
@@ -0,0 +1,46 @@
1
+ module Plumbing
2
+ class Chain
3
+ module Contracts
4
+ def pre_condition name, &validator
5
+ pre_conditions[name.to_sym] = validator
6
+ end
7
+
8
+ def validate_with contract_class
9
+ @validation_contract = contract_class
10
+ end
11
+
12
+ def post_condition name, &validator
13
+ post_conditions[name.to_sym] = validator
14
+ end
15
+
16
+ private
17
+
18
+ def pre_conditions
19
+ @pre_conditions ||= {}
20
+ end
21
+
22
+ def post_conditions
23
+ @post_conditions ||= {}
24
+ end
25
+
26
+ def validate_contract_for input
27
+ return true if @validation_contract.nil?
28
+ result = const_get(@validation_contract).new.call(input)
29
+ raise PreConditionError, result.errors.to_h.to_yaml unless result.success?
30
+ input
31
+ end
32
+
33
+ def validate_preconditions_for input
34
+ failed_preconditions = pre_conditions.select { |name, validator| !validator.call(input) }
35
+ raise PreConditionError, failed_preconditions.keys.join(", ") if failed_preconditions.any?
36
+ input
37
+ end
38
+
39
+ def validate_postconditions_for output
40
+ failed_postconditions = post_conditions.select { |name, validator| !validator.call(output) }
41
+ raise PostConditionError, failed_postconditions.keys.join(", ") if failed_postconditions.any?
42
+ output
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,40 @@
1
+ module Plumbing
2
+ class Chain
3
+ module Operations
4
+ def perform method, &implementation
5
+ implementation ||= ->(input, instance) { instance.send(method, input) }
6
+ operations << implementation
7
+ end
8
+
9
+ def embed method, class_name
10
+ implementation = ->(input, instance) { const_get(class_name).new.call(input) }
11
+ operations << implementation
12
+ end
13
+
14
+ def execute method
15
+ implementation ||= ->(input, instance) do
16
+ instance.send(method, input)
17
+ input
18
+ end
19
+ operations << implementation
20
+ end
21
+
22
+ def _call input, instance
23
+ validate_contract_for input
24
+ validate_preconditions_for input
25
+ result = input
26
+ operations.each do |operation|
27
+ result = operation.call(result, instance)
28
+ end
29
+ validate_postconditions_for result
30
+ result
31
+ end
32
+
33
+ private
34
+
35
+ def operations
36
+ @operations ||= []
37
+ end
38
+ end
39
+ end
40
+ end
@@ -1,59 +1,14 @@
1
+ require_relative "chain/contracts"
2
+ require_relative "chain/operations"
3
+
1
4
  module Plumbing
2
5
  # A chain of operations that are executed in sequence
3
6
  class Chain
4
- def call params
5
- self.class._call params, self
6
- end
7
-
8
- class << self
9
- def pre_condition name, &validator
10
- pre_conditions[name.to_sym] = validator
11
- end
12
-
13
- def perform method, &implementation
14
- implementation ||= ->(params, instance) { instance.send(method, params) }
15
- operations << implementation
16
- end
17
-
18
- def post_condition name, &validator
19
- post_conditions[name.to_sym] = validator
20
- end
21
-
22
- def _call params, instance
23
- validate_preconditions_for params
24
- result = params
25
- operations.each do |operation|
26
- result = operation.call(result, instance)
27
- end
28
- validate_postconditions_for result
29
- result
30
- end
31
-
32
- private
33
-
34
- def operations
35
- @operations ||= []
36
- end
37
-
38
- def pre_conditions
39
- @pre_conditions ||= {}
40
- end
41
-
42
- def post_conditions
43
- @post_conditions ||= {}
44
- end
45
-
46
- def validate_preconditions_for input
47
- failed_preconditions = pre_conditions.select { |name, validator| !validator.call(input) }
48
- raise PreConditionError, failed_preconditions.keys.join(", ") if failed_preconditions.any?
49
- input
50
- end
7
+ extend Plumbing::Chain::Contracts
8
+ extend Plumbing::Chain::Operations
51
9
 
52
- def validate_postconditions_for output
53
- failed_postconditions = post_conditions.select { |name, validator| !validator.call(output) }
54
- raise PostConditionError, failed_postconditions.keys.join(", ") if failed_postconditions.any?
55
- output
56
- end
10
+ def call input
11
+ self.class._call input, self
57
12
  end
58
13
  end
59
14
  end
@@ -9,10 +9,10 @@ module Plumbing
9
9
  class PostConditionError < Error; end
10
10
 
11
11
  # Error raised because an invalid [Event] object was pushed into the pipe
12
- InvalidEvent = Dry::Types::ConstraintError
12
+ class InvalidEvent < Error; end
13
13
 
14
14
  # Error raised because an invalid observer was registered
15
- InvalidObserver = Dry::Types::ConstraintError
15
+ class InvalidObserver < Error; end
16
16
 
17
17
  # Error raised because a BlockedPipe was used instead of an actual implementation of a Pipe
18
18
  class PipeIsBlocked < Plumbing::Error; end
@@ -1,17 +1,5 @@
1
- require "dry/types"
2
- require "dry/struct"
3
-
4
1
  module Plumbing
5
2
  # An immutable data structure representing an Event
6
- class Event < Dry::Struct
7
- module Types
8
- include Dry::Types()
9
- SequenceNumber = Strict::Integer
10
- Type = Strict::String
11
- Data = Strict::Hash.map(Coercible::Symbol, Nominal::Any).default({}.freeze)
12
- end
13
-
14
- attribute :type, Types::Type
15
- attribute :data, Types::Data
3
+ Event = Data.define :type, :data do
16
4
  end
17
5
  end
@@ -0,0 +1,36 @@
1
+ require "async/task"
2
+ require "async/semaphore"
3
+
4
+ module Plumbing
5
+ module Fiber
6
+ # An implementation of a pipe that uses Fibers
7
+ class Pipe < Plumbing::Pipe
8
+ attr_reader :active
9
+
10
+ def initialize limit: 4
11
+ super()
12
+ @limit = 4
13
+ @semaphore = Async::Semaphore.new(@limit)
14
+ end
15
+
16
+ def << event
17
+ raise Plumbing::InvalidEvent.new "event is not a Plumbing::Event" unless event.is_a? Plumbing::Event
18
+ @semaphore.async do |task|
19
+ dispatch event, task
20
+ end
21
+ end
22
+
23
+ protected
24
+
25
+ def dispatch event, task
26
+ @observers.collect do |observer|
27
+ task.async do
28
+ observer.call event
29
+ rescue => ex
30
+ puts "Error: #{ex}"
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -1,25 +1,20 @@
1
- require "dry/types"
2
- require_relative "blocked_pipe"
3
-
4
1
  module Plumbing
5
2
  # A pipe that filters events from a source pipe
6
- class Filter < BlockedPipe
7
- module Types
8
- include Dry::Types()
9
- Source = Instance(Plumbing::BlockedPipe)
10
- EventTypes = Array.of(Plumbing::Event::Types::Type)
11
- end
3
+ class Filter < Pipe
4
+ class InvalidFilter < Error; end
12
5
 
13
6
  # Chain this pipe to the source pipe
14
- # @param source [Plumbing::BlockedPipe]
7
+ # @param source [Plumbing::Pipe]
15
8
  # @param accepts [Array[String]] event types that this filter will allow through (or pass [] to allow all)
16
9
  # @param rejects [Array[String]] event types that this filter will not allow through
17
10
  def initialize source:, accepts: [], rejects: []
18
11
  super()
19
- @accepted_event_types = Types::EventTypes[accepts]
20
- @rejected_event_types = Types::EventTypes[rejects]
21
- Types::Source[source].add_observer do |event|
22
- filter_and_republish(event)
12
+ raise InvalidFilter.new "source must be a Plumbing::Pipe descendant" unless source.is_a? Plumbing::Pipe
13
+ raise InvalidFilter.new "accepts and rejects must be arrays" unless accepts.is_a?(Array) && rejects.is_a?(Array)
14
+ @accepted_event_types = accepts
15
+ @rejected_event_types = rejects
16
+ source.add_observer do |event|
17
+ filter_and_republish event
23
18
  end
24
19
  end
25
20
 
data/lib/plumbing/pipe.rb CHANGED
@@ -1,28 +1,79 @@
1
- require_relative "blocked_pipe"
2
-
3
1
  module Plumbing
4
- # An implementation of a pipe that uses Fibers
5
- class Pipe < BlockedPipe
2
+ # A basic pipe
3
+ class Pipe
4
+ # Subclasses should call `super()` to ensure the pipe is initialised corrected
6
5
  def initialize
7
- super
8
- @fiber = Fiber.new do |initial_event|
9
- start_run_loop initial_event
10
- end
6
+ @observers = []
11
7
  end
12
8
 
9
+ # Push an event into the pipe
10
+ # @param event [Plumbing::Event] the event to push into the pipe
13
11
  def << event
14
- @fiber.resume Types::Event[event]
12
+ raise Plumbing::InvalidEvent.new event unless event.is_a? Plumbing::Event
13
+ dispatch event
14
+ end
15
+
16
+ # A shortcut to creating and then pushing an event
17
+ # @param event_type [String] representing the type of event this is
18
+ # @param data [Hash] representing the event-specific data to be passed to the observers
19
+ def notify event_type, data = nil
20
+ Event.new(type: event_type, data: data).tap do |event|
21
+ self << event
22
+ end
23
+ end
24
+
25
+ # Add an observer to this pipe
26
+ # @param callable [Proc] (optional)
27
+ # @param &block [Block] (optional)
28
+ # @return an object representing this observer (dependent upon the implementation of the pipe itself)
29
+ # Either a `callable` or a `block` must be supplied. If the latter, it is converted to a [Proc]
30
+ def add_observer observer = nil, &block
31
+ observer ||= block.to_proc
32
+ raise Plumbing::InvalidObserver.new "observer_does_not_respond_to_call" unless observer.respond_to? :call
33
+ @observers << observer
15
34
  end
16
35
 
36
+ # Remove an observer from this pipe
37
+ # @param observer
38
+ # This removes the given observer from this pipe. The observer should have previously been returned by #add_observer and is implementation-specific
39
+ def remove_observer observer
40
+ @observers.delete observer
41
+ end
42
+
43
+ # Test whether the given observer is observing this pipe
44
+ # @param observer
45
+ # @return [boolean]
46
+ def is_observer? observer
47
+ @observers.include? observer
48
+ end
49
+
50
+ # Close this pipe and perform any cleanup.
51
+ # Subclasses should override this to perform their own shutdown routines and call `super` to ensure everything is tidied up
17
52
  def shutdown
18
- super
19
- @fiber.resume :shutdown
53
+ # clean up and release any observers, just in case
54
+ @observers = []
55
+ end
56
+
57
+ # Start this pipe
58
+ # Subclasses may override this method to add any implementation specific details.
59
+ # By default any supplied parameters are called to the subclass' `initialize` method
60
+ def self.start(**params)
61
+ new(**params)
20
62
  end
21
63
 
22
64
  protected
23
65
 
24
- def get_next_event
25
- Fiber.yield
66
+ # Dispatch an event to all observers
67
+ # @param event [Plumbing::Event]
68
+ # Enumerates all observers and `calls` them with this event
69
+ # Discards any errors raised by the observer so that all observers will be successfully notified
70
+ def dispatch event
71
+ @observers.collect do |observer|
72
+ observer.call event
73
+ rescue => ex
74
+ puts ex
75
+ ex
76
+ end
26
77
  end
27
78
  end
28
79
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Plumbing
4
- VERSION = "0.1.1"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/plumbing.rb CHANGED
@@ -1,11 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "dry/types"
4
- module Plumbing
5
- require_relative "plumbing/version"
3
+ require_relative "plumbing/version"
4
+
5
+ require_relative "plumbing/error"
6
+ require_relative "plumbing/event"
7
+ require_relative "plumbing/pipe"
8
+ require_relative "plumbing/filter"
9
+ require_relative "plumbing/chain"
6
10
 
7
- require_relative "plumbing/error"
8
- require_relative "plumbing/event"
9
- require_relative "plumbing/pipe"
10
- require_relative "plumbing/chain"
11
+ module Plumbing
11
12
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: standard-procedure-plumbing
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rahoul Baruah
@@ -9,36 +9,8 @@ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
11
  date: 2024-08-14 00:00:00.000000000 Z
12
- dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: dry-types
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - ">="
18
- - !ruby/object:Gem::Version
19
- version: '0'
20
- type: :runtime
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - ">="
25
- - !ruby/object:Gem::Version
26
- version: '0'
27
- - !ruby/object:Gem::Dependency
28
- name: dry-struct
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - ">="
32
- - !ruby/object:Gem::Version
33
- version: '0'
34
- type: :runtime
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - ">="
39
- - !ruby/object:Gem::Version
40
- version: '0'
41
- description: An event pipeline
12
+ dependencies: []
13
+ description: A composable event pipeline and sequential pipelines of operations
42
14
  email:
43
15
  - rahoulb@echodek.co
44
16
  executables: []
@@ -51,28 +23,29 @@ files:
51
23
  - ".standard.yml"
52
24
  - CHANGELOG.md
53
25
  - CODE_OF_CONDUCT.md
54
- - Guardfile
55
26
  - LICENSE
56
27
  - README.md
57
28
  - Rakefile
58
29
  - checksums/standard-procedure-plumbing-0.1.1.gem.sha512
30
+ - checksums/standard-procedure-plumbing-0.1.2.gem.sha512
59
31
  - lib/plumbing.rb
60
- - lib/plumbing/blocked_pipe.rb
61
32
  - lib/plumbing/chain.rb
62
- - lib/plumbing/concurrent/pipe.rb
33
+ - lib/plumbing/chain/contracts.rb
34
+ - lib/plumbing/chain/operations.rb
63
35
  - lib/plumbing/error.rb
64
36
  - lib/plumbing/event.rb
37
+ - lib/plumbing/fiber/pipe.rb
65
38
  - lib/plumbing/filter.rb
66
39
  - lib/plumbing/pipe.rb
67
40
  - lib/plumbing/version.rb
68
41
  - sig/plumbing.rbs
69
- homepage: https://theartandscienceofruby.com
42
+ homepage: https://github.com/standard-procedure/plumbing
70
43
  licenses: []
71
44
  metadata:
72
45
  allowed_push_host: https://rubygems.org
73
- homepage_uri: https://theartandscienceofruby.com
74
- source_code_uri: https://github.com
75
- changelog_uri: https://github.com
46
+ homepage_uri: https://github.com/standard-procedure/plumbing
47
+ source_code_uri: https://github.com/standard-procedure/plumbing
48
+ changelog_uri: https://github.com/standard-procedure/plumbing/blob/main/CHANGELOG.md
76
49
  post_install_message:
77
50
  rdoc_options: []
78
51
  require_paths:
@@ -91,5 +64,5 @@ requirements: []
91
64
  rubygems_version: 3.5.9
92
65
  signing_key:
93
66
  specification_version: 4
94
- summary: An event pipeline
67
+ summary: Plumbing - various pipelines for your ruby application
95
68
  test_files: []
data/Guardfile DELETED
@@ -1,32 +0,0 @@
1
- ignore(/bin/, /log/, /public/, /storage/, /tmp/)
2
-
3
- group :formatting do
4
- guard :standardrb, fix: true, all_on_start: true, progress: true do
5
- watch(/.+\.rb$/)
6
- watch(/.+\.thor$/)
7
- watch(/.+\.rake$/)
8
- watch(/Guardfile$/)
9
- watch(/Rakefile$/)
10
- watch(/Gemfile$/)
11
- end
12
- end
13
-
14
- group :development do
15
- guard :rspec, cmd: "bundle exec rspec" do
16
- watch("spec/.+_helper.rb") { "spec" }
17
- watch(%r{^spec/.+_spec\.rb$})
18
- watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
19
- end
20
-
21
- guard :bundler do
22
- require "guard/bundler"
23
- require "guard/bundler/verify"
24
- helper = Guard::Bundler::Verify.new
25
-
26
- files = ["Gemfile"]
27
- files += Dir["*.gemspec"] if files.any? { |f| helper.uses_gemspec?(f) }
28
-
29
- # Assume files are symlinked from somewhere
30
- files.each { |file| watch(helper.real_path(file)) }
31
- end
32
- end
@@ -1,115 +0,0 @@
1
- require "dry/types"
2
- require_relative "error"
3
- require_relative "event"
4
-
5
- module Plumbing
6
- # The "plumbing" for a Pipe.
7
- # This class is "blocked", in that it won't push any events to registered observers.
8
- # Instead, this is the basis for subclasses like [Plumbing::Pipe] which actually allow events to flow through them.
9
- class BlockedPipe
10
- module Types
11
- include Dry::Types()
12
- # Events must be Plumbing::Event instances or subclasses
13
- Event = Instance(Plumbing::Event)
14
- # Observers must have a `call` method
15
- Observer = Interface(:call)
16
- end
17
-
18
- # Create a new BlockedPipe
19
- # Subclasses should call `super()` to ensure the pipe is initialised corrected
20
- def initialize
21
- @observers = []
22
- end
23
-
24
- # Push an event into the pipe
25
- # @param event [Plumbing::Event] the event to push into the pipe
26
- # Subclasses should implement this method
27
- def << event
28
- raise PipeIsBlocked
29
- end
30
-
31
- # A shortcut to creating and then pushing an event
32
- # @param event_type [String] representing the type of event this is
33
- # @param data [Hash] representing the event-specific data to be passed to the observers
34
- def notify event_type, **data
35
- Event.new(type: event_type, data: data).tap do |event|
36
- self << event
37
- end
38
- end
39
-
40
- # Add an observer to this pipe
41
- # @param callable [Proc] (optional)
42
- # @param &block [Block] (optional)
43
- # @return an object representing this observer (dependent upon the implementation of the pipe itself)
44
- # Either a `callable` or a `block` must be supplied. If the latter, it is converted to a [Proc]
45
- def add_observer callable = nil, &block
46
- callable ||= block.to_proc
47
- Types::Observer[callable].tap do |observer|
48
- @observers << observer
49
- end
50
- end
51
-
52
- # Remove an observer from this pipe
53
- # @param observer
54
- # This removes the given observer from this pipe. The observer should have previously been returned by #add_observer and is implementation-specific
55
- def remove_observer observer
56
- @observers.delete observer
57
- end
58
-
59
- # Test whether the given observer is observing this pipe
60
- # @param observer
61
- # @return [boolean]
62
- def is_observer? observer
63
- @observers.include? observer
64
- end
65
-
66
- # Close this pipe and perform any cleanup.
67
- # Subclasses should override this to perform their own shutdown routines and call `super` to ensure everything is tidied up
68
- def shutdown
69
- # clean up and release any observers, just in case
70
- @observers = []
71
- end
72
-
73
- # Start this pipe
74
- # Subclasses may override this method to add any implementation specific details.
75
- # By default any supplied parameters are called to the subclass' `initialize` method
76
- def self.start(**params)
77
- new(**params)
78
- end
79
-
80
- protected
81
-
82
- # Get the next event from the queue
83
- # @return [Plumbing::Event]
84
- # Subclasses should implement this method
85
- def get_next_event
86
- raise PipeIsBlocked
87
- end
88
-
89
- # Start the event loop
90
- # This loop keeps running until `shutdown` is called
91
- # Some subclasses may need to replace this method to deal with their own specific implementations
92
- # @param initial_event [Plumbing::Event] optional; the first event in the queue
93
- def start_run_loop initial_event = nil
94
- loop do
95
- event = initial_event || get_next_event
96
- break if event == :shutdown
97
- dispatch event
98
- initial_event = nil
99
- end
100
- end
101
-
102
- # Dispatch an event to all observers
103
- # @param event [Plumbing::Event]
104
- # Enumerates all observers and `calls` them with this event
105
- # Discards any errors raised by the observer so that all observers will be successfully notified
106
- def dispatch event
107
- @observers.collect do |observer|
108
- observer.call event
109
- rescue => ex
110
- puts ex
111
- ex
112
- end
113
- end
114
- end
115
- end
@@ -1,72 +0,0 @@
1
- require "dry/types"
2
- require_relative "../blocked_pipe"
3
-
4
- module Plumbing
5
- module Concurrent
6
- class Pipe < Plumbing::BlockedPipe
7
- module Types
8
- include Dry::Types()
9
- # Observers must be Ractors
10
- Observer = Instance(Ractor)
11
- end
12
-
13
- def initialize
14
- super
15
- @queue = Ractor.new(self) do |instance|
16
- while (message = Ractor.receive) != :shutdown
17
- case message.first
18
- when :add_observer then instance.send :add_observing_ractor, message.last
19
- when :is_observer? then Ractor.yield(instance.send(:is_observing_ractor?, message.last))
20
- when :remove_observer then instance.send :remove_observing_ractor, message.last
21
- else instance.send :dispatch, message.last
22
- end
23
- end
24
- end
25
- end
26
-
27
- def add_observer ractor = nil, &block
28
- Plumbing::Concurrent::Pipe::Types::Observer[ractor].tap do |observer|
29
- @queue << [:add_observer, observer]
30
- end
31
- end
32
-
33
- def remove_observer ractor = nil, &block
34
- @queue << [:remove_observer, ractor]
35
- end
36
-
37
- def is_observer? ractor
38
- @queue << [:is_observer?, ractor]
39
- @queue.take
40
- end
41
-
42
- def << event
43
- @queue << [:dispatch, Plumbing::BlockedPipe::Types::Event[event]]
44
- end
45
-
46
- def shutdown
47
- @queue << :shutdown
48
- super
49
- end
50
-
51
- private
52
-
53
- def dispatch event
54
- @observers.each do |observer|
55
- observer << event
56
- end
57
- end
58
-
59
- def add_observing_ractor observer
60
- @observers << observer
61
- end
62
-
63
- def is_observing_ractor? observer
64
- @observers.include? observer
65
- end
66
-
67
- def remove_observing_ractor observer
68
- @observers.delete observer
69
- end
70
- end
71
- end
72
- end