E~wee~ctor: writing tiny Effector from scratch #3 — Simple API methods
Tags: effector, graphs, javascript
Published: 2020-04-04 09:46:01

Hi, folks!

In this article I want to implement some simple Effector API functions. But before we start, let's improve one thing.

You might have noticed, that we create auxiliary node and add it to the `next` array of other node quite often, like this:

```javascript
  event.map = fn => {
    const mapped = createEvent()

    // create new node
    const node = createNode({
      next: [mapped.graphite],
      seq: [compute(fn)],
    })

    // add it to the event's next nodes
    event.graphite.next.push(node)

    return mapped
  }
```

Let's improve `createNode` function, so it will do it for us:

```javascript
export const getGraph = unit => unit.graphite || unit

const arrify = units =>
  [units]
    .flat()          // flatten array
    .filter(Boolean) // filter out undefined values
    .map(getGraph)   // get graph nodes

export const createNode = ({ from, seq = [], to } = {}) => {
  const node = {
    next: arrify(to),
    seq,
  }
  arrify(from).forEach(n => n.next.push(node))
  return node
}
```

I've renamed parameter `next` to `to`, and added new parameter `from`, accepting _previous_ nodes.
`getGraph` helper function gives us ability to pass both units and nodes, without taking care of field `.graphite`. Also, with `arrify` helper function we can pass single unit or array of units to the `from` and `to` parameters.

Now any `createNode` call should be more readable:

> `from` node → `seq`uence of steps → `to` node

With this change we can rewrite example above as following:

```javascript
  event.map = fn => {
    const mapped = createEvent()

    // create new node
    // and automatically add it to the event's next nodes
    createNode({
      from: event,
      seq: [compute(fn)],
      to: mapped,
    })

    return mapped
  }
```

I wont show you all diffs of all `createNode` function occurrences, the changes are trivial, you can make them yourself, or check commit by the link in the end of the article, as usual :)

Let's move on to the API methods!

- - -

# forward

```javascript
export const forward = ({ from, to }) => {
  createNode({
    from,
    to,
  })
}
```

That's simple :)

⚠️ Well, not quite so, Effector's Forward returns so called _Subscription_, to be able to remove connection. We will implement subscriptions in later chapters.

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

Remember we can pass array of units/nodes to `createNode` function, so `forward` can handle arrays automatically!

![forward arrays](https://dev-to-uploads.s3.amazonaws.com/i/wineh48kelx67q83jrbx.png)

# merge

```javascript
export const merge = (...events) => {
  const event = createEvent()
  forward({
    from: events.flat(), // to support both arrays and rest parameters
    to: event,
  })
  return event
}
```

`merge` creates new event and forwards all given events to that new one.

⚠️ Effector's `merge` supports only arrays. I've added rest parameters support just because I can ^_^

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

# split

```javascript
const not = fn => value => !fn(value) // inverts comparator function

export const split = (event, cases) => {
  const result = {}

  for (const key in cases) {
    const fn = cases[key]
    result[key] = event.filter(fn)
    event = event.filter(not(fn))
  }

  result.__ = event
  return result
}
```

`split` function splits event into several events, which fire if source event matches corresponding comparator function.

"It may seem difficult at first, but everything is difficult at first."
— Miyamoto Musashi

So, take your time understanding this function.
And here is diagram of `split`:

![split](https://dev-to-uploads.s3.amazonaws.com/i/1ijr3pphrkptm2u1xvpo.png)

Or in a less detailed, but more beautiful form of a tree, `split` is actually looks like a recursive binary splitting:

![split tree](https://dev-to-uploads.s3.amazonaws.com/i/4nbha6yw9itwe1qood07.png)

# createApi

```javascript
export const createApi = (store, setters) => {
  const result = {}

  for (const key in setters) {
    const fn = setters[key]
    result[key] = createEvent()
    store.on(result[key], fn)
  }

  return result
}
```

`createApi` function is just a simple factory for events, and it auto-subscribes given store on each one of them.

# is

We can distinguish _events_ and _stores_ by doing `typeof` (_events_ are functions and _stores_ are plain objects). But this approach has a flaw – when we will implement _effects_ it will fail, because _effects_ are functions too. We could go further and check all properties – this is called _[duck typing](https://en.wikipedia.org/wiki/Duck_typing)_. But Effector does that very simple – each unit has a special field `kind`:

```diff
export const createEvent = () => {
  // --8<--
+  event.kind = 'event'
  return event
}

export const createStore = defaultState => {
  // --8<--
+  store.kind = 'store'
  return store
}
```

And with this field `kind` we can easily check our units:

```javascript
const is = type => any =>
  (any !== null) &&
  (typeof any === 'function' || typeof any === 'object') &&
  ('kind' in any) &&
  (type === undefined || any.kind === type)

export const unit = is()
export const event = is('event')
export const store = is('store')
```

# restore

`restore` behaves differently on different inputs, so we will need our brand new `is` functionality:

```javascript
export const restore = (unit, defaultState) => {
  if (is.store(unit)) {
    return unit
  }

  if (is.event(unit)) {
    return createStore(defaultState).on(unit, (_, x) => x)
  }

  const result = {}
  for (const key in unit) {
    result[key] = createStore(unit[key])
  }
  return result
}
```

`restore` function can also handle _effects_, but we don't have them yet.

- - -

Other API functions, like `sample`, `guard` and `combine`, we will describe in later chapters.

And as always, you can find all this chapter changes in [this commit](https://github.com/yumauri/eweector/commit/adfb21d003640b267bf14c856d57c8ce916b58a6).

Thank you for reading!
To be continued...