@inglorious/store 9.1.0 → 9.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +269 -0
- package/package.json +5 -1
- package/src/async.js +187 -0
- package/src/migration/rtk.js +326 -0
- package/src/store.js +20 -1
- package/types/async.d.ts +146 -0
- package/types/index.d.ts +1 -0
- package/types/migration/rtk.d.ts +402 -0
- package/types/store.d.ts +13 -0
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Adapter for migrating Redux Toolkit code to Inglorious Store
|
|
3
|
+
*
|
|
4
|
+
* This module provides utilities to convert RTK slices and async thunks into
|
|
5
|
+
* Inglorious types, enabling gradual migration from RTK to Inglorious Store.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { handleAsync } from "../async"
|
|
9
|
+
|
|
10
|
+
const SEP_LENGTH = 50
|
|
11
|
+
const INDENT_SPACES = 2
|
|
12
|
+
const SLICE_PLUS_ACTION = 2
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Converts an RTK createAsyncThunk to Inglorious handleAsync handlers
|
|
16
|
+
*
|
|
17
|
+
* @param {string} name - The thunk name (e.g., 'fetchTodos')
|
|
18
|
+
* @param {Function} payloadCreator - The async function that performs the operation
|
|
19
|
+
* @param {Object} [options] - Options for the conversion
|
|
20
|
+
* @param {Function} [options.onPending] - Handler for pending state (maps to start)
|
|
21
|
+
* @param {Function} [options.onFulfilled] - Handler for fulfilled state (maps to success)
|
|
22
|
+
* @param {Function} [options.onRejected] - Handler for rejected state (maps to error)
|
|
23
|
+
* @param {Function} [options.onSettled] - Handler called after completion (maps to finally)
|
|
24
|
+
* @param {"entity"|"type"|"global"} [options.scope] - Event scope
|
|
25
|
+
*
|
|
26
|
+
* @returns {Object} Inglorious handleAsync handlers
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* // RTK async thunk
|
|
30
|
+
* const fetchTodos = createAsyncThunk(
|
|
31
|
+
* 'todos/fetch',
|
|
32
|
+
* async (userId) => {
|
|
33
|
+
* const response = await fetch(`/api/users/${userId}/todos`)
|
|
34
|
+
* return response.json()
|
|
35
|
+
* }
|
|
36
|
+
* )
|
|
37
|
+
*
|
|
38
|
+
* // Convert to Inglorious
|
|
39
|
+
* const todoHandlers = convertAsyncThunk('fetchTodos', fetchTodos.payloadCreator, {
|
|
40
|
+
* onPending: (entity) => { entity.status = 'loading' },
|
|
41
|
+
* onFulfilled: (entity, todos) => {
|
|
42
|
+
* entity.status = 'success'
|
|
43
|
+
* entity.todos = todos
|
|
44
|
+
* },
|
|
45
|
+
* onRejected: (entity, error) => {
|
|
46
|
+
* entity.status = 'error'
|
|
47
|
+
* entity.error = error.message
|
|
48
|
+
* }
|
|
49
|
+
* })
|
|
50
|
+
*
|
|
51
|
+
* // Use in type
|
|
52
|
+
* const todoList = {
|
|
53
|
+
* init(entity) { entity.todos = []; entity.status = 'idle' },
|
|
54
|
+
* ...todoHandlers
|
|
55
|
+
* }
|
|
56
|
+
*/
|
|
57
|
+
export function convertAsyncThunk(name, payloadCreator, options = {}) {
|
|
58
|
+
const {
|
|
59
|
+
onPending,
|
|
60
|
+
onFulfilled,
|
|
61
|
+
onRejected,
|
|
62
|
+
onSettled,
|
|
63
|
+
scope = "entity",
|
|
64
|
+
} = options
|
|
65
|
+
|
|
66
|
+
return handleAsync(
|
|
67
|
+
name,
|
|
68
|
+
{
|
|
69
|
+
start: onPending
|
|
70
|
+
? (entity, payload, api) => {
|
|
71
|
+
onPending(entity, payload, api)
|
|
72
|
+
}
|
|
73
|
+
: undefined,
|
|
74
|
+
|
|
75
|
+
run: async (payload, api) => {
|
|
76
|
+
// RTK payloadCreator signature: (arg, thunkAPI)
|
|
77
|
+
// We need to adapt it to our simpler signature
|
|
78
|
+
return await payloadCreator(payload, { dispatch: api.notify })
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
success: onFulfilled
|
|
82
|
+
? (entity, result, api) => {
|
|
83
|
+
onFulfilled(entity, result, api)
|
|
84
|
+
}
|
|
85
|
+
: undefined,
|
|
86
|
+
|
|
87
|
+
error: onRejected
|
|
88
|
+
? (entity, error, api) => {
|
|
89
|
+
onRejected(entity, error, api)
|
|
90
|
+
}
|
|
91
|
+
: undefined,
|
|
92
|
+
|
|
93
|
+
finally: onSettled
|
|
94
|
+
? (entity, api) => {
|
|
95
|
+
onSettled(entity, api)
|
|
96
|
+
}
|
|
97
|
+
: undefined,
|
|
98
|
+
},
|
|
99
|
+
{ scope },
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Converts an RTK slice to an Inglorious type
|
|
105
|
+
*
|
|
106
|
+
* This is a helper for gradual migration. It converts RTK reducers to Inglorious
|
|
107
|
+
* event handlers while preserving the same mutation-based syntax (both use Immer/Mutative).
|
|
108
|
+
*
|
|
109
|
+
* @param {Object} slice - RTK slice created with createSlice
|
|
110
|
+
* @param {Object} [options] - Conversion options
|
|
111
|
+
* @param {Object} [options.asyncThunks] - Map of async thunk handlers
|
|
112
|
+
* Keys are thunk names, values are objects with onPending/onFulfilled/onRejected
|
|
113
|
+
* @param {Object} [options.extraHandlers] - Additional event handlers not from the slice
|
|
114
|
+
*
|
|
115
|
+
* @returns {Object} Inglorious type definition
|
|
116
|
+
*
|
|
117
|
+
* @example
|
|
118
|
+
* // RTK slice
|
|
119
|
+
* const todosSlice = createSlice({
|
|
120
|
+
* name: 'todos',
|
|
121
|
+
* initialState: { items: [], filter: 'all' },
|
|
122
|
+
* reducers: {
|
|
123
|
+
* addTodo: (state, action) => {
|
|
124
|
+
* state.items.push({ id: Date.now(), text: action.payload })
|
|
125
|
+
* },
|
|
126
|
+
* toggleTodo: (state, action) => {
|
|
127
|
+
* const todo = state.items.find(t => t.id === action.payload)
|
|
128
|
+
* if (todo) todo.completed = !todo.completed
|
|
129
|
+
* },
|
|
130
|
+
* setFilter: (state, action) => {
|
|
131
|
+
* state.filter = action.payload
|
|
132
|
+
* }
|
|
133
|
+
* }
|
|
134
|
+
* })
|
|
135
|
+
*
|
|
136
|
+
* const fetchTodos = createAsyncThunk('todos/fetch', async () => {
|
|
137
|
+
* const response = await fetch('/api/todos')
|
|
138
|
+
* return response.json()
|
|
139
|
+
* })
|
|
140
|
+
*
|
|
141
|
+
* // Convert to Inglorious
|
|
142
|
+
* const todoList = convertSlice(todosSlice, {
|
|
143
|
+
* asyncThunks: {
|
|
144
|
+
* fetchTodos: {
|
|
145
|
+
* onPending: (entity) => { entity.status = 'loading' },
|
|
146
|
+
* onFulfilled: (entity, todos) => { entity.items = todos },
|
|
147
|
+
* onRejected: (entity, error) => { entity.error = error.message }
|
|
148
|
+
* }
|
|
149
|
+
* }
|
|
150
|
+
* })
|
|
151
|
+
*
|
|
152
|
+
* // Use in store
|
|
153
|
+
* const store = createStore({
|
|
154
|
+
* types: { todoList },
|
|
155
|
+
* entities: { todos: { type: 'todoList', id: 'todos' } }
|
|
156
|
+
* })
|
|
157
|
+
*/
|
|
158
|
+
export function convertSlice(slice, options = {}) {
|
|
159
|
+
const { asyncThunks = {}, extraHandlers = {} } = options
|
|
160
|
+
|
|
161
|
+
const type = {
|
|
162
|
+
// Convert initialState to init handler
|
|
163
|
+
init(entity) {
|
|
164
|
+
const initialState = slice.getInitialState()
|
|
165
|
+
Object.assign(entity, initialState)
|
|
166
|
+
},
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Convert each reducer to an event handler
|
|
170
|
+
for (const [actionName, reducer] of Object.entries(slice.caseReducers)) {
|
|
171
|
+
type[actionName] = (entity, payload) => {
|
|
172
|
+
// Create a mock action object for the RTK reducer
|
|
173
|
+
const action = { type: `${slice.name}/${actionName}`, payload }
|
|
174
|
+
|
|
175
|
+
// RTK reducers expect to mutate state directly (via Immer)
|
|
176
|
+
// Inglorious handlers do the same (via Mutative)
|
|
177
|
+
// So we can call the reducer directly on the entity
|
|
178
|
+
reducer(entity, action)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Convert async thunks
|
|
183
|
+
for (const [thunkName, thunkHandlers] of Object.entries(asyncThunks)) {
|
|
184
|
+
const asyncHandlers = convertAsyncThunk(
|
|
185
|
+
thunkName,
|
|
186
|
+
thunkHandlers.payloadCreator || (async () => {}),
|
|
187
|
+
{
|
|
188
|
+
onPending: thunkHandlers.onPending,
|
|
189
|
+
onFulfilled: thunkHandlers.onFulfilled,
|
|
190
|
+
onRejected: thunkHandlers.onRejected,
|
|
191
|
+
onSettled: thunkHandlers.onSettled,
|
|
192
|
+
scope: thunkHandlers.scope,
|
|
193
|
+
},
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
Object.assign(type, asyncHandlers)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Add any extra handlers
|
|
200
|
+
Object.assign(type, extraHandlers)
|
|
201
|
+
|
|
202
|
+
return type
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Creates a migration guide for a slice
|
|
207
|
+
*
|
|
208
|
+
* Analyzes an RTK slice and generates a readable migration guide showing
|
|
209
|
+
* the equivalent Inglorious code.
|
|
210
|
+
*
|
|
211
|
+
* @param {Object} slice - RTK slice
|
|
212
|
+
* @returns {string} Migration guide text
|
|
213
|
+
*
|
|
214
|
+
* @example
|
|
215
|
+
* const guide = createMigrationGuide(todosSlice)
|
|
216
|
+
* console.log(guide)
|
|
217
|
+
* // Outputs:
|
|
218
|
+
* // Migration Guide for 'todos' slice
|
|
219
|
+
* //
|
|
220
|
+
* // RTK:
|
|
221
|
+
* // dispatch(addTodo('Buy milk'))
|
|
222
|
+
* //
|
|
223
|
+
* // Inglorious:
|
|
224
|
+
* // api.notify('#todos:addTodo', 'Buy milk')
|
|
225
|
+
*/
|
|
226
|
+
export function createMigrationGuide(slice) {
|
|
227
|
+
const lines = []
|
|
228
|
+
|
|
229
|
+
lines.push(`Migration Guide for '${slice.name}' slice`)
|
|
230
|
+
lines.push("")
|
|
231
|
+
lines.push("=".repeat(SEP_LENGTH))
|
|
232
|
+
lines.push("")
|
|
233
|
+
|
|
234
|
+
// Show state structure
|
|
235
|
+
lines.push("STATE STRUCTURE:")
|
|
236
|
+
lines.push("RTK:")
|
|
237
|
+
lines.push(
|
|
238
|
+
` state.${slice.name} = ${JSON.stringify(slice.getInitialState(), null, INDENT_SPACES)}`,
|
|
239
|
+
)
|
|
240
|
+
lines.push("")
|
|
241
|
+
lines.push("Inglorious:")
|
|
242
|
+
lines.push(` entities.${slice.name} = {`)
|
|
243
|
+
lines.push(` type: '${slice.name}',`)
|
|
244
|
+
lines.push(` id: '${slice.name}',`)
|
|
245
|
+
const initialState = slice.getInitialState()
|
|
246
|
+
for (const [key, value] of Object.entries(initialState)) {
|
|
247
|
+
lines.push(` ${key}: ${JSON.stringify(value)},`)
|
|
248
|
+
}
|
|
249
|
+
lines.push(" }")
|
|
250
|
+
lines.push("")
|
|
251
|
+
lines.push("=".repeat(SEP_LENGTH))
|
|
252
|
+
lines.push("")
|
|
253
|
+
|
|
254
|
+
// Show each reducer conversion
|
|
255
|
+
lines.push("ACTIONS / EVENTS:")
|
|
256
|
+
for (const actionName of Object.keys(slice.caseReducers)) {
|
|
257
|
+
lines.push("")
|
|
258
|
+
lines.push(`${actionName}:`)
|
|
259
|
+
lines.push(" RTK:")
|
|
260
|
+
lines.push(` dispatch(${actionName}(payload))`)
|
|
261
|
+
lines.push(" Inglorious:")
|
|
262
|
+
lines.push(` api.notify('#${slice.name}:${actionName}', payload)`)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
lines.push("")
|
|
266
|
+
lines.push("=".repeat(SEP_LENGTH))
|
|
267
|
+
lines.push("")
|
|
268
|
+
|
|
269
|
+
// Show selector conversion
|
|
270
|
+
lines.push("SELECTORS:")
|
|
271
|
+
lines.push("RTK:")
|
|
272
|
+
lines.push(
|
|
273
|
+
` const data = useSelector(state => state.${slice.name}.someField)`,
|
|
274
|
+
)
|
|
275
|
+
lines.push("Inglorious:")
|
|
276
|
+
lines.push(` const { someField } = api.getEntity('${slice.name}')`)
|
|
277
|
+
lines.push(" // or")
|
|
278
|
+
lines.push(
|
|
279
|
+
` const data = useSelector(state => state.entities.${slice.name}.someField)`,
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
return lines.join("\n")
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Helper to create a compatibility layer for existing RTK code
|
|
287
|
+
*
|
|
288
|
+
* This allows RTK-style dispatch calls to work with Inglorious Store
|
|
289
|
+
* during the migration period.
|
|
290
|
+
*
|
|
291
|
+
* @param {Object} api - Inglorious API
|
|
292
|
+
* @param {string} entityId - The entity ID to target
|
|
293
|
+
* @returns {Function} A dispatch function compatible with RTK actions
|
|
294
|
+
*
|
|
295
|
+
* @example
|
|
296
|
+
* const dispatch = createRTKCompatDispatch(api, 'todos')
|
|
297
|
+
*
|
|
298
|
+
* // RTK-style action
|
|
299
|
+
* dispatch({ type: 'todos/addTodo', payload: 'Buy milk' })
|
|
300
|
+
*
|
|
301
|
+
* // Translates to:
|
|
302
|
+
* // api.notify('#todos:addTodo', 'Buy milk')
|
|
303
|
+
*/
|
|
304
|
+
export function createRTKCompatDispatch(api, entityId) {
|
|
305
|
+
return (action) => {
|
|
306
|
+
if (typeof action === "function") {
|
|
307
|
+
// Thunk - not supported in compat mode
|
|
308
|
+
console.warn("Thunks are not supported in RTK compat mode")
|
|
309
|
+
return
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Extract action type and payload
|
|
313
|
+
const { type, payload } = action
|
|
314
|
+
|
|
315
|
+
// Convert RTK action type to Inglorious event
|
|
316
|
+
// RTK: 'todos/addTodo' -> Inglorious: '#todos:addTodo'
|
|
317
|
+
const parts = type.split("/")
|
|
318
|
+
if (parts.length === SLICE_PLUS_ACTION) {
|
|
319
|
+
const [, actionName] = parts
|
|
320
|
+
api.notify(`#${entityId}:${actionName}`, payload)
|
|
321
|
+
} else {
|
|
322
|
+
// Fallback for non-standard action types
|
|
323
|
+
api.notify(`#${entityId}:${type}`, payload)
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
package/src/store.js
CHANGED
|
@@ -13,6 +13,7 @@ import { augmentType, augmentTypes } from "./types.js"
|
|
|
13
13
|
* @param {Object} [config.entities] - The initial entities configuration.
|
|
14
14
|
* @param {Array} [config.systems] - The initial systems configuration.
|
|
15
15
|
* @param {Array} [config.middlewares] - The initial middlewares configuration.
|
|
16
|
+
* @param {boolean} [config.autoCreateEntities] - Creates entities if not defined in `config.entities`.
|
|
16
17
|
* @param {"auto" | "manual"} [config.updateMode] - The update mode (defaults to "auto").
|
|
17
18
|
* @returns {Object} The store with methods to interact with state and events.
|
|
18
19
|
*/
|
|
@@ -21,6 +22,7 @@ export function createStore({
|
|
|
21
22
|
entities: originalEntities = {},
|
|
22
23
|
systems = [],
|
|
23
24
|
middlewares = [],
|
|
25
|
+
autoCreateEntities = false,
|
|
24
26
|
updateMode = "auto",
|
|
25
27
|
} = {}) {
|
|
26
28
|
const listeners = new Set()
|
|
@@ -220,8 +222,25 @@ export function createStore({
|
|
|
220
222
|
const oldEntities = state ?? {}
|
|
221
223
|
const newEntities = augmentEntities(nextState)
|
|
222
224
|
|
|
225
|
+
if (autoCreateEntities) {
|
|
226
|
+
for (const typeName of Object.keys(types)) {
|
|
227
|
+
// Check if entity already exists
|
|
228
|
+
const hasEntity = Object.values(newEntities).some(
|
|
229
|
+
(entity) => entity.type === typeName,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
if (!hasEntity) {
|
|
233
|
+
// No entity for this type → auto-create minimal entity
|
|
234
|
+
newEntities[typeName] = {
|
|
235
|
+
id: typeName,
|
|
236
|
+
type: typeName,
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
223
242
|
state = newEntities
|
|
224
|
-
eventMap = new EventMap(types,
|
|
243
|
+
eventMap = new EventMap(types, newEntities)
|
|
225
244
|
incomingEvents = []
|
|
226
245
|
isProcessing = false
|
|
227
246
|
|
package/types/async.d.ts
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { Api, BaseEntity } from "./store"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Scope option for async handler events
|
|
5
|
+
* - "entity": Events scoped to specific entity (#entityId:event)
|
|
6
|
+
* - "type": Events scoped to entity type (typeName:event)
|
|
7
|
+
* - "global": Global events (event)
|
|
8
|
+
*/
|
|
9
|
+
export type AsyncScope = "entity" | "type" | "global"
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Configuration options for async handlers
|
|
13
|
+
*/
|
|
14
|
+
export interface AsyncOptions {
|
|
15
|
+
/**
|
|
16
|
+
* Controls how lifecycle events are routed
|
|
17
|
+
* @default "entity"
|
|
18
|
+
*/
|
|
19
|
+
scope?: AsyncScope
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Handler functions for async operation lifecycle
|
|
24
|
+
*/
|
|
25
|
+
export interface AsyncHandlers<
|
|
26
|
+
TEntity extends BaseEntity = BaseEntity,
|
|
27
|
+
TPayload = any,
|
|
28
|
+
TResult = any,
|
|
29
|
+
> {
|
|
30
|
+
/**
|
|
31
|
+
* Called synchronously before the async operation starts
|
|
32
|
+
* Use for setting loading states
|
|
33
|
+
*/
|
|
34
|
+
start?: (entity: TEntity, payload: TPayload, api: Api) => void
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The async operation to perform
|
|
38
|
+
* Receives payload and api (NOT entity - entity state should be modified in other handlers)
|
|
39
|
+
* @returns Promise or synchronous result
|
|
40
|
+
*/
|
|
41
|
+
run: (payload: TPayload, api: Api) => Promise<TResult> | TResult
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Called when the operation succeeds
|
|
45
|
+
* Receives the entity, the result from run(), and api
|
|
46
|
+
*/
|
|
47
|
+
success?: (entity: TEntity, result: TResult, api: Api) => void
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Called when the operation fails
|
|
51
|
+
* Receives the entity, the error, and api
|
|
52
|
+
*/
|
|
53
|
+
error?: (entity: TEntity, error: any, api: Api) => void
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Called after the operation completes (success or failure)
|
|
57
|
+
* Use for cleanup, resetting loading states, etc.
|
|
58
|
+
*/
|
|
59
|
+
finally?: (entity: TEntity, api: Api) => void
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Generated event handlers returned by handleAsync
|
|
64
|
+
* Contains handlers for all lifecycle stages
|
|
65
|
+
*/
|
|
66
|
+
export interface AsyncEventHandlers<
|
|
67
|
+
TEntity extends BaseEntity = BaseEntity,
|
|
68
|
+
TPayload = any,
|
|
69
|
+
TResult = any,
|
|
70
|
+
> {
|
|
71
|
+
/**
|
|
72
|
+
* Main trigger handler
|
|
73
|
+
* Dispatches start (if defined) and run events
|
|
74
|
+
*/
|
|
75
|
+
[key: string]: (
|
|
76
|
+
entity: TEntity,
|
|
77
|
+
payload: TPayload,
|
|
78
|
+
api: Api,
|
|
79
|
+
) => void | Promise<void>
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Creates a set of event handlers for managing an asynchronous operation lifecycle.
|
|
84
|
+
*
|
|
85
|
+
* Generates handlers for: trigger, start, run, success, error, finally
|
|
86
|
+
* These can be spread into an entity type definition.
|
|
87
|
+
*
|
|
88
|
+
* @template TEntity - The entity type (must extend BaseEntity)
|
|
89
|
+
* @template TPayload - The payload type passed to the operation
|
|
90
|
+
* @template TResult - The result type returned by the async operation
|
|
91
|
+
*
|
|
92
|
+
* @param type - The base event name (e.g., 'fetchTodos')
|
|
93
|
+
* @param handlers - Handler functions for each lifecycle stage
|
|
94
|
+
* @param options - Configuration options
|
|
95
|
+
* @returns Object containing all generated event handlers
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* ```typescript
|
|
99
|
+
* interface Todo {
|
|
100
|
+
* id: number
|
|
101
|
+
* text: string
|
|
102
|
+
* completed: boolean
|
|
103
|
+
* }
|
|
104
|
+
*
|
|
105
|
+
* interface TodoListEntity extends BaseEntity {
|
|
106
|
+
* type: 'todoList'
|
|
107
|
+
* todos: Todo[]
|
|
108
|
+
* status: 'idle' | 'loading' | 'success' | 'error'
|
|
109
|
+
* error?: string
|
|
110
|
+
* }
|
|
111
|
+
*
|
|
112
|
+
* const todoList = {
|
|
113
|
+
* init(entity: TodoListEntity) {
|
|
114
|
+
* entity.todos = []
|
|
115
|
+
* entity.status = 'idle'
|
|
116
|
+
* },
|
|
117
|
+
*
|
|
118
|
+
* ...handleAsync<TodoListEntity, void, Todo[]>('fetchTodos', {
|
|
119
|
+
* start(entity) {
|
|
120
|
+
* entity.status = 'loading'
|
|
121
|
+
* },
|
|
122
|
+
* async run(_, api) {
|
|
123
|
+
* const response = await fetch('/api/todos')
|
|
124
|
+
* return response.json()
|
|
125
|
+
* },
|
|
126
|
+
* success(entity, todos) {
|
|
127
|
+
* entity.status = 'success'
|
|
128
|
+
* entity.todos = todos
|
|
129
|
+
* },
|
|
130
|
+
* error(entity, error) {
|
|
131
|
+
* entity.status = 'error'
|
|
132
|
+
* entity.error = error.message
|
|
133
|
+
* }
|
|
134
|
+
* })
|
|
135
|
+
* }
|
|
136
|
+
* ```
|
|
137
|
+
*/
|
|
138
|
+
export function handleAsync<
|
|
139
|
+
TEntity extends BaseEntity = BaseEntity,
|
|
140
|
+
TPayload = any,
|
|
141
|
+
TResult = any,
|
|
142
|
+
>(
|
|
143
|
+
type: string,
|
|
144
|
+
handlers: AsyncHandlers<TEntity, TPayload, TResult>,
|
|
145
|
+
options?: AsyncOptions,
|
|
146
|
+
): AsyncEventHandlers<TEntity, TPayload, TResult>
|
package/types/index.d.ts
CHANGED