standard-procedure-plumbing 0.1.1 → 0.2.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.
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