V8 Promise

V8 promise is an advanced topic because it's usually hard to be mastered due to its multi-threaded nature. Javet enables applications to play with V8 promise in a decent way.

Promise and Resolver

Resolver is a new concept to some JavaScript developers. In fact, it is already an old friend. Inside new Promise((resolve, reject) => {});, (resolve, reject) is called resolver in V8. Javet exposes the V8 promise and resolver via the same interface IV8ValuePromise because in V8 they really are the same. So, they both share the same set of API. But the ownership of the API makes the difference as the following chart shows.

V8 Promise and Resolver

Lifecycle

The lifecycle is as the following chart shows.

  1. JavaScript application calls an API for certain resource. E.g. readFileAsync.

  2. Java application receives a callback from V8 for the resource.

  3. Java application creates a V8 promise resolver and holds the resolver.

  4. Java application gets a V8 promise from the resolver and returns that V8 promise as callback return.

  5. JavaScript application gets that promise and binds the .then() and .catch().

  6. Java application fetches the resource and calls the resolver via .resolve().

  7. JavaScript application receives the resource in .then() and processes the result.

V8 Promise Lifecycle

Register a Callback

V8ValuePromise accepts a IV8ValuePromise.ICallback to receive the callback from the V8 when the promise is resolved or rejected. The caller is supposed to call the register with a subclass of IV8ValuePromise.ICallback.

IV8ValuePromise.ICallback callback = new IV8ValuePromise.ICallback() {
    @Override
    public void onCatch(V8Value v8Value) {
        assertTrue(v8Value instanceof V8ValueError);
        // Handle the error.
    }

    @Override
    public void onFulfilled(V8Value v8Value) {
        // Handle the fulfillment.
    }

    @Override
    public void onRejected(V8Value v8Value) {
        // Handle the rejection.
    }
};

try (V8ValuePromise v8ValuePromise = v8Runtime.getExecutor(
        "new Promise((resolve, reject) => { /* Do whatever you want. */ })").execute()) {
    v8ValuePromise.register(callback);
    v8Runtime.await();
    // The callback happens.
} finally {
    v8Runtime.lowMemoryNotification();
}

Example fs.readFileAsync()

Requirements: Create a JavaScript API fs.readFileAsync() for reading a file in async manner.

The pseudo code is as following.

// Java application injects an interceptor as 'fs'.
v8Runtime.getGlobalObject().set("fs", fs);
// JavaScript application calls 'readFileAsync()' and registers 'then()'
fs.readFileAsync('a.log').then(fileContent => console.log(fileContent));
// Java application creates a resolver, pushes the resolver to task queue, returns a promise from the resolver.
@V8Function
public V8ValuePromise readFileAsync(String filePath) throws JavetException {
    V8ValuePromise v8ValuePromiseResolver = v8Runtime.createV8ValuePromise();
    queue.add(new Task(v8ValuePromiseResolver, filePath, timeout));
    return v8ValuePromiseResolver.getPromise();
}
// Java application fetches the file content and resolve/reject the promise in a background thread.
String fileContent = getFileContent(task.getFilePath());
try (V8ValuePromise promise = task.getPromise()) {
    if (fileContent == null) {
        promise.reject(v8Runtime.createV8ValueUndefined());
    } else {
        promise.resolve(fileContent);
    }
}
// JavaScript application prints the file content in console afterwards.

Note

  • Java application needs to have background thread(s) process async calls from V8.

  • Node.js mode has its own event loop. So, sometimes, Java application has to call await() after resolve() or reject().

  • Please refer to project Javenode for details.

Unhandled Rejection

Sometimes Java application breaks when unhandled rejection is raised.

In V8 mode, V8Runtime.setPromiseRejectCallback() allows Java application to register a callback implementing IJavetPromiseRejectCallback.

In Node.js mode, event unhandledRejection is recommended to be listened.

import process from 'process';

process.on('unhandledRejection', (reason, promise) => {
    console.log('Unhandled Rejection at:', promise, 'reason:', reason);
    // Application specific logging, throwing an error, or other logic here
});

Be careful, the V8Runtime.setPromiseRejectCallback() in V8 mode also works in Node.js mode and it can disable the built-in Node.js event unhandledRejection. Sometimes, this is a handy feature.

Please review the test cases for more detail.