Skip to content

lgierth/promise.rb

Repository files navigation

promise.rb Build Status Code Climate Coverage Status

Ruby implementation of the Promises/A+ spec. 100% mutation coverage, tested on MRI 1.9, 2.0, 2.1, 2.2, Rubinius, and JRuby.

Similar projects:

Note that promise.rb is probably not thread safe.

Installation

Add this line to your application's Gemfile:

gem 'promise.rb'

And then execute:

$ bundle

Or install it yourself as:

$ gem install promise.rb

Usage

This guide assumes that you are familiar with the Promises/A+ spec. It's a quick read, though.

promise.rb comes with a very primitive way of scheduling callback dispatch. It immediately executes the callback, instead of scheduling it for execution after Promise#fulfill or Promise#reject, as demanded by the spec:

onFulfilled or onRejected must not be called until the execution context stack contains only platform code.

Compliance can be achieved, for example, by running an event reactor like EventMachine:

require 'promise'
require 'eventmachine'

class MyPromise < Promise
  def defer
    EM.next_tick { yield }
  end
end

Now you can create MyPromise objects, and fulfill (or reject) them, as well as add callbacks to them:

def nonblocking_stuff
  promise = MyPromise.new
  EM.next_tick { promise.fulfill('value') }
  promise
end

EM.run do
  nonblocking_stuff.then { |value| p value }
  nonblocking_stuff.then(proc { |value| p value })
end

Rejection works similarly:

def failing_stuff
  promise = MyPromise.new
  EM.next_tick { promise.reject('reason') }
  promise
end

EM.run do
  failing_stuff.then(proc { |value| }, proc { |reason| p reason })
end

Waiting for fulfillment/rejection

promise.rb also comes with the utility method Promise#sync, which waits for the promise to be fulfilled and returns the value, or for it to be rejected and re-raises the reason. Using #sync requires you to implement #wait. You could for example cooperatively schedule fibers waiting for different promises:

require 'fiber'
require 'promise'
require 'eventmachine'

class MyPromise < Promise
  def defer
    EM.next_tick { yield }
  end

  def wait
    fiber = Fiber.current
    resume = proc do |arg|
      defer { fiber.resume(arg) }
    end

    self.then(resume, resume)
    Fiber.yield
  end
end

EM.run do
  promise = MyPromise.new
  Fiber.new { p promise.sync }.resume
  promise.fulfill
end

Or have the rejection reason re-raised from #sync:

EM.run do
  promise = MyPromise.new

  Fiber.new do
    begin
      promise.sync
    rescue
      p $!
    end
  end.resume

  promise.reject('reason')
end

Chaining promises

As per the A+ spec, every call to #then returns a new promise, which assumes the first promise's state. That means it passes its #fulfill and #reject methods to first promise's #then, short-circuiting the two promises. In case a callback returns a promise, it'll instead assume that promise's state.

Imagine the #fulfill and #reject calls in the following example happening somewhere in a background Fiber or so.

require 'promise'

Promise.new
  .tap(&:fulfill)
  .then { Promise.new.tap(&:fulfill) }
  .then { Promise.new.tap(&:reject) }
  .then(nil, proc { |reason| p reason })

In order to use the result of multiple promises, they can be grouped using Promise.all for chaining.

sum_promise = Promise.all([promise1, promise2]).then do |value1, value2|
  value1 + value2
end

Progress callbacks

Very simple progress callbacks, as per Promises/A, are supported as well. They have been dropped in A+, but I found them to be a useful mechanism - if kept simple. Callback dispatch happens immediately in the call to #progress, in the order of definition via #on_progress. Also note that #on_progress does not return a new promise for chaining - the progress mechanism is meant to be very lightweight, and ignores many of the constraints and guarantees of then.

promise = MyPromise.new
promise.on_progress { |status| p status }
promise.progress(:anything)

Unlicense

promise.rb is free and unencumbered public domain software. For more information, see unlicense.org or the accompanying UNLICENSE file.

Contributing

  1. Fork it
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create new Pull Request