npm package: @instructure/updown
Description: apply a set of inter-dependent functions in order
Latest version: 1.3.2

# updown

Apply a set of inter-dependent functions, and later undo their effects. This
routine is suitable when you need to run separate pieces of logic that are
order-dependent: the dependencies are explicit and made clear to the reader, and
the order in which they are declared does not govern the order in which they are
run.

```javascript
import up from 'updown'

await up({
  up: (a) => { /* do something when everything is ready */
    console.log(a) // => "hi"
  },
  requires: [
    {
      up: () => { /* but cause some side-effects first... */
        window.im_bad = 1

        return {
          down: () => { /* ...and clean them up when asked */
            delete window.im_bad
          }
        }
      }
    },
    { /* inject dependencies... */
      up: () => Promise.resolve({ value: 'hi' }),

      requires: [
        { /* ...but do even more things first... */
          up: ...,
          requires: [...]
        }
      ]
    }
  ]
}) // => { down: Function, value: undefined }
```

The functions can be asynchronous or not, the interface remains identical.

See also the `example.ts` file in this repository for sample usage that is
written in a somewhat more real-world way.

## Usage and going up

The input to the `up` function is a tree of objects - its nodes - that contain
two significant properties: (1) "up", which points to the function to run, and
(2) "requires", which is the list of other nodes that must be run _before_ it.

```javascript
const A = {
  up: () => {},
  requires: []
}

const B = {
  up: () => { ... },
  requires: [A]
}
```

A node is brought "up" exactly once, and the time that happens is when all of
its dependencies have been satisfied. In the example above, `A` will be brought
up before `B`. You don't need to worry about the order in which dependencies are
declared; `up` will figure that out.

A node is considered to be up when its `up` function returns. However, if it
returns a Promise, then `updown` will wait on that Promise to be settled before
considering it to be up. Once a node is determined to be up, then other nodes
which depend upon it will be brought up in turn.

With that said, the tree is expected to be free of circular dependencies. We
don't attempt to detect such cases and the behavior of `up` is undefined if
this constraint is not held.

## Producing values

A node may produce a value to be used by other nodes which depend upon it. The
value provided is opaque to and is not examined by the updown logic.

`up` may return `void` (or a Promise that resolves to `void`), in which case
the value is `undefined` and the `down` action is nothing. `up` may also
return either an Object (or a Promise which resolves to an Object); that Object
must match the type signature:

```javascript
{
  value?: unknown,
  down?: () => (void | Promise<void>)
}
```

It is the `value` property that is considered the value produced by the `up`
function and which is passed as an argument to dependents, discussed in the
next section.

### Considerations for positional arguments

The value itself is provided as an argument to the dependent's `up` function at
the position equal to where the dependency was declared in the `requires` set.

For example, the node `B` will receive the value of `A` as the second argument:

```javascript
const A = { up: () => ({ value: 5 }), requires: [] }
const B = { up: (_, valueOfA) => {},  requires: [C, A] }
                    │                               │
                    │                               └─ position of dependency
                    └─ position of dependency value
```

This symmetry suggests a particular arrangement for the dependencies we declare
in `requires` whereby ones that produce values are placed _before_ others that
don't. Consider a different example where nodes `A` and `B` do produce values
whereas `C` does not:

```javascript
const A = { up: ()  => ({ value: 1 }),     requires: [] }
const B = { up: (a) => ({ value: a + 1 }), requires: [A] }
const C = { up: ()  => (),                 requires: [A,B] }

up({
  up:       (c, a, b) => { console.log(c, a, b) /* => undefined, 1, 2 */ },
  requires: [C, A, B]
})
```

As you see in the body of `up` for the last node, `C`'s value was undefined and
looked awkward. Had we declared it _after_ `A` and `B`, its argument could then
be omitted entirely, which would make for a more natural interface:

```javascript
up({
  up:       (a, b) => { console.log(a, b) /* => 1, 2 */ },
  requires: [A, B, C]
})
```

In practice, the functions whose output you care about are declared earlier in
`requires` so that you may easily reference them.

Remember that the order in which you declare the dependencies does not affect
the order in which they are run, so you are safe to arrange them in a way that
suits you.

### Considerations for Capabilities that are not idempotent

Suppose we have two entirely separate trees of dependencies:

      const A1: Capability = { up: ..., requires: [B, C, E] };
      const A2: Capability = { up: ..., requires: [C, D, E] };

But C's up action is not idempotent so running it twice might result in undefined
behavior, so it can be protected with `oncePerPage` meaning it will only execute
once per global code execution. `oncePerPage` can be imported by name from the
`updown` package if needed.

E is also a dependency of both trees, but pretend that its up action is
idempotent, so it won't matter if we run it more than once. Thus it will run
twice in this case if not wrapped in `oncePerPage`.

Note that when possible, it's easier (and preferred) to simply define a new
dependency A with `requires: [ A1, A2 ]` and then `updown` itself would take care
of assuring that C only executed once. But sometimes disparate sections of code
running in the same environment need to run completely different instances of `up`
and all the respective sets of descendent requirements; whenever this is the case
one must be careful to use `oncePerPage` to guard shared capabilities from bringing
themselves up more than once if that would create problems.

The included `example.ts` demonstrates how to include `oncePerPage` and how to use
it to protect capability `up` functions from executing more than once.

### Considerations for values that are unresolved Promises

Be careful: If a node returns an object with a value which is an unresolved
Promise, `updown` will _not_ wait on it to be settled before considering your
node to be up! If you mean to block your dependents until a Promise settles,
be sure to return that Promise as the result of `up` itself.

For example, the following will cause `updown` to proceed immediately after
calling this `up` function:

```javascript
up({
  up: () => ({ value: new Promise(resolve => resolve(...)); })
});   // WILL NOT WAIT!
```

Whereas this will cause it to wait until the Promise settles:

```javascript
up({
  up: () => new Promise(resolve => resolve({ value: ... }))
});   // This will wait
```

The usual use case is for `up` itself to return the Promise, which allows
`updown` to automatically manage the asynchronous running of dependents at
the proper times. Since the `value` inside the return object is opaque to
`updown` an implementation is certainly free to set it to anything
appropriate for that code's logic, including an unresolved Promise; just be
aware that that alone will not block bringing up dependent nodes.

## Going down

A node that causes side-effects while going up should implement a counterpart
function that restores those effects by defining a function at the "down"
property of its output.

```javascript
{
  up: () => {
    window.be_bad = 1

    return {
      down: () => {
        delete window.be_bad
      }
    }
  },
  requires: []
}
```

Nodes are brought down in a LIFO fashion.

## Error handling

Should any node fail to go up - either by throwing an Error or by producing a
rejected Promise - the chain is aborted. The error is decorated with a custom
property "down" that can be used to tear down the nodes that were successfully
brought up to that point.

```javascript
try {
  await up(...)
}
catch (e) {
  // do something with error:
  ...

  // clean up:
  await e.down()
}
```

Errors caught while going down are tracked and provided to you but they do not
stop other nodes from going down. In this case, `down` rejects with the last
error encountered.

## Input integrity

The `up` routine makes no attempts at validating the structure of the nodes you
provide at runtime; it expects them to be valid. To interface properly with this
package, please make sure you're utilizing the available TypeScript types and
run your own integration through the `tsc` checker at build-time.