# Promises

A Promise-like implementation for BrightScript/Roku. This is the core functionality for BrighterScript's async/await functionality. Not to be confused with [roku-promise](https://github.com/rokucommunity/roku-promise).

[![build status](https://img.shields.io/github/actions/workflow/status/rokucommunity/promises/build.yml?branch=master&logo=github)](https://github.com/rokucommunity/promises/actions?query=branch%3Amaster+workflow%3Abuild)
[![security](https://img.shields.io/github/actions/workflow/status/rokucommunity/promises/security-audit.yml?branch=master&label=security&logo=data:image/svg%2Bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PHJlY3QgeD0iMyIgeT0iOCIgd2lkdGg9IjEwIiBoZWlnaHQ9IjciIHJ4PSIxIiBmaWxsPSJ3aGl0ZSIvPjxwYXRoIGQ9Ik01IDhWNWEzIDMgMCAwIDEgNiAwdjMiIGZpbGw9Im5vbmUiIHN0cm9rZT0id2hpdGUiIHN0cm9rZS13aWR0aD0iMiIvPjwvc3ZnPg==)](https://github.com/rokucommunity/promises/actions/workflows/security-audit.yml)
[![monthly downloads](https://img.shields.io/npm/dm/@rokucommunity/promises.svg?sanitize=true&logo=npm&logoColor=&label=npm)](https://npmcharts.com/compare/@rokucommunity/promises?minimal=true)
[![npm version](https://img.shields.io/npm/v/@rokucommunity/promises.svg?logo=npm&label=npm)](https://www.npmjs.com/package/@rokucommunity/promises)
[![license](https://img.shields.io/github/license/rokucommunity/promises.svg)](LICENSE)
[![Slack](https://img.shields.io/badge/Slack-RokuCommunity-4A154B?logo=slack)](https://join.slack.com/t/rokudevelopers/shared_invite/zt-4vw7rg6v-NH46oY7hTktpRIBM_zGvwA)

> [!CAUTION]
> The behavior of `.finally()` and `.onFinally()` has changed as of `v0.6.0`. When upgrading from prior versions please be aware that `finally` will no longer suppress rejections in your promise flows. See [#29](https://github.com/rokucommunity/promises/pull/29) for details on the fix.

## Installation
### ropm
The preferred installation method is via [ropm](https://www.npmjs.com/package/ropm)
```bash
npx ropm install promises@npm:@rokucommunity/promises
```

**NOTE:** if your project lives in a subdirectory, make sure you've configured your ropm `rootDir` folder properly. ([instructions](https://github.com/rokucommunity/ropm#rootdir))

### Manual install
1. Download the latest `promises.zip` release from [releases](https://github.com/rokucommunity/promises/releases) and extract the zip.
2. Copy the files into your `pkg:/source` and `pkg:/components` folders. Your project structure should look something like this if you've done it correctly:

    ```graphql
    pkg:/
    ├─ components/
    | ├─ Promise.xml <-- new file
    │ └─ MainScene.xml
    ├─ source/
    | ├─ Promises.bs <-- new file
    │ └─ main.brs
    └─ manifest
    ```

## Demos
You can check out a few demos in the [demos/](./demos) folder to see some good examples of how to use this library in practice.

## Anatomy of the `Promise` node
The heart of this library is the `Promise` SGNode type. Here's its contents:

```xml
<component name="Promise" extends="Node">
    <interface>
        <field id="promiseState" type="string" value="pending" alwaysNotify="true" />
    </interface>
</component>
```

`promiseState` represents the current status of the promise. Promises can have one of three states:
- `"resolved"` - the operation this promise represents has completed successfully. Often times a resolved promise will contain data.
- `"rejected"` - the asynchronous operation this promise represents has completed unsuccessfully. Often times this promise will include an error explaining what caused the rejection.
- `"pending"` - the promise has not yet been completed (i.e. the promise is _not_ resolved and _not_ rejected).

`promiseResult` is the "value" of the promise when resolved, or an [error](https://developer.roku.com/docs/references/brightscript/language/error-handling.md#the-exception-object) when rejected.

You'll notice there is no `<field id="promiseResult">` defined on the `Promise` node above. That's because, in order to support all possible return types, we cannot define the `promiseResult` field ahead of time because the BrightScript runtime will throw type mismatch errors when using a different field type than defined. The internal promise logic will automatically add the field when the promise is resolved or rejected.

If you're creating promises without using this library, you can resolve or reject a promise with the following logic. Be sure to set `promiseState` last to ensure that `promiseResult` is avaiable when the observers of `promiseState` are notified.

```brightscript
sub setPromiseResolved(promise, result)
    promise.update({ promiseResult: result }, true)
    promise.promiseState = "resolved"
end sub

sub setPromiseRejected(promise, error)
    promise.update({ promiseResult: error }, true)
    promise.promiseState = "rejected"
end sub
```
### Similarities to JavaScript promises

Much of this design is based on JavaScript [Promises](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise). However, there are some differences.

1. BrightScript does not have closures, so we couldn't implement the standard `then` function on the `Promise` SGNode because it would strip out the callback function and lose all context.
2. Our promises are also [deferred](https://dev.to/webduvet/deferred-promise-pattern-2j59) objects. Due to the nature of scenegraph nodes, we have no way of separating the promise instance from its resolution. In practice this isn't a big deal, but just keep in mind, there's no way to prevent a consumer of your promise instance from resolving it themselves, even though they shouldn't do that.

### Cross-library compatibility
This design has been written up as a specification. That meaning, it shouldn't matter which library creates the promise. Your own application code could write custom logic to make your own promises, and they should be interoperable with any other. The core way that promises are interoperable is that they have a field called `promiseState` for checking its state, and then getting the result from `promiseResult`.

## Differences from [roku-promise](https://github.com/rokucommunity/roku-promise)
[roku-promise](https://github.com/rokucommunity/roku-promise) is a popular promise-like library that was created by [@briandunnington](https://github.com/briandunnington) back in 2018. roku-promise creates tasks for you, executes the work, then returns some type of response to your code in the form of a callback.

The big difference is, @rokucommunity/promises does not manage tasks at all. The puropose of a promise is to create an object that represents the future completion of an asynchronous operation. It's not supposed to initiate or execute that operation, just represent its status.

So by using @rokucommunity/promises, you'll need to create `Task` nodes yourself, create the promises yourself (using our helper library), then mark the promise as "completed" when the task has finished its work.

## Usage
Typically you'll be creating promises from inside [Task](https://developer.roku.com/docs/references/scenegraph/control-nodes/task.md) nodes. Then, you'll return those promises immediately, but keep them around for when you are finished with the async task.

Here's a small example
To create a promise:
```brighterscript
promise = promises.create()
```

### No closures (but close enough)
The BrightScript runtime has no support for closures. However, we've found a creative way to pass state throughout an async flow to emulate most of the benefits of closures. Most of the `promises` observer functions accept an optional parameter, called `context`. This is an AA that is passed into every callback function. Here's the signature of `promises.onThen()`:

```brighterscript
function onThen(promise as dynamic, callback as function, context = "__INVALID__" as object) as dynamic
```

Consider this example:

```brighterscript
function logIn()
    context = {
        username: getUsernameFromRegistry(),
        authToken: invalid
    }
    ' assume this function returns a promise
    promise = getAuthTokenFromServer()
    promises.onThen(promise, function(response, context)
        context.authToken = response.authToken
        print context.username, context.authToken
    end function, context)
end function
```

Notice how the context is made avaiable inside your callback? Under the hood, we store the promise, the callback, and context all in a secret `m` variable, so they never pass through any node boundaries. That means you can store literally any variable you want on there, without worrying about the data getting stripped away by SceneGraph's data sanitization process. (don't worry, we clean all that stuff up when the promise resolves so there's no memory leaks)

### Chaining
Building on the previous example, there are situations where you may want to run several async operations in a row, waiting for each to complete before moving on to the next. That's where `promises.chain()` comes in. It handles chaining multiple async operations, and handling errors in the flow as well.

Here's the flow, written out in words:

- (async) fetch the username from the registry
- (async) fetch an auth token from the server using the username
- (async) fetch the user's profileImageUrl using the authToken
- we have all the user data. set it on scene and move on
- if anything fails in this flow, print an error message

 Here's an example of how you can do that:

```brighterscript
function logIn()
    context = {
        username: invalid,
        authToken: invalid,
        profileImageUrl: invalid
    }
    ' assume this function returns a promise
    usernamePromise = getUsernameFromRegistryAsync()
    promises.chain(usernamePromise, context).then(function(response, context)
        context.username = response.username
        'return a promise that forces the next callback to wait for it
        return getAuthToken(context.username)

    end function).then(function(response, context)
        context.authToken = response.authToken
        return getProfileImageUrl(context.authToken)

    end function).then(function(response, context)
        context.profileImageUrl = response.profileImageUrl

        'yay, we signed in. Set the user data on our scene so we can start watching stuff!
        m.top.userData = context

        'this catch function is called if any runtime exception or promise rejection happened during the async flows above
    end function).catch(function(error, context)
        print "Something went wrong logging the user in", error, context
    end function)
end function
```
### Parallel promises
Sometimes you want to run multiple network requests at the same time. You can use `promises.all()` for that. Here's a quick example

```brighterscript
function loadProfilePage(authToken as string)
    promise = promises.all([
        getProfileImageUrl(authToken),
        getUserData(authToken),
        getUpgradeOptions(authToken)
    ])
    promises.chain(promise).then(function(results)
        print results[0] ' profileImageUrl result
        print results[1] ' userData result
        print results[2] ' upgradeOptions result
    end function)
end function

```

## How it works
While the promise spec is interoperable with any other promise node created by other libraries, the `promises` namespace is the true magic of the @rokucommunity/promises library. We have several helper functions that enable you to chain multiple promises together, very much in the same way as javascript promises.

---

## Promises in Tasks

> Promises work great inside a [Task](https://developer.roku.com/docs/references/scenegraph/control-nodes/task.md) node too. With a small amount of setup using `promises.setMessagePort()` and `promises.wait2()`, you can write sequential async logic without restructuring your code around observers.

### Setup

In your task's run function, create an `roMessagePort`, register it with the promises library, then run your event loop using `promises.wait2()` instead of the native `wait()`:

```brighterscript
sub runTask()
    m.port = createObject("roMessagePort")
    promises.setMessagePort(m.port)

    ' ... your promise chains here ...

    while true
        message = promises.wait2(0, m.port)
        ' handle non-promise messages (e.g. field observations) here
    end while
end sub
```

`promises.wait2(timeoutMs, port)` works like the native `wait()` but automatically processes any promise events that arrive on the promises port. Pass `0` for an indefinite wait, or a timeout in milliseconds to bound the wait.

### How it works

On the render thread, promise resolution is driven by field observers — when a promise node's `promiseResult` field changes, observers fire and `.then()` callbacks run. Tasks don't have that mechanism by default.

`promises.setMessagePort()` tells the library to route those field-change notifications to your task's `roMessagePort` instead. `promises.wait2()` then drains that port on each iteration, processing any pending promise events before returning control to your loop. The net effect is the same observer-driven resolution, just happening inside your task's event loop rather than on the render thread.

This is why the event loop is required. Without it, promise callbacks would never fire.

### Sequential async steps

Return a promise from a `.then()` callback to wait for it before the next step runs:

```brighterscript
sub runTask()
    m.port = createObject("roMessagePort")
    promises.setMessagePort(m.port)
    m.top.observeField("request", m.port)

    promises.chain(promises.resolve(true))
        .then(function(response as object) as dynamic
            return doNetworkRequest("step1")
        end function)
        .then(function(step1Result as object) as dynamic
            return doNetworkRequest("step2")
        end function)
        .then(sub(step2Result as object)
            promises.resolve({ step1: "first", step2: "second" }, m.top.promise)
        end sub)
        .finally(sub()
            m.top.control = "STOP"
        end sub)

    responses = { "step1": "first", "step2": "second" }
    while true
        message = promises.wait2(0, m.port)
        if type(message) = "roSGNodeEvent" then
            requestId = message.getData()
            promises.resolve(responses[requestId], m.requestStorage[requestId].promise)
        end if
    end while
end sub
```

### Parallel requests in a task

Use `promises.all()` to wait for multiple concurrent requests:

```brighterscript
sub runTask()
    m.port = createObject("roMessagePort")
    promises.setMessagePort(m.port)
    m.top.observeField("request", m.port)

    p1 = doNetworkRequest("req-a")
    p2 = doNetworkRequest("req-b")
    p3 = doNetworkRequest("req-c")

    promises.chain(promises.all([p1, p2, p3]))
        .then(sub(results as object)
            promises.resolve(results, m.top.promise)
        end sub)
        .finally(sub()
            m.top.control = "STOP"
        end sub)

    responses = { "req-a": "a", "req-b": "b", "req-c": "c" }
    while true
        message = promises.wait2(0, m.port)
        if type(message) = "roSGNodeEvent" then
            requestId = message.getData()
            promises.resolve(responses[requestId], m.requestStorage[requestId].promise)
        end if
    end while
end sub
```

### Error handling in tasks

`.catch()` works the same as on the render thread. Return a resolved promise from `.catch()` to recover:

```brighterscript
promises.chain(promises.reject("boom"))
    .catch(function(error as object) as dynamic
        return promises.resolve("fallback")
    end function)
    .then(sub(result as object)
        promises.resolve(result, m.top.promise)
    end sub)
    .finally(sub()
        m.top.control = "STOP"
    end sub)
```

### Using a separate message port

You can also pass a *different* port than the one registered with `promises.setMessagePort()`. In that case, `wait2` waits on the port you provide, but still drains the promises port on each iteration — so promise callbacks continue to fire even if your task is listening elsewhere.

This is useful when your task needs to observe fields or other message sources on a dedicated port while still running promise chains:

```brighterscript
sub runTask()
    m.promisesPort = createObject("roMessagePort")
    promises.setMessagePort(m.promisesPort)

    m.taskPort = createObject("roMessagePort")
    m.top.observeField("someInput", m.taskPort)

    while true
        ' waits on taskPort, but also drains promisesPort each iteration
        message = promises.wait2(0, m.taskPort)
        if type(message) = "roSGNodeEvent" then
            ' handle field observations here
        end if
    end while
end sub
```

### peekMessage and getMessage

`promises.peekMessage(port)` and `promises.getMessage(port)` are non-blocking alternatives to `promises.wait2()`. They work like the native `port.peekMessage()` / `port.getMessage()`, but automatically consume and process any promise events at the front of the given port's queue before returning.

Unlike `wait2`, these functions only drain the port you pass — they do **not** also drain the registered promises port if it's different. Use them when you want to poll for messages without blocking, or when you're already looping with `wait2` and need to inspect the queue:

```brighterscript
sub runTask()
    m.port = createObject("roMessagePort")
    promises.setMessagePort(m.port)

    while true
        ' non-blocking check — returns invalid immediately if nothing is pending
        message = promises.getMessage(m.port)
        if message <> invalid
            ' handle message
        end if

        ' do other work here between checks
    end while
end sub
```

If you are using a separate port for non-promise messages, pass that port to `peekMessage`/`getMessage` directly — but be aware that promise events on the promises port will **not** be processed as a side effect. In that case, prefer `wait2` to keep both ports drained.

```brighterscript
sub runTask()
    m.promisesPort = createObject("roMessagePort")
    promises.setMessagePort(m.promisesPort)

    m.taskPort = createObject("roMessagePort")
    m.top.observeField("someInput", m.taskPort)

    while true
        ' drains both ports
        message = promises.wait2(0, m.taskPort)

        ' peek at taskPort only — promisesPort is NOT drained here
        pending = promises.peekMessage(m.taskPort)
    end while
end sub
```

## Accepted security advisories

Dependencies flagged by `npm audit` that we have reviewed and chosen not to upgrade are tracked in [audit-ci.jsonc](https://github.com/RokuCommunity/promises/blob/master/audit-ci.jsonc). Each entry includes the advisory ID, the date it was added, and the reason it does not apply to this project.
