standard-procedure-plumbing 0.3.3 → 0.4.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 +4 -4
 - data/README.md +58 -88
 - data/lib/plumbing/actor/async.rb +38 -0
 - data/lib/plumbing/actor/inline.rb +22 -0
 - data/lib/plumbing/actor/kernel.rb +9 -0
 - data/lib/plumbing/{valve → actor}/rails.rb +1 -1
 - data/lib/plumbing/{valve → actor}/threaded.rb +22 -27
 - data/lib/plumbing/actor/transporter.rb +61 -0
 - data/lib/plumbing/actor.rb +63 -0
 - data/lib/plumbing/config.rb +8 -8
 - data/lib/plumbing/pipe.rb +2 -3
 - data/lib/plumbing/version.rb +1 -1
 - data/lib/plumbing.rb +1 -1
 - data/spec/examples/{valve_spec.rb → actor_spec.rb} +14 -14
 - data/spec/examples/pipe_spec.rb +4 -4
 - data/spec/plumbing/a_pipe.rb +11 -9
 - data/spec/plumbing/actor/transporter_spec.rb +158 -0
 - data/spec/plumbing/actor_spec.rb +208 -0
 - metadata +27 -11
 - data/lib/plumbing/valve/async.rb +0 -43
 - data/lib/plumbing/valve/inline.rb +0 -20
 - data/lib/plumbing/valve/message.rb +0 -5
 - data/lib/plumbing/valve.rb +0 -71
 - data/spec/plumbing/valve_spec.rb +0 -171
 
    
        checksums.yaml
    CHANGED
    
    | 
         @@ -1,7 +1,7 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            ---
         
     | 
| 
       2 
2 
     | 
    
         
             
            SHA256:
         
     | 
| 
       3 
     | 
    
         
            -
              metadata.gz:  
     | 
| 
       4 
     | 
    
         
            -
              data.tar.gz:  
     | 
| 
      
 3 
     | 
    
         
            +
              metadata.gz: a51cb6cf16fcc70cd0f6607a461e817c7021ed9f0de445e1f628ff1a8de9e489
         
     | 
| 
      
 4 
     | 
    
         
            +
              data.tar.gz: 992e53f3a79cb4f3597b9758e10953156ec9dda073eff97bc740e6050746fddc
         
     | 
| 
       5 
5 
     | 
    
         
             
            SHA512:
         
     | 
| 
       6 
     | 
    
         
            -
              metadata.gz:  
     | 
| 
       7 
     | 
    
         
            -
              data.tar.gz:  
     | 
| 
      
 6 
     | 
    
         
            +
              metadata.gz: f4ff32c0bbd46d2433bcb051572914fa96378f5ffe6badd0fa77462565c058357b6803ff98afcf4534a739a571bb9084e39fe045ec4415dd084379d12e99e684
         
     | 
| 
      
 7 
     | 
    
         
            +
              data.tar.gz: 6a1d52c882d1edb1d5705383596e4b4c556f4f561770466a4bd18529e8e21630d3c522d323feb6fed8e0b87c22bc35b56075b47ae53a62a9176473061b0840a0
         
     | 
    
        data/README.md
    CHANGED
    
    | 
         @@ -1,16 +1,18 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            # Plumbing
         
     | 
| 
       2 
2 
     | 
    
         | 
| 
      
 3 
     | 
    
         
            +
            Actors, Observers and Data Pipelines.
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
       3 
5 
     | 
    
         
             
            ## Configuration
         
     | 
| 
       4 
6 
     | 
    
         | 
| 
       5 
     | 
    
         
            -
            The most important configuration setting is the `mode`, which governs how  
     | 
| 
      
 7 
     | 
    
         
            +
            The most important configuration setting is the `mode`, which governs how background tasks are handled.
         
     | 
| 
       6 
8 
     | 
    
         | 
| 
       7 
     | 
    
         
            -
            By default it is `:inline`, so every command or query is handled synchronously.  This is the ruby behaviour you know and love.
         
     | 
| 
      
 9 
     | 
    
         
            +
            By default it is `:inline`, so every command or query is handled synchronously.  This is the ruby behaviour you know and love (although see the section on `await` below).
         
     | 
| 
       8 
10 
     | 
    
         | 
| 
       9 
     | 
    
         
            -
             
     | 
