standard-procedure-plumbing 0.2.2 → 0.3.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: 6074870313ece34eb4b9565602db4de70bb5b7e14e8a9051d85bb46b6fc64bf5
4
- data.tar.gz: a81292b0ad9e87dfcce61d531e891c1b4ed25ccda3331ceb731c082c6e9c7c16
3
+ metadata.gz: 245665ccd98b92e9b49ae532c7efb1edb155b895e6db935cee4de16d8039a96c
4
+ data.tar.gz: 0a2b802e5e09dbc6f08259f0812c03bdb869002d90e04d5186f2ee716081ae44
5
5
  SHA512:
6
- metadata.gz: 45827e921f7bc272e0688a6477d405a81e8c9ea64405078782fbf5aa45658066bf7f9c5503692f4f6eadfdbb08d21221eb4e0b0731654e4808e0f7ec1111f9bb
7
- data.tar.gz: 1ba319545accf7051393845b1883d5eb69aeacba182c86f26ca6e95e7761711e922904d2ec5427872e8dcd6ccdf5fba25737fe5c20a887cc06ba23ab2184c81a
6
+ metadata.gz: 84678167d2319c1e6af1a502485351ec3cb5decc4a6313396a89503e0f04a52803062081eec15fafdf30b55b93ce5bf8cbd6109d4a4872feabeb78fd2d9c959a
7
+ data.tar.gz: 6a49385767a68aa48c44d110016312563b6517963fd7b338d3b6b0d2106b759d1dc22bd324bec573b3e2b4b1b57aa4dfbc2df19b6ce71e71ed0a51666e3bc3c0
data/CHANGELOG.md CHANGED
@@ -1,3 +1,8 @@
1
+ ## [0.3.0] - 2024-08-28
2
+
3
+ - Added Plumbing::Valve
4
+ - Reimplemented Plumbing::Pipe to use Plumbing::Valve
5
+
1
6
  ## [0.2.2] - 2024-08-25
2
7
 
3
8
  - Added Plumbing::RubberDuck
data/CODE_OF_CONDUCT.md CHANGED
@@ -1,84 +1,5 @@
1
1
  # Contributor Covenant Code of Conduct
2
2
 
3
- ## Our Pledge
3
+ **BE NICE**
4
4
 
5
- We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
6
-
7
- We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
8
-
9
- ## Our Standards
10
-
11
- Examples of behavior that contributes to a positive environment for our community include:
12
-
13
- * Demonstrating empathy and kindness toward other people
14
- * Being respectful of differing opinions, viewpoints, and experiences
15
- * Giving and gracefully accepting constructive feedback
16
- * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
17
- * Focusing on what is best not just for us as individuals, but for the overall community
18
-
19
- Examples of unacceptable behavior include:
20
-
21
- * The use of sexualized language or imagery, and sexual attention or
22
- advances of any kind
23
- * Trolling, insulting or derogatory comments, and personal or political attacks
24
- * Public or private harassment
25
- * Publishing others' private information, such as a physical or email
26
- address, without their explicit permission
27
- * Other conduct which could reasonably be considered inappropriate in a
28
- professional setting
29
-
30
- ## Enforcement Responsibilities
31
-
32
- Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
33
-
34
- Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
35
-
36
- ## Scope
37
-
38
- This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
39
-
40
- ## Enforcement
41
-
42
- Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at rahoulb@echodek.co. All complaints will be reviewed and investigated promptly and fairly.
43
-
44
- All community leaders are obligated to respect the privacy and security of the reporter of any incident.
45
-
46
- ## Enforcement Guidelines
47
-
48
- Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
49
-
50
- ### 1. Correction
51
-
52
- **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
53
-
54
- **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
55
-
56
- ### 2. Warning
57
-
58
- **Community Impact**: A violation through a single incident or series of actions.
59
-
60
- **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
61
-
62
- ### 3. Temporary Ban
63
-
64
- **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.
65
-
66
- **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
67
-
68
- ### 4. Permanent Ban
69
-
70
- **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
71
-
72
- **Consequence**: A permanent ban from any sort of public interaction within the community.
73
-
74
- ## Attribution
75
-
76
- This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0,
77
- available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
78
-
79
- Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
80
-
81
- [homepage]: https://www.contributor-covenant.org
82
-
83
- For answers to common questions about this code of conduct, see the FAQ at
84
- https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations.
5
+ If people think you're being a dick, you're probably being a dick, so stop it.
data/README.md CHANGED
@@ -1,215 +1,387 @@
1
1
  # Plumbing
2
2
 
