29.0.5 • Published 1 month ago

overmind-devtools v29.0.5

Weekly downloads
6,162
License
MIT
Repository
github
Last release
1 month ago

action-chain

Why

All modern frameworks has some concept of an action. The purpose of an action is to perform side effects, this being changing the state of the application or talking to the server. How you express this action is different in the different frameworks and tools, but they typically have one thing in common... they are expressed as one function with imperative code.

There is nothing wrong with imperative code, we need it, but it has some limitations:

  1. When a single function with imperative code grows it quickly becomes difficult to reason about what it does

  2. There is no way to track what the function does, because it is low level and we typically point directly to other libraries and functions

  3. It requires a lot of dicipline to make your code reusable and composable

action-chain moves you into a functional world by exposing a chaining API, much like RxJS. But instead of being focused on value transformation, action-chain is focused on side effects. With its "developer experience" driven implementation it allows for building developer tools that can visual all execution.

Create an action-chain

import { Action, NoValueAction, actionChainFactory, actionFactory } from 'action-chain'

// The context holds all side effects you want to access in
// your chain. Expressed as simple objects with methods. You would
// use this to wrap existing libraries, exposing a domain specific
// api for your action chain
const context = {
  say: {
    hello: () => 'hello',
    goodbye: () => 'goodbye'
  }
}

type Context = typeof context

// The action chain manages execution of the actions and
// provides the context to them
const actionChain = actionChainFactory<Context>(context)

// You define your own factory for creating actions. It
// can define an initial value type and returns an
// action factory with the chain defined. You type out
// conditional "Action" or "NoValueAction" to allow
// typed actions to require a value and untyped actions
// to not require a value
const action = function <InitialValue>(): InitialValue extends undefined
  ? NoValueAction<Context, InitialValue>
  : Action<Context, InitialValue> {
  return actionFactory<Context, InitialValue>(actionChain)
}

Define actions

const test = action<string>()
  .map((name, { say }) => `${say.hello()} ${name}`)

test('Bob') // "hello Bob"

Track actions

actionChain.on('action:start', (details) => {
  /*
    {
      actionId: 0,
      executionId: 0,
    }
  */
})
actionChain.on('operator:start', (details) => {
  /*
    {
      actionId: 0,
      executionId: 0,
      operatorId: 0,
      name: '',
      type: 'map',
      path: []
    }
  */
})
actionChain.on('operator:end', (details) => {
  /*
    {
      actionId: 0,
      executionId: 0,
      operatorId: 0,
      name: '',
      type: 'map',
      path: [],
      isAsync: false,
      result: 'hello Bob'
    }
  */
})
actionChain.on('action:end', (details) => {
  /*
    {
      actionId: 0,
      executionId: 0,
    }
  */
})

Operators

do

Allows you to run effects and passes the current value a long to the next operator.

const test = action()
  .do((_, { localStorage }) => {
    localStorage.set('foo', 'bar')
  })

map

Maps to a new value, passed to the next operator.

const test = action<string>()
  .map((value) => value.toUpperCase())

try

If returning a promise, run paths based on resolved or rejected.

const test = action<string>()
  .try((_, { api }) => api.getUser(), {
    success: action(),
    error: action()
  })

when

Executes true or false path based on boolean value.

const test = action<string>()
  .when((value) => value.length > 3, {
    true: action(),
    false: action()
  })

filter

Stops execution when false.

const test = action<string>()
  .filter(() => false)
  // does not run
  .map(() => 'foo')

debounce

Debounces execution.

const test = action<string>()
  .debounce(100)
  // Runs when 100 milliseconds has passed since
  // last time the "test" action was called
  .map(() => 'foo')

Extend operators

import { Action, NoValueAction, actionChainFactory, actionFactory, Execution } from 'action-chain'

// There are two types of actions. Actions that takes an initial value
interface MyAction<Context, InitialValue, Value = InitialValue>
  extends MyOperators<Context, InitialValue, Value>,
    Action<Context, InitialValue, Value> {}