| 
      
 11 
     | 
    
         
            +
            `:async` mode handles tasks using fibers (via the [Async gem](https://socketry.github.io/async/index.html)).  Your code should include the "async" gem in its bundle, as Plumbing does not load it by default.
         
     | 
| 
       10 
12 
     | 
    
         | 
| 
       11 
     | 
    
         
            -
             
     | 
| 
      
 13 
     | 
    
         
            +
            `:threaded` mode handles tasks using a thread pool via [Concurrent Ruby](https://ruby-concurrency.github.io/concurrent-ruby/master/Concurrent/Promises.html)).  Your code should include the "concurrent-ruby" gem in its bundle, as Plumbing does not load it by default.
         
     | 
| 
       12 
14 
     | 
    
         | 
| 
       13 
     | 
    
         
            -
             
     | 
| 
      
 15 
     | 
    
         
            +
            However, `:threaded` mode is not safe for Ruby on Rails applications.  In this case, use `:rails` mode, which is identical to `:threaded`, except it wraps the tasks in the Rails executor.  This ensures your actors do not interfere with the Rails framework.  Note that the Concurrent Ruby's default `:io` scheduler will create extra threads at times of high demand, which may put pressure on the ActiveRecord database connection pool.  A future version of plumbing will allow the thread pool to be adjusted with a maximum number of threads, preventing contention with the connection pool.
         
     | 
| 
       14 
16 
     | 
    
         | 
| 
       15 
17 
     | 
    
         
             
            The `timeout` setting is used when performing queries - it defaults to 30s.
         
     | 
| 
       16 
18 
     | 
    
         | 
| 
         @@ -154,28 +156,53 @@ You can also verify that the output generated is as expected by defining a `post 
     | 
|
| 
       154 
156 
     | 
    
         
             
              # => ["first", "external", "third"]
         
     | 
| 
       155 
157 
     | 
    
         
             
            ```
         
     | 
| 
       156 
158 
     | 
    
         | 
| 
       157 
     | 
    
         
            -
            ## Plumbing:: 
     | 
| 
      
 159 
     | 
    
         
            +
            ## Plumbing::Actor - safe asynchronous objects
         
     | 
| 
      
 160 
     | 
    
         
            +
             
     | 
| 
      
 161 
     | 
    
         
            +
            An [actor](https://en.wikipedia.org/wiki/Actor_model) defines the messages an object can receive, similar to a regular object.
         
     | 
| 
      
 162 
     | 
    
         
            +
            However, in traditional object-orientated programming, a thread of execution moves from one object to another.  If there are multiple threads, then each object may be accessed concurrently, leading to race conditions or data-integrity problems - and very hard to track bugs.
         
     | 
| 
      
 163 
     | 
    
         
            +
             
     | 
| 
      
 164 
     | 
    
         
            +
            Actors are different.  Conceptually, each actor has it's own thread of execution, isolated from every other actor in the system.  When one actor sends a message to another actor, the receiver does not execute its method in the caller's thread.  Instead, it places the message on a queue and waits until its own thread is free to process the work.  If the caller would like to access the return value from the method, then it must wait until the receiver has finished processing.
         
     | 
| 
      
 165 
     | 
    
         
            +
             
     | 
| 
      
 166 
     | 
    
         
            +
            This means each actor is only ever accessed by a single thread and the vast majority of concurrency issues are eliminated.
         
     | 
| 
      
 167 
     | 
    
         
            +
             
     | 
| 
      
 168 
     | 
    
         
            +
            [Plumbing::Actor](/lib/plumbing/actor.rb) allows you to define the `async` public interface to your objects.  Calling `.start` builds a proxy to the actual instance of your object and ensures that any messages sent are handled in a manner appropriate to the current mode - immediately for inline mode, using fibers for async mode and using threads for threaded and rails mode.
         
     | 
| 
      
 169 
     | 
    
         
            +
             
     | 
| 
      
 170 
     | 
    
         
            +
            When sending messages to an actor, this just works.
         
     | 
| 
      
 171 
     | 
    
         
            +
             
     | 
| 
      
 172 
     | 
    
         
            +
            However, as the caller, you do not have direct access to the return values of the messages that you send.  Instead, you must call `#await` - or alternatively, wrap your call in `await { ... }`.  `await` is added in to ruby's `Kernel` so it is available everywhere.  This then makes the caller's thread block until the receiver's thread has finished its work and returned a value.  Or if the receiver raises an exception, that exception is then re-raised in the calling thread.
         
     | 
| 
      
 173 
     | 
    
         
            +
             
     | 
| 
      
 174 
     | 
    
         
            +
            The actor model does not eliminate every possible concurrency issue.  If you use `await`, it is possible to deadlock yourself.
         
     | 
| 
      
 175 
     | 
    
         
            +
            Actor A, running in Thread 1, sends a message to Actor B and then awaits the result, meaning Thread 1 is blocked.  Actor B, running in Thread 2, starts to work, but needs to ask Actor A a question.  So it sends a message to Actor A and awaits the result.  Thread 2 is now blocked, waiting for Actor A to respond.  But Actor A, running in Thread 1, is blocked, waiting for Actor B to respond.
         
     | 
| 
      
 176 
     | 
    
         
            +
             
     | 
| 
      
 177 
     | 
    
         
            +
            This potential deadlock only occurs if you use `await` and have actors that call back in to each other.  If your objects are strictly layered, or you never use `await` (perhaps, instead using a Pipe to observe events), then this particular deadlock should not occur.  However, just in case, every call to `await` has a timeout defaulting to 30s.
         
     | 
| 
      
 178 
     | 
    
         
            +
             
     | 
| 
      
 179 
     | 
    
         
            +
            ### Inline actors
         
     | 
| 
      
 180 
     | 
    
         
            +
             
     | 
| 
      
 181 
     | 
    
         
            +
            Even though inline mode is not asynchronous, you must still use `await` to access the results from another actor.  However, as deadlocks are impossible in a single thread, there is no timeout.
         
     | 
| 
      
 182 
     | 
    
         
            +
             
     | 
| 
      
 183 
     | 
    
         
            +
            ### Async actors
         
     | 
| 
      
 184 
     | 
    
         
            +
             
     | 
| 
      
 185 
     | 
    
         
            +
            Using async mode is probably the easiest way to add concurrency to your application.  It uses fibers to allow for "concurrency but not parallelism" - that is execution will happen in the background but your objects or data will never be accessed by two things at the exact same time.
         
     | 
| 
       158 
186 
     | 
    
         | 
| 
       159 
     | 
    
         
            -
             
     | 
| 
      
 187 
     | 
    
         
            +
            ### Threaded actprs
         
     | 
| 
       160 
188 
     | 
    
         | 
| 
       161 
     | 
    
         
            -
             
     | 
| 
      
 189 
     | 
    
         
            +
            Using threaded (or rails) mode gives you concurrency and parallelism.  If all your public objects are actors and you are careful about callbacks then the actor model will keep your code safe.  But there are a couple of extra things to consider.
         
     | 
| 
       162 
190 
     | 
    
         | 
| 
       163 
     | 
    
         
            -
             
     | 
| 
      
 191 
     | 
    
         
            +
            Firstly, when you pass parameters or return results between threads, those objects are "transported" across the boundaries.
         
     | 
| 
      
 192 
     | 
    
         
            +
            Most objects are `clone`d. Hashes, keyword arguments and arrays have their contents recursively transported.  And any object that uses `GlobalID::Identification` (for example, ActiveRecord models) are marshalled into a GlobalID, then unmarshalled back in to their original object.  This is to prevent the same object from being amended in both the caller and receiver's threads.
         
     | 
| 
      
 193 
     | 
    
         
            +
            Secondly, when you pass a block (or Proc parameter) to another actor, the block/proc will be executed in the receiver's thread.  This means you must not access any variables that would normally be in scope for your block (whether local variables or instance variables of other objects - see note below)  This is because you will be accessing them from a different thread to where they were defined, leading to potential race conditions.  And, if you access any actors, you must not use `await` or you risk a deadlock.  If you do pass a block or proc parameter, you should limit your actions to sending a message to other actors without awaiting the results.
         
     | 
| 
       164 
194 
     | 
    
         | 
| 
       165 
     | 
    
         
            -
            -  
     | 
| 
       166 
     | 
    
         
            -
            - Queries return a value so the caller blocks until the actor has returned a value
         
     | 
| 
       167 
     | 
    
         
            -
            - However, if you call a query and pass `ignore_result: true` then the query will not block, although you will not be able to access the return value - this is for commands that do something and then return a result based on that work (which you may or may not be interested in - see Plumbing::Pipe#add_observer)
         
     | 
| 
       168 
     | 
    
         
            -
            - None of the above applies if the `Plumbing mode` is set to `:inline` (which is the default) - in this case, the actor behaves like normal ruby code
         
     | 
| 
      
 195 
     | 
    
         
            +
            (Note: we break that rule in the specs for the Pipe object - we use a block observer that sets the value on a local variable.  That's because it is a controlled situation where we know there are only two threads involved and we are explicitly waiting for the second thread to complete.  For almost every app that uses actors, there will be multiple threads and it will be impossible to predict the access patterns).
         
     | 
| 
       169 
196 
     | 
    
         | 
| 
       170 
     | 
    
         
            -
            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  
     | 
| 
      
 197 
     | 
    
         
            +
            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 actor are available in this proxy - so you don't have to worry about callers bypassing the actor's internal context.
         
     | 
| 
       171 
198 
     | 
    
         | 
| 
       172 
199 
     | 
    
         
             
            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.
         
     | 
| 
       173 
200 
     | 
    
         | 
| 
       174 
     | 
    
         
            -
            Also be aware that if you use  
     | 
| 
      
 201 
     | 
    
         
            +
            Also be aware that if you use actors in one place, you need to use them everywhere - especially if you're using threads or ractors (coming soon).  This is because as the actor sends messages to its collaborators, those calls will be made from within the actor's internal context.  If the collaborators are also actors, the subsequent messages will be handled correctly, if not, data consistency bugs could occur.
         
     | 
| 
       175 
202 
     | 
    
         | 
| 
       176 
203 
     | 
    
         
             
            ### Usage
         
     | 
| 
       177 
204 
     | 
    
         | 
| 
       178 
     | 
    
         
            -
            [Defining an actor](/spec/examples/ 
     | 
| 
      
 205 
     | 
    
         
            +
            [Defining an actor](/spec/examples/actor_spec.rb)
         
     | 
| 
       179 
206 
     | 
    
         | 
| 
       180 
207 
     | 
    
         
             
            ```ruby
         
     | 
| 
       181 
208 
     | 
    
         
             
              require "plumbing"
         
     | 
| 
         @@ -183,7 +210,7 @@ Also be aware that if you use valves in one place, you need to use them everywhe 
     | 
|
| 
       183 
210 
     | 
    
         
             
              class Employee
         
     | 
| 
       184 
211 
     | 
    
         
             
                attr_reader :name, :job_title
         
     | 
| 
       185 
212 
     | 
    
         | 
| 
       186 
     | 
    
         
            -
                include Plumbing:: 
     | 
| 
      
 213 
     | 
    
         
            +
                include Plumbing::Actor
         
     | 
| 
       187 
214 
     | 
    
         
             
                query :name, :job_title, :greet_slowly
         
     | 
| 
       188 
215 
     | 
    
         
             
                command :promote
         
     | 
| 
       189 
216 
     | 
    
         | 
| 
         @@ -202,92 +229,35 @@ Also be aware that if you use valves in one place, you need to use them everywhe 
     | 
|
| 
       202 
229 
     | 
    
         
             
                  "H E L L O"
         
     | 
| 
       203 
230 
     | 
    
         
             
                end
         
     | 
| 
       204 
231 
     | 
    
         
             
              end
         
     | 
| 
       205 
     | 
    
         
            -
            ```
         
     | 
| 
       206 
     | 
    
         
            -
             
     | 
| 
       207 
     | 
    
         
            -
            [Acting inline](/spec/examples/valve_spec.rb) with no concurrency
         
     | 
| 
       208 
     | 
    
         
            -
             
     | 
| 
       209 
     | 
    
         
            -
            ```ruby
         
     | 
| 
       210 
     | 
    
         
            -
              require "plumbing"
         
     | 
| 
       211 
     | 
    
         
            -
             
     | 
| 
       212 
     | 
    
         
            -
              @person = Employee.start "Alice"
         
     | 
| 
       213 
     | 
    
         
            -
             
     | 
| 
       214 
     | 
    
         
            -
              puts @person.name
         
     | 
| 
       215 
     | 
    
         
            -
              # => "Alice"
         
     | 
| 
       216 
     | 
    
         
            -
              puts @person.job_title
         
     | 
| 
       217 
     | 
    
         
            -
              # => "Sales assistant"
         
     | 
| 
       218 
     | 
    
         
            -
             
     | 
| 
       219 
     | 
    
         
            -
              @person.promote
         
     | 
| 
       220 
     | 
    
         
            -
              # this will block for 0.5 seconds
         
     | 
| 
       221 
     | 
    
         
            -
              puts @person.job_title
         
     | 
| 
       222 
     | 
    
         
            -
              # => "Sales manager"
         
     | 
| 
       223 
     | 
    
         
            -
             
     | 
| 
       224 
     | 
    
         
            -
              @person.greet_slowly
         
     | 
| 
       225 
     | 
    
         
            -
              # this will block for 0.2 seconds before returning "H E L L O"
         
     | 
| 
       226 
     | 
    
         
            -
             
     | 
| 
       227 
     | 
    
         
            -
              @person.greet_slowly(ignore_result: true)
         
     | 
| 
       228 
     | 
    
         
            -
              # this will block for 0.2 seconds (as the mode is :inline) before returning nil
         
     | 
| 
       229 
     | 
    
         
            -
            ```
         
     | 
| 
       230 
     | 
    
         
            -
             
     | 
| 
       231 
     | 
    
         
            -
            [Using fibers](/spec/examples/valve_spec.rb) with concurrency but no parallelism
         
     | 
| 
       232 
232 
     | 
    
         | 
| 
       233 
     | 
    
         
            -
            ```ruby
         
     | 
| 
       234 
     | 
    
         
            -
              require "plumbing"
         
     | 
| 
       235 
     | 
    
         
            -
              require "async"
         
     | 
| 
       236 
     | 
    
         
            -
             
     | 
| 
       237 
     | 
    
         
            -
              Plumbing.configure mode: :async
         
     | 
| 
       238 
233 
     | 
    
         
             
              @person = Employee.start "Alice"
         
     | 
| 
       239 
234 
     | 
    
         | 
| 
       240 
     | 
    
         
            -
               
     | 
| 
       241 
     | 
    
         
            -
              # => 
     | 
| 
       242 
     | 
    
         
            -
               
     | 
| 
       243 
     | 
    
         
            -
              # 
     | 
| 
      
 235 
     | 
    
         
            +
              await { @person.name }
         
     | 
| 
      
 236 
     | 
    
         
            +
              # =>  "Alice"
         
     | 
| 
      
 237 
     | 
    
         
            +
              await { @person.job_title }
         
     | 
| 
      
 238 
     | 
    
         
            +
              # => "Sales assistant"
         
     | 
| 
       244 
239 
     | 
    
         | 
| 
       245 
     | 
    
         
            -
               
     | 
| 
       246 
     | 
    
         
            -
               
     | 
| 
       247 
     | 
    
         
            -
               
     | 
| 
       248 
     | 
    
         
            -
              # => "Sales manager" (this will block for 0.5s because #job_title query will not start until the #promote command has completed)
         
     | 
| 
      
 240 
     | 
    
         
            +
              # `greet_slowly` is a query so will block until a response is received
         
     | 
| 
      
 241 
     | 
    
         
            +
              await { @person.greet_slowly }
         
     | 
| 
      
 242 
     | 
    
         
            +
              # =>  "H E L L O"
         
     | 
| 
       249 
243 
     | 
    
         | 
| 
      
 244 
     | 
    
         
            +
              # we're not awaiting the result, so this will run in the background (unless we're using inline mode)
         
     | 
| 
       250 
245 
     | 
    
         
             
              @person.greet_slowly
         
     | 
| 
       251 
     | 
    
         
            -
              # this will block for 0.2 seconds before returning "H E L L O"
         
     | 
| 
       252 
     | 
    
         
            -
             
     | 
| 
       253 
     | 
    
         
            -
              @person.greet_slowly(ignore_result: true)
         
     | 
| 
       254 
     | 
    
         
            -
              # this will not block and returns nil
         
     | 
| 
       255 
     | 
    
         
            -
            ```
         
     | 
| 
       256 
     | 
    
         
            -
             
     | 
| 
       257 
     | 
    
         
            -
            [Using threads](/spec/examples/valve_spec.rb) with concurrency and some parallelism
         
     | 
| 
       258 
     | 
    
         
            -
             
     | 
| 
       259 
     | 
    
         
            -
            ```ruby
         
     | 
| 
       260 
     | 
    
         
            -
              require "plumbing"
         
     | 
| 
       261 
     | 
    
         
            -
              require "concurrent"
         
     | 
| 
       262 
     | 
    
         
            -
             
     | 
| 
       263 
     | 
    
         
            -
              Plumbing.configure mode: :threaded
         
     | 
| 
       264 
     | 
    
         
            -
              @person = Employee.start "Alice"
         
     | 
| 
       265 
     | 
    
         
            -
             
     | 
| 
       266 
     | 
    
         
            -
              puts @person.name
         
     | 
| 
       267 
     | 
    
         
            -
              # => "Alice"
         
     | 
| 
       268 
     | 
    
         
            -
              puts @person.job_title
         
     | 
| 
       269 
     | 
    
         
            -
              # => "Sales assistant"
         
     | 
| 
       270 
246 
     | 
    
         | 
| 
      
 247 
     | 
    
         
            +
              # This will run in the background
         
     | 
| 
       271 
248 
     | 
    
         
             
              @person.promote
         
     | 
| 
       272 
     | 
    
         
            -
              # this will  
     | 
| 
       273 
     | 
    
         
            -
               
     | 
| 
       274 
     | 
    
         
            -
              # => "Sales manager" 
     | 
| 
       275 
     | 
    
         
            -
             
     | 
| 
       276 
     | 
    
         
            -
              @person.greet_slowly
         
     | 
| 
       277 
     | 
    
         
            -
              # this will block for 0.2 seconds before returning "H E L L O"
         
     | 
| 
       278 
     | 
    
         
            -
             
     | 
| 
       279 
     | 
    
         
            -
              @person.greet_slowly(ignore_result: true)
         
     | 
| 
       280 
     | 
    
         
            -
              # this will not block and returns nil
         
     | 
| 
      
 249 
     | 
    
         
            +
              # this will block, as we wait for the result from #job_title and #job_title will not run until after #promote has completed
         
     | 
| 
      
 250 
     | 
    
         
            +
              await { @person.job_title }
         
     | 
| 
      
 251 
     | 
    
         
            +
              # => "Sales manager"
         
     | 
| 
       281 
252 
     | 
    
         
             
            ```
         
     | 
| 
       282 
253 
     | 
    
         | 
| 
       283 
     | 
    
         
            -
             
     | 
| 
       284 
254 
     | 
    
         
             
            ## Plumbing::Pipe - a composable observer
         
     | 
| 
       285 
255 
     | 
    
         | 
| 
       286 
256 
     | 
    
         
             
            [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)).
         
     | 
| 
       287 
257 
     | 
    
         | 
| 
       288 
258 
     | 
    
         
             
            [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.
         
     | 
| 
       289 
259 
     | 
    
         | 
| 
       290 
     | 
    
         
            -
            Pipes are implemented as  
     | 
| 
      
 260 
     | 
    
         
            +
            Pipes are implemented as actors, 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 actor to maintain safety.
         
     | 
| 
       291 
261 
     | 
    
         | 
| 
       292 
262 
     | 
    
         
             
            ### Usage
         
     | 
| 
       293 
263 
     | 
    
         | 
| 
         @@ -488,7 +458,7 @@ Then: 
     | 
|
| 
       488 
458 
     | 
    
         
             
            ```ruby
         
     | 
| 
       489 
459 
     | 
    
         
             
            require 'plumbing'
         
     | 
| 
       490 
460 
     | 
    
         | 
| 
       491 
     | 
    
         
            -
            # Set the mode for your  
     | 
| 
      
 461 
     | 
    
         
            +
            # Set the mode for your Actors and Pipes
         
     | 
| 
       492 
462 
     | 
    
         
             
            Plumbing.config mode: :async
         
     | 
| 
       493 
463 
     | 
    
         
             
            ```
         
     | 
| 
       494 
464 
     | 
    
         | 
| 
         @@ -0,0 +1,38 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            require "async"
         
     | 
| 
      
 2 
     | 
    
         
            +
            require "async/semaphore"
         
     | 
| 
      
 3 
     | 
    
         
            +
            require "timeout"
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
            module Plumbing
         
     | 
| 
      
 6 
     | 
    
         
            +
              module Actor
         
     | 
| 
      
 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 
     | 
    
         
            +
                  # Send the message to the target and wrap the result
         
     | 
| 
      
 17 
     | 
    
         
            +
                  def send_message message_name, *args, &block
         
     | 
| 
      
 18 
     | 
    
         
            +
                    task = @semaphore.async do
         
     | 
| 
      
 19 
     | 
    
         
            +
                      @target.send message_name, *args, &block
         
     | 
| 
      
 20 
     | 
    
         
            +
                    end
         
     | 
| 
      
 21 
     | 
    
         
            +
                    Result.new(task)
         
     | 
| 
      
 22 
     | 
    
         
            +
                  end
         
     | 
| 
      
 23 
     | 
    
         
            +
             
     | 
| 
      
 24 
     | 
    
         
            +
                  Result = Data.define(:task) do
         
     | 
| 
      
 25 
     | 
    
         
            +
                    def await
         
     | 
| 
      
 26 
     | 
    
         
            +
                      Timeout.timeout(Plumbing::Actor.timeout) do
         
     | 
| 
      
 27 
     | 
    
         
            +
                        task.wait
         
     | 
| 
      
 28 
     | 
    
         
            +
                      end
         
     | 
| 
      
 29 
     | 
    
         
            +
                    end
         
     | 
| 
      
 30 
     | 
    
         
            +
                  end
         
     | 
| 
      
 31 
     | 
    
         
            +
                  private_constant :Result
         
     | 
| 
      
 32 
     | 
    
         
            +
                end
         
     | 
| 
      
 33 
     | 
    
         
            +
             
     | 
| 
      
 34 
     | 
    
         
            +
                def self.timeout
         
     | 
| 
      
 35 
     | 
    
         
            +
                  Plumbing.config.timeout
         
     | 
| 
      
 36 
     | 
    
         
            +
                end
         
     | 
| 
      
 37 
     | 
    
         
            +
              end
         
     | 
| 
      
 38 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,22 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            module Plumbing
         
     | 
| 
      
 2 
     | 
    
         
            +
              module Actor
         
     | 
| 
      
 3 
     | 
    
         
            +
                class Inline
         
     | 
| 
      
 4 
     | 
    
         
            +
                  def initialize target
         
     | 
| 
      
 5 
     | 
    
         
            +
                    @target = target
         
     | 
| 
      
 6 
     | 
    
         
            +
                  end
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
                  # Send the message to the target and wrap the result
         
     | 
| 
      
 9 
     | 
    
         
            +
                  def send_message(message_name, *, &)
         
     | 
| 
      
 10 
     | 
    
         
            +
                    value = @target.send(message_name, *, &)
         
     | 
| 
      
 11 
     | 
    
         
            +
                    Result.new(value)
         
     | 
| 
      
 12 
     | 
    
         
            +
                  rescue => ex
         
     | 
| 
      
 13 
     | 
    
         
            +
                    Result.new(ex)
         
     | 
| 
      
 14 
     | 
    
         
            +
                  end
         
     | 
| 
      
 15 
     | 
    
         
            +
             
     | 
| 
      
 16 
     | 
    
         
            +
                  Result = Data.define(:value) do
         
     | 
| 
      
 17 
     | 
    
         
            +
                    def await = value.is_a?(Exception) ? raise(value) : value
         
     | 
| 
      
 18 
     | 
    
         
            +
                  end
         
     | 
| 
      
 19 
     | 
    
         
            +
                  private_constant :Result
         
     | 
| 
      
 20 
     | 
    
         
            +
                end
         
     | 
| 
      
 21 
     | 
    
         
            +
              end
         
     | 
| 
      
 22 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -2,9 +2,10 @@ require "concurrent/array" 
     | 
|
| 
       2 
2 
     | 
    
         
             
            require "concurrent/mvar"
         
     | 
| 
       3 
3 
     | 
    
         
             
            require "concurrent/immutable_struct"
         
     | 
| 
       4 
4 
     | 
    
         
             
            require "concurrent/promises"
         
     | 
| 
      
 5 
     | 
    
         
            +
            require_relative "transporter"
         
     | 
| 
       5 
6 
     | 
    
         | 
| 
       6 
7 
     | 
    
         
             
            module Plumbing
         
     | 
| 
       7 
     | 
    
         
            -
              module  
     | 
| 
      
 8 
     | 
    
         
            +
              module Actor
         
     | 
| 
       8 
9 
     | 
    
         
             
                class Threaded
         
     | 
| 
       9 
10 
     | 
    
         
             
                  attr_reader :target
         
     | 
| 
       10 
11 
     | 
    
         | 
| 
         @@ -13,17 +14,12 @@ module Plumbing 
     | 
|
| 
       13 
14 
     | 
    
         
             
                    @queue = Concurrent::Array.new
         
     | 
| 
       14 
15 
     | 
    
         
             
                  end
         
     | 
| 
       15 
16 
     | 
    
         | 
| 
       16 
     | 
    
         
            -
                  #  
     | 
| 
       17 
     | 
    
         
            -
                  def  
     | 
| 
       18 
     | 
    
         
            -
                     
     | 
| 
       19 
     | 
    
         
            -
             
     | 
| 
       20 
     | 
    
         
            -
             
     | 
| 
       21 
     | 
    
         
            -
             
     | 
| 
       22 
     | 
    
         
            -
                  def tell(message, *, **, &)
         
     | 
| 
       23 
     | 
    
         
            -
                    add_message_to_queue(message, *, **, &)
         
     | 
| 
       24 
     | 
    
         
            -
                    nil
         
     | 
| 
       25 
     | 
    
         
            -
                  rescue
         
     | 
| 
       26 
     | 
    
         
            -
                    nil
         
     | 
| 
      
 17 
     | 
    
         
            +
                  # Send the message to the target and wrap the result
         
     | 
| 
      
 18 
     | 
    
         
            +
                  def send_message message_name, *args, &block
         
     | 
| 
      
 19 
     | 
    
         
            +
                    Message.new(@target, message_name, Plumbing::Actor.transporter.marshal(*args), block, Concurrent::MVar.new).tap do |message|
         
     | 
| 
      
 20 
     | 
    
         
            +
                      @queue << message
         
     | 
| 
      
 21 
     | 
    
         
            +
                      send_messages if @queue.size == 1
         
     | 
| 
      
 22 
     | 
    
         
            +
                    end
         
     | 
| 
       27 
23 
     | 
    
         
             
                  end
         
     | 
| 
       28 
24 
     | 
    
         | 
| 
       29 
25 
     | 
    
         
             
                  protected
         
     | 
| 
         @@ -42,26 +38,25 @@ module Plumbing 
     | 
|
| 
       42 
38 
     | 
    
         
             
                    end
         
     | 
| 
       43 
39 
     | 
    
         
             
                  end
         
     | 
| 
       44 
40 
     | 
    
         | 
| 
       45 
     | 
    
         
            -
                   
     | 
| 
       46 
     | 
    
         
            -
                    Message.new(@target, message_name, args, params, block, Concurrent::MVar.new).tap do |message|
         
     | 
| 
       47 
     | 
    
         
            -
                      @queue << message
         
     | 
| 
       48 
     | 
    
         
            -
                      send_messages if @queue.size == 1
         
     | 
| 
       49 
     | 
    
         
            -
                    end
         
     | 
| 
       50 
     | 
    
         
            -
                  end
         
     | 
| 
       51 
     | 
    
         
            -
             
     | 
| 
       52 
     | 
    
         
            -
                  class Message < Concurrent::ImmutableStruct.new(:target, :name, :args, :params, :block, :result)
         
     | 
| 
       53 
     | 
    
         
            -
                    def value
         
     | 
| 
       54 
     | 
    
         
            -
                      result.take(Plumbing.config.timeout).tap do |value|
         
     | 
| 
       55 
     | 
    
         
            -
                        raise value if value.is_a? Exception
         
     | 
| 
       56 
     | 
    
         
            -
                      end
         
     | 
| 
       57 
     | 
    
         
            -
                    end
         
     | 
| 
       58 
     | 
    
         
            -
             
     | 
| 
      
 41 
     | 
    
         
            +
                  class Message < Concurrent::ImmutableStruct.new(:target, :message_name, :packed_args, :unsafe_block, :result)
         
     | 
| 
       59 
42 
     | 
    
         
             
                    def call
         
     | 
| 
       60 
     | 
    
         
            -
                       
     | 
| 
      
 43 
     | 
    
         
            +
                      args = Plumbing::Actor.transporter.unmarshal(*packed_args)
         
     | 
| 
      
 44 
     | 
    
         
            +
                      value = target.send message_name, *args, &unsafe_block
         
     | 
| 
      
 45 
     | 
    
         
            +
                      result.put Plumbing::Actor.transporter.marshal(value)
         
     | 
| 
       61 
46 
     | 
    
         
             
                    rescue => ex
         
     | 
| 
       62 
47 
     | 
    
         
             
                      result.put ex
         
     | 
| 
       63 
48 
     | 
    
         
             
                    end
         
     | 
| 
      
 49 
     | 
    
         
            +
             
     | 
| 
      
 50 
     | 
    
         
            +
                    def await
         
     | 
| 
      
 51 
     | 
    
         
            +
                      value = Plumbing::Actor.transporter.unmarshal(*result.take(Plumbing.config.timeout)).first
         
     | 
| 
      
 52 
     | 
    
         
            +
                      raise value if value.is_a? Exception
         
     | 
| 
      
 53 
     | 
    
         
            +
                      value
         
     | 
| 
      
 54 
     | 
    
         
            +
                    end
         
     | 
| 
       64 
55 
     | 
    
         
             
                  end
         
     | 
| 
       65 
56 
     | 
    
         
             
                end
         
     | 
| 
      
 57 
     | 
    
         
            +
             
     | 
| 
      
 58 
     | 
    
         
            +
                def self.transporter
         
     | 
| 
      
 59 
     | 
    
         
            +
                  @transporter ||= Plumbing::Actor::Transporter.new
         
     | 
| 
      
 60 
     | 
    
         
            +
                end
         
     | 
| 
       66 
61 
     | 
    
         
             
              end
         
     | 
| 
       67 
62 
     | 
    
         
             
            end
         
     | 
| 
         @@ -0,0 +1,61 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            require "global_id"
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Plumbing
         
     | 
| 
      
 4 
     | 
    
         
            +
              module Actor
         
     | 
| 
      
 5 
     | 
    
         
            +
                class Transporter
         
     | 
| 
      
 6 
     | 
    
         
            +
                  def marshal *arguments
         
     | 
| 
      
 7 
     | 
    
         
            +
                    pack_array arguments
         
     | 
| 
      
 8 
     | 
    
         
            +
                  end
         
     | 
| 
      
 9 
     | 
    
         
            +
             
     | 
| 
      
 10 
     | 
    
         
            +
                  def unmarshal *arguments
         
     | 
| 
      
 11 
     | 
    
         
            +
                    unpack_array arguments
         
     | 
| 
      
 12 
     | 
    
         
            +
                  end
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
                  private
         
     | 
| 
      
 15 
     | 
    
         
            +
             
     | 
| 
      
 16 
     | 
    
         
            +
                  def pack argument
         
     | 
| 
      
 17 
     | 
    
         
            +
                    case argument
         
     | 
| 
      
 18 
     | 
    
         
            +
                    when GlobalID::Identification then pack_global_id argument
         
     | 
| 
      
 19 
     | 
    
         
            +
                    when Array then pack_array argument
         
     | 
| 
      
 20 
     | 
    
         
            +
                    when Hash then pack_hash argument
         
     | 
| 
      
 21 
     | 
    
         
            +
                    else argument.clone
         
     | 
| 
      
 22 
     | 
    
         
            +
                    end
         
     | 
| 
      
 23 
     | 
    
         
            +
                  end
         
     | 
| 
      
 24 
     | 
    
         
            +
             
     | 
| 
      
 25 
     | 
    
         
            +
                  def pack_array arguments
         
     | 
| 
      
 26 
     | 
    
         
            +
                    arguments.map { |a| pack a }
         
     | 
| 
      
 27 
     | 
    
         
            +
                  end
         
     | 
| 
      
 28 
     | 
    
         
            +
             
     | 
| 
      
 29 
     | 
    
         
            +
                  def pack_hash arguments
         
     | 
| 
      
 30 
     | 
    
         
            +
                    arguments.transform_values { |v| pack v }
         
     | 
| 
      
 31 
     | 
    
         
            +
                  end
         
     | 
| 
      
 32 
     | 
    
         
            +
             
     | 
| 
      
 33 
     | 
    
         
            +
                  def pack_global_id argument
         
     | 
| 
      
 34 
     | 
    
         
            +
                    argument.to_global_id.to_s
         
     | 
| 
      
 35 
     | 
    
         
            +
                  end
         
     | 
| 
      
 36 
     | 
    
         
            +
             
     | 
| 
      
 37 
     | 
    
         
            +
                  def unpack argument
         
     | 
| 
      
 38 
     | 
    
         
            +
                    case argument
         
     | 
| 
      
 39 
     | 
    
         
            +
                    when String then unpack_string argument
         
     | 
| 
      
 40 
     | 
    
         
            +
                    when Array then unpack_array argument
         
     | 
| 
      
 41 
     | 
    
         
            +
                    when Hash then unpack_hash argument
         
     | 
| 
      
 42 
     | 
    
         
            +
                    else argument
         
     | 
| 
      
 43 
     | 
    
         
            +
                    end
         
     | 
| 
      
 44 
     | 
    
         
            +
                  end
         
     | 
| 
      
 45 
     | 
    
         
            +
             
     | 
| 
      
 46 
     | 
    
         
            +
                  def unpack_array arguments
         
     | 
| 
      
 47 
     | 
    
         
            +
                    arguments.map { |a| unpack a }
         
     | 
| 
      
 48 
     | 
    
         
            +
                  end
         
     | 
| 
      
 49 
     | 
    
         
            +
             
     | 
| 
      
 50 
     | 
    
         
            +
                  def unpack_hash arguments
         
     | 
| 
      
 51 
     | 
    
         
            +
                    arguments.to_h do |key, value|
         
     | 
| 
      
 52 
     | 
    
         
            +
                      [key, unpack(value)]
         
     | 
| 
      
 53 
     | 
    
         
            +
                    end
         
     | 
| 
      
 54 
     | 
    
         
            +
                  end
         
     | 
| 
      
 55 
     | 
    
         
            +
             
     | 
| 
      
 56 
     | 
    
         
            +
                  def unpack_string argument
         
     | 
| 
      
 57 
     | 
    
         
            +
                    argument.start_with?("gid://") ? GlobalID::Locator.locate(argument) : argument
         
     | 
| 
      
 58 
     | 
    
         
            +
                  end
         
     | 
| 
      
 59 
     | 
    
         
            +
                end
         
     | 
| 
      
 60 
     | 
    
         
            +
              end
         
     | 
| 
      
 61 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,63 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            require_relative "actor/kernel"
         
     | 
| 
      
 2 
     | 
    
         
            +
            require_relative "actor/inline"
         
     | 
| 
      
 3 
     | 
    
         
            +
             
     | 
| 
      
 4 
     | 
    
         
            +
            module Plumbing
         
     | 
| 
      
 5 
     | 
    
         
            +
              module Actor
         
     | 
| 
      
 6 
     | 
    
         
            +
                def self.included base
         
     | 
| 
      
 7 
     | 
    
         
            +
                  base.extend ClassMethods
         
     | 
| 
      
 8 
     | 
    
         
            +
                end
         
     | 
| 
      
 9 
     | 
    
         
            +
             
     | 
| 
      
 10 
     | 
    
         
            +
                module ClassMethods
         
     | 
| 
      
 11 
     | 
    
         
            +
                  # Create a new actor instance and build a proxy for it using the current mode
         
     | 
| 
      
 12 
     | 
    
         
            +
                  # @return [Object] the proxy for the actor instance
         
     | 
| 
      
 13 
     | 
    
         
            +
                  def start(*, **, &)
         
     | 
| 
      
 14 
     | 
    
         
            +
                    build_proxy_for(new(*, **, &))
         
     | 
| 
      
 15 
     | 
    
         
            +
                  end
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
                  # Define the async messages that this actor can respond to
         
     | 
| 
      
 18 
     | 
    
         
            +
                  # @param names [Array<Symbol>] the names of the async messages
         
     | 
| 
      
 19 
     | 
    
         
            +
                  def async(*names) = async_messages.concat(names.map(&:to_sym))
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
                  # List the async messages that this actor can respond to
         
     | 
| 
      
 22 
     | 
    
         
            +
                  def async_messages = @async_messages ||= []
         
     | 
| 
      
 23 
     | 
    
         
            +
             
     | 
| 
      
 24 
     | 
    
         
            +
                  def inherited subclass
         
     | 
| 
      
 25 
     | 
    
         
            +
                    subclass.async_messages.concat async_messages
         
     | 
| 
      
 26 
     | 
    
         
            +
                  end
         
     | 
| 
      
 27 
     | 
    
         
            +
             
     | 
| 
      
 28 
     | 
    
         
            +
                  private
         
     | 
| 
      
 29 
     | 
    
         
            +
             
     | 
| 
      
 30 
     | 
    
         
            +
                  def build_proxy_for(target)
         
     | 
| 
      
 31 
     | 
    
         
            +
                    proxy_class_for(target.class).new(target)
         
     | 
| 
      
 32 
     | 
    
         
            +
                  end
         
     | 
| 
      
 33 
     | 
    
         
            +
             
     | 
| 
      
 34 
     | 
    
         
            +
                  def proxy_class_for target_class
         
     | 
| 
      
 35 
     | 
    
         
            +
                    Plumbing.config.actor_proxy_class_for(target_class) || register_actor_proxy_class_for(target_class)
         
     | 
| 
      
 36 
     | 
    
         
            +
                  end
         
     | 
| 
      
 37 
     | 
    
         
            +
             
     | 
| 
      
 38 
     | 
    
         
            +
                  def proxy_base_class = const_get PROXY_BASE_CLASSES[Plumbing.config.mode]
         
     | 
| 
      
 39 
     | 
    
         
            +
             
     | 
| 
      
 40 
     | 
    
         
            +
                  PROXY_BASE_CLASSES = {
         
     | 
| 
      
 41 
     | 
    
         
            +
                    inline: "Plumbing::Actor::Inline",
         
     | 
| 
      
 42 
     | 
    
         
            +
                    async: "Plumbing::Actor::Async",
         
     | 
| 
      
 43 
     | 
    
         
            +
                    threaded: "Plumbing::Actor::Threaded",
         
     | 
| 
      
 44 
     | 
    
         
            +
                    rails: "Plumbing::Actor::Rails"
         
     | 
| 
      
 45 
     | 
    
         
            +
                  }.freeze
         
     | 
| 
      
 46 
     | 
    
         
            +
                  private_constant :PROXY_BASE_CLASSES
         
     | 
| 
      
 47 
     | 
    
         
            +
             
     | 
| 
      
 48 
     | 
    
         
            +
                  def register_actor_proxy_class_for target_class
         
     | 
| 
      
 49 
     | 
    
         
            +
                    Plumbing.config.register_actor_proxy_class_for(target_class, build_proxy_class)
         
     | 
| 
      
 50 
     | 
    
         
            +
                  end
         
     | 
| 
      
 51 
     | 
    
         
            +
             
     | 
| 
      
 52 
     | 
    
         
            +
                  def build_proxy_class
         
     | 
| 
      
 53 
     | 
    
         
            +
                    Class.new(proxy_base_class).tap do |proxy_class|
         
     | 
| 
      
 54 
     | 
    
         
            +
                      async_messages.each do |message|
         
     | 
| 
      
 55 
     | 
    
         
            +
                        proxy_class.define_method message do |*args, &block|
         
     | 
| 
      
 56 
     | 
    
         
            +
                          send_message(message, *args, &block)
         
     | 
| 
      
 57 
     | 
    
         
            +
                        end
         
     | 
| 
      
 58 
     | 
    
         
            +
                      end
         
     | 
| 
      
 59 
     | 
    
         
            +
                    end
         
     | 
| 
      
 60 
     | 
    
         
            +
                  end
         
     | 
| 
      
 61 
     | 
    
         
            +
                end
         
     | 
| 
      
 62 
     | 
    
         
            +
              end
         
     | 
| 
      
 63 
     | 
    
         
            +
            end
         
     | 
    
        data/lib/plumbing/config.rb
    CHANGED
    
    | 
         @@ -1,12 +1,12 @@ 
     | 
|
| 
       1 
     | 
    
         
            -
            # Pipes, pipelines,  
     | 
| 
      
 1 
     | 
    
         
            +
            # Pipes, pipelines, actors and rubber ducks
         
     | 
| 
       2 
2 
     | 
    
         
             
            module Plumbing
         
     | 
| 
       3 
     | 
    
         
            -
              Config = Data.define :mode, : 
     | 
| 
       4 
     | 
    
         
            -
                def  
     | 
| 
       5 
     | 
    
         
            -
                   
     | 
| 
      
 3 
     | 
    
         
            +
              Config = Data.define :mode, :actor_proxy_classes, :timeout do
         
     | 
| 
      
 4 
     | 
    
         
            +
                def actor_proxy_class_for target_class
         
     | 
| 
      
 5 
     | 
    
         
            +
                  actor_proxy_classes[target_class]
         
     | 
| 
       6 
6 
     | 
    
         
             
                end
         
     | 
| 
       7 
7 
     | 
    
         | 
| 
       8 
     | 
    
         
            -
                def  
     | 
| 
       9 
     | 
    
         
            -
                   
     | 
| 
      
 8 
     | 
    
         
            +
                def register_actor_proxy_class_for target_class, proxy_class
         
     | 
| 
      
 9 
     | 
    
         
            +
                  actor_proxy_classes[target_class] = proxy_class
         
     | 
| 
       10 
10 
     | 
    
         
             
                end
         
     | 
| 
       11 
11 
     | 
    
         
             
              end
         
     | 
| 
       12 
12 
     | 
    
         
             
              private_constant :Config
         
     | 
| 
         @@ -23,7 +23,7 @@ module Plumbing 
     | 
|
| 
       23 
23 
     | 
    
         
             
              # @option timeout [Integer] the timeout (in seconds) to use (30s is the default)
         
     | 
| 
       24 
24 
     | 
    
         
             
              # @yield optional block - after the block has completed its execution, the configuration is restored to its previous state (useful for test suites)
         
     | 
| 
       25 
25 
     | 
    
         
             
              def self.configure(**params, &block)
         
     | 
| 
       26 
     | 
    
         
            -
                new_config = Config.new(**config.to_h.merge(params).merge( 
     | 
| 
      
 26 
     | 
    
         
            +
                new_config = Config.new(**config.to_h.merge(params).merge(actor_proxy_classes: {}))
         
     | 
| 
       27 
27 
     | 
    
         
             
                if block.nil?
         
     | 
| 
       28 
28 
     | 
    
         
             
                  set_configuration_to new_config
         
     | 
| 
       29 
29 
     | 
    
         
             
                else
         
     | 
| 
         @@ -45,7 +45,7 @@ module Plumbing 
     | 
|
| 
       45 
45 
     | 
    
         
             
              private_class_method :set_configuration_and_yield
         
     | 
| 
       46 
46 
     | 
    
         | 
| 
       47 
47 
     | 
    
         
             
              def self.configs
         
     | 
| 
       48 
     | 
    
         
            -
                @configs ||= [Config.new(mode: :inline, timeout: 30,  
     | 
| 
      
 48 
     | 
    
         
            +
                @configs ||= [Config.new(mode: :inline, timeout: 30, actor_proxy_classes: {})]
         
     | 
| 
       49 
49 
     | 
    
         
             
              end
         
     | 
| 
       50 
50 
     | 
    
         
             
              private_class_method :configs
         
     | 
| 
       51 
51 
     | 
    
         
             
            end
         
     | 
    
        data/lib/plumbing/pipe.rb
    CHANGED
    
    | 
         @@ -1,10 +1,9 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            module Plumbing
         
     | 
| 
       2 
2 
     | 
    
         
             
              # A basic pipe
         
     | 
| 
       3 
3 
     | 
    
         
             
              class Pipe
         
     | 
| 
       4 
     | 
    
         
            -
                include Plumbing:: 
     | 
| 
      
 4 
     | 
    
         
            +
                include Plumbing::Actor
         
     | 
| 
       5 
5 
     | 
    
         | 
| 
       6 
     | 
    
         
            -
                 
     | 
| 
       7 
     | 
    
         
            -
                query :add_observer, :is_observer?
         
     | 
| 
      
 6 
     | 
    
         
            +
                async :notify, :<<, :remove_observer, :add_observer, :is_observer?, :shutdown
         
     | 
| 
       8 
7 
     | 
    
         | 
| 
       9 
8 
     | 
    
         
             
                # Push an event into the pipe
         
     | 
| 
       10 
9 
     | 
    
         
             
                # @param event [Plumbing::Event] the event to push into the pipe
         
     | 
    
        data/lib/plumbing/version.rb
    CHANGED
    
    
    
        data/lib/plumbing.rb
    CHANGED