3
+ ## Configuration
4
+
5
+ The most important configuration setting is the `mode`, which governs how messages are handled by Valves.
6
+
7
+ By default it is `:inline`, so every command or query is handled synchronously.
8
+
9
+ If it is set to `:async`, commands and queries will be handled using fibers (via the [Async gem](https://socketry.github.io/async/index.html)).
10
+
11
+ The `timeout` setting is used when performing queries - it defaults to 30s.
12
+
13
+ ```ruby
14
+ require "plumbing"
15
+ puts Plumbing.config.mode
16
+ # => :inline
17
+
18
+ Plumbing.configure mode: :async, timeout: 10
19
+
20
+ puts Plumbing.config.mode
21
+ # => :async
22
+ ```
23
+
24
+ If you are running a test suite, you can temporarily update the configuration by passing a block.
25
+
26
+ ```ruby
27
+ require "plumbing"
28
+ puts Plumbing.config.mode
29
+ # => :inline
30
+
31
+ Plumbing.configure mode: :async do
32
+ puts Plumbing.config.mode
33
+ # => :async
34
+ first_test
35
+ second_test
36
+ end
37
+
38
+ puts Plumbing.config.mode
39
+ # => :inline
40
+ ```
41
+
3
42
  ## Plumbing::Pipeline - transform data through a pipeline
4
43
 
5
- Define a sequence of operations that proceed in order, passing their output from one operation as the input to another.
44
+ Define a sequence of operations that proceed in order, passing their output from one operation as the input to another. [Unix pipes](https://en.wikipedia.org/wiki/Pipeline_(Unix)) in Ruby.
6
45
 
7
46
  Use `perform` to define a step that takes some input and returns a different output.
8
- Use `execute` to define a step that takes some input and returns that same input.
9
- Use `embed` to define a step that uses another `Plumbing::Chain` class to generate the output.
10
-
11
- 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`.
47
+ Specify `using` to re-use an existing `Plumbing::Pipeline` as a step within this pipeline.
48
+ Use `execute` to define a step that takes some input, performs an action but passes the input, unchanged, to the next step.
12
49
 
13
- If you don't want to use dry-validation, you can instead define a `pre_condition` (although there's nothing to stop you defining a contract as well as pre_conditions - with the contract being verified first).
50
+ 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`. Alternatively, you can define a `pre_condition` to test that the inputs are valid.
14
51
 
15
52
  You can also verify that the output generated is as expected by defining a `post_condition`.
16
53
 
17
54
  ### Usage:
18
55
 
56
+ [Building an array using multiple steps with a pre-condition and post-condition](/spec/examples/pipeline_spec.rb)
57
+
19
58
  ```ruby
20
- require "plumbing"
21
- class BuildSequence < Plumbing::Pipeline
22
- pre_condition :must_be_an_array do |input|
23
- # you could replace this with a `validate` definition (using a Dry::Validation::Contract) if you prefer
24
- input.is_a? Array
25
- end
59
+ require "plumbing"
60
+ class BuildArray < Plumbing::Pipeline
61
+ perform :add_first
62
+ perform :add_second
63
+ perform :add_third
64
+
65
+ pre_condition :must_be_an_array do |input|
66
+ input.is_a? Array
67
+ end
26
68
 
27
- post_condition :must_have_three_elements do |output|
28
- # this is a stupid post-condition but 🤷🏾‍♂️, this is just an example
29
- output.length == 3
30
- end
69
+ post_condition :must_have_three_elements do |output|
70
+ output.length == 3
71
+ end
31
72
 
32
- perform :add_first
33
- perform :add_second
34
- perform :add_third
73
+ private
35
74
 
36
- private
75
+ def add_first(input) = input << "first"
37
76
 
38
- def add_first input
39
- input << "first"
77
+ def add_second(input) = input << "second"
78
+
79
+ def add_third(input) = input << "third"
40
80
  end
41
81
 
42
- def add_second input
43
- input << "second"
82
+ BuildArray.new.call []
83
+ # => ["first", "second", "third"]
84
+
85
+ BuildArray.new.call 1
86
+ # => Plumbing::PreconditionError("must_be_an_array")
87
+
88
+ BuildArray.new.call ["extra element"]
89
+ # => Plumbing::PostconditionError("must_have_three_elements")
90
+ ```
91
+
92
+ [Validating input parameters with a contract](/spec/examples/pipeline_spec.rb)
93
+ ```ruby
94
+ require "plumbing"
95
+ require "dry/validation"
96
+
97
+ class SayHello < Plumbing::Pipeline
98
+ validate_with "SayHello::Input"
99
+ perform :say_hello
100
+
101
+ private
102
+
103
+ def say_hello input
104
+ "Hello #{input[:name]} - I will now send a load of annoying marketing messages to #{input[:email]}"
105
+ end
106
+
107
+ class Input < Dry::Validation::Contract
108
+ params do
109
+ required(:name).filled(:string)
110
+ required(:email).filled(:string)
111
+ end
112
+ rule :email do
113
+ key.failure("must be a valid email") unless /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i.match? value
114
+ end
115
+ end
44
116
  end
45
117
 
46
- def add_third input
47
- input << "third"
118
+ SayHello.new.call(name: "Alice", email: "alice@example.com")
119
+ # => Hello Alice - I will now send a load of annoying marketing messages to alice@example.com
120
+
121
+ SayHello.new.call(some: "other data")
122
+ # => Plumbing::PreConditionError
123
+ ```
124
+
125
+ [Building a pipeline through composition](/spec/examples/pipeline_spec.rb)
126
+
127
+ ```ruby
128
+ require "plumbing"
129
+ class ExternalStep < Plumbing::Pipeline
130
+ perform :add_item_to_array
131
+
132
+ private
133
+
134
+ def add_item_to_array(input) = input << "external"
48
135
  end
49
- end
50
136
 
51
- BuildSequence.new.call []
52
- # => ["first", "second", "third"]
137
+ class BuildSequenceWithExternalStep < Plumbing::Pipeline
138
+ perform :add_first
139
+ perform :add_second, using: "ExternalStep"
140
+ perform :add_third
141
+
142
+ private
53
143
 
54
- BuildSequence.new.call 1
55
- # => Plumbing::PreconditionError("must_be_an_array")
144
+ def add_first(input) = input << "first"
145
+
146
+ def add_third(input) = input << "third"
147
+ end
56
148
 
57
- BuildSequence.new.call ["extra element"]
58
- # => Plumbing::PostconditionError("must_have_three_elements")
149
+ BuildSequenceWithExternalStep.new.call([])
150
+ # => ["first", "external", "third"]
59
151
  ```
60
152
 
153
+ ## Plumbing::Valve - safe asynchronous objects
61
154
 
62
- ## Plumbing::Pipe - a composable observer
155
+ An [actor](https://en.wikipedia.org/wiki/Actor_model) defines the messages an object can receive, similar to a regular object. However, a normal object if accessed concurrently can have data consistency issues and race conditions leading to hard-to-reproduce bugs. Actors, however, ensure that, no matter which thread (or fiber) is sending the message, the internal processing of the message (the method definition) is handled sequentially. This means the internal state of an object is never accessed concurrently, eliminating those issues.
63
156
 
64
- [Observers](https://ruby-doc.org/3.3.0/stdlibs/observer/Observable.html) in Ruby are a pattern where objects (observers) register their interest in another object (the observable). This pattern is common throughout programming languages (event listeners in Javascript, the dependency protocol in [Smalltalk](https://en.wikipedia.org/wiki/Smalltalk)).
157
+ [Plumbing::Valve](/lib/plumbing/valve.rb) ensures that all messages received are channelled into a concurrency-safe queue. This allows you to take an existing class and ensures that messages received via its public API are made concurrency-safe.
65
158
 
66
- [Plumbing::Pipe](lib/plumbing/pipe.rb) makes observers "composable". Instead of simply registering for notifications from the observable, we observe a stream of notifications, which could be produced by multiple observables, all being sent through the same pipe. We can then chain observers together, composing a "pipeline" of operations from a single source of events.
159
+ Include the Plumbing::Valve module into your class, define the messages the objects can respond to and set the `Plumbing` configuration to set the desired concurrency model. Messages themselves are split into two categories: commands and queries. Commands have no return value so when the message is sent, the caller does not block and execution continues immediately. Queries return a value so the caller must block until the actor has returned a value.
67
160
 
68
- ### Usage
161
+ Instead of constructing your object with `.new`, use `.start`. This builds a proxy object that wraps the target instance and dispatches messages through a safe mechanism. Only messages that have been defined as part of the valve are available in this proxy - so you don't have to worry about callers bypassing the valve's internal context.
162
+
163
+ Even when using actors, there is one condition where concurrency may cause issues. If object A makes a query to object B which in turn makes a query back to object A, you will hit a deadlock. This is because A is waiting on the response from B but B is now querying, and waiting for, A. This does not apply to commands because they do not wait for a response. However, when writing queries, be careful who you interact with - the configuration allows you to set a timeout (defaulting to 30s) in case this happens.
164
+
165
+ Also be aware that if you use valves in one place, you need to use them everywhere - especially if you're using threads or ractors (coming soon). This is because as the valve sends messages to its collaborators, those calls will be made from within the valve's internal context. If the collaborators are also valves, the subsequent messages will be handled correctly, if not, data consistency bugs could occur.
166
+
167
+ ### Usage
168
+
169
+ [Defining an actor](/spec/examples/valve_spec.rb)
69
170
 
70
- A simple observer:
71
171
  ```ruby
72
- require "plumbing"
172
+ require "plumbing"
173
+
174
+ class Employee
175
+ attr_reader :name, :job_title
176
+
177
+ include Plumbing::Valve
178
+ query :name, :job_title
179
+ command :promote
180
+
181
+ def initialize(name)
182
+ @name = name
183
+ @job_title = "Sales assistant"
184
+ end
73
185
 
74
- @source = Plumbing::Pipe.start
186
+ def promote
187
+ sleep 0.5
188
+ @job_title = "Sales manager"
189
+ end
190
+ end
191
+ ```
75
192
 
76
- @observer = @source.add_observer do |event|
77
- puts event.type
78
- end
193
+ [Acting inline](/spec/examples/valve_spec.rb) with no concurrency
79
194
 
80
- @source.notify "something_happened", message: "But what was it?"
81
- # => "something_happened"
195
+ ```ruby
196
+ require "plumbing"
197
+
198
+ @person = Employee.start "Alice"
199
+
200
+ puts @person.name
201
+ # => "Alice"
202
+ puts @person.job_title
203
+ # => "Sales assistant"
204
+
205
+ @person.promote
206
+ # this will block for 0.5 seconds
207
+ puts @person.job_title
208
+ # => "Sales manager"
82
209
  ```
83
210
 
84
- Simple filtering:
211
+ [Using fibers](/spec/examples/valve_spec.rb) with concurrency but no parallelism
212
+
85
213
  ```ruby
86
- require "plumbing"
214
+ require "plumbing"
215
+ require "async"
216
+
217
+ Plumbing.configure mode: :async
218
+ @person = Employee.start "Alice"
219
+
220
+ puts @person.name
221
+ # => "Alice"
222
+ puts @person.job_title
223
+ # => "Sales assistant"
224
+
225
+ @person.promote
226
+ # this will return immediately without blocking
227
+ puts @person.job_title
228
+ # => "Sales manager" (this will block for 0.5s because #job_title query will not start until the #promote command has completed)
229
+ ```
230
+
231
+ ## Plumbing::Pipe - a composable observer
232
+
233
+ [Observers](https://ruby-doc.org/3.3.0/stdlibs/observer/Observable.html) in Ruby are a pattern where objects (observers) register their interest in another object (the observable). This pattern is common throughout programming languages (event listeners in Javascript, the dependency protocol in [Smalltalk](https://en.wikipedia.org/wiki/Smalltalk)).
87
234
 
88
- @source = Plumbing::Pipe.start
235
+ [Plumbing::Pipe](lib/plumbing/pipe.rb) makes observers "composable". Instead of simply just registering for notifications from a single observable, we can build sequences of pipes. These sequences can filter notifications and route them to different listeners, or merge multiple sources into a single stream of notifications.
89
236
 
90
- @filter = Plumbing::Filter.start source: @source do |event|
91
- %w[important urgent].include? event.type
92
- end
237
+ Pipes are implemented as valves, meaning that event notifications can be dispatched asynchronously. The observer's callback will be triggered from within the pipe's internal context so you should immediately trigger a command on another valve to maintain safety.
93
238
 
94
- @observer = @filter.add_observer do |event|
95
- puts event.type
96
- end
239
+ ### Usage
240
+
241
+ [A simple observer](/spec/examples/pipe_spec.rb):
242
+ ```ruby
243
+ require "plumbing"
97
244
 
98
- @source.notify "important", message: "ALERT! ALERT!"
99
- # => "important"
245
+ @source = Plumbing::Pipe.start
246
+ @observer = @source.add_observer do |event|
247
+ puts event.type
248
+ end
100
249
 
101
- @source.notify "unimportant", message: "Nothing to see here"
102
- # => <no output>
250
+ @source.notify "something_happened", message: "But what was it?"
251
+ # => "something_happened"
103
252
  ```
104
253
 
105
- Custom filtering:
254
+ [Simple filtering](/spec/examples/pipe_spec.rb):
106
255
  ```ruby
107
- require "plumbing"
256
+ require "plumbing"
108
257
 
109
- class EveryThirdEvent < Plumbing::CustomFilter
110
- def initialize source:
111
- super source: source
112
- @events = []
258
+ @source = Plumbing::Pipe.start
259
+ @filter = Plumbing::Filter.start source: @source do |event|
260
+ %w[important urgent].include? event.type
261
+ end
262
+ @observer = @filter.add_observer do |event|
263
+ puts event.type
113
264
  end
114
265
 
115
- def received event
116
- @events << event
117
- # if we've already stored 2 events in the buffer then broadcast the newest event and clear the buffer
118
- if @events.count >= 2
119
- @events.clear
120
- self << event
266
+ @source.notify "important", message: "ALERT! ALERT!"
267
+ # => "important"
268
+ @source.notify "unimportant", message: "Nothing to see here"
269
+ # => <no output>
270
+ ```
271
+
272
+ [Custom filtering](/spec/examples/pipe_spec.rb):
273
+ ```ruby
274
+ require "plumbing"
275
+ class EveryThirdEvent < Plumbing::CustomFilter
276
+ def initialize source:
277
+ super source: source
278
+ @events = []
121
279
  end
122
- end
123
- end
124
280
 
125
- @source = Plumbing::Pipe.start
126
- @filter = EveryThirdEvent.new(source: @source)
281
+ def received event
282
+ # store this event into our buffer
283
+ @events << event
284
+ # if this is the third event we've received then clear the buffer and broadcast the latest event
285
+ if @events.count >= 3
286
+ @events.clear
287
+ self << event
288
+ end
289
+ end
290
+ end
127
291
 
128
- @observer = @filter.add_observer do |event|
129
- puts event.type
130
- end
292
+ @source = Plumbing::Pipe.start
293
+ @filter = EveryThirdEvent.start(source: @source)
294
+ @observer = @filter.add_observer do |event|
295
+ puts event.type
296
+ end
131
297
 
132
- 1.upto 10 do |i|
133
- @source.notify i.to_s
134
- end
135
- # => "3"
136
- # => "6"
137
- # => "9"
298
+ 1.upto 10 do |i|
299
+ @source.notify i.to_s
300
+ end
301
+ # => "3"
302
+ # => "6"
303
+ # => "9"
138
304
  ```
139
305
 
140
- Joining multiple sources
306
+ [Joining multiple sources](/spec/examples/pipe_spec.rb):
141
307
  ```ruby
142
- require "plumbing"
308
+ require "plumbing"
143
309
 
144
- @first_source = Plumbing::Pipe.start
145
- @second_source = Plumbing::Pipe.start
310
+ @first_source = Plumbing::Pipe.start
311
+ @second_source = Plumbing::Pipe.start
146
312
 
147
- @junction = Plumbing::Junction.start @first_source, @second_source
313
+ @junction = Plumbing::Junction.start @first_source, @second_source
148
314
 
149
- @observer = @junction.add_observer do |event|
150
- puts event.type
151
- end
315
+ @observer = @junction.add_observer do |event|
316
+ puts event.type
317
+ end
152
318
 
153
- @first_source.notify "one"
154
- # => "one"
155
- @second_source.notify "two"
156
- # => "two"
319
+ @first_source.notify "one"
320
+ # => "one"
321
+ @second_source.notify "two"
322
+ # => "two"
157
323
  ```
158
324
 
159
- Dispatching events asynchronously (using Fibers)
325
+ [Dispatching events asynchronously (using Fibers)](/spec/examples/pipe_spec.rb):
160
326
  ```ruby
161
- require "plumbing"
162
- require "plumbing/event_dispatcher/fiber"
163
- require "async"
327
+ require "plumbing"
328
+ require "async"
164
329
 
165
- # `limit` controls how many fibers can dispatch events concurrently - the default is 4
166
- @first_source = Plumbing::Pipe.start dispatcher: Plumbing::EventDispatcher::Fiber.new limit: 8
167
- @second_source = Plumbing::Pipe.start dispatcher: Plumbing::EventDispatcher::Fiber.new limit: 2
330
+ Plumbing.configure mode: :async
168
331
 
169
- @junction = Plumbing::Junction.start @first_source, @second_source, dispatcher: Plumbing::EventDispatcher::Fiber.new
332
+ Sync do
333
+ @first_source = Plumbing::Pipe.start
334
+ @second_source = Plumbing::Pipe.start
170
335
 
171
- @filter = Plumbing::Filter.start source: @junction, dispatcher: Plumbing::EventDispatcher::Fibernew do |event|
172
- %w[one-one two-two].include? event.type
173
- end
336
+ @junction = Plumbing::Junction.start @first_source, @second_source
174
337
 
175
- Sync do
176
- @first_source.notify "one-one"
177
- @first_source.notify "one-two"
178
- @second_source.notify "two-one"
179
- @second_source.notify "two-two"
180
- end
181
- ```
338
+ @filter = Plumbing::Filter.start source: @junction do |event|
339
+ %w[one-one two-two].include? event.type
340
+ end
182
341
 
342
+ @first_source.notify "one-one"
343
+ @first_source.notify "one-two"
344
+ @second_source.notify "two-one"
345
+ @second_source.notify "two-two"
346
+ end
347
+ ```
183
348
 
184
349
  ## Plumbing::RubberDuck - duck types and type-casts
185
350
 
186
- Define an [interface or protocol](https://en.wikipedia.org/wiki/Interface_(object-oriented_programming) specifying which messages you expect to be able to send. Then cast an object into that type, which first tests that the object can respond to those messages and limits you to sending those messages and no others.
351
+ Define an [interface or protocol](https://en.wikipedia.org/wiki/Interface_(object-oriented_programming)) specifying which messages you expect to be able to send. Then cast an object into that type, which first tests that the object can respond to those messages and then builds a proxy that responds to just those messages and no others (so no-one can abuse the specific type-casting you have specified). However, if you take one of these proxies, you can safely re-cast it as another type (as long as the original target object is castable).
187
352
 
188
353
 
189
354
  ### Usage
190
355
 
191
356
  Define your interface (Person in this example), then cast your objects (instances of PersonData and CarData).
192
357
 
358
+ [Casting objects as duck-types](/spec/examples/rubber_duck_spec.rb):
193
359
  ```ruby
194
- require "plumbing"
195
-
196
- Person = Plumbing::RubberDuck.define :first_name, :last_name, :email
197
-
198
- PersonData = Struct.new(:first_name, :last_name, :email, :favourite_food)
199
- CarData = Struct.new(:make, :model, :colour)
200
-
201
- @porsche_911 = CarData.new "Porsche", "911", "black"
202
- @person = @porsche_911.as Person
203
- # => Raises a TypeError
204
-
205
- @alice = PersonData.new "Alice", "Aardvark", "alice@example.com", "Ice cream"
206
- @person = @alice.as Person
207
- @person.first_name
208
- # => "Alice"
209
- @person.email
210
- # => "alice@example.com"
211
- @person.favourite_food
212
- # => NoMethodError - even though :favourite_food is a field in PersonData, it is not included in the definition of Person so cannot be accessed through the RubberDuck type
360
+ require "plumbing"
361
+
362
+ Person = Plumbing::RubberDuck.define :first_name, :last_name, :email
363
+ LikesFood = Plumbing::RubberDuck.define :favourite_food
364
+
365
+ PersonData = Struct.new(:first_name, :last_name, :email, :favourite_food)
366
+ CarData = Struct.new(:make, :model, :colour)
367
+
368
+ @porsche_911 = CarData.new "Porsche", "911", "black"
369
+ @person = @porsche_911.as Person
370
+ # => Raises a TypeError as CarData does not respond_to #first_name, #last_name, #email
371
+
372
+ @alice = PersonData.new "Alice", "Aardvark", "alice@example.com", "Ice cream"
373
+ @person = @alice.as Person
374
+ @person.first_name
375
+ # => "Alice"
376
+ @person.email
377
+ # => "alice@example.com"
378
+ @person.favourite_food
379
+ # => NoMethodError - #favourite_food is not part of the Person rubber duck (even though it is part of the underlying PersonData struct)
380
+
381
+ # Cast our Person into a LikesFood rubber duck
382
+ @hungry = @person.as LikesFood
383
+ @hungry.favourite_food
384
+ # => "Ice cream"
213
385
  ```
214
386
 
215
387
  ## Installation
@@ -0,0 +1 @@
1
+ f9f003afc58c61f5cf75127e1c92e95aebeb8f414e47bbd80661cedfbe379709ba22246f759e6ba1c8a8986f57ef6cb5d615157633e18abc6a3fa518325dc544
@@ -0,0 +1,51 @@
1
+ # Pipes, pipelines, valves and rubber ducks
2
+ module Plumbing
3
+ Config = Data.define :mode, :valve_proxy_classes, :timeout do
4
+ def valve_proxy_class_for target_class
5
+ valve_proxy_classes[target_class]
6
+ end
7
+
8
+ def register_valve_proxy_class_for target_class, proxy_class
9
+ valve_proxy_classes[target_class] = proxy_class
10
+ end
11
+ end
12
+ private_constant :Config
13
+
14
+ # Access the current configuration
15
+ # @return [Config]
16
+ def self.config
17
+ configs.last
18
+ end
19
+
20
+ # Configure the plumbing
21
+ # @param params [Hash] the configuration options
22
+ # @option mode [Symbol] the mode to use (:inline is the default, :async uses fibers)
23
+ # @option timeout [Integer] the timeout (in seconds) to use (30s is the default)
24
+ # @yield optional block - after the block has completed its execution, the configuration is restored to its previous state (useful for test suites)
25
+ def self.configure(**params, &block)
26
+ new_config = Config.new(**config.to_h.merge(params).merge(valve_proxy_classes: {}))
27
+ if block.nil?
28
+ set_configuration_to new_config
29
+ else
30
+ set_configuration_and_yield new_config, &block
31
+ end
32
+ end
33
+
34
+ def self.set_configuration_to config
35
+ configs << config
36
+ end
37
+ private_class_method :set_configuration_to
38
+
39
+ def self.set_configuration_and_yield(new_config, &block)
40
+ set_configuration_to new_config
41
+ yield
42
+ ensure
43
+ configs.pop
44
+ end
45
+ private_class_method :set_configuration_and_yield
46
+
47
+ def self.configs
48
+ @configs ||= [Config.new(mode: :inline, timeout: 30, valve_proxy_classes: {})]
49
+ end
50
+ private_class_method :configs
51
+ end
@@ -0,0 +1,15 @@
1
+ module Plumbing
2
+ # A pipe that can be subclassed to filter events from a source pipe
3
+ class CustomFilter < Pipe
4
+ # Chain this pipe to the source pipe
5
+ # @param source [Plumbing::Observable] the source from which to receive and filter events
6
+ def initialize source:
7
+ super()
8
+ source.as(Observable).add_observer { |event| received event }
9
+ end
10
+
11
+ protected
12
+
13
+ def received(event) = raise NoMethodError.new("Subclass should define #received")
14
+ end
15
+ end
@@ -1,5 +1,4 @@
1
1
  module Plumbing
2
2
  # An immutable data structure representing an Event
3
- Event = Data.define :type, :data do
4
- end
3
+ Event = Data.define :type, :data
5
4
  end
@@ -1,20 +1,20 @@
1
+ require_relative "custom_filter"
1
2
  module Plumbing
2
3
  # A pipe that filters events from a source pipe
3
- class Filter < Pipe
4
+ class Filter < CustomFilter
4
5
  # Chain this pipe to the source pipe
5
- # @param source [Plumbing::Pipe]
6
+ # @param source [Plumbing::Observable] the source from which to receive and filter events
6
7
  # @param &accepts [Block] a block that returns a boolean value - true to accept the event, false to reject it
7
- def initialize source:, dispatcher: nil, &accepts
8
- super(dispatcher: dispatcher)
8
+ # @yield [Plumbing::Event] event the event that is currently being processed
9
+ # @yieldreturn [Boolean] true to accept the event, false to reject it
10
+ def initialize source:, &accepts
11
+ super(source: source)
9
12
  @accepts = accepts.as(Callable)
10
- source.as(Observable).add_observer do |event|
11
- filter_and_republish event
12
- end
13
13
  end
14
14
 
15
- private
15
+ protected
16
16
 
17
- def filter_and_republish event
17
+ def received(event)
18
18
  return nil unless @accepts.call event
19
19
  dispatch event
20
20
  end
@@ -2,9 +2,9 @@ module Plumbing
2
2
  # A pipe that filters events from a source pipe
3
3
  class Junction < Pipe
4
4
  # Chain multiple sources to this pipe
5
- # @param [Array<Plumbing::Pipe>]
6
- def initialize *sources, dispatcher: nil
7
- super(dispatcher: dispatcher)
5
+ # @param sources [Array<Plumbing::Observable>] the sources which will be joined and relayed
6
+ def initialize *sources
7
+ super()
8
8
  @sources = sources.collect { |source| add(source) }
9
9
  end
10
10
 
data/lib/plumbing/pipe.rb CHANGED
@@ -1,12 +1,10 @@
1
1
  module Plumbing
2
2
  # A basic pipe
3
3
  class Pipe
4
- require_relative "event_dispatcher"
4
+ include Plumbing::Valve
5
5
 
6
- # Subclasses should call `super()` to ensure the pipe is initialised corrected
7
- def initialize dispatcher: nil
8
- @dispatcher = dispatcher.nil? ? EventDispatcher.new : dispatcher.as(DispatchesEvents)
9
- end
6
+ command :notify, :<<, :remove_observer, :shutdown
7
+ query :add_observer, :is_observer?
10
8
 
11
9
  # Push an event into the pipe
12
10
  # @param event [Plumbing::Event] the event to push into the pipe
@@ -25,40 +23,33 @@ module Plumbing
25
23
  end
26
24
 
27
25
  # Add an observer to this pipe
28
- # @param callable [Proc] (optional)
29
- # @param &block [Block] (optional)
30
- # @return an object representing this observer (dependent upon the implementation of the pipe itself)
31
26
  # Either a `callable` or a `block` must be supplied. If the latter, it is converted to a [Proc]
32
- def add_observer(observer = nil, &)
33
- @dispatcher.add_observer(observer, &)
27
+ # @param callable [#call] (optional)
28
+ # @param &block [Block] (optional)
29
+ # @return [#call]
30
+ def add_observer(observer = nil, &block)
31
+ observer ||= block.to_proc
32
+ observers << observer.as(Callable).target
33
+ observer
34
34
  end
35
35
 
36
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
37
+ # @param observer [#call] remove the observer from this pipe (where the observer was previously added by #add_observer)
39
38
  def remove_observer observer
40
- @dispatcher.remove_observer observer
39
+ observers.delete observer
41
40
  end
42
41
 
43
42
  # Test whether the given observer is observing this pipe
44
- # @param observer
45
- # @return [boolean]
43
+ # @param [#call] observer
44
+ # @return [Boolean]
46
45
  def is_observer? observer
47
- @dispatcher.is_observer? observer
46
+ observers.include? observer
48
47
  end
49
48
 
50
49
  # Close this pipe and perform any cleanup.
51
50
  # Subclasses should override this to perform their own shutdown routines and call `super` to ensure everything is tidied up
52
51
  def shutdown
53
- # clean up and release any observers, just in case
54
- @dispatcher.shutdown
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(*, **, &)
61
- new(*, **, &)
52
+ observers.clear
62
53
  end
63
54
 
64
55
  protected
@@ -68,7 +59,16 @@ module Plumbing
68
59
  # Enumerates all observers and `calls` them with this event
69
60
  # Discards any errors raised by the observer so that all observers will be successfully notified
70
61
  def dispatch event
71
- @dispatcher.dispatch event
62
+ observers.each do |observer|
63
+ observer.call event
64
+ rescue => ex
65
+ puts ex
66
+ ex
67
+ end
68
+ end
69
+
70
+ def observers
71
+ @observers ||= []
72
72
  end
73
73
  end
74
74
  end
@@ -1,14 +1,24 @@
1
1
  module Plumbing
2
2
  class Pipeline
3
+ # Validate input and output data with pre and post conditions or [Dry::Validation::Contract]s
3
4
  module Contracts
5
+ # @param name [Symbol] the name of the precondition
6
+ # @param &validator [Block] a block that returns a boolean value - true to accept the input, false to reject it
7
+ # @yield [Object] input the input data to be validated
8
+ # @yieldreturn [Boolean] true to accept the input, false to reject it
4
9
  def pre_condition name, &validator
5
10
  pre_conditions[name.to_sym] = validator
6
11
  end
7
12
 
13
+ # @param [String] contract_class the class name of the [Dry::Validation::Contract] that will be used to validate the input data
8
14
  def validate_with contract_class
9
15
  @validation_contract = contract_class
10
16
  end
11
17
 
18
+ # @param name [Symbol] the name of the postcondition
19
+ # @param &validator [Block] a block that returns a boolean value - true to accept the input, false to reject it
20
+ # @yield [Object] output the output data to be validated
21
+ # @yieldreturn [Boolean] true to accept the output, false to reject it
12
22
  def post_condition name, &validator
13
23
  post_conditions[name.to_sym] = validator
14
24
  end
@@ -1,10 +1,26 @@
1
1
  module Plumbing
2
2
  class Pipeline
3
+ # Defining the operations that will be performed on the input data
3
4
  module Operations
5
+ # Add an operation to the pipeline
6
+ # Operations are processed in order, unless interrupted by an exception
7
+ # The output from the previous operation is fed in as the input to the this operation
8
+ # and the output from this operation is fed in as the input to the next operation
9
+ #
10
+ # @param method [Symbol] the method to be called on the input data
11
+ # @param using [String, Class] the optional class name or class that will be used to perform the operation
12
+ # @param &implementation [Block] the optional block that will be used to perform the operation (instead of calling a method)
13
+ # @yield [Object] input the input data to be processed
14
+ # @yieldreturn [Object] the output data
4
15
  def perform method, using: nil, &implementation
5
16
  using.nil? ? perform_internal(method, &implementation) : perform_external(method, using)
6
17
  end
7
18
 
19
+ # Add an operation which does not alter the input data to the pipeline
20
+ # The output from the previous operation is fed in as the input to the this operation
21
+ # but the output from this operation is discarded and the previous input is fed in to the next operation
22
+ #
23
+ # @param method [Symbol] the method to be called on the input data
8
24
  def execute method
9
25
  implementation ||= ->(input, instance) do
10
26
  instance.send(method, input)
@@ -13,6 +29,7 @@ module Plumbing
13
29
  operations << implementation
14
30
  end
15
31
 
32
+ # Internal use only
16
33
  def _call input, instance
17
34
  validate_contract_for input
18
35
  validate_preconditions_for input
@@ -7,6 +7,9 @@ module Plumbing
7
7
  extend Plumbing::Pipeline::Contracts
8
8
  extend Plumbing::Pipeline::Operations
9
9
 
10
+ # Start the pipeline operation with the given input
11
+ # @param input [Object] the input data to be processed
12
+ # @return [Object] the output data
10
13
  def call input
11
14
  self.class._call input, self
12
15
  end
@@ -1,6 +1,8 @@
1
1
  module Plumbing
2
2
  class RubberDuck
3
3
  ::Object.class_eval do
4
+ # Cast the object to a duck-type
5
+ # @return [Plumbing::RubberDuck::Proxy] the duck-type proxy
4
6
  def as duck_type
5
7
  duck_type.proxy_for self
6
8
  end
@@ -1,5 +1,6 @@
1
1
  module Plumbing
2
2
  class RubberDuck
3
+ # Proxy object that forwards the duck-typed methods to the target object
3
4
  class Proxy
4
5
  attr_reader :target
5
6
 
@@ -8,6 +9,8 @@ module Plumbing
8
9
  @duck_type = duck_type
9
10
  end
10
11
 
12
+ # Convert the proxy to the given duck-type, ensuring that existing proxies are not duplicated
13
+ # @return [Plumbing::RubberDuck::Proxy] the proxy for the given duck-type
11
14
  def as duck_type
12
15
  (duck_type == @duck_type) ? self : duck_type.proxy_for(target)
13
16
  end
@@ -9,16 +9,25 @@ module Plumbing
9
9
  @proxy_classes = {}
10
10
  end
11
11
 
12
+ # Verify that the given object responds to the required methods
13
+ # @param object [Object] the object to verify
14
+ # @return [Object] the object if it passes the verification
15
+ # @raise [TypeError] if the object does not respond to the required methods
12
16
  def verify object
13
17
  missing_methods = @methods.reject { |method| object.respond_to? method }
14
18
  raise TypeError, "Expected object to respond to #{missing_methods.join(", ")}" unless missing_methods.empty?
15
19
  object
16
20
  end
17
21
 
22
+ # Test if the given object is a proxy
23
+ # @param object [Object] the object to test
24
+ # @return [Boolean] true if the object is a proxy, false otherwise
18
25
  def proxy_for object
19
26
  is_a_proxy?(object) || build_proxy_for(object)
20
27
  end
21
28
 
29
+ # Define a new rubber duck type
30
+ # @param *methods [Array<Symbol>] the methods that the duck-type should respond to
22
31
  def self.define *methods
23
32
  new(*methods)
24
33
  end
@@ -0,0 +1,42 @@
1
+ require "async"
2
+ require "async/semaphore"
3
+ require "timeout"
4
+
5
+ module Plumbing
6
+ module Valve
7
+ class Async
8
+ attr_reader :target
9
+
10
+ def initialize target
11
+ @target = target
12
+ @queue = []
13
+ @semaphore = ::Async::Semaphore.new(1)
14
+ end
15
+
16
+ # Ask the target to answer the given message
17
+ def ask(message, *args, **params, &block)
18
+ task = @semaphore.async do
19
+ @target.send message, *args, **params, &block
20
+ end
21
+ Timeout.timeout(timeout) do
22
+ task.wait
23
+ end
24
+ end
25
+
26
+ # Tell the target to execute the given message
27
+ def tell(message, *args, **params, &block)
28
+ @semaphore.async do |task|
29
+ @target.send message, *args, **params, &block
30
+ rescue
31
+ nil
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def timeout
38
+ Plumbing.config.timeout
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,19 @@
1
+ module Plumbing
2
+ module Valve
3
+ class Inline
4
+ def initialize target
5
+ @target = target
6
+ end
7
+
8
+ # Ask the target to answer the given message
9
+ def ask(message, ...)
10
+ @target.send(message, ...)
11
+ end
12
+
13
+ # Tell the target to execute the given message
14
+ def tell(message, ...)
15
+ @target.send(message, ...)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,5 @@
1
+ module Plumbing
2
+ module Valve
3
+ Message = Struct.new :message, :args, :params, :block, :result, :status
4
+ end
5
+ end
@@ -0,0 +1,71 @@
1
+ require_relative "valve/inline"
2
+
3
+ module Plumbing
4
+ module Valve
5
+ def self.included base
6
+ base.extend ClassMethods
7
+ end
8
+
9
+ module ClassMethods
10
+ # Create a new valve instance and build a proxy for it using the current mode
11
+ # @return [Plumbing::Valve::Base] the proxy for the valve instance
12
+ def start(*, **, &)
13
+ build_proxy_for(new(*, **, &))
14
+ end
15
+
16
+ # Define the queries that this valve can answer
17
+ # @param names [Array<Symbol>] the names of the queries
18
+ def query(*names) = queries.concat(names.map(&:to_sym))
19
+
20
+ # List the queries that this valve can answer
21
+ def queries = @queries ||= []
22
+
23
+ # Define the commands that this valve can execute
24
+ # @param names [Array<Symbol>] the names of the commands
25
+ def command(*names) = commands.concat(names.map(&:to_sym))
26
+
27
+ # List the commands that this valve can execute
28
+ def commands = @commands ||= []
29
+
30
+ def inherited subclass
31
+ subclass.commands.concat commands
32
+ subclass.queries.concat queries
33
+ end
34
+
35
+ private
36
+
37
+ def build_proxy_for(target)
38
+ proxy_class_for(target.class).new(target)
39
+ end
40
+
41
+ def proxy_class_for target_class
42
+ Plumbing.config.valve_proxy_class_for(target_class) || register_valve_proxy_class_for(target_class)
43
+ end
44
+
45
+ def proxy_base_class = const_get "Plumbing::Valve::#{Plumbing.config.mode.to_s.capitalize}"
46
+
47
+ def register_valve_proxy_class_for target_class
48
+ Plumbing.config.register_valve_proxy_class_for(target_class, build_proxy_class)
49
+ end
50
+
51
+ def build_proxy_class
52
+ Class.new(proxy_base_class).tap do |proxy_class|
53
+ queries.each do |query|
54
+ proxy_class.define_method query do |*args, **params, &block|
55
+ ask(query, *args, **params, &block)
56
+ end
57
+ end
58
+
59
+ commands.each do |command|
60
+ proxy_class.define_method command do |*args, **params, &block|
61
+ tell(command, *args, **params, &block)
62
+ nil
63
+ rescue
64
+ nil
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Plumbing
4
- VERSION = "0.2.2"
4
+ VERSION = "0.3.0"
5
5
  end
data/lib/plumbing.rb CHANGED
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Plumbing
4
+ require_relative "plumbing/config"
5
+ require_relative "plumbing/valve"
4
6
  require_relative "plumbing/rubber_duck"
5
7
  require_relative "plumbing/types"
6
8
  require_relative "plumbing/error"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: standard-procedure-plumbing
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rahoul Baruah
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-08-25 00:00:00.000000000 Z
11
+ date: 2024-08-28 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: A composable event pipeline and sequential pipelines of operations
14
14
  email:
@@ -32,11 +32,12 @@ files:
32
32
  - checksums/standard-procedure-plumbing-0.2.0.gem.sha512
33
33
  - checksums/standard-procedure-plumbing-0.2.1.gem.sha512
34
34
  - checksums/standard-procedure-plumbing-0.2.2.gem.sha512
35
+ - checksums/standard-procedure-plumbing-0.3.0.gem.sha512
35
36
  - lib/plumbing.rb
37
+ - lib/plumbing/config.rb
38
+ - lib/plumbing/custom_filter.rb
36
39
  - lib/plumbing/error.rb
37
40
  - lib/plumbing/event.rb
38
- - lib/plumbing/event_dispatcher.rb
39
- - lib/plumbing/event_dispatcher/fiber.rb
40
41
  - lib/plumbing/filter.rb
41
42
  - lib/plumbing/junction.rb
42
43
  - lib/plumbing/pipe.rb
@@ -47,6 +48,10 @@ files:
47
48
  - lib/plumbing/rubber_duck/object.rb
48
49
  - lib/plumbing/rubber_duck/proxy.rb
49
50
  - lib/plumbing/types.rb
51
+ - lib/plumbing/valve.rb
52
+ - lib/plumbing/valve/async.rb
53
+ - lib/plumbing/valve/inline.rb
54
+ - lib/plumbing/valve/message.rb
50
55
  - lib/plumbing/version.rb
51
56
  - sig/plumbing.rbs
52
57
  homepage: https://github.com/standard-procedure/plumbing
@@ -1,61 +0,0 @@
1
- require "async/task"
2
- require "async/semaphore"
3
-
4
- module Plumbing
5
- class EventDispatcher
6
- class Fiber < EventDispatcher
7
- def initialize limit: 4
8
- super()
9
- @semaphore = Async::Semaphore.new(limit)
10
- @queue = Set.new
11
- @paused = false
12
- end
13
-
14
- def dispatch event
15
- @queue << event
16
- dispatch_events unless @paused
17
- end
18
-
19
- def pause
20
- @paused = true
21
- end
22
-
23
- def resume
24
- @paused = false
25
- dispatch_events
26
- end
27
-
28
- def queue_size
29
- @queue.size
30
- end
31
-
32
- def shutdown
33
- super
34
- @queue.clear
35
- end
36
-
37
- private
38
-
39
- def dispatch_events
40
- @semaphore.async do |task|
41
- events = @queue.dup
42
- @queue.clear
43
- events.each do |event|
44
- dispatch_event event, task
45
- end
46
- end
47
- end
48
-
49
- def dispatch_event event, task
50
- @observers.each do |observer|
51
- task.async do
52
- observer.call event
53
- rescue => ex
54
- puts ex
55
- ex
56
- end
57
- end
58
- end
59
- end
60
- end
61
- end
@@ -1,34 +0,0 @@
1
- module Plumbing
2
- class EventDispatcher
3
- def initialize observers: []
4
- @observers = observers.as(Collection)
5
- end
6
-
7
- def add_observer observer = nil, &block
8
- observer ||= block.to_proc
9
- @observers << observer.as(Callable).target
10
- observer
11
- end
12
-
13
- def remove_observer observer
14
- @observers.delete observer
15
- end
16
-
17
- def is_observer? observer
18
- @observers.include? observer
19
- end
20
-
21
- def dispatch event
22
- @observers.each do |observer|
23
- observer.call event
24
- rescue => ex
25
- puts ex
26
- ex
27
- end
28
- end
29
-
30
- def shutdown
31
- @observers = []
32
- end
33
- end
34
- end