@inglorious/store 11.0.4 → 11.1.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 CHANGED
@@ -1381,75 +1381,145 @@ store.dispatch({ type: "eventName", payload }) // Redux-compatible alternative
1381
1381
 
1382
1382
  ### 🧩 Type Safety
1383
1383
 
1384
- Inglorious Store is written in JavaScript but comes with powerful TypeScript support out of the box, allowing for a fully type-safe experience similar to Redux Toolkit, but with less boilerplate.
1384
+ Inglorious Store is written in JavaScript but comes with TypeScript types that check your whole app with no casts: each type against its entity, each handler's parameters, and every entity read through the api.
1385
1385
 
1386
- You can achieve strong type safety by defining an interface for your `types` configuration. This allows you to statically define the shape of your entity handlers, ensuring that all required handlers are present and correctly typed.
1386
+ **1. Describe your entities and state**
1387
1387
 
1388
- Here’s how you can set it up for a TodoMVC-style application:
1388
+ ```typescript
1389
+ // types/index.d.ts
1390
+ import type {
1391
+ Api,
1392
+ BaseEntity,
1393
+ EntitiesState,
1394
+ EntityType,
1395
+ } from "@inglorious/store"
1396
+
1397
+ export interface CounterEntity extends BaseEntity {
1398
+ type: "Counter"
1399
+ value: number
1400
+ }
1401
+
1402
+ export interface LabelEntity extends BaseEntity {
1403
+ type: "Label"
1404
+ text: string
1405
+ }
1406
+
1407
+ export type AppEntity = CounterEntity | LabelEntity
1408
+
1409
+ // The entities by id
1410
+ export interface AppState extends EntitiesState<AppEntity> {
1411
+ counter: CounterEntity
1412
+ label: LabelEntity
1413
+ }
1414
+
1415
+ export type AppApi = Api<AppEntity, AppState>
1416
+
1417
+ // A type for one of the app's entities
1418
+ export type AppType<TEntity extends AppEntity> = EntityType<TEntity, AppApi>
1419
+ ```
1389
1420
 
1390
- **1. Define Your Types**
1421
+ Every field of an entity is declared, so a misspelled one is an error. The store gives each entity its key in the state as `id`: leave it out of the initial entities, and handlers receive it as a `string`. With `@inglorious/web`, import `Api` from there instead: it is the same type, with `render`.
1391
1422
 
1392
- First, create an interface that describes your entire `types` configuration. This interface will enforce the structure of your event handlers.
1423
+ **2. Write your types**
1424
+
1425
+ Declare each type's events and their payloads, then write the type against that declaration:
1393
1426
 
1394
1427
  ```typescript
1395
- // src/store/types.ts
1396
- import type {
1397
- FormEntity,
1398
- ListEntity,
1399
- FooterEntity,
1400
- // ... other payload types
1401
- } from "../../types"
1402
-
1403
- // Define the static shape of the types configuration
1404
- interface TodoListTypes {
1405
- form: {
1406
- inputChange: (entity: FormEntity, value: string) => void
1407
- formSubmit: (entity: FormEntity) => void
1408
- }
1409
- list: {
1410
- formSubmit: (entity: ListEntity, value: string) => void
1411
- toggleClick: (entity: ListEntity, id: number) => void
1412
- // ... other handlers
1413
- }
1414
- footer: {
1415
- filterClick: (entity: FooterEntity, id: string) => void
1416
- }
1428
+ // types/index.d.ts
1429
+ export type CounterType = {
1430
+ increment: (entity: CounterEntity) => void
1431
+ add: (entity: CounterEntity, amount: number) => void
1432
+ copyLabel: (entity: CounterEntity, payload: void, api: AppApi) => void
1417
1433
  }
1434
+ ```
1418
1435
 
