Skip to content

namehillsoftware/handoff

Repository files navigation

Build Status Coverage Status Download

Handoff, an A+ like Promise Implementation for Java

Logo

Handoff is a simple, A+ like Promises implementation for Java. It allows easy control flow to be written for asynchronous processes in Java:

public Promise<PlaybackFile> promiseFirstFile() {
    return new Promise<>(messenger -> {
        final IPositionedFileQueueProvider queueProvider = positionedFileQueueProviders.get(nowPlaying.isRepeating);
        try {
            final PreparedPlayableFileQueue preparedPlaybackQueue = preparedPlaybackQueueResourceManagement.initializePreparedPlaybackQueue(queueProvider.provideQueue(playlist, playlistPosition));
            startPlayback(preparedPlaybackQueue, filePosition)
                .firstElement() // Easily move from one asynchronous library (RxJava) to Handoff
                .subscribe(
                    playbackFile -> messenger.sendResolution(playbackFile.asPositionedFile()), // Resolve
                    messenger::sendRejection); // Reject
        } catch (Exception e) {
            messenger.sendRejection(e);
        }
    });
}

And the promise can then be chained as expected. The method then is used to continue execution immediately using the promised result:

playlist.promiseFirstFile()
    .then(f -> { // Perform another action immediately with the result - this continues on the same thread the result was returned on
        // perform action
        return f; // return a new type if wanted, return null to represent Void
    });

For instances where another promise is needed, the method eventually should be used, and excuse should be used for catching errors, which will fall through if not caught earlier in the method chain.

playlist.promiseFirstFile()
    .eventually(f -> { // Handoff the result to a method that is expected to produce a new promise
        return new Promise<>(m -> {

        });
    })
    .excuse(e -> { // Do something with an error, errors fall through from the top, like with try/catch
        return e;
    });

Installation

Handoff can be installed via Gradle:

dependencies {
    implementation 'com.namehillsoftware:handoff:0.12.0'
}

Usage

Promise Creation

Handoff makes it easy to make any asynchronous process a promise. Take for example an asynchronous OKHTTP3 call. The Promise class can be extended to wrap the OKHTTP3 callback interface:

class PromisedResponse extends Promise<String> implements Callback {

    @Override
    public void onFailure(Request request, IOException e) {
        reject(e);
    }

    @Override
    public void onResponse(Response response) throws IOException {
        resolve(response.body().string());
    }
}

Or the overloaded constructor that passes a messenger in can be used:

new Promise<>(m -> {
    okHttpClient.newCall(request).enqueue(new Callback() {
        @Override
        public void onFailure(Request request, IOException e) {
            m.sendRejection(e);
        }

        @Override
        public void onResponse(Response response) throws IOException {
            m.sendResolution(response.body().string());
        }
    });
});

When the resolution is already known, an overloaded Promise constructor is available: new Promise<>(data);. Anything that inherits from Throwable will become a rejection: new Promise<>(error). A null result can be returned using Promise.empty().

Promises can also be combined using Promise.whenAll(Promise<Resolution> promises...) which will resolve when all promises complete, and Promise.whenAny(Promise<Resolution> promises...) which will resolve when the first promise completes.

Continuations

Continuations are where Promises really shine. Promise-style continuations allow asynchronous control flow to be much more imperative in nature. In order to continue execution on the same thread as the promise was delivered on, use the then convention:

playlist.promiseFirstFile()
    .then(f -> { // Perform another action immediately with the result - this continues on the same thread the result was returned on
        // perform action
        return f; // return a new type if wanted, return null to represent Void
    });

If it is needed for a function to execute if the promise is resolved or rejected, the overloaded method of then can be used:

playlist.promiseFirstFile()
    .then(
        f -> { // Perform another action immediately with the result - this continues on the same thread the result was returned on
        // perform action
        return null; // return null to represent Void
    },
    error -> {
        Logger.error("An error occured!", error); // Log some error, continue on as normal
        return null;
    });

Handoff also nicely supports handing off to another promise. Due to type erasure in Java, an overloaded then method could not be used, as is done in other languages. Instead, use the eventually method, signifying the continuation won't immediately complete:

playlist.promiseFirstFile()
    .eventually(f -> { // Handoff the result to a method that is expected to produce a new promise
        return new Promise<>(m -> {

        });
    })

