@inglorious/store 11.0.2 → 11.0.4

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 CHANGED
@@ -30,7 +30,7 @@ Game engines solved state complexity years ago — Inglorious Store brings those
30
30
  - ✅ Entity-based state (manage multiple instances effortlessly)
31
31
  - ✅ No action creators, thunks, or slices
32
32
  - ✅ Predictable, testable, purely functional code
33
- - ✅ Built-in lifecycle events (`add`, `remove`)
33
+ - ✅ Built-in lifecycle events (`add`, `remove`, `patch`, `replace`)
34
34
  - ✅ 10x faster immutability than Redux Toolkit (Mutative vs Immer)
35
35
 
36
36
  ---
@@ -364,6 +364,56 @@ Inglorious Store has a few built-in events that you can use:
364
364
 
365
365
  - `add`: adds a new entity to the state. Triggers a `create` lifecycle event.
366
366
  - `remove`: removes an entity from the state. Triggers a `destroy` lifecycle event.
367
+ - `patch`: changes some of a standing entity and leaves the rest alone.
368
+ - `replace`: installs a new configuration on a standing entity, dropping what it does
369
+ not mention.
370
+
371
+ `patch` and `replace` cover the case `add` and `remove` do not: changing something that is
372
+ already in the world without taking it out of it. They are the same pair the web settled
373
+ long ago — `POST`, `DELETE`, `PUT`, `PATCH` — for the same reason:
374
+
375
+ ```javascript
376
+ // A patch touches only what it names. Anything else the entity already had survives,
377
+ // which is what makes it safe to nudge part of something mid-flight.
378
+ store.notify("patch", { id: "counter1", value: 10 })
379
+
380
+ // A replace says the whole of it is different, so anything it does not name is gone.
381
+ store.notify("replace", { id: "counter1", type: "Counter", value: 10 })
382
+ ```
383
+
384
+ A field set to `undefined` is set to _nothing_ in either, rather than being left alone:
385
+
386
+ ```javascript
387
+ // The key stops being mapped. This is not the same as saying nothing about it, which
388
+ // would leave whatever it was mapped to last time standing.
389
+ store.notify("patch", {
390
+ id: "keyboard",
391
+ mapping: { Escape: "pause", ArrowUp: undefined },
392
+ })
393
+ ```
394
+
395
+ Neither runs `create` or `destroy`: the entity never stopped standing, and something that
396
+ was never removed cannot have been destroyed. Naming a different `type` in either moves
397
+ the entity onto that type's handlers.
398
+
399
+ The practical use is a world whose rules change with its state — a keyboard whose keys
400
+ mean one thing on a menu and another in play:
401
+
402
+ ```javascript
403
+ const types = {
404
+ Keyboard: { keyDown: (entity, key) => report(key) },
405
+ Game: [
406
+ scenes({
407
+ menu: () => [{ id: "keyboard", type: "Keyboard", mapping: MENU_KEYS }],
408
+ play: () => [{ id: "keyboard", type: "Keyboard", mapping: PLAY_KEYS }],
409
+ }),
410
+ fsm({/* … */}),
411
+ ],
412
+ }
413
+ ```
414
+
415
+ The keyboard is one keyboard, not two: the mapping is patched when the state moves, and
416
+ `isDeepEqual` from `@inglorious/utils` is what tells the two apart.
367
417
 
368
418
  The lifecycle events can be used to define event handlers similar to constructor and destructor methods in OOP:
369
419
 
@@ -634,6 +684,19 @@ The wrapper snapshots the keys in the patch, applies them in `Start`, restores t
634
684
 
635
685
  ---
636
686
 
687
+ ### Reading a store from outside a handler
688
+
689
+ `api.getEntity` is the answer during a handler. Outside one — a test driving a world, or
690
+ anything else holding the store rather than an event — the store says the same thing
691
+ itself:
692
+
693
+ ```javascript
694
+ store.getEntity("user") // one entity, or undefined
695
+ store.getState() // the lot
696
+ ```
697
+
698
+ There is one lookup rather than two spellings of it: `api.getEntity` asks the store.
699
+
637
700
  ### Event scoping
638
701
 
639
702
  By default, lifecycle events are **scoped to the triggering entity**:
@@ -1256,6 +1319,54 @@ Each handler receives three arguments:
1256
1319
 