1419
- export const types: TodoListTypes = {
1420
- form: {
1421
- inputChange(entity, value) {
1422
- entity.value = value
1423
- },
1424
- formSubmit(entity) {
1425
- entity.value = ""
1426
- },
1436
+ ```typescript
1437
+ // src/store/counter.ts
1438
+ import type { CounterType } from "../../types"
1439
+
1440
+ export const Counter: CounterType = {
1441
+ increment(entity) {
1442
+ entity.value++
1443
+ },
1444
+
1445
+ add(entity, amount) {
1446
+ entity.value += amount
1447
+ },
1448
+
1449
+ copyLabel(entity, _payload, api) {
1450
+ entity.value = api.getEntity("label")?.text.length ?? 0
1427
1451
  },
1428
- // ... other type implementations
1429
1452
  }
1430
1453
  ```
1431
1454
 
1432
- With `TodoListTypes`, TypeScript will throw an error if you forget a handler (e.g., `formSubmit`) or if its signature is incorrect.
1455
+ The parameters are inferred from `CounterType`, and TypeScript checks the type against it: a misspelled or missing handler is an error, and so is `Counter.add(counter, "2")`. Declare it with `type`, not `interface`: an interface does not fit the store's `types`.
1433
1456
 
1434
- **2. Create the Store**
1457
+ For less rigour with less to write, skip the declaration and use the shorthand, `satisfies`. It still infers the entity and the api, but it neither lists the events nor types a payload you do not annotate:
1435
1458
 
1436
- When creating your store, you'll pass the `types` object. To satisfy the store's generic `TypesConfig`, you may need to use a double cast (`as unknown as`). This is a safe and intentional way to bridge your specific, statically-checked configuration with the store's more generic type.
1459
+ ```typescript
1460
+ export const Counter = {
1461
+ increment(entity) {
1462
+ entity.value++
1463
+ },
1464
+ add(entity, amount: number) {
1465
+ entity.value += amount
1466
+ },
1467
+ } satisfies AppType<CounterEntity>
1468
+ ```
1469
+
1470
+ **3. Create the store**
1437
1471
 
1438
1472
  ```typescript
1439
1473
  // src/store/index.ts
1440
- import { createStore, type TypesConfig } from "@inglorious/store"
1441
- import { types } from "./types"
1442
- import type { TodoListEntity, TodoListState } from "../../types"
1474
+ import { createStore } from "@inglorious/store"
1443
1475
 
1444
- export const store = createStore<TodoListEntity, TodoListState>({
1445
- types: types as unknown as TypesConfig<TodoListEntity>,
1446
- // ... other store config
1476
+ import type { AppEntity, AppState } from "../../types"
1477
+ import { Counter } from "./counter"
1478
+ import { Label } from "./label"
1479
+
1480
+ export const store = createStore<AppEntity, AppState>({
1481
+ types: { Counter, Label },
1482
+ entities: {
1483
+ counter: { type: "Counter", value: 0 },
1484
+ label: { type: "Label", text: "Hello" },
1485
+ },
1447
1486
  })
1448
1487
  ```
1449
1488
 
1450
- **3. Enjoy Full Type Safety**
1489
+ Each type is checked against the entities of its name: `Counter` against `CounterEntity`. A name no entity has, such as a page rendered by name, takes any type. Behaviors type the same way: `Live: [Chart, withRealtime]`.
1490
+
1491
+ **4. Use types from libraries**
1492
+
1493
+ An entity drawn by a library type extends that type's entity interface:
1494
+
1495
+ ```typescript
1496
+ import type { ChartEntity } from "@inglorious/charts"
1497
+ import type { RouterEntity } from "@inglorious/web/router"
1498
+
1499
+ interface AppRouterEntity extends RouterEntity {
1500
+ type: "Router"
1501
+ }
1502
+
1503
+ interface SalesEntity extends ChartEntity {
1504
+ type: "Sales"
1505
+ }
1506
+
1507
+ // types: { Router, Sales: Chart }
1508
+ ```
1509
+
1510
+ **5. Test your handlers**
1511
+
1512
+ ```typescript
1513
+ import { trigger } from "@inglorious/store/test"
1514
+
1515
+ const { entity, events } = trigger(counter, Counter.add, 2)
1516
+ ```
1517
+
1518
+ `trigger` checks the payload against the handler, and returns the updated entity, typed.
1519
+
1520
+ **Upgrading from casts**
1451
1521
 
1452
- Now, your store is fully type-safe. The hooks provided by `@inglorious/react-store` will also be correctly typed.
1522
+ Older apps passed `types` as `types as unknown as TypesConfig<AppEntity>` and described each type with an interface. Drop the cast, turn each `interface CounterType {` into `type CounterType = {`, and declare every entity field, which `BaseEntity` no longer allows by default. An entity that extended `BaseEntity, Omit<RouterEntity, "id">` now just extends `RouterEntity`.
1453
1523
 
1454
1524
  ---
1455
1525
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inglorious/store",
3
- "version": "11.0.4",
3
+ "version": "11.1.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",
@@ -62,9 +62,10 @@
62
62
  "@inglorious/utils": "5.0.0"
63
63
  },
64
64
  "devDependencies": {
65
- "@inglorious/eslint-config": "1.1.2",
65
+ "@inglorious/eslint-config": "1.1.4",
66
66
  "@reduxjs/toolkit": "^2.12.0",
67
67
  "prettier": "^3.9.6",
68
+ "typescript": "~7.0.2",
68
69
  "vite": "^8.2.2",
69
70
  "vitest": "^5.0.0"
70
71
  },
@@ -75,6 +76,7 @@
75
76
  "format": "prettier --write .",
76
77
  "lint": "eslint .",
77
78
  "test:watch": "vitest",
78
- "test": "vitest run"
79
+ "test": "vitest run && tsc -p type-tests",
80
+ "test:types": "tsc -p type-tests"
79
81
  }
