@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 +117 -47
- package/package.json +5 -3
- package/src/test.js +1 -1
- package/types/api.d.ts +15 -8
- package/types/select.d.ts +36 -1
- package/types/store.d.ts +118 -19
- package/types/test.d.ts +31 -21
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
|
|
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
|
-
|
|
1386
|
+
**1. Describe your entities and state**
|
|
1387
1387
|
|
|
1388
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
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
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1441
|
-
import { types } from "./types"
|
|
1442
|
-
import type { TodoListEntity, TodoListState } from "../../types"
|
|
1474
|
+
import { createStore } from "@inglorious/store"
|
|
1443
1475
|
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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({
|
|
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
|
-
|
|
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: () =>
|
|
18
|
-
getType: (typeName: string) => EntityType
|
|
19
|
-
setType: (typeName: string, type:
|
|
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>(
|
|
27
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
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<
|
|
28
|
-
|
|
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
|
-
*
|
|
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<
|
|
49
|
-
|
|
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
|
-
|
|
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: () =>
|
|
113
|
-
getType: (typeName: string) => EntityType
|
|
114
|
-
setType: (typeName: string, type:
|
|
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>(
|
|
118
|
-
|
|
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>(
|
|
16
|
-
|
|
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<
|
|
28
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
60
|
+
entity: Stored<TEntity>,
|
|
51
61
|
payload: TPayload,
|
|
52
|
-
api:
|
|
53
|
-
) =>
|
|
54
|
-
eventPayload
|
|
55
|
-
api?: MockApi
|
|
56
|
-
): TriggerResult<
|
|
62
|
+
api: never,
|
|
63
|
+
) => unknown,
|
|
64
|
+
eventPayload?: TPayload,
|
|
65
|
+
api?: MockApi,
|
|
66
|
+
): TriggerResult<TEntity>
|