1257
1320
  - **`create(entity)`** - triggered when entity added via `add` event, visible only to that entity
1258
1321
  - **`destroy(entity)`** - triggered when entity removed via `remove` event, visible only to that entity
1322
+ - **`pause`** - halts the world, see below
1323
+ - **`resume`** - unhalts it
1324
+ - **`add` / `remove`** - put a thing into the world or take it out, triggering `create` / `destroy`
1325
+ - **`patch` / `replace`** - change a thing that is already standing, the first touching only
1326
+ what it names and the second installing all of it
1327
+ - **`update`** - the frame tick, disabled while the world is paused
1328
+
1329
+ #### Pausing the world
1330
+
1331
+ `notify("pause")` stops the store handing out `update` events, and `notify("resume")` starts it again:
1332
+
1333
+ ```javascript
1334
+ store.notify("pause")
1335
+ store.notify("update")
1336
+ store.update() // nothing updated
1337
+
1338
+ store.notify("resume")
1339
+ store.notify("update")
1340
+ store.update() // updated as normal
1341
+ ```
1342
+
1343
+ This is for anything that moves by integrating a delta time. Those entities do not need to know about pausing at all: their `update` is simply never called, so they cannot drift.
1344
+
1345
+ Everything else keeps flowing. Input handlers, custom events and rendering are untouched, which is what lets a pause screen take itself back off:
1346
+
1347
+ ```javascript
1348
+ // Still works while the world is halted
1349
+ pauseMenu.pause(entity, _, api) {
1350
+ api.notify("resume")
1351
+ }
1352
+ ```
1353
+
1354
+ An entity opts out by setting `updatesWhilePaused`, which is for overlays and menus rather than for gameplay:
1355
+
1356
+ ```javascript
1357
+ const Overlay = [{ update(entity, dt) { entity.ticks++ } }]
1358
+
1359
+ // in entities
1360
+ overlay: { type: "Overlay", updatesWhilePaused: true }
1361
+ ```
1362
+
1363
+ It is a property of the entity rather than of the type so that a type stays a plain index signature of handlers, which is what keeps it freely callable and indexable from TypeScript.
1364
+
1365
+ The default is to **stop**. Anything that keeps updating has to say so, so forgetting a flag pauses the thing you meant to pause rather than freezing the menu.
1366
+
1367
+ `reset()` clears the halt, so replacing the entities does not leave the world frozen.
1368
+
1369
+ Note that `pause` and `resume` keep dispatching like any other event, unlike `add` and `remove`. The store acts on them _and_ every type that handles them still gets called, so one notification can both halt the world and let the game record why.
1259
1370
 
1260
1371
  ### Notify vs Dispatch
1261
1372
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inglorious/store",
3
- "version": "11.0.2",
3
+ "version": "11.0.4",
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",
@@ -55,11 +55,11 @@
55
55
  },
56
56
  "sideEffects": false,
57
57
  "dependencies": {
58
- "@inglorious/utils": "4.0.0",
58
+ "@inglorious/utils": "5.0.0",
59
59
  "mutative": "^1.3.0"
60
60
  },
61
61
  "peerDependencies": {
62
- "@inglorious/utils": "4.0.0"
62
+ "@inglorious/utils": "5.0.0"
63
63
  },
