SPAC: Promise-Based Actions
Tags: javascript, framework
Published: 2020-12-21 06:36:27

_SPAC is a custom JavaScript framework for client-side, single-page web applications. It stands for "Stateful Pages, Actions and Components". Its design goal is to provide robust and simple entities that help you to structure apps. Pages and components provide the HTML, JavaScript functions and UI interactions. Actions govern external API calls. You define these entities in plain JavaScript, load up the central controller, and your app is ready to be served. Read the development journey of SPAC in my series: <https://admantium.com/category/spac-framework/>._

When implementing your single-page-apps, state and UI interactions happen inside the browser. But how do you organize complex functions that will search data, connect to external APIs and other similar tasks? You can ship them with the same code to the browser, and execute them in the browser. But you will reach the point where data complexity or functional costs become too high, and you need to implement these features in a backend. In SPAC, all complex functions and API interaction are handled by Action entities. Actions encapsulate pure functions through a standardized interface. When called, a Promise object is returned that can be consumed and further processed by the caller.

_This article originally appeared at [my blog](https://admantium.com/blog/spac07_actions)_.

## Action Interface

Action entities in SPAC provide these methods:

- `action()`: The self-contained, pure function that is called in this action.
- `run()`: The method to trigger an action instance, which receives arguments and passes them to the action

## Action Definition

Actions are defined be creating a class with the parent `Action`. In its body, it just defines the `action` method.

Actions should be pure functions and loosely coupled, they should not use any externals data references, but should receive the data they operate on.

Here is an example for an action that will search for a keyword inside an object - the class is defined in Line 2, the function in Line 3.

```js
import { Action } from 'spac'

export default class SearchApiAction extends Action {
  action (keyword, inventory) {
    var res = []
    for (let [name, definition] of Object.entries(inventory)) {
      const occurences = JSON.stringify(definition).match(
        new RegExp(keyword, 'ig')
      )
      const score = (occurences && occurences.length) || 0
      res.push({ name, score, definition })
    }
    return res.sort((a, b) => b.score - a.score)
  }
}
```

## Running an Action

The `run()` method executes the defined action. It will return a new `Promise` object (Line 5), in which the defined action will be tried to be `resolved` (Line 7). All arguments passed to `run` will be passed to the action with `apply`. If there is an error, the action execution is `rejected` and an error is thrown (Line 10).

```js
class Action extends Interface('action') {
  run (...args) {
    const className = this.name

    return new Promise((resolve, reject) => {
      try {
        resolve(this.action.apply(this, args))
      } catch (e) {
        reject(
          new (class ActionError extends Error {
            message = `Action ${className} failed`
          })()
        )
      }
    })
  }
}
```

When an action is called, the returned Promise object can be consumed by using the `.then` function and adding a callback, or the `async/await` keywords to resolve the action synchronously.

## Example: Calling external APIs

In cases where you call external APIs, the action is a simple wrapper for the function call.

For example, in my app ApiBlaze, the frontend and backend communicate via web sockets. The action emits a web socket message, and it defines an event listener for the answer, which executes a callback function to process the result.

```js
import { Action } from 'spac'
import websocket from '../globals/connect.js'

export default class SearchApiAction extends Action {
  action (keyword, cb) {
    websocket.emit('app:api-search-action', keyword)
    websocket.on('app:api-search-action', json => {
      cb(json)
    })
  }
}
```

The search is triggered on the index page when any key is pressed inside the search bar. The DOM event `onkeyup` is processed by the `handleKeyUp`. This handler calls a method (Line 4), which in turn calls the action (Line 9). The action call receives a callback function, which takes the action result to update the state. If there is in error instead, the state will not change, but a message is shown (Line 11).

```js
handleKeyUp (e) {
  e.preventDefault()
  this.updateState({ apiSearchQuery: e.target.value })
  this.triggerSearch(e.target.value)
}

async triggerSearch (keyword) {
  try {
    new SearchApiAction().run(keyword, data => this.updateState({ apiList: data }))
  } catch (e) {
    console.error("Could not execute SearchApiAction")
  }
}
```

## Conclusion

Actions enable pages and components to call functions that communicate with a backend server or an external API. Actions are objects with an `action()` method. When executed with `run()`, all arguments are passed, preserved in type and order, and a Promise object is returned. This Promise can be consumed by the called to receive the result and process them further.