Skip to content
On this page

Async Operations

In Inglorious Store, your event handlers can be async, and you get deterministic behavior automatically. Inside an async handler, you can access other parts of state (read-only), and you can trigger other events via api.notify().

javascript
export const types = {
  todoList: {
    async loadTodos(entity, payload, api) {
      try {
        entity.loading = true
        const { name } = api.getEntity("user")
        const response = await fetch(`/api/todos/${name}`)
        const data = await response.json()
        api.notify("todosLoaded", todos)
      } catch (error) {
        api.notify("loadFailed", error.message)
      }
    },

    todosLoaded(entity, todos) {
      entity.todos = todos
      entity.loading = false
    },

    loadFailed(entity, error) {
      entity.error = error
      entity.loading = false
    },
  },
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

Notice: you don't need pending/fulfilled/rejected actions. You stay in control of the flow — no hidden action chains. The api object passed to handlers provides:

  • api.getEntities() - read entire state
  • api.getEntities(typeName) - read entities by type
  • api.getEntity(id) - read one entity
  • api.notify(type, payload) - trigger other events (queued, not immediate)
  • api.dispatch(action) - optional, if you prefer Redux-style dispatching
  • api.getTypes() - access type definitions (mainly for middleware/plugins)
  • api.getType(typeName) - access type definition (mainly for overrides)

All events triggered via api.notify() enter the queue and process together, maintaining predictability and testability.

handleAsync

The handleAsync helper generates a set of event handlers representing the lifecycle of an async operation.

typescript
handleAsync(type, handlers, options?)
1

Think of the flow like a newspaper article:

  • start writes the headline and sets the scene
  • run gathers the reporting
  • success publishes the story
  • error prints the correction
  • finally archives the notes

Example:

typescript
import { handleAsync } from "@inglorious/store/async"

const todoList = {
  ...handleAsync("fetchTodos", {
    async run(payload) {
      const res = await fetch("/api/todos")
      return res.json()
    },

    success(entity, todos) {
      entity.todos = todos
    },

    error(entity, error) {
      entity.error = error.message
    },

    finally(entity) {
      entity.loading = false
    },
  }),
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

Triggering fetchTodos emits the following events:

fetchTodos
fetchTodosRun
fetchTodosSuccess | fetchTodosError
fetchTodosFinally
1
2
3
4

Each step is an event handler, not an implicit callback.

Optional start handler

Use start for synchronous setup (loading flags, resets, optimistic state):

typescript
handleAsync("save", {
  start(entity) {
    entity.loading = true
  },
  async run(payload) {
    return api.save(payload)
  },
})
1
2
3
4
5
6
7
8

If omitted, no Start event is generated.

Optimistic Updates

If you want optimistic UI state, wrap the async behavior with optimistic. This helper lives at @inglorious/store/optimistic, so only users who need it import it:

typescript
import { handleAsync } from "@inglorious/store/async"
import { optimistic } from "@inglorious/store/optimistic"

const saveTodo = optimistic(
  handleAsync("saveTodo", {
    async run(payload) {
      const res = await fetch("/api/todos", {
        method: "POST",
        body: JSON.stringify(payload),
      })
      if (!res.ok) throw new Error("Failed")
      return res.json()
    },
    success(entity, todo) {
      entity.todos = entity.todos.map((item) =>
        item.id === todo.tempId ? todo : item,
      )
    },
  }),
  (entity, payload) => ({
    todos: [
      ...entity.todos,
      {
        id: payload.tempId,
        title: payload.title,
        completed: payload.completed,
        pending: true,
      },
    ],
  }),
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

If the optimistic state does not depend on the payload, use a static patch instead:

typescript
const saveSettings = optimistic(
  handleAsync("saveSettings", {
    async run(payload) {
      return api.save(payload)
    },
  }),
  { status: "saving" },
)
1
2
3
4
5
6
7
8

The wrapper stores a shallow snapshot of the patched keys, applies the optimistic patch during Start, and restores the previous values if the request fails.

Event scoping

By default, lifecycle events are scoped to the triggering entity:

#entityId:fetchTodosSuccess
1

You can override this behavior:

typescript
handleAsync("bootstrap", handlers, { scope: "global" })
1

Available scopes:

  • "entity" (default)
  • "type"
  • "global"

Key rule: Async code must not access entities after await. All updates happen in event handlers.

Released under the MIT License.