64
64
  "devDependencies": {
65
65
  "@inglorious/eslint-config": "1.1.2",
package/src/api.js CHANGED
@@ -37,10 +37,14 @@ export function createApi(store, extras) {
37
37
  },
38
38
  /**
39
39
  * Retrieves a single entity by ID.
40
+ *
41
+ * The store answers this for itself, so there is one lookup rather than two
42
+ * spellings of the same one.
43
+ *
40
44
  * @param {string} id
41
45
  * @returns {Object | undefined}
42
46
  */
43
- getEntity: (id) => store.getState()[id],
47
+ getEntity: store.getEntity,
44
48
  /**
45
49
  * Runs a selector against the current state.
46
50
  *
@@ -1,4 +1,4 @@
1
- import { deserialize } from "@inglorious/utils/objects"
1
+ import { deserialize } from "@inglorious/utils/object.js"
2
2
 
3
3
  const LAST_STATE = 1
4
4
 
@@ -1,15 +1,25 @@
1
- import { deserialize, extend, serialize } from "@inglorious/utils/objects"
1
+ import { deserialize, extend, serialize } from "@inglorious/utils/object.js"
2
2
 
3
- // A constant for the server's WebSocket URL.
4
- const DEFAULT_SERVER_URL = `ws://${window.location.hostname}:3000`
5
3
  const DEFAULT_RECONNECTION_DELAY = 1000
6
4
 
5
+ /**
6
+ * Where the server is, when the caller has not said.
7
+ *
8
+ * Read when it is asked for rather than when this module is imported, so that importing it
9
+ * does not require a browser -- which is what lets the store be imported anywhere.
10
+ *
11
+ * @returns {string} The server's WebSocket URL.
12
+ */
13
+ function defaultServerUrl() {
14
+ return `ws://${window.location.hostname}:3000`
15
+ }
16
+
7
17
  /**
8
18
  * Creates and returns the multiplayer middleware.
9
19
  * @returns {Function} The middleware function.
10
20
  */
11
21
  export function multiplayerMiddleware(config = {}) {
12
- const serverUrl = config.serverUrl ?? DEFAULT_SERVER_URL
22
+ const serverUrl = config.serverUrl ?? defaultServerUrl()
13
23
  const reconnectionDelay =
14
24
  config.reconnectionDelay ?? DEFAULT_RECONNECTION_DELAY
15
25
 
package/src/entities.js CHANGED
@@ -1,4 +1,4 @@
1
- import { map } from "@inglorious/utils/objects"
1
+ import { map } from "@inglorious/utils/object.js"
2
2
 
3
3
  /**
4
4
  * @typedef {Object.<string, any>} Entity - An object representing an entity.
@@ -5,7 +5,7 @@
5
5
  * Inglorious types, enabling gradual migration from RTK to Inglorious Store.
6
6
  */
7
7
 
8
- import { extend } from "@inglorious/utils/objects"
8
+ import { extend } from "@inglorious/utils/object.js"
9
9
 
10
10
  import { handleAsync } from "../async"
11
11
 
package/src/store.js CHANGED
@@ -63,7 +63,7 @@ export function createStore({
63
63
 
64
64
  const types = augmentTypes(originalTypes)
65
65
 
66
- let state, eventMap, incomingEvents, isProcessing
66
+ let state, eventMap, incomingEvents, isProcessing, isHalted
67
67
  reset()
68
68
 
69
69
  const baseStore = {
@@ -75,6 +75,7 @@ export function createStore({
75
75
  getType,
76
76
  setType,
77
77
  getState,
78
+ getEntity,
78
79
  setState,
79
80
  reset,
80
81
  }
@@ -118,6 +119,14 @@ export function createStore({
118
119
  return []
119
120
  }
120
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
+ if (!incomingEvents.length) {
127
+ return []
128
+ }
129
+
121
130
  isProcessing = true
122
131
  const processedEvents = []
123
132
  let nextState
@@ -147,6 +156,13 @@ export function createStore({
147
156
  * deep copy produced by `structuredClone` (full clone). The current state is not
148
157
  * modified, and only becomes the new state once this function returns.
149
158
  *
159
+ * A change made by a handler is not visible to the next handler in the same pass:
160
+ * `getState` still returns the state as it was before the pass began. This is on
161
+ * purpose -- every event in the pass sees one consistent world, rather than some
162
+ * changes having landed and others not -- and it means a handler that asks what is
163
+ * left is asking about the past. Anything reacting to a change should be told the
164
+ * change rather than go looking for it.
165
+ *
150
166
  * @param {Entities} draft - The draft to apply the queued events to.
151
167
  * @returns {void}
152
168
  */
@@ -155,15 +171,64 @@ export function createStore({
155
171
  const event = incomingEvents.shift()
156
172
  processedEvents.push(event)
157
173
 
174
+ // Pausing and resuming are the store's own, and they keep dispatching like any
175
+ // other event, so the game behaviour still sees them and anything else that
176
+ // wants to react gets the same notification.
177
+ if (event.type === "pause") {
178
+ isHalted = true
179
+ }
180
+
181
+ if (event.type === "resume") {
182
+ isHalted = false
183
+ }
184
+
158
185
  // Handle special system events
186
+ // `add` and `remove` change the world and are then dispatched like any
187
+ // other event, so that whatever is watching can hear about it. They are
188
+ // broadcast: the entity named by the payload is not the only one that wants
189
+ // to know, and something counting down what it put up needs to hear about
190
+ // each of them going.
191
+ //
192
+ // They are not the counterpart to `create` and `destroy`, which reach only
193
+ // the entity they are about so that those read as a constructor and a
194
+ // destructor without every handler beginning by checking whether it is the
195
+ // one being talked about.
196
+ //
197
+ // The two carry different payloads, and that is on purpose: a payload
198
+ // carries the least that can be had. A removal needs only the id, because
199
+ // everything else about the entity can still be looked up by it. An
200
+ // addition carries the entity itself, because there is nothing yet to look
201
+ // it up by -- the same asymmetry as removing a user by id but updating one
202
+ // by the patched user.
159
203
  if (event.type === "add") {
160
204
  addEntity(draft, event.payload)
161
- continue
162
205
  }
163
206
 
164
207
  if (event.type === "remove") {
165
208
  removeEntity(draft, event.payload)
166
- continue
209
+ }
210
+
211
+ // A thing that stands through a change of state can still be told a different
212
+ // configuration, in either of the two ways the world offers.
213
+ //
214
+ // `patch` changes some of it and leaves the rest alone, which is how a screen
215
+ // takes over one field of something it shares with the screen before it. A field
216
+ // named as `undefined` is set to nothing rather than being left alone, which is
217
+ // how a key is taken off a mapping.
218
+ //
219
+ // `replace` installs what it is given and drops everything it does not mention,
220
+ // which is how the whole of something is said to be different.
221
+ //
222
+ // Neither takes the thing out of the world, so neither runs `create` or `destroy`:
223
+ // it never stopped standing, and a thing that was never removed cannot have been
224
+ // destroyed. This is the same pair the world has to hand for anything else --
225
+ // `add` and `remove` beside them, and post/delete/put/patch before that.
226
+ if (event.type === "patch") {
227
+ patchEntity(draft, event.payload)
228
+ }
229
+
230
+ if (event.type === "replace") {
231
+ replaceEntity(draft, event.payload)
167
232
  }
168
233
 
169
234
  // Parse the event to get handler name
@@ -172,9 +237,21 @@ export function createStore({
172
237
  // Get entities that should handle this event (filtered by EventMap)
173
238
  const entityIds = eventMap.getEntitiesForEvent(event.type)
174
239
 
240
+ // A halted world stops handing out updates, because anything that moves by
241
+ // integrating a delta time would otherwise carry on regardless. An entity that
242
+ // sets `updatesWhilePaused` is exempt, which is how an overlay, a button or the
243
+ // thing that takes the pause back off keeps working while the rest stands still.
244
+ //
245
+ // It is read off the entity rather than the type so that the type stays an index
246
+ // signature of handlers, which is what lets it be called and indexed freely.
247
+ const halted = isHalted && handlerName === "update"
248
+
175
249
  for (const id of entityIds) {
176
250
  const entity = draft[id]
177
251
  const type = types[entity.type]
252
+
253
+ if (halted && !entity.updatesWhilePaused) continue
254
+
178
255
  const handle = type[handlerName]
179
256
 
180
257
  handle?.(entity, event.payload, api)
@@ -209,8 +286,14 @@ export function createStore({
209
286
  * - 'form:submit' - only form entities
210
287
  * - 'form[loginForm]:submit' - only loginForm entity
211
288
  *
289
+ * Two of these are the store's own. 'pause' stops the store handing out 'update'
290
+ * events, and 'resume' starts it again, which is how anything that moves by
291
+ * integrating a delta time stops without being told about it. A type declares
292
+ * `updatesWhilePaused` to keep updating regardless. Both keep dispatching to types
293
+ *
212
294
  * @param {string} type - The event type to notify.
213
- * @param {any} payload - The event payload.
295
+ * @param {any} payload - The event payload. Carries the least that can be had
296
+ * about what happened.
214
297
  */
215
298
  function notify(type, payload) {
216
299
  // NOTE: it's important to invoke store.dispatch instead of dispatch, otherwise we cannot override it
@@ -249,7 +332,10 @@ export function createStore({
249
332
  }
250
333
 
251
334
  /**
252
- * Sets an augmented type configuration given its name.
335
+ * Sets an augmented type configuration given its name. Replacing a type, as
336
+ * hot module reloading does, notifies subscribers, since what its entities
337
+ * render may have changed. Adding a type does not: the store is unchanged,
338
+ * and a type can be added while rendering.
253
339
  * @param {string} typeName - The name of the type to set.
254
340
  * @param {Type} type - The type configuration.
255
341
  * @returns {void}
@@ -275,6 +361,10 @@ export function createStore({
275
361
  type: typeName,
276
362
  })
277
363
  }
364
+
365
+ if (oldType) {
366
+ listeners.forEach((onUpdate) => onUpdate())
367
+ }
278
368
  }
279
369
 
280
370
  /**
@@ -285,6 +375,20 @@ export function createStore({
285
375
  return state
286
376
  }
287
377
 
378
+ /**
379
+ * One entity by its id, or `undefined` if there is no such entity.
380
+ *
381
+ * This is what `api.getEntity` answers with during a handler, exposed here for the
382
+ * code that runs outside one: a test looking at a world it is driving, or anything else
383
+ * holding the store rather than an event's `api`.
384
+ *
385
+ * @param {string} id - The entity's id.
386
+ * @returns {Object|undefined} The entity, as the last finished pass left it.
387
+ */
388
+ function getEntity(id) {
389
+ return state?.[id]
390
+ }
391
+
288
392
  /**
289
393
  * Sets the entire state of the store.
290
394
  * This is useful for importing state or setting initial state from a server.
@@ -352,6 +456,7 @@ export function createStore({
352
456
  * @returns {void}
353
457
  */
354
458
  function reset() {
459
+ isHalted = false
355
460
  setState(originalEntities)
356
461
  }
357
462
 
@@ -370,6 +475,75 @@ export function createStore({
370
475
  incomingEvents.unshift({ type: `#${id}:create` })
371
476
  }
372
477
 
478
+ /**
479
+ * Moves a standing thing onto the handlers of whatever type it now names.
480
+ *
481
+ * A type is the only thing about a thing that cannot be merged: there is no such thing
482
+ * as being half a sort, so naming a different one means taking the thing out of the old
483
+ * one's list of entities before putting it in the new one.
484
+ *
485
+ * @param {Entities} draft - The draft (structural sharing) or the copy (full clone) to mutate.
486
+ * @param {string} id - The thing being moved.
487
+ * @param {string} previousType - The type it was under.
488
+ * @param {string|undefined} type - The type it is under now, if it has named one.
489
+ * @returns {void}
490
+ */
491
+ function moveToType(draft, id, previousType, type) {
492
+ if (!type || type === previousType) return
493
+
494
+ eventMap.removeEntity(id, types[previousType], previousType)
495
+ eventMap.addEntity(id, types[type], type)
496
+ }
497
+
498
+ /**
499
+ * Merges a new configuration into a thing that is already standing.
500
+ *
501
+ * Only the fields the configuration names are touched, so anything the world has done
502
+ * to the thing since it was made survives unless the
503
+ * new configuration says otherwise. A field named as `undefined` is set to nothing,
504
+ * which is what taking a key off a mapping means.
505
+ *
506
+ * @param {Entities} draft - The draft (structural sharing) or the copy (full clone) to mutate.
507
+ * @param {Entity} payload - The thing and the fields to merge into it.
508
+ * @returns {void}
509
+ */
510
+ function patchEntity(draft, payload) {
511
+ const { id, ...config } = payload
512
+ const existing = draft[id]
513
+
514
+ // Configuring something that is not there is not a thing that can be done. The world
515
+ // is built in one pass, so a scene naming a thing another scene is yet to put up is
516
+ // ordinary rather than a mistake, and is not worth failing over.
517
+ if (!existing) return
518
+
519
+ draft[id] = augmentEntity(id, { ...existing, ...config })
520
+
521
+ moveToType(draft, id, existing.type, config.type)
522
+ }
523
+
524
+ /**
525
+ * Installs a new configuration on a standing thing, dropping what it does not mention.
526
+ *
527
+ * The thing keeps its place in the world and its name; everything else is whatever it
528
+ * is told, and anything it is not told is gone. A field set to `undefined` is set to
529
+ * nothing here as much as it is in a patch -- the difference between the two is what
530
+ * happens to the fields neither mentions.
531
+ *
532
+ * @param {Entities} draft - The draft (structural sharing) or the copy (full clone) to mutate.
533
+ * @param {Entity} payload - The thing and the fields it is to be.
534
+ * @returns {void}
535
+ */
536
+ function replaceEntity(draft, payload) {
537
+ const { id, ...config } = payload
538
+ const existing = draft[id]
539
+
540
+ if (!existing) return
541
+
542
+ draft[id] = augmentEntity(id, config)
543
+
544
+ moveToType(draft, id, existing.type, config.type)
545
+ }
546
+
373
547
  /**
374
548
  * Queues the destruction of an entity. The entity is removed by the `#id:destroy` event.
375
549
  * @param {Entities} draft - The draft (structural sharing) or the copy (full clone) to mutate.
package/src/types.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { ensureArray } from "@inglorious/utils/data-structures/array.js"
2
2
  import { pipe } from "@inglorious/utils/functions"
3
- import { extend, map } from "@inglorious/utils/objects"
3
+ import { extend, map } from "@inglorious/utils/object.js"
4
4
 
5
5
  /**
6
6
  * Augments a single type by composing its behaviors and mixins.
package/types/api.d.ts CHANGED
@@ -21,7 +21,11 @@ export interface Api<
21
21
  (): TState
22
22
  (typeName: string): TEntity[]
23
23
  }
24
- getEntity: (id: string) => TEntity | undefined
24
+ /** A known id gives that entity's own type; any other id, the union. */
25
+ getEntity: {
26
+ <TId extends keyof TState & string>(id: TId): TState[TId] | undefined
27
+ (id: string): TEntity | undefined
28
+ }
25
29
  select: <TResult>(selector: (state: TState) => TResult) => TResult
26
30
  dispatch: (event: Event) => void
27
31
  notify: (type: string, payload?: any) => void
package/types/store.d.ts CHANGED
@@ -17,7 +17,12 @@ export interface Event<T = any> {
17
17
  }
18
18
 
19
19
  /**
20
- * Entity type definition with event handlers
20
+ * Entity type definition with event handlers.
21
+ *
22
+ * `updatesWhilePaused` opts the type out of a pause: the store stops handing out
23
+ * `update` events while the world is halted, and a type that says this keeps
24
+ * receiving them. It is for overlays and menus; anything that moves should leave it
25
+ * off so that it stops.
21
26
  */
22
27
  export type EntityType<TEntity extends BaseEntity = BaseEntity> = {
23
28
  [K: string]: (entity: TEntity, payload: any, api: Api) => void
@@ -74,7 +79,8 @@ export interface StoreConfig<
74
79
  TState extends EntitiesState<TEntity> = EntitiesState<TEntity>,
75
80
  > {
76
81
  types?: TypesConfig<TEntity>
77
- entities?: TState
82
+ /** The initial entities. With `autoCreateEntities`, those the store creates itself can be left out. */
83
+ entities?: Partial<TState>
78
84
  systems?: System<TState>[]
79
85
  middlewares?: Middleware<TEntity, TState>[]
80
86
  autoCreateEntities?: boolean
@@ -107,6 +113,10 @@ export interface Store<
107
113
  getType: (typeName: string) => EntityType<TEntity>
108
114
  setType: (typeName: string, type: EntityType<TEntity>) => void
109
115
  getState: () => TState
116
+ getEntity: {
117
+ <TId extends keyof TState & string>(id: TId): TState[TId] | undefined
118
+ (id: string): TState[string] | undefined
119
+ }
110
120
  setState: (nextState: TState) => void
111
121
  reset: () => void
112
122
  _api?: Api<TEntity, TState>
package/types/test.d.ts CHANGED
@@ -11,7 +11,10 @@ export interface MockApi<
11
11
  (): TState
12
12
  (typeName: string): TEntity[]
13
13
  }
14
- getEntity: (id: string) => TEntity | undefined
14
+ getEntity: {
15
+ <TId extends keyof TState & string>(id: TId): TState[TId] | undefined
16
+ (id: string): TEntity | undefined
17
+ }
15
18
  select: <TResult>(selector: (state: TState) => TResult) => TResult
16
19
  dispatch: (event: Event) => void
17
20
  notify: (type: string, payload?: any) => void