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().
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
},
},
}
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 stateapi.getEntities(typeName)- read entities by typeapi.getEntity(id)- read one entityapi.notify(type, payload)- trigger other events (queued, not immediate)api.dispatch(action)- optional, if you prefer Redux-style dispatchingapi.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.
handleAsync(type, handlers, options?)
Think of the flow like a newspaper article:
startwrites the headline and sets the scenerungathers the reportingsuccesspublishes the storyerrorprints the correctionfinallyarchives the notes
Example:
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
},
}),
}
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
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):
handleAsync("save", {
start(entity) {
entity.loading = true
},
async run(payload) {
return api.save(payload)
},
})
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:
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,
},
],
}),
)
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:
const saveSettings = optimistic(
handleAsync("saveSettings", {
async run(payload) {
return api.save(payload)
},
}),
{ status: "saving" },
)
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
You can override this behavior:
handleAsync("bootstrap", handlers, { scope: "global" })
Available scopes:
"entity"(default)"type""global"
Key rule: Async code must not access entities after
await. All updates happen in event handlers.
Inglorious Store