@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inglorious/store",
3
- "version": "11.1.0",
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').AsyncEventHandlers<TEntity, TPayload, TResult>} behavior
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').AsyncEventHandlers<TEntity, TPayload, TResult>}
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: Api) => void
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: Api) => Promise<TResult> | TResult
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: Api) => void
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: Api) => void
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: Api) => void
61
+ finally?: (entity: TEntity, api: TApi) => void
61
62
  }
62
63
 
63
64
  /**
64
- * Generated event handlers returned by handleAsync
65
- * Contains handlers for all lifecycle stages
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 interface AsyncEventHandlers<
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
- * Main trigger handler
74
- * Dispatches start (if defined) and run events
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: Api,
80
- ) => void | Promise<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: 'todoList'
140
+ * type: 'TodoList'
108
141
  * todos: Todo[]
109
142
  * status: 'idle' | 'loading' | 'success' | 'error'
110
143
  * error?: string
111
144
  * }
112
145
  *
113
- * const todoList = {
114
- * init(entity: TodoListEntity) {
146
+ * const TodoList = {
147
+ * create(entity) {
115
148
  * entity.todos = []
116
149
  * entity.status = 'idle'
117
150
  * },
118
151
  *
119
- * ...handleAsync<TodoListEntity, void, Todo[]>('fetchTodos', {
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: string,
145
- handlers: AsyncHandlers<TEntity, TPayload, TResult>,
179
+ type: TType,
180
+ handlers: AsyncHandlers<TEntity, TPayload, TResult, TApi>,
146
181
  options?: AsyncOptions,
147
- ): AsyncEventHandlers<TEntity, TPayload, TResult>
182
+ ): AsyncEvents<TType, TEntity, TPayload, TResult, TApi>
@@ -1,5 +1,5 @@
1
1
  import { Api } from "./api"
2
- import { AsyncEventHandlers } from "./async"
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: AsyncEventHandlers<TEntity, TPayload, TResult>,
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: Api,
28
+ api: TApi,
24
29
  ) => Record<string, any> | null | undefined),
25
- ): AsyncEventHandlers<TEntity, TPayload, TResult>
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<any, any> = 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<any, any> = 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<any, any> = 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<any, any> = 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<any, any> = Api,
105
+ TApi extends Api = Api,
106
106
  > = Behavior<TEntity, TApi> | Behavior<TEntity, TApi>[]
107
107
 
108
108
  /**