@inglorious/store 12.1.1 → 12.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 +74 -2
- package/package.json +4 -4
- package/src/behaviors/async.js +29 -10
- package/src/behaviors/optimistic.js +84 -41
- package/src/behaviors/state-machine/fsm.js +0 -6
- package/src/behaviors/state-machine/scenes.js +0 -6
- package/src/client/devtools.js +10 -34
- package/src/client/multiplayer-middleware.js +1 -14
- package/src/event-map.js +4 -2
- package/src/migration/rtk.js +0 -24
- package/src/store.js +20 -50
- package/types/api.d.ts +10 -4
- package/types/behaviors/async.d.ts +19 -5
- package/types/behaviors/optimistic.d.ts +9 -2
- package/types/events.d.ts +149 -0
- package/types/index.d.ts +1 -0
- package/types/migration/rtk.d.ts +0 -2
- package/types/store.d.ts +78 -6
package/src/migration/rtk.js
CHANGED
|
@@ -76,8 +76,6 @@ export function convertAsyncThunk(name, payloadCreator, options = {}) {
|
|
|
76
76
|
: undefined,
|
|
77
77
|
|
|
78
78
|
run: async (payload, api) => {
|
|
79
|
-
// RTK payloadCreator signature: (arg, thunkAPI)
|
|
80
|
-
// Use dispatch for action compatibility, with notify fallback in tests/minimal APIs.
|
|
81
79
|
const dispatch = api.dispatch
|
|
82
80
|
? api.dispatch.bind(api)
|
|
83
81
|
: (action) => {
|
|
@@ -170,15 +168,12 @@ export function convertSlice(slice, options = {}) {
|
|
|
170
168
|
const { asyncThunks = {}, extraHandlers = {}, extraActions = [] } = options
|
|
171
169
|
|
|
172
170
|
const type = {
|
|
173
|
-
// Convert initialState to create handler.
|
|
174
|
-
// Preserve explicitly preloaded fields from entities (common in tests/SSR hydration).
|
|
175
171
|
create(entity) {
|
|
176
172
|
const initialState = slice.getInitialState()
|
|
177
173
|
Object.assign(entity, extend(initialState, entity))
|
|
178
174
|
},
|
|
179
175
|
}
|
|
180
176
|
|
|
181
|
-
// Convert each reducer to an event handler
|
|
182
177
|
for (const [actionName, reducer] of Object.entries(slice.caseReducers)) {
|
|
183
178
|
const thunkConfig = asyncThunks[actionName]
|
|
184
179
|
const isAsyncThunkReducer =
|
|
@@ -249,22 +244,15 @@ export function convertSlice(slice, options = {}) {
|
|
|
249
244
|
}
|
|
250
245
|
|
|
251
246
|
const handler = (entity, payload) => {
|
|
252
|
-
// Create a mock action object for the RTK reducer
|
|
253
247
|
const action = { type: `${slice.name}/${actionName}`, payload }
|
|
254
248
|
|
|
255
|
-
// RTK reducers expect to mutate state directly (via Immer)
|
|
256
|
-
// Inglorious handlers do the same (via Mutative)
|
|
257
|
-
// So we can call the reducer directly on the entity
|
|
258
249
|
reducer(entity, action)
|
|
259
250
|
}
|
|
260
251
|
|
|
261
|
-
// Expose both Inglorious-style event names and RTK action-type aliases.
|
|
262
|
-
// This enables direct dispatch of RTK slice actions without extra bridge wiring.
|
|
263
252
|
type[actionName] = handler
|
|
264
253
|
type[`${slice.name}/${actionName}`] = handler
|
|
265
254
|
}
|
|
266
255
|
|
|
267
|
-
// Convert explicitly configured async thunks that are not embedded in slice reducers.
|
|
268
256
|
for (const [thunkName, thunkHandlers] of Object.entries(asyncThunks)) {
|
|
269
257
|
if (thunkHandlers?.payloadCreator && !type[thunkName]) {
|
|
270
258
|
const asyncHandlers = convertAsyncThunk(
|
|
@@ -283,9 +271,6 @@ export function convertSlice(slice, options = {}) {
|
|
|
283
271
|
}
|
|
284
272
|
}
|
|
285
273
|
|
|
286
|
-
// Register additional RTK action types (e.g. createAction + extraReducers).
|
|
287
|
-
// RTK does not expose extraReducers through slice.caseReducers, so callers can
|
|
288
|
-
// provide the action creators/types explicitly and we route them through slice.reducer.
|
|
289
274
|
for (const action of extraActions) {
|
|
290
275
|
const actionType = typeof action === "string" ? action : action?.type
|
|
291
276
|
if (typeof actionType !== "string" || type[actionType]) continue
|
|
@@ -295,7 +280,6 @@ export function convertSlice(slice, options = {}) {
|
|
|
295
280
|
}
|
|
296
281
|
}
|
|
297
282
|
|
|
298
|
-
// Add any extra handlers
|
|
299
283
|
Object.assign(type, extraHandlers)
|
|
300
284
|
|
|
301
285
|
return type
|
|
@@ -330,7 +314,6 @@ export function createMigrationGuide(slice) {
|
|
|
330
314
|
lines.push("=".repeat(SEP_LENGTH))
|
|
331
315
|
lines.push("")
|
|
332
316
|
|
|
333
|
-
// Show state structure
|
|
334
317
|
lines.push("STATE STRUCTURE:")
|
|
335
318
|
lines.push("RTK:")
|
|
336
319
|
lines.push(
|
|
@@ -350,7 +333,6 @@ export function createMigrationGuide(slice) {
|
|
|
350
333
|
lines.push("=".repeat(SEP_LENGTH))
|
|
351
334
|
lines.push("")
|
|
352
335
|
|
|
353
|
-
// Show each reducer conversion
|
|
354
336
|
lines.push("ACTIONS / EVENTS:")
|
|
355
337
|
for (const actionName of Object.keys(slice.caseReducers)) {
|
|
356
338
|
lines.push("")
|
|
@@ -365,7 +347,6 @@ export function createMigrationGuide(slice) {
|
|
|
365
347
|
lines.push("=".repeat(SEP_LENGTH))
|
|
366
348
|
lines.push("")
|
|
367
349
|
|
|
368
|
-
// Show selector conversion
|
|
369
350
|
lines.push("SELECTORS:")
|
|
370
351
|
lines.push("RTK:")
|
|
371
352
|
lines.push(
|
|
@@ -403,22 +384,17 @@ export function createMigrationGuide(slice) {
|
|
|
403
384
|
export function createRTKCompatDispatch(api, entityId) {
|
|
404
385
|
return (action) => {
|
|
405
386
|
if (typeof action === "function") {
|
|
406
|
-
// Thunk - not supported in compat mode
|
|
407
387
|
console.warn("Thunks are not supported in RTK compat mode")
|
|
408
388
|
return
|
|
409
389
|
}
|
|
410
390
|
|
|
411
|
-
// Extract action type and payload
|
|
412
391
|
const { type, payload } = action
|
|
413
392
|
|
|
414
|
-
// Convert RTK action type to Inglorious event
|
|
415
|
-
// RTK: 'todos/addTodo' -> Inglorious: '#todos:addTodo'
|
|
416
393
|
const parts = type.split("/")
|
|
417
394
|
if (parts.length === SLICE_PLUS_ACTION) {
|
|
418
395
|
const [, actionName] = parts
|
|
419
396
|
api.notify(`#${entityId}:${actionName}`, payload)
|
|
420
397
|
} else {
|
|
421
|
-
// Fallback for non-standard action types
|
|
422
398
|
api.notify(`#${entityId}:${type}`, payload)
|
|
423
399
|
}
|
|
424
400
|
}
|
package/src/store.js
CHANGED
|
@@ -72,7 +72,7 @@ export function createStore({
|
|
|
72
72
|
subscribe,
|
|
73
73
|
update,
|
|
74
74
|
notify,
|
|
75
|
-
dispatch,
|
|
75
|
+
dispatch,
|
|
76
76
|
getTypes,
|
|
77
77
|
getType,
|
|
78
78
|
setType,
|
|
@@ -119,10 +119,6 @@ export function createStore({
|
|
|
119
119
|
return []
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
// Nothing queued means nothing to apply and nothing to publish: skipping the draft
|
|
123
|
-
// entirely, rather than copying the world just to arrive back where it started.
|
|
124
|
-
// This is what keeps a loop that pumps every animation frame cheap on the frames
|
|
125
|
-
// where neither the game nor the player had anything to say.
|
|
126
122
|
if (!incomingEvents.length) {
|
|
127
123
|
return []
|
|
128
124
|
}
|
|
@@ -172,9 +168,6 @@ export function createStore({
|
|
|
172
168
|
const event = incomingEvents.shift()
|
|
173
169
|
processedEvents.push(event)
|
|
174
170
|
|
|
175
|
-
// Pausing and resuming are the store's own, and they keep dispatching like any
|
|
176
|
-
// other event, so the game behaviour still sees them and anything else that
|
|
177
|
-
// wants to react gets the same notification.
|
|
178
171
|
if (event.type === "pause") {
|
|
179
172
|
isHalted = true
|
|
180
173
|
}
|
|
@@ -183,24 +176,6 @@ export function createStore({
|
|
|
183
176
|
isHalted = false
|
|
184
177
|
}
|
|
185
178
|
|
|
186
|
-
// Handle special system events
|
|
187
|
-
// `add` and `remove` change the world and are then dispatched like any
|
|
188
|
-
// other event, so that whatever is watching can hear about it. They are
|
|
189
|
-
// broadcast: the entity named by the payload is not the only one that wants
|
|
190
|
-
// to know, and something counting down what it put up needs to hear about
|
|
191
|
-
// each of them going.
|
|
192
|
-
//
|
|
193
|
-
// They are not the counterpart to `create` and `destroy`, which reach only
|
|
194
|
-
// the entity they are about so that those read as a constructor and a
|
|
195
|
-
// destructor without every handler beginning by checking whether it is the
|
|
196
|
-
// one being talked about.
|
|
197
|
-
//
|
|
198
|
-
// The two carry different payloads, and that is on purpose: a payload
|
|
199
|
-
// carries the least that can be had. A removal needs only the id, because
|
|
200
|
-
// everything else about the entity can still be looked up by it. An
|
|
201
|
-
// addition carries the entity itself, because there is nothing yet to look
|
|
202
|
-
// it up by -- the same asymmetry as removing a user by id but updating one
|
|
203
|
-
// by the patched user.
|
|
204
179
|
if (event.type === "add") {
|
|
205
180
|
addEntity(draft, event.payload)
|
|
206
181
|
}
|
|
@@ -209,21 +184,6 @@ export function createStore({
|
|
|
209
184
|
removeEntity(draft, event.payload)
|
|
210
185
|
}
|
|
211
186
|
|
|
212
|
-
// A thing that stands through a change of state can still be told a different
|
|
213
|
-
// configuration, in either of the two ways the world offers.
|
|
214
|
-
//
|
|
215
|
-
// `patch` changes some of it and leaves the rest alone, which is how a screen
|
|
216
|
-
// takes over one field of something it shares with the screen before it. A field
|
|
217
|
-
// named as `undefined` is set to nothing rather than being left alone, which is
|
|
218
|
-
// how a key is taken off a mapping.
|
|
219
|
-
//
|
|
220
|
-
// `replace` installs what it is given and drops everything it does not mention,
|
|
221
|
-
// which is how the whole of something is said to be different.
|
|
222
|
-
//
|
|
223
|
-
// Neither takes the thing out of the world, so neither runs `create` or `destroy`:
|
|
224
|
-
// it never stopped standing, and a thing that was never removed cannot have been
|
|
225
|
-
// destroyed. This is the same pair the world has to hand for anything else --
|
|
226
|
-
// `add` and `remove` beside them, and post/delete/put/patch before that.
|
|
227
187
|
if (event.type === "patch") {
|
|
228
188
|
patchEntity(draft, event.payload)
|
|
229
189
|
}
|
|
@@ -232,10 +192,8 @@ export function createStore({
|
|
|
232
192
|
replaceEntity(draft, event.payload)
|
|
233
193
|
}
|
|
234
194
|
|
|
235
|
-
// Parse the event to get handler name
|
|
236
195
|
const { event: handlerName } = parseEvent(event.type)
|
|
237
196
|
|
|
238
|
-
// Get entities that should handle this event (filtered by EventMap)
|
|
239
197
|
const entityIds = eventMap.getEntitiesForEvent(event.type)
|
|
240
198
|
|
|
241
199
|
// A halted world stops handing out updates, because anything that moves by
|
|
@@ -248,8 +206,6 @@ export function createStore({
|
|
|
248
206
|
const halted = isHalted && handlerName === "update"
|
|
249
207
|
|
|
250
208
|
for (const id of entityIds) {
|
|
251
|
-
// Looked at before it is taken, because an entity the pause holds back is
|
|
252
|
-
// not going to be written to, and taking it would copy it for nothing.
|
|
253
209
|
const entity = draft.entities[id]
|
|
254
210
|
const type = types[entity.type]
|
|
255
211
|
|
|
@@ -273,7 +229,6 @@ export function createStore({
|
|
|
273
229
|
}
|
|
274
230
|
}
|
|
275
231
|
|
|
276
|
-
// Systems process events by handler name (not scoped)
|
|
277
232
|
systems.forEach((system) => {
|
|
278
233
|
const handle = system[handlerName]
|
|
279
234
|
handle?.(draft.forSystems, event.payload, api)
|
|
@@ -338,7 +293,8 @@ export function createStore({
|
|
|
338
293
|
* Sets an augmented type configuration given its name. Replacing a type, as
|
|
339
294
|
* hot module reloading does, notifies subscribers, since what its entities
|
|
340
295
|
* render may have changed. Adding a type does not: the store is unchanged,
|
|
341
|
-
* and a type can be added while rendering.
|
|
296
|
+
* and a type can be added while rendering. Entities already of a type being
|
|
297
|
+
* added get their `create` now: when they were added, it reached no handler.
|
|
342
298
|
* @param {string} typeName - The name of the type to set.
|
|
343
299
|
* @param {Type} type - The type configuration.
|
|
344
300
|
* @returns {void}
|
|
@@ -354,6 +310,7 @@ export function createStore({
|
|
|
354
310
|
if (entity.type === typeName) {
|
|
355
311
|
eventMap.removeEntity(id, oldType, typeName)
|
|
356
312
|
eventMap.addEntity(id, newType, typeName)
|
|
313
|
+
if (!oldType) notify(`#${id}:create`)
|
|
357
314
|
}
|
|
358
315
|
}
|
|
359
316
|
|
|
@@ -516,9 +473,6 @@ export function createStore({
|
|
|
516
473
|
// does not mention, and what it keeps must not be shared with the previous state.
|
|
517
474
|
const existing = draft.take(id)
|
|
518
475
|
|
|
519
|
-
// Configuring something that is not there is not a thing that can be done. The world
|
|
520
|
-
// is built in one pass, so a scene naming a thing another scene is yet to put up is
|
|
521
|
-
// ordinary rather than a mistake, and is not worth failing over.
|
|
522
476
|
if (!existing) return
|
|
523
477
|
|
|
524
478
|
draft.put(id, augmentEntity(id, { ...existing, ...config }))
|
|
@@ -561,6 +515,22 @@ export function createStore({
|
|
|
561
515
|
}
|
|
562
516
|
}
|
|
563
517
|
|
|
518
|
+
/**
|
|
519
|
+
* Defines a behavior for an entity. It does nothing at runtime: it hands back
|
|
520
|
+
* the behavior it is given, and JavaScript apps never need it.
|
|
521
|
+
*
|
|
522
|
+
* It is for TypeScript apps, like Vue's `defineComponent` or Vite's
|
|
523
|
+
* `defineConfig`: its declaration in `types/store.d.ts` checks each member of
|
|
524
|
+
* the behavior by its name, so that a template's sub-renderers can take props
|
|
525
|
+
* while the handlers still get the entity, which a plain annotation cannot
|
|
526
|
+
* express. See that declaration for why.
|
|
527
|
+
*
|
|
528
|
+
* @returns {(behavior: object) => object} A function that returns the behavior it is given.
|
|
529
|
+
*/
|
|
530
|
+
export function defineType() {
|
|
531
|
+
return (behavior) => behavior
|
|
532
|
+
}
|
|
533
|
+
|
|
564
534
|
/**
|
|
565
535
|
* A draft that writes straight into the entities it is given.
|
|
566
536
|
*
|
package/types/api.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
Store,
|
|
8
8
|
Stored,
|
|
9
9
|
} from "./store"
|
|
10
|
+
import type { AnyEvents, Notify } from "./events"
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* API object exposed to handlers.
|
|
@@ -14,10 +15,14 @@ import type {
|
|
|
14
15
|
* An app's own `Api<AppEntity, AppState>` is an `Api`: a component typed with
|
|
15
16
|
* the plain `Api` accepts it. So the members that read types give the plain
|
|
16
17
|
* `EntityType`, rather than one typed for the app's entities.
|
|
18
|
+
*
|
|
19
|
+
* Told the app's events, `Api<AppEntity, AppState, AppEvents>` checks what
|
|
20
|
+
* `notify` sends (see `EventsOf`), and is still an `Api`.
|
|
17
21
|
*/
|
|
18
22
|
export interface Api<
|
|
19
23
|
TEntity extends BaseEntity = BaseEntity,
|
|
20
24
|
TState extends EntitiesState<TEntity> = EntitiesState<TEntity>,
|
|
25
|
+
TEvents = AnyEvents,
|
|
21
26
|
> {
|
|
22
27
|
getTypes: () => Record<string, EntityType>
|
|
23
28
|
getType: (typeName: string) => EntityType
|
|
@@ -35,8 +40,8 @@ export interface Api<
|
|
|
35
40
|
}
|
|
36
41
|
select: <TResult>(selector: (state: TState) => TResult) => TResult
|
|
37
42
|
dispatch: (event: Event) => void
|
|
38
|
-
notify:
|
|
39
|
-
[key: string]: any
|
|
43
|
+
notify: Notify<TEvents, TEntity>
|
|
44
|
+
[key: string]: any
|
|
40
45
|
}
|
|
41
46
|
|
|
42
47
|
/**
|
|
@@ -45,7 +50,8 @@ export interface Api<
|
|
|
45
50
|
export function createApi<
|
|
46
51
|
TEntity extends BaseEntity = BaseEntity,
|
|
47
52
|
TState extends EntitiesState<TEntity> = EntitiesState<TEntity>,
|
|
53
|
+
TEvents = AnyEvents,
|
|
48
54
|
>(
|
|
49
|
-
store: Store<TEntity, TState>,
|
|
55
|
+
store: Store<TEntity, TState, TEvents>,
|
|
50
56
|
extras?: Record<string, any>,
|
|
51
|
-
): Api<TEntity, TState>
|
|
57
|
+
): Api<TEntity, TState, TEvents>
|
|
@@ -73,6 +73,9 @@ export interface AsyncHandlers<
|
|
|
73
73
|
* ```
|
|
74
74
|
*
|
|
75
75
|
* The `scope` option changes where the events are sent, not their names.
|
|
76
|
+
*
|
|
77
|
+
* Each call gets a `callId` that all its events carry, so that a wrapper such
|
|
78
|
+
* as `optimistic` can tell overlapping calls apart.
|
|
76
79
|
*/
|
|
77
80
|
export type AsyncEvents<
|
|
78
81
|
TType extends string,
|
|
@@ -85,27 +88,38 @@ export type AsyncEvents<
|
|
|
85
88
|
} & {
|
|
86
89
|
[K in `${TType}Start`]?: (
|
|
87
90
|
entity: TEntity,
|
|
88
|
-
payload: TPayload
|
|
91
|
+
event: AsyncCall<{ payload: TPayload }>,
|
|
89
92
|
api: TApi,
|
|
90
93
|
) => void
|
|
91
94
|
} & {
|
|
92
95
|
[K in `${TType}Run`]: (
|
|
93
96
|
entity: TEntity,
|
|
94
|
-
payload: TPayload
|
|
97
|
+
event: AsyncCall<{ payload: TPayload }>,
|
|
95
98
|
api: TApi,
|
|
96
99
|
) => Promise<void>
|
|
97
100
|
} & {
|
|
98
101
|
[K in `${TType}Success`]: (
|
|
99
102
|
entity: TEntity,
|
|
100
|
-
result: TResult
|
|
103
|
+
event: AsyncCall<{ result: TResult }>,
|
|
101
104
|
api: TApi,
|
|
102
105
|
) => void
|
|
103
106
|
} & {
|
|
104
|
-
[K in `${TType}Error`]: (
|
|
107
|
+
[K in `${TType}Error`]: (
|
|
108
|
+
entity: TEntity,
|
|
109
|
+
event: AsyncCall<{ error: any }>,
|
|
110
|
+
api: TApi,
|
|
111
|
+
) => void
|
|
105
112
|
} & {
|
|
106
|
-
[K in `${TType}Finally`]: (
|
|
113
|
+
[K in `${TType}Finally`]: (
|
|
114
|
+
entity: TEntity,
|
|
115
|
+
event: AsyncCall,
|
|
116
|
+
api: TApi,
|
|
117
|
+
) => void
|
|
107
118
|
}
|
|
108
119
|
|
|
120
|
+
/** What an event of one call of an async behavior carries: its `callId`. */
|
|
121
|
+
export type AsyncCall<T = {}> = T & { callId: number }
|
|
122
|
+
|
|
109
123
|
/**
|
|
110
124
|
* Creates a set of event handlers for managing an asynchronous operation lifecycle.
|
|
111
125
|
*
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Api } from "../api"
|
|
2
|
-
import { AsyncEvents } from "./async"
|
|
2
|
+
import { AsyncCall, AsyncEvents } from "./async"
|
|
3
3
|
import { BaseEntity } from "../store"
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -9,6 +9,10 @@ import { BaseEntity } from "../store"
|
|
|
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
11
|
*
|
|
12
|
+
* Each call keeps its own snapshot, so calls can overlap. Give `rollback` to
|
|
13
|
+
* undo a failed call's change yourself, such as removing the temporary item it
|
|
14
|
+
* added to a list, in place of restoring the snapshot.
|
|
15
|
+
*
|
|
12
16
|
* The behavior keeps the events `handleAsync` named and typed, and always has
|
|
13
17
|
* a start event.
|
|
14
18
|
*/
|
|
@@ -27,10 +31,13 @@ export function optimistic<
|
|
|
27
31
|
payload: TPayload,
|
|
28
32
|
api: TApi,
|
|
29
33
|
) => Record<string, any> | null | undefined),
|
|
34
|
+
options?: {
|
|
35
|
+
rollback?: (entity: TEntity, payload: TPayload, api: TApi) => void
|
|
36
|
+
},
|
|
30
37
|
): AsyncEvents<TType, TEntity, TPayload, TResult, TApi> & {
|
|
31
38
|
[K in `${TType}Start`]: (
|
|
32
39
|
entity: TEntity,
|
|
33
|
-
payload: TPayload
|
|
40
|
+
event: AsyncCall<{ payload: TPayload }>,
|
|
34
41
|
api: TApi,
|
|
35
42
|
) => void
|
|
36
43
|
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// The events a store answers, so that `notify` can check them: the event's
|
|
2
|
+
// name, wherever it is sent, and its payload.
|
|
3
|
+
|
|
4
|
+
import type { BaseEntity, Stored } from "./store"
|
|
5
|
+
|
|
6
|
+
declare const anyEvents: unique symbol
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The events of an api or a store that has not been told its events: any
|
|
10
|
+
* name, any payload. It is the default, so an untyped app type-checks as
|
|
11
|
+
* before.
|
|
12
|
+
*/
|
|
13
|
+
export type AnyEvents = { readonly [anyEvents]: true }
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The events a store's types answer, by name, with their payloads, read from
|
|
17
|
+
* the types' own declarations: the second parameter of each handler is its
|
|
18
|
+
* payload, and a handler that takes none has no payload. `render` and the
|
|
19
|
+
* other `render…` members are renderers, not events, so they are left out.
|
|
20
|
+
*
|
|
21
|
+
* ```ts
|
|
22
|
+
* type CounterType = {
|
|
23
|
+
* add: (entity: CounterEntity, amount: number) => void
|
|
24
|
+
* reset: (entity: CounterEntity) => void
|
|
25
|
+
* }
|
|
26
|
+
*
|
|
27
|
+
* type AppEvents = EventsOf<CounterType | LabelType>
|
|
28
|
+
* // { add: number; reset: void; ...the label's events }
|
|
29
|
+
*
|
|
30
|
+
* type AppApi = Api<AppEntity, AppState, AppEvents>
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* An event two types answer takes either type's payload.
|
|
34
|
+
*/
|
|
35
|
+
export type EventsOf<TTypes> = {
|
|
36
|
+
[TEvent in HandlerName<TTypes>]: TTypes extends unknown
|
|
37
|
+
? TEvent extends keyof TTypes
|
|
38
|
+
? PayloadOf<TTypes[TEvent]>
|
|
39
|
+
: never
|
|
40
|
+
: never
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The events the store answers itself, whatever its types: an entity
|
|
45
|
+
* arriving, leaving or changing, and the world pausing and resuming.
|
|
46
|
+
*/
|
|
47
|
+
export type BuiltInEvents<TEntity extends BaseEntity = BaseEntity> = {
|
|
48
|
+
add: TEntity & { id: string }
|
|
49
|
+
remove: string
|
|
50
|
+
patch: Partial<TEntity> & { id: string }
|
|
51
|
+
replace: Stored<TEntity>
|
|
52
|
+
pause: void
|
|
53
|
+
resume: void
|
|
54
|
+
update: number | undefined
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* How an event is sent: by its name alone, broadcast to whoever answers it,
|
|
59
|
+
* or after a target that narrows it down to a type (`Type:event`), an entity
|
|
60
|
+
* (`#id:event`) or both (`Type#id:event`). Only the event's name is checked:
|
|
61
|
+
* the target is usually built at runtime, such as `` `#${entity.id}:submit` ``.
|
|
62
|
+
*/
|
|
63
|
+
export type EventType<TEvents> =
|
|
64
|
+
EventName<TEvents> | `${string}:${EventName<TEvents>}`
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Sends an event. Told the store's events, it checks the event's name and
|
|
68
|
+
* its payload: `notify("#debate:lineAdd", line)` must name an event the
|
|
69
|
+
* store's types answer, and `line` must be that event's payload. An event
|
|
70
|
+
* with no payload takes none. Otherwise any name and payload go.
|
|
71
|
+
*/
|
|
72
|
+
export type Notify<
|
|
73
|
+
TEvents = AnyEvents,
|
|
74
|
+
TEntity extends BaseEntity = BaseEntity,
|
|
75
|
+
> = AnyEvents extends TEvents
|
|
76
|
+
? (type: string, payload?: any) => void
|
|
77
|
+
: TypedNotify<WithBuiltIns<TEvents, BuiltInEvents<TEntity>>>
|
|
78
|
+
|
|
79
|
+
type EventName<TEvents> = keyof TEvents & string
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The app's events and the store's own, by name. An app event named like a
|
|
83
|
+
* built-in one, such as an `add` that takes an amount, takes either payload,
|
|
84
|
+
* because its handlers hear both: the store broadcasts entities as `add` too.
|
|
85
|
+
*/
|
|
86
|
+
type WithBuiltIns<TEvents, TBuiltIns> = {
|
|
87
|
+
[K in keyof TEvents | keyof TBuiltIns]:
|
|
88
|
+
| (K extends keyof TEvents ? TEvents[K] : never)
|
|
89
|
+
| (K extends keyof TBuiltIns ? TBuiltIns[K] : never)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** The event a sent type names: what follows its target, if it has one. */
|
|
93
|
+
type EventOf<TType extends string> = TType extends `${string}:${infer TEvent}`
|
|
94
|
+
? TEvent
|
|
95
|
+
: TType
|
|
96
|
+
|
|
97
|
+
/** The payload as `notify`'s last parameters: none, optional or required. */
|
|
98
|
+
type PayloadArgs<TPayload> = [TPayload] extends [void]
|
|
99
|
+
? [payload?: undefined]
|
|
100
|
+
: undefined extends TPayload
|
|
101
|
+
? [payload?: TPayload]
|
|
102
|
+
: [payload: TPayload]
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* A typed `notify`, declared as a method and taken out by name, with an
|
|
106
|
+
* overload that nothing can call: it only lets a typed api or store fit
|
|
107
|
+
* where a plain one is expected, such as a component that takes any app's
|
|
108
|
+
* api. It comes first, so that a call that fits neither is explained by the
|
|
109
|
+
* typed one.
|
|
110
|
+
*/
|
|
111
|
+
type TypedNotify<TEvents> = {
|
|
112
|
+
notify(type: never, payload: never): void
|
|
113
|
+
notify<TType extends EventType<TEvents>>(
|
|
114
|
+
type: TType,
|
|
115
|
+
...payload: PayloadArgs<TEvents[EventOf<TType> & keyof TEvents]>
|
|
116
|
+
): void
|
|
117
|
+
}["notify"]
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The handlers' names of each type: every function but the renderers. Only
|
|
121
|
+
* named members count: a type that also takes any handler, through an index
|
|
122
|
+
* signature such as `[handler: string]: unknown`, keeps its named events.
|
|
123
|
+
*/
|
|
124
|
+
type HandlerName<TTypes> = TTypes extends unknown
|
|
125
|
+
? keyof {
|
|
126
|
+
[
|
|
127
|
+
K in keyof TTypes as string extends K
|
|
128
|
+
? never
|
|
129
|
+
: number extends K
|
|
130
|
+
? never
|
|
131
|
+
: K extends `render${string}`
|
|
132
|
+
? never
|
|
133
|
+
: TTypes[K] extends (...args: never[]) => unknown
|
|
134
|
+
? K
|
|
135
|
+
: never
|
|
136
|
+
]: true
|
|
137
|
+
} &
|
|
138
|
+
string
|
|
139
|
+
: never
|
|
140
|
+
|
|
141
|
+
/** A handler's payload: its second parameter, or none if it takes only the entity. */
|
|
142
|
+
type PayloadOf<THandler> = THandler extends (
|
|
143
|
+
entity: never,
|
|
144
|
+
...rest: infer TRest
|
|
145
|
+
) => unknown
|
|
146
|
+
? TRest extends []
|
|
147
|
+
? void
|
|
148
|
+
: TRest[0]
|
|
149
|
+
: never
|
package/types/index.d.ts
CHANGED
package/types/migration/rtk.d.ts
CHANGED