80
82
  }
package/src/test.js CHANGED
@@ -130,7 +130,7 @@ export function createMockApi(entities) {
130
130
  * expect(entity.value).toBe(10)
131
131
  */
132
132
  export function trigger(entity, eventHandler, eventPayload, api) {
133
- api ??= createMockApi({ entities: { [entity.id]: entity } })
133
+ api ??= createMockApi({ [entity.id]: entity })
134
134
 
135
135
  return {
136
136
  entity: create(entity, (draft) => {
package/types/api.d.ts CHANGED
@@ -1,30 +1,37 @@
1
1
  import type {
2
+ AnyTypeDefinition,
2
3
  BaseEntity,
3
4
  EntitiesState,
4
5
  EntityType,
5
6
  Event,
6
7
  Store,
7
- TypesConfig,
8
+ Stored,
8
9
  } from "./store"
9
10
 
10
11
  /**
11
- * API object exposed to handlers
12
+ * API object exposed to handlers.
13
+ *
14
+ * An app's own `Api<AppEntity, AppState>` is an `Api`: a component typed with
15
+ * the plain `Api` accepts it. So the members that read types give the plain
16
+ * `EntityType`, rather than one typed for the app's entities.
12
17
  */
13
18
  export interface Api<
14
19
  TEntity extends BaseEntity = BaseEntity,
15
20
  TState extends EntitiesState<TEntity> = EntitiesState<TEntity>,
16
21
  > {
17
- getTypes: () => TypesConfig<TEntity>
18
- getType: (typeName: string) => EntityType<TEntity>
19
- setType: (typeName: string, type: EntityType<TEntity>) => void
22
+ getTypes: () => Record<string, EntityType>
23
+ getType: (typeName: string) => EntityType
24
+ setType: (typeName: string, type: AnyTypeDefinition) => void
20
25
  getEntities: {
21
26
  (): TState
22
- (typeName: string): TEntity[]
27
+ (typeName: string): Stored<TEntity>[]
23
28
  }
24
29
  /** A known id gives that entity's own type; any other id, the union. */
25
30
  getEntity: {
26
- <TId extends keyof TState & string>(id: TId): TState[TId] | undefined
27
- (id: string): TEntity | undefined
31
+ <TId extends keyof TState & string>(
32
+ id: TId,
33
+ ): Stored<TState[TId]> | undefined
34
+ (id: string): Stored<TEntity> | undefined
28
35
  }
29
36
  select: <TResult>(selector: (state: TState) => TResult) => TResult
30
37
  dispatch: (event: Event) => void
package/types/select.d.ts CHANGED
@@ -12,9 +12,36 @@ export type OutputSelector<TState = any, TResult = any> = (
12
12
  state: TState,
13
13
  ) => TResult
14
14
 
15
+ /** Any input selector, whatever state it reads. */
16
+ type AnySelector = (state: never) => unknown
17
+
18
+ /** What the input selectors return, in order: the result function's arguments. */
19
+ type SelectedValues<TSelectors extends readonly AnySelector[]> = {
20
+ [K in keyof TSelectors]: ReturnType<TSelectors[K]>
21
+ }
22
+
23
+ /** The state every input selector can read: the intersection of theirs. */
24
+ type SelectorsState<TSelectors extends readonly AnySelector[]> =
25
+ UnionToIntersection<Parameters<TSelectors[number]>[0]>
26
+
27
+ type UnionToIntersection<TUnion> = (
28
+ TUnion extends unknown ? (value: TUnion) => void : never
29
+ ) extends (value: infer TIntersection) => void
30
+ ? TIntersection
31
+ : never
32
+
15
33
  /**
16
- * Creates a memoized derived computation from input selectors.
34
+ * Creates a memoized derived computation from input selectors. Everything is
35
+ * inferred: the result function's arguments, and the state the selector reads.
17
36
  */
37
+ export function compute<
38
+ const TSelectors extends readonly AnySelector[],
39
+ TResult,
40
+ >(
41
+ resultFunc: (...args: SelectedValues<TSelectors>) => TResult,
42
+ inputSelectors: TSelectors,
43
+ ): OutputSelector<SelectorsState<TSelectors>, TResult>
44
+ /** With explicit type arguments: the state, the selected values and the result. */
18
45
  export function compute<TState, TInputs extends readonly unknown[], TResult>(
19
46
  resultFunc: (...args: TInputs) => TResult,
20
47
  inputSelectors: {
@@ -26,6 +53,14 @@ export function compute<TState, TInputs extends readonly unknown[], TResult>(
26
53
  * Redux-compatible alias for `compute`.
27
54
  * Prefer `compute` in new code.
28
55
  */
56
+ export function createSelector<
57
+ const TSelectors extends readonly AnySelector[],
58
+ TResult,
59
+ >(
60
+ inputSelectors: TSelectors,
61
+ resultFunc: (...args: SelectedValues<TSelectors>) => TResult,
62
+ ): OutputSelector<SelectorsState<TSelectors>, TResult>
63
+ /** With explicit type arguments: the state, the selected values and the result. */
29
64
  export function createSelector<
30
65
  TState,
31
66
  TInputs extends readonly unknown[],
package/types/store.d.ts CHANGED
@@ -1,13 +1,33 @@
1
1
  import type { Api } from "./api"
2
2
 
3
3
  /**
4
- * Base entity structure
4
+ * What every entity has. Declare an entity's other fields on its own interface:
5
+ *
6
+ * ```ts
7
+ * interface CounterEntity extends BaseEntity {
8
+ * type: "Counter"
9
+ * value: number
10
+ * }
11
+ * ```
12
+ *
13
+ * The store gives each entity its key in the state as `id`, so it can be left
14
+ * out of the initial entities; handlers always receive it (see `Stored`).
5
15
  */
6
16
  export interface BaseEntity {
7
17
  type: string
8
- [key: string]: any
18
+ id?: string
19
+ /**
20
+ * Opts the entity out of a pause: the store stops handing out `update`
21
+ * events while the world is halted, and an entity that says this keeps
22
+ * receiving them. It is for overlays and menus; anything that moves should
23
+ * leave it off so that it stops.
24
+ */
25
+ updatesWhilePaused?: boolean
9
26
  }
10
27
 
28
+ /** An entity as the store holds it, and as handlers receive it: with its id. */
29
+ export type Stored<TEntity extends BaseEntity> = TEntity & { id: string }
30
+
11
31
  /**
12
32
  * Event structure
13
33
  */
@@ -17,17 +37,80 @@ export interface Event<T = any> {
17
37
  }
18
38
 
19
39
  /**
20
- * Entity type definition with event handlers.
40
+ * An event handler: it receives the entity, the event's payload and the api.
41
+ */
42
+ export type EventHandler<
43
+ TEntity extends BaseEntity = BaseEntity,
44
+ TApi extends Api<any, any> = Api,
45
+ > = (entity: Stored<TEntity>, payload: any, api: TApi) => unknown
46
+
47
+ /**
48
+ * Any type for one kind of entity: its event handlers, by event name, and its
49
+ * `render`. The store checks `types` against it.
21
50
  *
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.
51
+ * For rigour, declare a type's events and payloads with a `type` alias (an
52
+ * interface does not fit `types`) and write the type against it:
53
+ *
54
+ * ```ts
55
+ * type CounterType = {
56
+ * add: (entity: CounterEntity, amount: number) => void
57
+ * }
58
+ * const Counter: CounterType = {
59
+ * add(entity, amount) {
60
+ * entity.value += amount
61
+ * },
62
+ * }
63
+ * ```
64
+ *
65
+ * As a shorthand, `satisfies` infers the entity and the api without the
66
+ * declaration, but neither lists the events nor types unannotated payloads:
67
+ *
68
+ * ```ts
69
+ * const Counter = {
70
+ * add(entity, amount: number) {
71
+ * entity.value += amount
72
+ * },
73
+ * } satisfies EntityType<CounterEntity, AppApi>
74
+ * ```
26
75
  */
27
- export type EntityType<TEntity extends BaseEntity = BaseEntity> = {
28
- [K: string]: (entity: TEntity, payload: any, api: Api) => void
76
+ export type EntityType<
77
+ TEntity extends BaseEntity = BaseEntity,
78
+ TApi extends Api<any, any> = Api,
79
+ > = {
80
+ /** Draws the entity. Unlike an event handler, it receives the api second. */
81
+ render?: Renderer<TEntity, TApi>
82
+ [member: string]: EventHandler<TEntity, TApi> | undefined
29
83
  }
30
84
 
85
+ /** A type's `render`: it receives the entity and the api. */
86
+ export type Renderer<
87
+ TEntity extends BaseEntity = BaseEntity,
88
+ TApi extends Api<any, any> = Api,
89
+ > = (entity: Stored<TEntity>, api: TApi) => unknown
90
+
91
+ /**
92
+ * A behavior: a type, or a function that extends the type it is given. A type
93
+ * can also be an array of behaviors, applied in order: `[Chart, withRealtime]`.
94
+ */
95
+ export type Behavior<
96
+ TEntity extends BaseEntity = BaseEntity,
97
+ TApi extends Api<any, any> = Api,
98
+ > =
99
+ | EntityType<TEntity, TApi>
100
+ | ((type: EntityType<TEntity, TApi>) => EntityType<TEntity, TApi>)
101
+
102
+ /** What the store's `types` holds for one type name: a behavior, or several. */
103
+ export type TypeDefinition<
104
+ TEntity extends BaseEntity = BaseEntity,
105
+ TApi extends Api<any, any> = Api,
106
+ > = Behavior<TEntity, TApi> | Behavior<TEntity, TApi>[]
107
+
108
+ /**
109
+ * A type definition for any entity. A handler that accepts some entity also
110
+ * accepts `never`, so every type fits here, whatever entity it is for.
111
+ */
112
+ export type AnyTypeDefinition = TypeDefinition<never, never>
113
+
31
114
  /**
32
115
  * System definition with event handlers
33
116
  */
@@ -43,11 +126,23 @@ export type EntitiesState<TEntity extends BaseEntity = BaseEntity> = {
43
126
  }
44
127
 
45
128
  /**
46
- * Types configuration
129
+ * The store's `types`. In a typed store, each entity's type name takes a type
130
+ * for that entity, so `types: { Counter }` checks `Counter` against
131
+ * `CounterEntity`. Names no entity has, such as a page rendered by name, take
132
+ * any type.
47
133
  */
48
- export type TypesConfig<TEntity extends BaseEntity = BaseEntity> = {
49
- [typeName: string]: EntityType<TEntity>
50
- }
134
+ export type TypesConfig<
135
+ TEntity extends BaseEntity = BaseEntity,
136
+ TState extends EntitiesState<TEntity> = EntitiesState<TEntity>,
137
+ > = string extends TEntity["type"]
138
+ ? { [typeName: string]: AnyTypeDefinition }
139
+ : {
140
+ [TName in TEntity["type"]]?: TypeDefinition<
141
+ Extract<TEntity, { type: TName }>,
142
+ Api<TEntity, TState>
143
+ >
144
+ } & { [typeName: string]: unknown }
145
+
51
146
  /**
52
147
  * Whether each event triggers an update, or updates are batched until `update()` is called.
53
148
  */
@@ -78,7 +173,9 @@ export interface StoreConfig<
78
173
  TEntity extends BaseEntity = BaseEntity,
79
174
  TState extends EntitiesState<TEntity> = EntitiesState<TEntity>,
80
175
  > {
81
- types?: TypesConfig<TEntity>
176
+ // The entity type comes from the type arguments, `entities` or
177
+ // `middlewares`, never from `types`, which are checked against it.
178
+ types?: NoInfer<TypesConfig<TEntity, TState>>
82
179
  /** The initial entities. With `autoCreateEntities`, those the store creates itself can be left out. */
83
180
  entities?: Partial<TState>
84
181
  systems?: System<TState>[]
@@ -109,13 +206,15 @@ export interface Store<
109
206
  update: () => Event[]
110
207
  notify: (type: string, payload?: any) => void
111
208
  dispatch: (event: Event) => void
112
- getTypes: () => TypesConfig<TEntity>
113
- getType: (typeName: string) => EntityType<TEntity>
114
- setType: (typeName: string, type: EntityType<TEntity>) => void
209
+ getTypes: () => Record<string, EntityType>
210
+ getType: (typeName: string) => EntityType
211
+ setType: (typeName: string, type: AnyTypeDefinition) => void
115
212
  getState: () => TState
116
213
  getEntity: {
117
- <TId extends keyof TState & string>(id: TId): TState[TId] | undefined
118
- (id: string): TState[string] | undefined
214
+ <TId extends keyof TState & string>(
215
+ id: TId,
216
+ ): Stored<TState[TId]> | undefined
217
+ (id: string): Stored<TEntity> | undefined
119
218
  }
120
219
  setState: (nextState: TState) => void
121
220
  reset: () => void
package/types/test.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { BaseEntity, EntitiesState } from "./store"
1
+ import type { BaseEntity, EntitiesState, Event, Stored } from "./store"
2
2
 
3
3
  /**
4
4
  * Mock API for testing (subset of full API)
@@ -9,28 +9,33 @@ export interface MockApi<
9
9
  > {
10
10
  getEntities: {
11
11
  (): TState
12
- (typeName: string): TEntity[]
12
+ (typeName: string): Stored<TEntity>[]
13
13
  }
14
+ /** A known id gives that entity's own type; any other id, the union. */
14
15
  getEntity: {
15
- <TId extends keyof TState & string>(id: TId): TState[TId] | undefined
16
- (id: string): TEntity | undefined
16
+ <TId extends keyof TState & string>(
17
+ id: TId,
18
+ ): Stored<TState[TId]> | undefined
19
+ (id: string): Stored<TEntity> | undefined
17
20
  }
18
21
  select: <TResult>(selector: (state: TState) => TResult) => TResult
19
22
  dispatch: (event: Event) => void
20
23
  notify: (type: string, payload?: any) => void
24
+ /** The events dispatched so far. */
21
25
  getEvents: () => Event[]
22
26
  }
23
27
 
24
28
  /**
25
- * Result of trigger function
29
+ * Result of trigger function: the entity after the handler ran, and the
30
+ * events it dispatched.
26
31
  */
27
- export interface TriggerResult<TState extends EntitiesState = EntitiesState> {
28
- entities: TState
32
+ export interface TriggerResult<TEntity extends BaseEntity = BaseEntity> {
33
+ entity: TEntity
29
34
  events: Event[]
30
35
  }
31
36
 
32
37
  /**
33
- * Creates a mock API for testing event handlers
38
+ * Creates a mock API for testing event handlers, over the given entities.
34
39
  */
35
40
  export function createMockApi<
36
41
  TEntity extends BaseEntity = BaseEntity,
@@ -38,19 +43,24 @@ export function createMockApi<
38
43
  >(entities: TState): MockApi<TEntity, TState>
39
44
 
40
45
  /**
41
- * Triggers an event handler in isolation for testing
46
+ * Runs an event handler on a copy of an entity, as the store would, and
47
+ * returns the updated copy with the events it dispatched. Without an api, the
48
+ * handler gets a mock one holding just this entity.
49
+ *
50
+ * Any type's handler fits, whatever api it is typed with: it receives a
51
+ * `MockApi`, which covers reading entities and dispatching events.
52
+ *
53
+ * ```ts
54
+ * const { entity, events } = trigger(counter, Counter.increment, 5)
55
+ * ```
42
56
  */
43
- export function trigger<
44
- TEntity extends BaseEntity = BaseEntity,
45
- TState extends EntitiesState<TEntity> = EntitiesState<TEntity>,
46
- TPayload = any,
47
- >(
48
- entities: TState,
57
+ export function trigger<TEntity extends BaseEntity, TPayload = any>(
58
+ entity: TEntity,
49
59
  eventHandler: (
50
- state: TState,
60
+ entity: Stored<TEntity>,
51
61
  payload: TPayload,
52
- api: MockApi<TEntity, TState>,
53
- ) => void,
54
- eventPayload: TPayload,
55
- api?: MockApi<TEntity, TState>,
56
- ): TriggerResult<TState>
62
+ api: never,
63
+ ) => unknown,
64
+ eventPayload?: TPayload,
65
+ api?: MockApi,
66
+ ): TriggerResult<TEntity>