// And those who do not 
interface NoValueMyAction<Context, InitialValue, Value = InitialValue>
  extends MyOperators<Context, InitialValue, Value>,
    NoValueAction<Context, InitialValue, Value> {}

// You type out your operators and all of them will return either
// an action with an initial value or not, based on the "InitialValue"
// typing
interface MyOperators<Context, InitialValue, Value> {
  log(): InitialValue extends undefined
    ? NoValueMyAction<Context, InitialValue, Value>
    : MyAction<Context, InitialValue, Value>
}

// Create a new actionFactory which composes the default one and implements
// the new operators
function myActionFactory<Context, InitialValue, Value = InitialValue>(
  actionChain: ActionChain<Context>,
  initialActionId?: number,
  runOperators?: (
    value: any,
    execution: Execution,
    path: string[]
  ) => any | Promise<any>
): InitialValue extends undefined
  ? NoValueMyAction<Context, InitialValue, Value>
  : MyAction<Context, InitialValue, Value> {
  return Object.assign(
    actionFactory<Context, InitialValue, Value>(
      actionChain,
      initialActionId,
      runOperators
    ) as any,
    {
      log() {
        const operator = (value) => {
          console.log(value)
          return value
        }

        const [
          chain,
          initialActionId,
          runOperators,
        ] = this.createOperatorResult('log', '', operator)

        return myActionFactory<Context, InitialValue, Value>(
          chain,
          initialActionId,
          runOperators
        )
      },
    }
  )
}

const myAction = function<
  InitialValue = undefined
>(): InitialValue extends undefined
  ? NoValueMyAction<Context, InitialValue>
  : MyAction<Context, InitialValue> {
  return myActionFactory<Context, InitialValue>(actionChain)
}
29.0.5

8 months ago

29.0.4

8 months ago

29.0.3

10 months ago

29.0.2

2 years ago

29.0.1

3 years ago

29.0.0

3 years ago

28.0.0

3 years ago

27.0.0

4 years ago

26.0.2

4 years ago

26.0.1

4 years ago

26.0.0

4 years ago

25.1.1

4 years ago

25.1.0

4 years ago

25.0.1

4 years ago

25.0.0

4 years ago

24.1.0

4 years ago

24.0.1

4 years ago

24.0.0

4 years ago

23.1.3

4 years ago

23.1.2

4 years ago

23.1.1

4 years ago

23.1.0

4 years ago

23.0.2

4 years ago

23.0.1

4 years ago

23.0.0

4 years ago

22.0.0

4 years ago

21.0.0

5 years ago

20.3.0

5 years ago

20.2.0

5 years ago

20.1.2

5 years ago

20.1.1

5 years ago

20.1.0

5 years ago

20.0.1

5 years ago

20.0.0

5 years ago

19.0.1

5 years ago

19.0.0

5 years ago

18.1.0

5 years ago

18.0.0

5 years ago

17.0.0

5 years ago

16.1.3

5 years ago

16.1.2

5 years ago

16.1.1

5 years ago

16.1.0

5 years ago

16.0.0

5 years ago

15.0.0

5 years ago

14.1.0

5 years ago

14.0.0

5 years ago

13.0.1

5 years ago

13.0.0

5 years ago

12.0.1

5 years ago

12.0.0

5 years ago

11.0.0

5 years ago

10.0.0

5 years ago

9.1.0

5 years ago

9.0.0

5 years ago

8.0.0

5 years ago

7.0.1

5 years ago

7.0.0

5 years ago

6.0.0

5 years ago

5.1.1

5 years ago

5.1.0

5 years ago

5.0.0

5 years ago

4.0.1

6 years ago

4.0.0

6 years ago

3.0.0

6 years ago

2.4.0

6 years ago

2.3.0

6 years ago

2.2.0

6 years ago

2.1.0

6 years ago

2.0.1

6 years ago

2.0.0

6 years ago

1.0.2

6 years ago

1.0.1

6 years ago