eventually supports the overloaded error method:

playlist.promiseFirstFile()
    .eventually(
        f -> { // Handoff the result to a method that is expected to produce a new promise
        return new Promise<>(m -> {

        });
    },
    e -> {
        Logger.error("An error occured!", error); // Log some error, continue on as normal
        return Promise.empty();
    })

Errors

Errors fall through like they would in a try/catch in synchronous, traditional Java. Errors are not strongly typed:

playlist.promiseFirstFile()
    .then(f -> { // Perform another action immediately with the result - this continues on the same thread the result was returned on
        // perform action
        throw new IOException("Uh oh!"); // return null to represent Void
    })
    .then(o -> {
        // Code here won't be executed
    })
    .excuse(error -> {
        Logger.error("An error occured!", error); // Log some error, continue on as normal

        if (error instanceof IOException)
        Logger.error("It was an IO Error too!");

        return null;
    });

Once trapped in a method chain, that error will go away within that method chain.

Errors can also be handled eventually:

playlist.promiseFirstFile()
    .then(f -> { // Perform another action immediately with the result - this continues on the same thread the result was returned on
        // perform action
        throw new IOException("Uh oh!"); // return null to represent Void
    })
    .then(o -> {
        // Code here won't be executed
    })
    .excuseEventually(error -> {
        Logger.error("An error occured!", error); // Log some error, continue on as normal

        if (error instanceof IOException)
        Logger.error("It was an IO Error too!");

        return Promise.empty();
    });

Unhandled rejections

Unhandled rejections can sometimes be useful to know about. These can occur in situations like this:

playlist.promiseFirstFile()
    .then(f -> { // Perform another action immediately with the result - this continues on the same thread the result was returned on
        // perform action
        throw new IOException("Uh oh!"); // return null to represent Void
    }); // Nothing is done with the exception

To handle unhandled rejections, set a listener on Promise.Rejections.setUnhandledRejectionsReceiver(...).

Guaranteed Execution

Like the finally block, Handoff has control blocks which always guarantee execution. For a continuation that must happen immediately, continue using must:

final InputStream is = body.byteStream(); 
playlist.promiseFirstFile()
    .then(f -> { // Perform another action immediately with the result - this continues on the same thread the result was returned on
        // perform action
        throw new IOException("Uh oh!"); // return null to represent Void
    })
    .then(o -> {
        // Code here won't be executed
    })
    .must(() -> is.close()) // In spite of the error, this control block will execute
    .excuse(error -> {
        Logger.error("An error occured!", error); // Log some error, continue on as normal

        if (error instanceof IOException)
        Logger.error("It was an IO Error too!");

        return null;
    });

For a continuation that must happen eventually, use inevitably:

final PromisedStream is = new PromisedStream<>(body.byteStream()); // theoretical "Promised stream" which closes asynchronously
playlist.promiseFirstFile()
    .then(f -> { // Perform another action immediately with the result - this continues on the same thread the result was returned on
        // perform action
        throw new IOException("Uh oh!"); // return null to represent Void
    })
    .then(o -> {
        // Code here won't be executed
    })
    .inevitably(() -> is.promiseClose())) // In spite of the error, this control block will execute
    .excuse(error -> {
        Logger.error("An error occured!", error); // Log some error, continue on as normal

        if (error instanceof IOException)
        Logger.error("It was an IO Error too!");

        return null;
    });

Cancellation

All promises implement Cancellable. When cancel() is called on a promise, it sends a cancellation message to the CancellationResponse passed into awaitCancellation(...). The CancellationResponse can be changed anytime before the promise is resolved or rejected.

// Implement a CancellationResponse as well, and assign it in the constructor.
class PromisedResponse extends Promise<String> implements Callback, CancellationResponse {
    
    private final Call call;
    
    public PromisedResponse(Call call) {
        this.call = call;
        awaitCancellation(this);
        call.enqueue(this);
    }

    @Override
    public void onFailure(Request request, IOException e) {
        reject(e);
    }

    @Override
    public void onResponse(Response response) throws IOException {
        resolve(response.body().string());
    }
    
    @Override
    public void cancellationRequested() {
        call.cancel();
    }
}