@inglorious/store 11.1.0 → 11.2.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/package.json +1 -1
- package/src/optimistic.js +5 -4
- package/types/async.d.ts +60 -25
- package/types/optimistic.d.ts +15 -4
- package/types/store.d.ts +5 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inglorious/store",
|
|
3
|
-
"version": "11.
|
|
3
|
+
"version": "11.2.0",
|
|
4
4
|
"description": "A state manager for real-time, collaborative apps, inspired by game development patterns and compatible with Redux.",
|
|
5
5
|
"author": "Matteo Antony Mistretta <antony.mistretta@gmail.com> (https://ingloriouscoderz.it)",
|
|
6
6
|
"license": "MIT",
|
package/src/optimistic.js
CHANGED
|
@@ -11,12 +11,13 @@ const START = 0
|
|
|
11
11
|
* @template {import('./store').BaseEntity} TEntity
|
|
12
12
|
* @template TPayload
|
|
13
13
|
* @template TResult
|
|
14
|
+
* @template {string} TType
|
|
14
15
|
*
|
|
15
|
-
* @param {import('./async').
|
|
16
|
+
* @param {import('./async').AsyncEvents<TType, TEntity, TPayload, TResult>} behavior
|
|
16
17
|
* @param {Record<string, any> | ((entity: TEntity, payload: TPayload, api: import('./store').Api) => Record<string, any> | null | undefined)} optimistic
|
|
17
18
|
* Shallow patch applied optimistically on start, or a factory that returns it.
|
|
18
19
|
*
|
|
19
|
-
* @returns {import('./async').
|
|
20
|
+
* @returns {import('./async').AsyncEvents<TType, TEntity, TPayload, TResult>}
|
|
20
21
|
*
|
|
21
22
|
* @example
|
|
22
23
|
* const saveItem = optimistic(
|
|
@@ -108,9 +109,9 @@ export function optimistic(behavior, optimistic) {
|
|
|
108
109
|
behavior[errorEvent]?.(entity, error, api)
|
|
109
110
|
},
|
|
110
111
|
|
|
111
|
-
[finallyEvent](entity, api) {
|
|
112
|
+
[finallyEvent](entity, payload, api) {
|
|
112
113
|
try {
|
|
113
|
-
behavior[finallyEvent]?.(entity, api)
|
|
114
|
+
behavior[finallyEvent]?.(entity, payload, api)
|
|
114
115
|
} finally {
|
|
115
116
|
snapshots.delete(getEntityKey(entity))
|
|
116
117
|
}
|
package/types/async.d.ts
CHANGED
|
@@ -27,57 +27,83 @@ export interface AsyncHandlers<
|
|
|
27
27
|
TEntity extends BaseEntity = BaseEntity,
|
|
28
28
|
TPayload = any,
|
|
29
29
|
TResult = any,
|
|
30
|
+
TApi extends Api = Api,
|
|
30
31
|
> {
|
|
31
32
|
/**
|
|
32
33
|
* Called synchronously before the async operation starts
|
|
33
34
|
* Use for setting loading states
|
|
34
35
|
*/
|
|
35
|
-
start?: (entity: TEntity, payload: TPayload, api:
|
|
36
|
+
start?: (entity: TEntity, payload: TPayload, api: TApi) => void
|
|
36
37
|
|
|
37
38
|
/**
|
|
38
39
|
* The async operation to perform
|
|
39
40
|
* Receives payload and api (NOT entity - entity state should be modified in other handlers)
|
|
40
41
|
* @returns Promise or synchronous result
|
|
41
42
|
*/
|
|
42
|
-
run: (payload: TPayload, api:
|
|
43
|
+
run: (payload: TPayload, api: TApi) => Promise<TResult> | TResult
|
|
43
44
|
|
|
44
45
|
/**
|
|
45
46
|
* Called when the operation succeeds
|
|
46
47
|
* Receives the entity, the result from run(), and api
|
|
47
48
|
*/
|
|
48
|
-
success?: (entity: TEntity, result: TResult, api:
|
|
49
|
+
success?: (entity: TEntity, result: TResult, api: TApi) => void
|
|
49
50
|
|
|
50
51
|
/**
|
|
51
52
|
* Called when the operation fails
|
|
52
53
|
* Receives the entity, the error, and api
|
|
53
54
|
*/
|
|
54
|
-
error?: (entity: TEntity, error: any, api:
|
|
55
|
+
error?: (entity: TEntity, error: any, api: TApi) => void
|
|
55
56
|
|
|
56
57
|
/**
|
|
57
58
|
* Called after the operation completes (success or failure)
|
|
58
59
|
* Use for cleanup, resetting loading states, etc.
|
|
59
60
|
*/
|
|
60
|
-
finally?: (entity: TEntity, api:
|
|
61
|
+
finally?: (entity: TEntity, api: TApi) => void
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
/**
|
|
64
|
-
*
|
|
65
|
-
*
|
|
65
|
+
* The events `handleAsync(type, ...)` generates, by name: `type`, `typeRun`,
|
|
66
|
+
* `typeSuccess`, `typeError`, `typeFinally` and, when a `start` handler is
|
|
67
|
+
* given, `typeStart`. A type alias can list them all in one go:
|
|
68
|
+
*
|
|
69
|
+
* ```ts
|
|
70
|
+
* type TodoListType = {
|
|
71
|
+
* render: (entity: TodoListEntity, api: AppApi) => TemplateResult
|
|
72
|
+
* } & AsyncEvents<"fetchTodos", TodoListEntity, void, Todo[], AppApi>
|
|
73
|
+
* ```
|
|
74
|
+
*
|
|
75
|
+
* The `scope` option changes where the events are sent, not their names.
|
|
66
76
|
*/
|
|
67
|
-
export
|
|
77
|
+
export type AsyncEvents<
|
|
78
|
+
TType extends string,
|
|
68
79
|
TEntity extends BaseEntity = BaseEntity,
|
|
69
80
|
TPayload = any,
|
|
70
81
|
TResult = any,
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
[key: string]: (
|
|
82
|
+
TApi extends Api = Api,
|
|
83
|
+
> = {
|
|
84
|
+
[K in TType]: (entity: TEntity, payload: TPayload, api: TApi) => void
|
|
85
|
+
} & {
|
|
86
|
+
[K in `${TType}Start`]?: (
|
|
77
87
|
entity: TEntity,
|
|
78
88
|
payload: TPayload,
|
|
79
|
-
api:
|
|
80
|
-
) => void
|
|
89
|
+
api: TApi,
|
|
90
|
+
) => void
|
|
91
|
+
} & {
|
|
92
|
+
[K in `${TType}Run`]: (
|
|
93
|
+
entity: TEntity,
|
|
94
|
+
payload: TPayload,
|
|
95
|
+
api: TApi,
|
|
96
|
+
) => Promise<void>
|
|
97
|
+
} & {
|
|
98
|
+
[K in `${TType}Success`]: (
|
|
99
|
+
entity: TEntity,
|
|
100
|
+
result: TResult,
|
|
101
|
+
api: TApi,
|
|
102
|
+
) => void
|
|
103
|
+
} & {
|
|
104
|
+
[K in `${TType}Error`]: (entity: TEntity, error: any, api: TApi) => void
|
|
105
|
+
} & {
|
|
106
|
+
[K in `${TType}Finally`]: (entity: TEntity, payload: void, api: TApi) => void
|
|
81
107
|
}
|
|
82
108
|
|
|
83
109
|
/**
|
|
@@ -86,9 +112,16 @@ export interface AsyncEventHandlers<
|
|
|
86
112
|
* Generates handlers for: trigger, start, run, success, error, finally
|
|
87
113
|
* These can be spread into an entity type definition.
|
|
88
114
|
*
|
|
115
|
+
* The event name, the entity, the payload, the result and the api are inferred
|
|
116
|
+
* from `type` and from the handlers' parameters, or from the type the handlers
|
|
117
|
+
* are spread into (`satisfies EntityType<TodoListEntity, AppApi>`). Explicit
|
|
118
|
+
* type arguments still work, but the event names are then plain strings.
|
|
119
|
+
*
|
|
89
120
|
* @template TEntity - The entity type (must extend BaseEntity)
|
|
90
121
|
* @template TPayload - The payload type passed to the operation
|
|
91
122
|
* @template TResult - The result type returned by the async operation
|
|
123
|
+
* @template TApi - The api the handlers receive, such as the app's own
|
|
124
|
+
* @template TType - The base event name, inferred from `type`
|
|
92
125
|
*
|
|
93
126
|
* @param type - The base event name (e.g., 'fetchTodos')
|
|
94
127
|
* @param handlers - Handler functions for each lifecycle stage
|
|
@@ -104,23 +137,23 @@ export interface AsyncEventHandlers<
|
|
|
104
137
|
* }
|
|
105
138
|
*
|
|
106
139
|
* interface TodoListEntity extends BaseEntity {
|
|
107
|
-
* type: '
|
|
140
|
+
* type: 'TodoList'
|
|
108
141
|
* todos: Todo[]
|
|
109
142
|
* status: 'idle' | 'loading' | 'success' | 'error'
|
|
110
143
|
* error?: string
|
|
111
144
|
* }
|
|
112
145
|
*
|
|
113
|
-
* const
|
|
114
|
-
*
|
|
146
|
+
* const TodoList = {
|
|
147
|
+
* create(entity) {
|
|
115
148
|
* entity.todos = []
|
|
116
149
|
* entity.status = 'idle'
|
|
117
150
|
* },
|
|
118
151
|
*
|
|
119
|
-
* ...handleAsync
|
|
152
|
+
* ...handleAsync('fetchTodos', {
|
|
120
153
|
* start(entity) {
|
|
121
154
|
* entity.status = 'loading'
|
|
122
155
|
* },
|
|
123
|
-
* async run(_, api) {
|
|
156
|
+
* async run(_: void, api): Promise<Todo[]> {
|
|
124
157
|
* const response = await fetch('/api/todos')
|
|
125
158
|
* return response.json()
|
|
126
159
|
* },
|
|
@@ -133,15 +166,17 @@ export interface AsyncEventHandlers<
|
|
|
133
166
|
* entity.error = error.message
|
|
134
167
|
* }
|
|
135
168
|
* })
|
|
136
|
-
* }
|
|
169
|
+
* } satisfies EntityType<TodoListEntity, AppApi>
|
|
137
170
|
* ```
|
|
138
171
|
*/
|
|
139
172
|
export function handleAsync<
|
|
140
173
|
TEntity extends BaseEntity = BaseEntity,
|
|
141
174
|
TPayload = any,
|
|
142
175
|
TResult = any,
|
|
176
|
+
TApi extends Api = Api,
|
|
177
|
+
TType extends string = string,
|
|
143
178
|
>(
|
|
144
|
-
type:
|
|
145
|
-
handlers: AsyncHandlers<TEntity, TPayload, TResult>,
|
|
179
|
+
type: TType,
|
|
180
|
+
handlers: AsyncHandlers<TEntity, TPayload, TResult, TApi>,
|
|
146
181
|
options?: AsyncOptions,
|
|
147
|
-
):
|
|
182
|
+
): AsyncEvents<TType, TEntity, TPayload, TResult, TApi>
|
package/types/optimistic.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Api } from "./api"
|
|
2
|
-
import {
|
|
2
|
+
import { AsyncEvents } from "./async"
|
|
3
3
|
import { BaseEntity } from "./store"
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -8,18 +8,29 @@ import { BaseEntity } from "./store"
|
|
|
8
8
|
* Pass either a static shallow patch object or a factory that returns one.
|
|
9
9
|
* The wrapper snapshots the current values for those keys, applies the patch
|
|
10
10
|
* on start, and restores the previous values automatically if the operation fails.
|
|
11
|
+
*
|
|
12
|
+
* The behavior keeps the events `handleAsync` named and typed, and always has
|
|
13
|
+
* a start event.
|
|
11
14
|
*/
|
|
12
15
|
export function optimistic<
|
|
13
16
|
TEntity extends BaseEntity = BaseEntity,
|
|
14
17
|
TPayload = any,
|
|
15
18
|
TResult = any,
|
|
19
|
+
TApi extends Api = Api,
|
|
20
|
+
TType extends string = string,
|
|
16
21
|
>(
|
|
17
|
-
behavior:
|
|
22
|
+
behavior: AsyncEvents<TType, TEntity, TPayload, TResult, TApi>,
|
|
18
23
|
optimistic:
|
|
19
24
|
| Record<string, any>
|
|
20
25
|
| ((
|
|
21
26
|
entity: TEntity,
|
|
22
27
|
payload: TPayload,
|
|
23
|
-
api:
|
|
28
|
+
api: TApi,
|
|
24
29
|
) => Record<string, any> | null | undefined),
|
|
25
|
-
):
|
|
30
|
+
): AsyncEvents<TType, TEntity, TPayload, TResult, TApi> & {
|
|
31
|
+
[K in `${TType}Start`]: (
|
|
32
|
+
entity: TEntity,
|
|
33
|
+
payload: TPayload,
|
|
34
|
+
api: TApi,
|
|
35
|
+
) => void
|
|
36
|
+
}
|
package/types/store.d.ts
CHANGED
|
@@ -41,7 +41,7 @@ export interface Event<T = any> {
|
|
|
41
41
|
*/
|
|
42
42
|
export type EventHandler<
|
|
43
43
|
TEntity extends BaseEntity = BaseEntity,
|
|
44
|
-
TApi extends Api
|
|
44
|
+
TApi extends Api = Api,
|
|
45
45
|
> = (entity: Stored<TEntity>, payload: any, api: TApi) => unknown
|
|
46
46
|
|
|
47
47
|
/**
|
|
@@ -75,7 +75,7 @@ export type EventHandler<
|
|
|
75
75
|
*/
|
|
76
76
|
export type EntityType<
|
|
77
77
|
TEntity extends BaseEntity = BaseEntity,
|
|
78
|
-
TApi extends Api
|
|
78
|
+
TApi extends Api = Api,
|
|
79
79
|
> = {
|
|
80
80
|
/** Draws the entity. Unlike an event handler, it receives the api second. */
|
|
81
81
|
render?: Renderer<TEntity, TApi>
|
|
@@ -85,7 +85,7 @@ export type EntityType<
|
|
|
85
85
|
/** A type's `render`: it receives the entity and the api. */
|
|
86
86
|
export type Renderer<
|
|
87
87
|
TEntity extends BaseEntity = BaseEntity,
|
|
88
|
-
TApi extends Api
|
|
88
|
+
TApi extends Api = Api,
|
|
89
89
|
> = (entity: Stored<TEntity>, api: TApi) => unknown
|
|
90
90
|
|
|
91
91
|
/**
|
|
@@ -94,7 +94,7 @@ export type Renderer<
|
|
|
94
94
|
*/
|
|
95
95
|
export type Behavior<
|
|
96
96
|
TEntity extends BaseEntity = BaseEntity,
|
|
97
|
-
TApi extends Api
|
|
97
|
+
TApi extends Api = Api,
|
|
98
98
|
> =
|
|
99
99
|
| EntityType<TEntity, TApi>
|
|
100
100
|
| ((type: EntityType<TEntity, TApi>) => EntityType<TEntity, TApi>)
|
|
@@ -102,7 +102,7 @@ export type Behavior<
|
|
|
102
102
|
/** What the store's `types` holds for one type name: a behavior, or several. */
|
|
103
103
|
export type TypeDefinition<
|
|
104
104
|
TEntity extends BaseEntity = BaseEntity,
|
|
105
|
-
TApi extends Api
|
|
105
|
+
TApi extends Api = Api,
|
|
106
106
|
> = Behavior<TEntity, TApi> | Behavior<TEntity, TApi>[]
|
|
107
107
|
|
|
108
108
|
/**
|