E~wee~ctor: writing tiny Effector from scratch #4 — Effect
Tags: effector, graphs, javascript
Published: 2020-04-10 18:12:36

Hey, all!

Upon this moment we've implemented two main Effector's entities – an `event` and a `store` – and were avoiding an `effect`. So, let's accept this challenge!

First of all, according to [documentation](https://effector.now.sh/docs/api/effector/effect), an _effect_ is a container for async function. It is used for side-effects, like interaction with a server, or any timeouts and intervals. Actually, you can use any function inside an _effect_, it doesn't need to be asynchronous in general, but it is so in most cases. But it is important in the Effector ecosystem to use _effects_ for side-effects.

An _effect_ is a complex entity, and contains of a dozen nodes and other entities:
- `done` – is an _event_ triggered when handler is resolved
- `fail` – is an _event_ triggered when handler is rejected or throws error
- `finally` – is an _event_ triggered when handler is resolved, rejected or throws error
- `doneData` – is an _event_ triggered with result of effect execution
- `failData` – is an _event_ triggered with error thrown by effect
- `pending` – is a boolean _store_ containing a `true` value until the effect is resolved or rejected
- `inFlight` – is a _store_ showing how many effect calls aren't settled yet

Here is what we will start with:

```javascript
export const createEffect = ({ handler }) => {
  const effect = payload => launch(effect, payload)
  effect.graphite = createNode()
  effect.watch = watch(effect)

  effect.prepend = fn => {
    const prepended = createEvent()
    createNode({
      from: prepended,
      seq: [compute(fn)],
      to: effect,
    })
    return prepended
  }

  // TODO

  effect.kind = 'effect'
  return effect
}
```

This stub looks exactly like part of an _event_. In fact, Effector uses an event under the hood as a base for an effect, but we will create it from scratch for simplicity.

The only differences from an event here yet is that `createEffect` function accepts an object with the `handler` field. And `effect.kind` is `"effect"`, so we can distinguish effects from other entities.

Now let's add a method `use` to change `handler`:

```javascript
  effect.use = fn => (handler = fn)
  effect.use.getCurrent = () => handler
```

And create bunch of child events for the effect:

```javascript
  const anyway = createEvent()
  const done = anyway.filterMap(({ status, ...rest }) => {
    if (status === 'done') return rest
  })
  const fail = anyway.filterMap(({ status, ...rest }) => {
    if (status === 'fail') return rest
  })
  const doneData = done.map(({ result }) => result)
  const failData = fail.map(({ error }) => error)

  effect.finally = anyway
  effect.done = done
  effect.fail = fail
  effect.doneData = doneData
  effect.failData = failData
```

Hereby we've created all the events for our effect. Base event is `effect.finally` (`finally` is a reserved word, so we can't name a variable like this, so we use name `anyway` for it). All other events are derived from this base event:

