@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
package/README.md
CHANGED
|
@@ -433,6 +433,26 @@ store.notify("toggle", "todo1")
|
|
|
433
433
|
// Each list's toggle handler runs; only the one with todo1 actually updates
|
|
434
434
|
```
|
|
435
435
|
|
|
436
|
+
Alternatively, you can use the **targeted notification syntax** to filter events at the dispatch level:
|
|
437
|
+
|
|
438
|
+
- `notify("type:event")`: notifies only entities of a specific type.
|
|
439
|
+
- `notify("#id:event")`: notifies only a specific entity by ID.
|
|
440
|
+
- `notify("type#id:event")`: notifies a specific entity of a specific type.
|
|
441
|
+
|
|
442
|
+
```javascript
|
|
443
|
+
const types = {
|
|
444
|
+
todoList: {
|
|
445
|
+
toggle(entity) {
|
|
446
|
+
const todo = entity.todos.find((t) => t.id === entity.id)
|
|
447
|
+
if (todo) todo.completed = !todo.completed
|
|
448
|
+
},
|
|
449
|
+
},
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Notify only the entity with ID 'work'
|
|
453
|
+
store.notify("#todo1:toggle")
|
|
454
|
+
```
|
|
455
|
+
|
|
436
456
|
### ⚡ Async Operations
|
|
437
457
|
|
|
438
458
|
In **Redux/RTK**, logic should be written inside pure functions as much as possible — specifically in reducers, not action creators. But what if I need to access some other part of the state that is not visible to the reducer? What if I need to combine async behavior with sync behavior? This is where the choice of "where does my logic live?" matters.
|
|
@@ -478,6 +498,192 @@ Notice: you don't need pending/fulfilled/rejected actions. You stay in control o
|
|
|
478
498
|
|
|
479
499
|
All events triggered via `api.notify()` enter the queue and process together, maintaining predictability and testability.
|
|
480
500
|
|
|
501
|
+
### `handleAsync`
|
|
502
|
+
|
|
503
|
+
The `handleAsync` helper generates a set of event handlers representing the lifecycle of an async operation.
|
|
504
|
+
|
|
505
|
+
```ts
|
|
506
|
+
handleAsync(type, handlers, options?)
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
Example:
|
|
510
|
+
|
|
511
|
+
```ts
|
|
512
|
+
handleAsync("fetchTodos", {
|
|
513
|
+
async run(payload) {
|
|
514
|
+
const res = await fetch("/api/todos")
|
|
515
|
+
return res.json()
|
|
516
|
+
},
|
|
517
|
+
|
|
518
|
+
success(entity, todos) {
|
|
519
|
+
entity.todos = todos
|
|
520
|
+
},
|
|
521
|
+
|
|
522
|
+
error(entity, error) {
|
|
523
|
+
entity.error = error.message
|
|
524
|
+
},
|
|
525
|
+
|
|
526
|
+
finally(entity) {
|
|
527
|
+
entity.loading = false
|
|
528
|
+
},
|
|
529
|
+
})
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
---
|
|
533
|
+
|
|
534
|
+
### Lifecycle events
|
|
535
|
+
|
|
536
|
+
Triggering `fetchTodos` emits the following events:
|
|
537
|
+
|
|
538
|
+
```
|
|
539
|
+
fetchTodos
|
|
540
|
+
fetchTodosRun
|
|
541
|
+
fetchTodosSuccess | fetchTodosError
|
|
542
|
+
fetchTodosFinally
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
Each step is an **event handler**, not an implicit callback.
|
|
546
|
+
|
|
547
|
+
---
|
|
548
|
+
|
|
549
|
+
### Optional `start` handler
|
|
550
|
+
|
|
551
|
+
Use `start` for synchronous setup (loading flags, resets, optimistic state):
|
|
552
|
+
|
|
553
|
+
```ts
|
|
554
|
+
handleAsync("save", {
|
|
555
|
+
start(entity) {
|
|
556
|
+
entity.loading = true
|
|
557
|
+
},
|
|
558
|
+
async run(payload) {
|
|
559
|
+
return api.save(payload)
|
|
560
|
+
},
|
|
561
|
+
})
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
If omitted, no `Start` event is generated.
|
|
565
|
+
|
|
566
|
+
---
|
|
567
|
+
|
|
568
|
+
### Event scoping
|
|
569
|
+
|
|
570
|
+
By default, lifecycle events are **scoped to the triggering entity**:
|
|
571
|
+
|
|
572
|
+
```
|
|
573
|
+
#entityId:fetchTodosSuccess
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
You can override this behavior:
|
|
577
|
+
|
|
578
|
+
```ts
|
|
579
|
+
handleAsync("bootstrap", handlers, { scope: "global" })
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
Available scopes:
|
|
583
|
+
|
|
584
|
+
- `"entity"` (default)
|
|
585
|
+
- `"type"`
|
|
586
|
+
- `"global"`
|
|
587
|
+
|
|
588
|
+
---
|
|
589
|
+
|
|
590
|
+
> **Key rule:** Async code must not access entities after `await`. All updates happen in event handlers.
|
|
591
|
+
|
|
592
|
+
---
|
|
593
|
+
|
|
594
|
+
## 🧩 Migrating from Redux Toolkit (RTK)
|
|
595
|
+
|
|
596
|
+
Inglorious Store now provides utilities to **gradually migrate from RTK slices and thunks**, leveraging `handleAsync` to simplify async logic.
|
|
597
|
+
|
|
598
|
+
### Converting Async Thunks
|
|
599
|
+
|
|
600
|
+
```javascript
|
|
601
|
+
import { convertAsyncThunk } from "@inglorious/store/rtk"
|
|
602
|
+
|
|
603
|
+
const fetchTodos = async (userId) => {
|
|
604
|
+
const res = await fetch(`/api/users/${userId}/todos`)
|
|
605
|
+
return res.json()
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const todoHandlers = convertAsyncThunk("fetchTodos", fetchTodos, {
|
|
609
|
+
onPending: (entity) => {
|
|
610
|
+
entity.status = "loading"
|
|
611
|
+
},
|
|
612
|
+
onFulfilled: (entity, todos) => {
|
|
613
|
+
entity.status = "success"
|
|
614
|
+
entity.todos = todos
|
|
615
|
+
},
|
|
616
|
+
onRejected: (entity, error) => {
|
|
617
|
+
entity.status = "error"
|
|
618
|
+
entity.error = error.message
|
|
619
|
+
},
|
|
620
|
+
})
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
```javascript
|
|
624
|
+
const todoList = {
|
|
625
|
+
init(entity) {
|
|
626
|
+
entity.todos = []
|
|
627
|
+
entity.status = "idle"
|
|
628
|
+
},
|
|
629
|
+
...todoHandlers,
|
|
630
|
+
}
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
### Converting Slices
|
|
634
|
+
|
|
635
|
+
```javascript
|
|
636
|
+
import { convertSlice } from "@inglorious/store/rtk"
|
|
637
|
+
|
|
638
|
+
const todoListType = convertSlice(todosSlice, {
|
|
639
|
+
asyncThunks: {
|
|
640
|
+
fetchTodos: {
|
|
641
|
+
onPending: (entity) => {
|
|
642
|
+
entity.status = "loading"
|
|
643
|
+
},
|
|
644
|
+
onFulfilled: (entity, todos) => {
|
|
645
|
+
entity.items = todos
|
|
646
|
+
},
|
|
647
|
+
onRejected: (entity, error) => {
|
|
648
|
+
entity.error = error.message
|
|
649
|
+
},
|
|
650
|
+
},
|
|
651
|
+
},
|
|
652
|
+
})
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
- Reducers become event handlers automatically.
|
|
656
|
+
- Async thunks become `handleAsync` events.
|
|
657
|
+
- Initial state is applied via an `init` handler.
|
|
658
|
+
- Extra handlers can be added if needed.
|
|
659
|
+
|
|
660
|
+
---
|
|
661
|
+
|
|
662
|
+
### RTK-Style Dispatch Compatibility
|
|
663
|
+
|
|
664
|
+
```javascript
|
|
665
|
+
const dispatch = createRTKCompatDispatch(api, "todos")
|
|
666
|
+
dispatch({ type: "todos/addTodo", payload: "Buy milk" })
|
|
667
|
+
// becomes: api.notify('#todos:addTodo', 'Buy milk')
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
> Thunks are **not supported** in compat mode; convert them using `convertAsyncThunk`.
|
|
671
|
+
|
|
672
|
+
---
|
|
673
|
+
|
|
674
|
+
### Migration Guide
|
|
675
|
+
|
|
676
|
+
```javascript
|
|
677
|
+
import { createMigrationGuide } from "@inglorious/store/rtk"
|
|
678
|
+
|
|
679
|
+
const guide = createMigrationGuide(todosSlice)
|
|
680
|
+
console.log(guide)
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
Outputs a readable guide mapping RTK calls to Inglorious events.
|
|
684
|
+
|
|
685
|
+
---
|
|
686
|
+
|
|
481
687
|
### 🧪 Testing
|
|
482
688
|
|
|
483
689
|
Event handlers are pure functions (or can be treated as such), making them easy to test in isolation, much like Redux reducers. The `@inglorious/store/test` module provides utility functions to make this even simpler.
|
|
@@ -794,12 +1000,75 @@ const store = createStore({
|
|
|
794
1000
|
types, // Object: entity type definitions
|
|
795
1001
|
entities, // Object: initial entities
|
|
796
1002
|
systems, // Array (optional): global state handlers
|
|
1003
|
+
autoCreateEntities, // Boolean (optional): false (default) or true
|
|
797
1004
|
updateMode, // String (optional): 'auto' (default) or 'manual'
|
|
798
1005
|
})
|
|
799
1006
|
```
|
|
800
1007
|
|
|
801
1008
|
**Returns:** A Redux-compatible store
|
|
802
1009
|
|
|
1010
|
+
**Options:**
|
|
1011
|
+
|
|
1012
|
+
- **`types`** (required) - Object defining entity type behaviors
|
|
1013
|
+
- **`entities`** (required) - Object containing initial entity instances
|
|
1014
|
+
- **`systems`** (optional) - Array of global state handlers
|
|
1015
|
+
- **`autoCreateEntities`** (optional) - Automatically create singleton entities for types not defined in `entities`:
|
|
1016
|
+
- `false` (default) - Only use explicitly defined entities
|
|
1017
|
+
- `true` - Auto-create entities matching their type name
|
|
1018
|
+
- **`updateMode`** (optional) - Controls when React components re-render:
|
|
1019
|
+
- `'auto'` (default) - Automatic updates after each event
|
|
1020
|
+
- `'manual'` - Manual control via `api.update()`
|
|
1021
|
+
|
|
1022
|
+
#### Auto-Create Entities
|
|
1023
|
+
|
|
1024
|
+
When `autoCreateEntities: true`, the store automatically creates singleton entities for any type that doesn't have a corresponding entity defined. This is particularly useful for singleton-type entities that behave like components, eliminating the need to switch between type definitions and entity declarations.
|
|
1025
|
+
|
|
1026
|
+
```javascript
|
|
1027
|
+
const types = {
|
|
1028
|
+
settings: {
|
|
1029
|
+
setTheme(entity, theme) {
|
|
1030
|
+
entity.theme = theme
|
|
1031
|
+
},
|
|
1032
|
+
},
|
|
1033
|
+
analytics: {
|
|
1034
|
+
track(entity, event) {
|
|
1035
|
+
entity.events.push(event)
|
|
1036
|
+
},
|
|
1037
|
+
},
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
// Without autoCreateEntities (default)
|
|
1041
|
+
const entities = {
|
|
1042
|
+
settings: { type: "settings", theme: "dark" },
|
|
1043
|
+
analytics: { type: "analytics", events: [] },
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
// With autoCreateEntities: true
|
|
1047
|
+
const entities = {
|
|
1048
|
+
// settings and analytics will be auto-created as:
|
|
1049
|
+
// settings: { type: "settings" }
|
|
1050
|
+
// analytics: { type: "analytics" }
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
const store = createStore({
|
|
1054
|
+
types,
|
|
1055
|
+
entities,
|
|
1056
|
+
autoCreateEntities: true,
|
|
1057
|
+
})
|
|
1058
|
+
|
|
1059
|
+
// Both approaches work the same way
|
|
1060
|
+
store.notify("settings:setTheme", "light")
|
|
1061
|
+
store.notify("analytics:track", { action: "click" })
|
|
1062
|
+
```
|
|
1063
|
+
|
|
1064
|
+
**When to use `autoCreateEntities`:**
|
|
1065
|
+
|
|
1066
|
+
- ✅ Building web applications with singleton services (settings, auth, analytics)
|
|
1067
|
+
- ✅ Component-like entities that only need one instance
|
|
1068
|
+
- ✅ Rapid prototyping where you want to add types without ceremony
|
|
1069
|
+
- ❌ Game development with multiple entity instances (players, enemies, items)
|
|
1070
|
+
- ❌ When you need fine control over initial entity state
|
|
1071
|
+
|
|
803
1072
|
### Types Definition
|
|
804
1073
|
|
|
805
1074
|
```javascript
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inglorious/store",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.3.0",
|
|
4
4
|
"description": "A state manager for real-time, collaborative apps, inspired by game development patterns and compatible with Redux.",
|
|
5
5
|
"author": "IceOnFire <antony.mistretta@gmail.com> (https://ingloriouscoderz.it)",
|
|
6
6
|
"license": "MIT",
|
|
@@ -36,6 +36,10 @@
|
|
|
36
36
|
"types": "./types/client/devtools.d.ts",
|
|
37
37
|
"import": "./src/client/devtools.js"
|
|
38
38
|
},
|
|
39
|
+
"./migration/rtk": {
|
|
40
|
+
"types": "./types/migration/rtk.d.ts",
|
|
41
|
+
"import": "./src/migration/rtk.js"
|
|
42
|
+
},
|
|
39
43
|
"./*": {
|
|
40
44
|
"types": "./types/*.d.ts",
|
|
41
45
|
"import": "./src/*"
|
package/src/async.js
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates a set of event handlers for managing an asynchronous operation lifecycle.
|
|
3
|
+
*
|
|
4
|
+
* This helper generates handlers for all stages of an async operation: start, run,
|
|
5
|
+
* success, error, and finally. The lifecycle events are automatically scoped based
|
|
6
|
+
* on the provided options.
|
|
7
|
+
*
|
|
8
|
+
* @template {import('./store').BaseEntity} TEntity - The entity type
|
|
9
|
+
* @template TPayload - The payload type passed to the operation
|
|
10
|
+
* @template TResult - The result type returned by the async operation
|
|
11
|
+
*
|
|
12
|
+
* @param {string} type - The base event name (e.g., 'fetchTodos').
|
|
13
|
+
* Generated handlers will be named: `type`, `typeStart`, `typeRun`, `typeSuccess`, `typeError`, `typeFinally`
|
|
14
|
+
*
|
|
15
|
+
* @param {Object} handlers - The handler functions for each lifecycle stage.
|
|
16
|
+
* @param {(entity: TEntity, payload: TPayload, api: import('./store').Api) => void} [handlers.start]
|
|
17
|
+
* Called synchronously before the async operation starts.
|
|
18
|
+
* Use for setting loading states. Receives: (entity, payload, api)
|
|
19
|
+
* @param {(payload: TPayload, api: import('./store').Api) => Promise<TResult> | TResult} handlers.run
|
|
20
|
+
* The async operation to perform. Must return a Promise or value.
|
|
21
|
+
* **Note:** Receives (payload, api) - NOT entity. Entity state should be modified in other handlers.
|
|
22
|
+
* @param {(entity: TEntity, result: TResult, api: import('./store').Api) => void} [handlers.success]
|
|
23
|
+
* Called when the operation succeeds.
|
|
24
|
+
* Receives: (entity, result, api) where result is the resolved value from run()
|
|
25
|
+
* @param {(entity: TEntity, error: any, api: import('./store').Api) => void} [handlers.error]
|
|
26
|
+
* Called when the operation fails.
|
|
27
|
+
* Receives: (entity, error, api) where error is the caught exception
|
|
28
|
+
* @param {(entity: TEntity, api: import('./store').Api) => void} [handlers.finally]
|
|
29
|
+
* Called after the operation completes (success or failure).
|
|
30
|
+
* Use for cleanup, resetting loading states. Receives: (entity, api)
|
|
31
|
+
*
|
|
32
|
+
* @param {Object} [options] - Configuration options.
|
|
33
|
+
* @param {"entity" | "type" | "global"} [options.scope="entity"]
|
|
34
|
+
* Controls how lifecycle events are routed:
|
|
35
|
+
* - "entity": notify `#entityId:event` (default, safest - events only affect the triggering entity)
|
|
36
|
+
* - "type": notify `typeName:event` (broadcasts to all entities of this type)
|
|
37
|
+
* - "global": notify `event` (global broadcast to any listener)
|
|
38
|
+
*
|
|
39
|
+
* @returns {Object} An object containing the generated event handlers that can be spread into a type:
|
|
40
|
+
* - `[type]`: Main trigger - dispatches Start (if defined) and Run events
|
|
41
|
+
* - `[typeStart]`: (if start handler provided) Executes the start handler
|
|
42
|
+
* - `[typeRun]`: Executes the async operation, then dispatches Success or Error, then Finally
|
|
43
|
+
* - `[typeSuccess]`: Executes the success handler
|
|
44
|
+
* - `[typeError]`: Executes the error handler
|
|
45
|
+
* - `[typeFinally]`: Executes the finally handler
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* // Basic usage - fetch todos with loading state
|
|
49
|
+
* const todoList = {
|
|
50
|
+
* init(entity) {
|
|
51
|
+
* entity.todos = []
|
|
52
|
+
* entity.status = 'idle'
|
|
53
|
+
* },
|
|
54
|
+
*
|
|
55
|
+
* ...handleAsync('fetchTodos', {
|
|
56
|
+
* start(entity) {
|
|
57
|
+
* entity.status = 'loading'
|
|
58
|
+
* },
|
|
59
|
+
* async run(payload, api) {
|
|
60
|
+
* const response = await fetch('/api/todos')
|
|
61
|
+
* return response.json()
|
|
62
|
+
* },
|
|
63
|
+
* success(entity, todos) {
|
|
64
|
+
* entity.status = 'success'
|
|
65
|
+
* entity.todos = todos
|
|
66
|
+
* },
|
|
67
|
+
* error(entity, error) {
|
|
68
|
+
* entity.status = 'error'
|
|
69
|
+
* entity.error = error.message
|
|
70
|
+
* },
|
|
71
|
+
* finally(entity) {
|
|
72
|
+
* entity.lastFetched = Date.now()
|
|
73
|
+
* }
|
|
74
|
+
* })
|
|
75
|
+
* }
|
|
76
|
+
*
|
|
77
|
+
* // Trigger from UI
|
|
78
|
+
* api.notify('#todoList:fetchTodos')
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* // With type scope - affects all entities of this type
|
|
82
|
+
* const counter = {
|
|
83
|
+
* ...handleAsync('sync', {
|
|
84
|
+
* async run(payload, api) {
|
|
85
|
+
* const response = await fetch('/api/sync')
|
|
86
|
+
* return response.json()
|
|
87
|
+
* },
|
|
88
|
+
* success(entity, result) {
|
|
89
|
+
* entity.synced = true
|
|
90
|
+
* entity.lastSync = result.timestamp
|
|
91
|
+
* }
|
|
92
|
+
* }, { scope: 'type' })
|
|
93
|
+
* }
|
|
94
|
+
*
|
|
95
|
+
* // Triggers sync for ALL counter entities
|
|
96
|
+
* api.notify('counter:sync')
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* // Minimal - just run and success
|
|
100
|
+
* const user = {
|
|
101
|
+
* ...handleAsync('login', {
|
|
102
|
+
* async run({ username, password }, api) {
|
|
103
|
+
* const response = await fetch('/api/login', {
|
|
104
|
+
* method: 'POST',
|
|
105
|
+
* body: JSON.stringify({ username, password })
|
|
106
|
+
* })
|
|
107
|
+
* return response.json()
|
|
108
|
+
* },
|
|
109
|
+
* success(entity, user) {
|
|
110
|
+
* entity.currentUser = user
|
|
111
|
+
* entity.isAuthenticated = true
|
|
112
|
+
* }
|
|
113
|
+
* })
|
|
114
|
+
* }
|
|
115
|
+
*
|
|
116
|
+
* @example
|
|
117
|
+
* // Without start handler
|
|
118
|
+
* const data = {
|
|
119
|
+
* ...handleAsync('fetch', {
|
|
120
|
+
* async run(payload, api) {
|
|
121
|
+
* return fetch(`/api/data/${payload.id}`).then(r => r.json())
|
|
122
|
+
* },
|
|
123
|
+
* success(entity, result) {
|
|
124
|
+
* entity.data = result
|
|
125
|
+
* },
|
|
126
|
+
* error(entity, error) {
|
|
127
|
+
* console.error('Fetch failed:', error)
|
|
128
|
+
* }
|
|
129
|
+
* })
|
|
130
|
+
* }
|
|
131
|
+
*/
|
|
132
|
+
export function handleAsync(type, handlers, options = {}) {
|
|
133
|
+
const { scope = "entity" } = options
|
|
134
|
+
|
|
135
|
+
function notify(api, entity, event, payload) {
|
|
136
|
+
switch (scope) {
|
|
137
|
+
case "entity":
|
|
138
|
+
api.notify(`#${entity.id}:${event}`, payload)
|
|
139
|
+
break
|
|
140
|
+
case "type":
|
|
141
|
+
api.notify(`${entity.type}:${event}`, payload)
|
|
142
|
+
break
|
|
143
|
+
case "global":
|
|
144
|
+
api.notify(event, payload)
|
|
145
|
+
break
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
[type](entity, payload, api) {
|
|
151
|
+
if (handlers.start) {
|
|
152
|
+
notify(api, entity, `${type}Start`, payload)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
notify(api, entity, `${type}Run`, payload)
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
...(handlers.start && {
|
|
159
|
+
[`${type}Start`](entity, payload, api) {
|
|
160
|
+
handlers.start(entity, payload, api)
|
|
161
|
+
},
|
|
162
|
+
}),
|
|
163
|
+
|
|
164
|
+
async [`${type}Run`](entity, payload, api) {
|
|
165
|
+
try {
|
|
166
|
+
const result = await handlers.run(payload, api)
|
|
167
|
+
notify(api, entity, `${type}Success`, result)
|
|
168
|
+
} catch (error) {
|
|
169
|
+
notify(api, entity, `${type}Error`, error)
|
|
170
|
+
} finally {
|
|
171
|
+
notify(api, entity, `${type}Finally`)
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
|
|
175
|
+
[`${type}Success`](entity, result, api) {
|
|
176
|
+
handlers.success?.(entity, result, api)
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
[`${type}Error`](entity, error, api) {
|
|
180
|
+
handlers.error?.(entity, error, api)
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
[`${type}Finally`](entity, _, api) {
|
|
184
|
+
handlers.finally?.(entity, api)
|
|
185
|
+
},
|
|
186
|
+
}
|
|
187
|
+
}
|