![Events](https://dev-to-uploads.s3.amazonaws.com/i/ndibkxwocyljvj45saoh.png)

Looking at the code above I feel urgent desire to extract common logic to helper functions:

```javascript
const status = name => ({ status, ...rest }) =>
  status === name ? rest : undefined

const field = name => object => object[name]

// --8<--

  const anyway = createEvent()
  const done = anyway.filterMap(status('done'))
  const fail = anyway.filterMap(status('fail'))
  const doneData = done.map(field('result'))
  const failData = fail.map(field('error'))
```

Now let's add stores `pending` and `inFlight`:

```javascript
  effect.inFlight = createStore(0)
    .on(effect, x => x + 1)
    .on(anyway, x => x - 1)
  effect.pending = effect.inFlight.map(amount => amount > 0)
```

That is simple: store `inFlight` subscribes to the effect itself and its `finally` event. And boolean store `pending` is `true` when `inFlight` has positive value.

![With stores](https://dev-to-uploads.s3.amazonaws.com/i/qml7jg2t7a8mp1tqknra.png)

And now we've come close to the main part of the effect – running our side-effect function `handler`. We will just add a single _step_ to our main effect's node, where the `handler` will be launched:

```javascript
  effect.graphite.seq.push(
    compute(params => {
      try {
        const promise = handler(params)
        if (promise instanceof Promise) {
          promise
            .then(result => launch(anyway, { status: 'done', params, result }))
            .catch(error => launch(anyway, { status: 'fail', params, error }))
        } else {
          launch(anyway, { status: 'done', params, result: promise })
        }
      } catch (error) {
        launch(anyway, { status: 'fail', params, error })
      }
      return params
    })
  )
```

- we run the handler inside try-catch block, so if we get a synchronous exception – it will be caught
- if `handler` returns a Promise, we wait for it to settle
- if `handler` returns not a Promise, we just use returned value as a result
- in any case we _launch_ result (either successful or failed) to the `finally` event, so it will be processed to the `done`/`fail`/`doneData`/`failData` events automatically

Here is one important thing left though, without which this code will not work properly:

1. _Steps_ are executed during the computation cycle inside the kernel
2. We use function `launch` inside the _step_, while we are inside the computation cycle
3. Function `launch` starts the computation cycle

Do you see the problem?

We have one single queue to process, and secondary run of the computation cycle inside the already running computation cycle will mess it all around! We don't want this, so let's add a guard to protect from this situation in our kernel:

```javascript
let running = false
const exec = () => {
  if (running) return
  running = true

  // --8<--

  running = false
}
```

After this fix _step_ inside effect's node will work perfectly.

But there is one more thing to fix: effect should return a Promise, so it can be awaited. For now our effect's function, which is tied to the node, is exactly the same as function for an event – it just launches given payload to the node (and returns nothing):

```javascript
  const effect = payload => launch(effect, payload)
```

But it should return a Promise, as was said. And we should be able to somehow resolve or reject this Promise from inside _step_.

And here we need so called **Deferred** object. This is a common pattern to have a Promise, which can be settled _from outside_. [Here is a nice explanation of this approach](https://lucybain.com/blog/2016/js-promises-vs-deferred/), read this, if you didn't meet deferred objects yet.

> A Promise represents a value that is not yet known.
> A Deferred represents work that is not yet finished.

```javascript
export const defer = () => {
  const deferred = {}

  deferred.promise = new Promise((resolve, reject) => {
    deferred.resolve = resolve
    deferred.reject = reject
  })

  // we need this to avoid 'unhandled exception' warning
  deferred.promise.catch(() => {})

  return deferred
}
```

`defer` function creates a deferred object. Now we can use this deferred object to return a Promise from an effect, and settle it from inside a _step_. But we also need to consider a situation, when the effect is called not directly, but from the other graph node, for example like `forward({ from: event, to: effect })`. In that case we don't need to create useless Deferred object.

Let's use helper class to distinguish direct and indirect call cases. We could use simple object, but we can't be sure, that one day effect will not receive exactly this shape of an object as a payload. So we use internal class and `instanceof` check, to be sure, that only our code can create class instance.

⚠️ Effector checks this differently, using call stack, provided by the kernel, but we will go simple way :)

```javascript
function Payload(params, resolve, reject) {
  this.params = params
  this.resolve = resolve
  this.reject = reject
}
```

Now we need to change main function, and then add one more step to check use case:

```javascript
  const effect = payload => {
    const deferred = defer()
    launch(effect, new Payload(payload, deferred.resolve, deferred.reject))
    return deferred.promise
  }

  // --8<--

    compute(data =>
      data instanceof Payload
        ? data // we get this data directly
        : new Payload( // we get this data indirectly through graph
            data,
            () => {}, // dumb resolve function
            () => {} // dumb reject function
          )
    )
```

After this _step_ next one will get a `Payload` instance in both cases, either effect was called directly or indirectly.  We need to change our existing step to handle this new `Payload` instance instead of plain params.

```javascript
// helper function to handle successful case
const onDone = (event, params, resolve) => result => {
  launch(event, { status: 'done', params, result })
  resolve(result)
}

// helper function to handle failed case
const onFail = (event, params, reject) => error => {
  launch(event, { status: 'fail', params, error })
  reject(error)
}

// --8<--

    compute(({ params, resolve, reject }) => {
      const handleDone = onDone(anyway, params, resolve)
      const handleFail = onFail(anyway, params, reject)
      try {
        const promise = handler(params)
        if (promise instanceof Promise) {
          promise.then(handleDone).catch(handleFail)
        } else {
          handleDone(promise)
        }
      } catch (error) {
        handleFail(error)
      }
      return params
    })
```

And that's it, our effect shines and ready!

- - -

I am slightly worried, that reading this chapter could be difficult, and someone could not glue code pieces together. As always, you can find whole changes in [this commit](https://github.com/yumauri/eweector/commit/6a6de2b3a38c0f091bc743d61aa536597f2c8d5a), so feel free to check it out!

Thank you for reading!
To be continued...