@couch-kit/core 0.6.0 → 0.8.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/CHANGELOG.md +12 -0
- package/README.md +71 -2
- package/dist/index.js +106 -2
- package/lib/index.d.ts +2 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/middleware.d.ts +67 -0
- package/lib/middleware.d.ts.map +1 -0
- package/lib/replay.d.ts +42 -0
- package/lib/replay.d.ts.map +1 -0
- package/lib/types.d.ts +18 -1
- package/lib/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +2 -0
- package/src/middleware.ts +204 -0
- package/src/replay.ts +80 -0
- package/src/types.ts +34 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @couch-kit/core
|
|
2
2
|
|
|
3
|
+
## 0.8.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#21](https://github.com/faluciano/react-native-couch-kit/pull/21) [`f1e836d`](https://github.com/faluciano/react-native-couch-kit/commit/f1e836d8f17821f0da20db7c3c552552c9b22133) Thanks [@faluciano](https://github.com/faluciano)! - Add `replayActions` utility function for replaying recorded game sessions against a reducer. Includes `Recording`, `RecordedAction`, `StateSnapshot`, and `ReplayResult` types.
|
|
8
|
+
|
|
9
|
+
## 0.7.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- [#19](https://github.com/faluciano/react-native-couch-kit/pull/19) [`978616b`](https://github.com/faluciano/react-native-couch-kit/commit/978616b767031e02e24fe48de8dbdf37d15be783) Thanks [@faluciano](https://github.com/faluciano)! - Add middleware system with `applyMiddleware`, `actionLogger`, and `actionValidator`. Middleware can observe and transform actions flowing through `createGameReducer`. Includes error boundaries for fault tolerance.
|
|
14
|
+
|
|
3
15
|
## 0.6.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -33,12 +33,12 @@ interface GameState extends IGameState {
|
|
|
33
33
|
score: number;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
type GameAction = { type: "SCORE" } | { type: "RESET" };
|
|
36
|
+
type GameAction = { type: "SCORE"; payload: number } | { type: "RESET" };
|
|
37
37
|
|
|
38
38
|
const gameReducer = (state: GameState, action: GameAction): GameState => {
|
|
39
39
|
switch (action.type) {
|
|
40
40
|
case "SCORE":
|
|
41
|
-
return { ...state, score: state.score +
|
|
41
|
+
return { ...state, score: state.score + action.payload };
|
|
42
42
|
case "RESET":
|
|
43
43
|
return { ...state, score: 0 };
|
|
44
44
|
default:
|
|
@@ -47,6 +47,75 @@ const gameReducer = (state: GameState, action: GameAction): GameState => {
|
|
|
47
47
|
};
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
+
### Middleware
|
|
51
|
+
|
|
52
|
+
`createGameReducer` accepts an optional second argument with a `middleware` array. Middlewares follow the Redux pattern — each is a three-layer curried function that can observe, transform, or block actions before they reach the reducer.
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
import {
|
|
56
|
+
createGameReducer,
|
|
57
|
+
actionLogger,
|
|
58
|
+
actionValidator,
|
|
59
|
+
type Middleware,
|
|
60
|
+
} from "@couch-kit/core";
|
|
61
|
+
|
|
62
|
+
const reducer = createGameReducer(gameReducer, {
|
|
63
|
+
middleware: [
|
|
64
|
+
actionLogger(),
|
|
65
|
+
actionValidator({ SCORE: (action) => action.payload > 0 }),
|
|
66
|
+
],
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Middlewares execute in array order: the first middleware sees the action first on the way in and last on the way out. Each middleware is wrapped in an error boundary — if one throws, it is skipped and the action continues to the next layer.
|
|
71
|
+
|
|
72
|
+
#### `actionLogger(options?)`
|
|
73
|
+
|
|
74
|
+
Logs every dispatched action with its previous state, action payload, and resulting state.
|
|
75
|
+
|
|
76
|
+
| Option | Type | Default | Description |
|
|
77
|
+
| ----------- | --------- | ------- | ----------------------------------------------- |
|
|
78
|
+
| `collapsed` | `boolean` | `true` | Use `console.groupCollapsed` instead of `group` |
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
actionLogger(); // collapsed groups (default)
|
|
82
|
+
actionLogger({ collapsed: false }); // expanded groups
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
#### `actionValidator(schema)`
|
|
86
|
+
|
|
87
|
+
Validates actions against a schema before they reach the reducer. If a validator returns `false`, the action is dropped and a warning is logged.
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
import { actionValidator, type ActionSchema } from "@couch-kit/core";
|
|
91
|
+
|
|
92
|
+
const schema: ActionSchema<GameAction> = {
|
|
93
|
+
SCORE: (action) => typeof action.payload === "number" && action.payload > 0,
|
|
94
|
+
RESET: () => true,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const validator = actionValidator<GameState, GameAction>(schema);
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Actions without a matching validator in the schema pass through unchanged. Internal actions (types prefixed with `__`) are never validated.
|
|
101
|
+
|
|
102
|
+
#### Custom middleware
|
|
103
|
+
|
|
104
|
+
A middleware is a function with the signature:
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
const myMiddleware: Middleware<GameState, GameAction> =
|
|
108
|
+
(api) => (next) => (action) => {
|
|
109
|
+
// `api.getState()` returns the current state
|
|
110
|
+
// `next(action)` passes the action to the next layer
|
|
111
|
+
// Return the resulting state
|
|
112
|
+
const result = next(action);
|
|
113
|
+
return result;
|
|
114
|
+
};
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
> **Note:** Internal actions (`__HYDRATE__`, `__PLAYER_JOINED__`, etc.) flow through middleware and are observable, but custom middleware should **not** block them. Doing so will break framework behaviour.
|
|
118
|
+
|
|
50
119
|
### Interfaces
|
|
51
120
|
|
|
52
121
|
#### `IPlayer`
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,77 @@
|
|
|
1
|
+
// src/middleware.ts
|
|
2
|
+
function applyMiddleware(...middlewares) {
|
|
3
|
+
return (reducer) => {
|
|
4
|
+
let currentState;
|
|
5
|
+
const api = {
|
|
6
|
+
getState: () => currentState
|
|
7
|
+
};
|
|
8
|
+
const baseDispatch = (action) => {
|
|
9
|
+
currentState = reducer(currentState, action);
|
|
10
|
+
return currentState;
|
|
11
|
+
};
|
|
12
|
+
const chain = middlewares.map((mw, index) => {
|
|
13
|
+
const layer = mw(api);
|
|
14
|
+
return (next) => {
|
|
15
|
+
return (action) => {
|
|
16
|
+
let nextCalled = false;
|
|
17
|
+
const safeNext = (a) => {
|
|
18
|
+
nextCalled = true;
|
|
19
|
+
return next(a);
|
|
20
|
+
};
|
|
21
|
+
const safeHandler = layer(safeNext);
|
|
22
|
+
try {
|
|
23
|
+
return safeHandler(action);
|
|
24
|
+
} catch (error) {
|
|
25
|
+
console.error(`[couch-kit] Middleware ${index} threw on action "${action.type}":`, error);
|
|
26
|
+
return nextCalled ? api.getState() : next(action);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
const dispatch = chain.reduceRight((next, layer) => layer(next), baseDispatch);
|
|
32
|
+
return (state, action) => {
|
|
33
|
+
currentState = state;
|
|
34
|
+
return dispatch(action);
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function actionLogger(options) {
|
|
39
|
+
const collapsed = options?.collapsed ?? true;
|
|
40
|
+
return (api) => (next) => (action) => {
|
|
41
|
+
const prevState = api.getState();
|
|
42
|
+
const group = collapsed ? console.groupCollapsed : console.group;
|
|
43
|
+
group(`[couch-kit] action ${action.type}`);
|
|
44
|
+
console.log("prev state", prevState);
|
|
45
|
+
console.log("action", action);
|
|
46
|
+
try {
|
|
47
|
+
const nextState = next(action);
|
|
48
|
+
console.log("next state", nextState);
|
|
49
|
+
return nextState;
|
|
50
|
+
} finally {
|
|
51
|
+
console.groupEnd();
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
var _internalTypes;
|
|
56
|
+
function getInternalTypes() {
|
|
57
|
+
if (!_internalTypes) {
|
|
58
|
+
_internalTypes = new Set(Object.values(InternalActionTypes));
|
|
59
|
+
}
|
|
60
|
+
return _internalTypes;
|
|
61
|
+
}
|
|
62
|
+
function actionValidator(schema) {
|
|
63
|
+
return (_api) => (next) => (action) => {
|
|
64
|
+
if (getInternalTypes().has(action.type))
|
|
65
|
+
return next(action);
|
|
66
|
+
const validate = schema[action.type];
|
|
67
|
+
if (validate && !validate(action)) {
|
|
68
|
+
console.warn(`[couch-kit] Action "${action.type}" failed validation, skipping`);
|
|
69
|
+
return _api.getState();
|
|
70
|
+
}
|
|
71
|
+
return next(action);
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
1
75
|
// src/types.ts
|
|
2
76
|
var InternalActionTypes = {
|
|
3
77
|
HYDRATE: "__HYDRATE__",
|
|
@@ -6,8 +80,8 @@ var InternalActionTypes = {
|
|
|
6
80
|
PLAYER_RECONNECTED: "__PLAYER_RECONNECTED__",
|
|
7
81
|
PLAYER_REMOVED: "__PLAYER_REMOVED__"
|
|
8
82
|
};
|
|
9
|
-
function createGameReducer(reducer) {
|
|
10
|
-
|
|
83
|
+
function createGameReducer(reducer, options) {
|
|
84
|
+
const baseReducer = (state, action) => {
|
|
11
85
|
switch (action.type) {
|
|
12
86
|
case InternalActionTypes.HYDRATE:
|
|
13
87
|
return action.payload;
|
|
@@ -67,6 +141,10 @@ function createGameReducer(reducer) {
|
|
|
67
141
|
return reducer(state, action);
|
|
68
142
|
}
|
|
69
143
|
};
|
|
144
|
+
const middlewares = options?.middleware;
|
|
145
|
+
if (!middlewares || middlewares.length === 0)
|
|
146
|
+
return baseReducer;
|
|
147
|
+
return applyMiddleware(...middlewares)(baseReducer);
|
|
70
148
|
}
|
|
71
149
|
// src/protocol.ts
|
|
72
150
|
var MessageTypes = {
|
|
@@ -122,13 +200,39 @@ function toErrorMessage(error) {
|
|
|
122
200
|
return error.message;
|
|
123
201
|
return String(error);
|
|
124
202
|
}
|
|
203
|
+
// src/replay.ts
|
|
204
|
+
function replayActions(recording, reducer) {
|
|
205
|
+
const snapshots = [];
|
|
206
|
+
let currentState = recording.initialState;
|
|
207
|
+
for (let i = 0;i < recording.actions.length; i++) {
|
|
208
|
+
const { action, timestamp } = recording.actions[i];
|
|
209
|
+
currentState = reducer(currentState, action);
|
|
210
|
+
snapshots.push({
|
|
211
|
+
state: currentState,
|
|
212
|
+
action,
|
|
213
|
+
timestamp,
|
|
214
|
+
index: i
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
const endTimestamp = recording.endTimestamp ?? (recording.actions.length > 0 ? recording.actions[recording.actions.length - 1].timestamp : recording.startTimestamp);
|
|
218
|
+
return {
|
|
219
|
+
finalState: currentState,
|
|
220
|
+
snapshots,
|
|
221
|
+
duration: endTimestamp - recording.startTimestamp,
|
|
222
|
+
actionCount: recording.actions.length
|
|
223
|
+
};
|
|
224
|
+
}
|
|
125
225
|
export {
|
|
126
226
|
toErrorMessage,
|
|
227
|
+
replayActions,
|
|
127
228
|
isValidSecret,
|
|
128
229
|
generateId,
|
|
129
230
|
derivePlayerIdLegacy,
|
|
130
231
|
derivePlayerId,
|
|
131
232
|
createGameReducer,
|
|
233
|
+
applyMiddleware,
|
|
234
|
+
actionValidator,
|
|
235
|
+
actionLogger,
|
|
132
236
|
MessageTypes,
|
|
133
237
|
MAX_PENDING_PINGS,
|
|
134
238
|
MAX_FRAME_SIZE,
|
package/lib/index.d.ts
CHANGED
package/lib/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,UAAU,CAAC;AACzB,cAAc,cAAc,CAAC"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { IGameState, IAction, InternalAction, GameReducer } from "./types";
|
|
2
|
+
/** The API surface available to each middleware. */
|
|
3
|
+
export interface MiddlewareAPI<S extends IGameState> {
|
|
4
|
+
getState: () => S;
|
|
5
|
+
}
|
|
6
|
+
/** A dispatch function that accepts user actions or internal actions. */
|
|
7
|
+
export type MiddlewareDispatch<S extends IGameState, A extends IAction> = (action: A | InternalAction<S>) => S;
|
|
8
|
+
/**
|
|
9
|
+
* Redux-style middleware — a three-layer curried function:
|
|
10
|
+
*
|
|
11
|
+
* 1. Receives the middleware API (`getState`).
|
|
12
|
+
* 2. Receives `next` (the downstream dispatch).
|
|
13
|
+
* 3. Receives the `action` and returns the new state.
|
|
14
|
+
*/
|
|
15
|
+
export type Middleware<S extends IGameState = IGameState, A extends IAction = IAction> = (api: MiddlewareAPI<S>) => (next: MiddlewareDispatch<S, A>) => (action: A | InternalAction<S>) => S;
|
|
16
|
+
/**
|
|
17
|
+
* Composes an array of middlewares into a higher-order function that wraps a
|
|
18
|
+
* game reducer.
|
|
19
|
+
*
|
|
20
|
+
* Middleware ordering follows the Redux convention: the **first** middleware in
|
|
21
|
+
* the array is the outermost layer — it sees the action first on the way in and
|
|
22
|
+
* last on the way out.
|
|
23
|
+
*
|
|
24
|
+
* Each middleware layer is wrapped in a try / catch. If a middleware throws, the
|
|
25
|
+
* error is logged and the action falls through to the next layer so a single
|
|
26
|
+
* broken middleware never tears down the entire dispatch chain. If the middleware
|
|
27
|
+
* already called `next` before throwing, the error boundary returns the current
|
|
28
|
+
* state instead of re-dispatching to prevent double-dispatch.
|
|
29
|
+
*/
|
|
30
|
+
export declare function applyMiddleware<S extends IGameState, A extends IAction>(...middlewares: Middleware<S, A>[]): (reducer: GameReducer<S, A | InternalAction<S>>) => GameReducer<S, A | InternalAction<S>>;
|
|
31
|
+
/** Options for the built-in action logger middleware. */
|
|
32
|
+
export interface ActionLoggerOptions {
|
|
33
|
+
/** Use `console.groupCollapsed` instead of `console.group`. Defaults to `true`. */
|
|
34
|
+
collapsed?: boolean;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Middleware that logs every dispatched action together with the previous and
|
|
38
|
+
* next state.
|
|
39
|
+
*
|
|
40
|
+
* ```ts
|
|
41
|
+
* createGameReducer(reducer, { middleware: [actionLogger()] });
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export declare function actionLogger<S extends IGameState, A extends IAction>(options?: ActionLoggerOptions): Middleware<S, A>;
|
|
45
|
+
/**
|
|
46
|
+
* A map from action type strings to validator functions. Each validator receives
|
|
47
|
+
* the fully-typed action and returns `true` if the action is valid.
|
|
48
|
+
*/
|
|
49
|
+
export type ActionSchema<A extends IAction> = {
|
|
50
|
+
[K in A["type"]]?: (action: A & {
|
|
51
|
+
type: K;
|
|
52
|
+
}) => boolean;
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Middleware that validates actions against a schema before they reach the
|
|
56
|
+
* reducer. Invalid actions are silently dropped (with a console warning).
|
|
57
|
+
*
|
|
58
|
+
* Internal actions are never validated — they always pass through.
|
|
59
|
+
*
|
|
60
|
+
* ```ts
|
|
61
|
+
* const validator = actionValidator<GameState, GameAction>({
|
|
62
|
+
* SCORE: (action) => action.payload > 0,
|
|
63
|
+
* });
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
export declare function actionValidator<S extends IGameState, A extends IAction>(schema: ActionSchema<A>): Middleware<S, A>;
|
|
67
|
+
//# sourceMappingURL=middleware.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../src/middleware.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAOhF,oDAAoD;AACpD,MAAM,WAAW,aAAa,CAAC,CAAC,SAAS,UAAU;IACjD,QAAQ,EAAE,MAAM,CAAC,CAAC;CACnB;AAED,yEAAyE;AACzE,MAAM,MAAM,kBAAkB,CAAC,CAAC,SAAS,UAAU,EAAE,CAAC,SAAS,OAAO,IAAI,CACxE,MAAM,EAAE,CAAC,GAAG,cAAc,CAAC,CAAC,CAAC,KAC1B,CAAC,CAAC;AAEP;;;;;;GAMG;AACH,MAAM,MAAM,UAAU,CACpB,CAAC,SAAS,UAAU,GAAG,UAAU,EACjC,CAAC,SAAS,OAAO,GAAG,OAAO,IACzB,CACF,GAAG,EAAE,aAAa,CAAC,CAAC,CAAC,KAClB,CAAC,IAAI,EAAE,kBAAkB,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,GAAG,cAAc,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;AAM9E;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,UAAU,EAAE,CAAC,SAAS,OAAO,EACrE,GAAG,WAAW,EAAE,UAAU,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,GACjC,CACD,OAAO,EAAE,WAAW,CAAC,CAAC,EAAE,CAAC,GAAG,cAAc,CAAC,CAAC,CAAC,CAAC,KAC3C,WAAW,CAAC,CAAC,EAAE,CAAC,GAAG,cAAc,CAAC,CAAC,CAAC,CAAC,CAwDzC;AAMD,yDAAyD;AACzD,MAAM,WAAW,mBAAmB;IAClC,mFAAmF;IACnF,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,CAAC,SAAS,UAAU,EAAE,CAAC,SAAS,OAAO,EAClE,OAAO,CAAC,EAAE,mBAAmB,GAC5B,UAAU,CAAC,CAAC,EAAE,CAAC,CAAC,CAmBlB;AAeD;;;GAGG;AACH,MAAM,MAAM,YAAY,CAAC,CAAC,SAAS,OAAO,IAAI;KAC3C,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,GAAG;QAAE,IAAI,EAAE,CAAC,CAAA;KAAE,KAAK,OAAO;CACxD,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,UAAU,EAAE,CAAC,SAAS,OAAO,EACrE,MAAM,EAAE,YAAY,CAAC,CAAC,CAAC,GACtB,UAAU,CAAC,CAAC,EAAE,CAAC,CAAC,CAgBlB"}
|
package/lib/replay.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { GameReducer, IAction, IGameState } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* A recorded action with timing metadata.
|
|
4
|
+
*/
|
|
5
|
+
export interface RecordedAction<A extends IAction = IAction> {
|
|
6
|
+
action: A;
|
|
7
|
+
timestamp: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* A snapshot of state at a point in time during replay.
|
|
11
|
+
*/
|
|
12
|
+
export interface StateSnapshot<S extends IGameState = IGameState> {
|
|
13
|
+
state: S;
|
|
14
|
+
action: IAction;
|
|
15
|
+
timestamp: number;
|
|
16
|
+
index: number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* A recording that can be replayed against a reducer.
|
|
20
|
+
*/
|
|
21
|
+
export interface Recording<S extends IGameState = IGameState, A extends IAction = IAction> {
|
|
22
|
+
initialState: S;
|
|
23
|
+
actions: RecordedAction<A>[];
|
|
24
|
+
startTimestamp: number;
|
|
25
|
+
endTimestamp?: number;
|
|
26
|
+
metadata?: Record<string, unknown>;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The result of replaying a recording.
|
|
30
|
+
*/
|
|
31
|
+
export interface ReplayResult<S extends IGameState = IGameState> {
|
|
32
|
+
finalState: S;
|
|
33
|
+
snapshots: StateSnapshot<S>[];
|
|
34
|
+
duration: number;
|
|
35
|
+
actionCount: number;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Replays a recording against a reducer, producing the final state
|
|
39
|
+
* and intermediate snapshots for each action applied.
|
|
40
|
+
*/
|
|
41
|
+
export declare function replayActions<S extends IGameState = IGameState, A extends IAction = IAction>(recording: Recording<S, A>, reducer: GameReducer<S, A>): ReplayResult<S>;
|
|
42
|
+
//# sourceMappingURL=replay.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"replay.d.ts","sourceRoot":"","sources":["../src/replay.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAEhE;;GAEG;AACH,MAAM,WAAW,cAAc,CAAC,CAAC,SAAS,OAAO,GAAG,OAAO;IACzD,MAAM,EAAE,CAAC,CAAC;IACV,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,aAAa,CAAC,CAAC,SAAS,UAAU,GAAG,UAAU;IAC9D,KAAK,EAAE,CAAC,CAAC;IACT,MAAM,EAAE,OAAO,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;GAEG;AACH,MAAM,WAAW,SAAS,CACxB,CAAC,SAAS,UAAU,GAAG,UAAU,EACjC,CAAC,SAAS,OAAO,GAAG,OAAO;IAE3B,YAAY,EAAE,CAAC,CAAC;IAChB,OAAO,EAAE,cAAc,CAAC,CAAC,CAAC,EAAE,CAAC;IAC7B,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED;;GAEG;AACH,MAAM,WAAW,YAAY,CAAC,CAAC,SAAS,UAAU,GAAG,UAAU;IAC7D,UAAU,EAAE,CAAC,CAAC;IACd,SAAS,EAAE,aAAa,CAAC,CAAC,CAAC,EAAE,CAAC;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,CAAC,SAAS,UAAU,GAAG,UAAU,EACjC,CAAC,SAAS,OAAO,GAAG,OAAO,EAC3B,SAAS,EAAE,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,YAAY,CAAC,CAAC,CAAC,CA4BzE"}
|
package/lib/types.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Middleware } from "./middleware";
|
|
1
2
|
/**
|
|
2
3
|
* Represents a player connected to the game session.
|
|
3
4
|
*
|
|
@@ -84,6 +85,18 @@ export declare const InternalActionTypes: {
|
|
|
84
85
|
* Used by both `GameHostProvider` and `useGameClient` to define game logic.
|
|
85
86
|
*/
|
|
86
87
|
export type GameReducer<S extends IGameState, A extends IAction> = (state: S, action: A) => S;
|
|
88
|
+
/** Options for `createGameReducer`. */
|
|
89
|
+
export interface CreateGameReducerOptions<S extends IGameState, A extends IAction> {
|
|
90
|
+
/**
|
|
91
|
+
* Optional middleware stack. Middlewares are applied in order — the first
|
|
92
|
+
* middleware in the array is the outermost layer and sees every action first.
|
|
93
|
+
*
|
|
94
|
+
* Middleware can observe **and** transform both user actions and internal
|
|
95
|
+
* actions. Custom middleware should avoid blocking internal actions (types
|
|
96
|
+
* prefixed with `__`) to prevent breaking framework behaviour.
|
|
97
|
+
*/
|
|
98
|
+
middleware?: Middleware<S, A>[];
|
|
99
|
+
}
|
|
87
100
|
/**
|
|
88
101
|
* Wraps a user-provided reducer with automatic handling of internal actions:
|
|
89
102
|
*
|
|
@@ -95,6 +108,10 @@ export type GameReducer<S extends IGameState, A extends IAction> = (state: S, ac
|
|
|
95
108
|
*
|
|
96
109
|
* The wrapped reducer accepts both `A` (user actions) and `InternalAction<S>`.
|
|
97
110
|
* User reducers only need to handle their own action types.
|
|
111
|
+
*
|
|
112
|
+
* When `options.middleware` is provided, the middleware stack wraps around the
|
|
113
|
+
* entire reducer (including internal-action handling). Middleware can observe
|
|
114
|
+
* all actions flowing through the reducer.
|
|
98
115
|
*/
|
|
99
|
-
export declare function createGameReducer<S extends IGameState, A extends IAction>(reducer: GameReducer<S, A>): GameReducer<S, A | InternalAction<S>>;
|
|
116
|
+
export declare function createGameReducer<S extends IGameState, A extends IAction>(reducer: GameReducer<S, A>, options?: CreateGameReducerOptions<S, A>): GameReducer<S, A | InternalAction<S>>;
|
|
100
117
|
//# sourceMappingURL=types.d.ts.map
|
package/lib/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE/C;;;;;GAKG;AACH,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,OAAO,CAAC;IAChB,SAAS,EAAE,OAAO,CAAC;CACpB;AAED;;;;;GAKG;AACH,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAED;;;;;GAKG;AACH,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;GASG;AACH,MAAM,MAAM,cAAc,CAAC,CAAC,SAAS,UAAU,GAAG,UAAU,IACxD;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,OAAO,EAAE,CAAC,CAAA;CAAE,GACnC;IACE,IAAI,EAAE,mBAAmB,CAAC;IAC1B,OAAO,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CACxD,GACD;IAAE,IAAI,EAAE,iBAAiB,CAAC;IAAC,OAAO,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,GAC1D;IAAE,IAAI,EAAE,wBAAwB,CAAC;IAAC,OAAO,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,GACjE;IAAE,IAAI,EAAE,oBAAoB,CAAC;IAAC,OAAO,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,CAAC;AAElE,+CAA+C;AAC/C,eAAO,MAAM,mBAAmB;;;;;;CAMtB,CAAC;AAEX;;;;;GAKG;AACH,MAAM,MAAM,WAAW,CAAC,CAAC,SAAS,UAAU,EAAE,CAAC,SAAS,OAAO,IAAI,CACjE,KAAK,EAAE,CAAC,EACR,MAAM,EAAE,CAAC,KACN,CAAC,CAAC;AAEP,uCAAuC;AACvC,MAAM,WAAW,wBAAwB,CACvC,CAAC,SAAS,UAAU,EACpB,CAAC,SAAS,OAAO;IAEjB;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,UAAU,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;CACjC;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,SAAS,UAAU,EAAE,CAAC,SAAS,OAAO,EACvE,OAAO,EAAE,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC,EAC1B,OAAO,CAAC,EAAE,wBAAwB,CAAC,CAAC,EAAE,CAAC,CAAC,GACvC,WAAW,CAAC,CAAC,EAAE,CAAC,GAAG,cAAc,CAAC,CAAC,CAAC,CAAC,CAgFvC"}
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import type { IGameState, IAction, InternalAction, GameReducer } from "./types";
|
|
2
|
+
import { InternalActionTypes } from "./types";
|
|
3
|
+
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Middleware types
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
|
|
8
|
+
/** The API surface available to each middleware. */
|
|
9
|
+
export interface MiddlewareAPI<S extends IGameState> {
|
|
10
|
+
getState: () => S;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** A dispatch function that accepts user actions or internal actions. */
|
|
14
|
+
export type MiddlewareDispatch<S extends IGameState, A extends IAction> = (
|
|
15
|
+
action: A | InternalAction<S>,
|
|
16
|
+
) => S;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Redux-style middleware — a three-layer curried function:
|
|
20
|
+
*
|
|
21
|
+
* 1. Receives the middleware API (`getState`).
|
|
22
|
+
* 2. Receives `next` (the downstream dispatch).
|
|
23
|
+
* 3. Receives the `action` and returns the new state.
|
|
24
|
+
*/
|
|
25
|
+
export type Middleware<
|
|
26
|
+
S extends IGameState = IGameState,
|
|
27
|
+
A extends IAction = IAction,
|
|
28
|
+
> = (
|
|
29
|
+
api: MiddlewareAPI<S>,
|
|
30
|
+
) => (next: MiddlewareDispatch<S, A>) => (action: A | InternalAction<S>) => S;
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// applyMiddleware
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Composes an array of middlewares into a higher-order function that wraps a
|
|
38
|
+
* game reducer.
|
|
39
|
+
*
|
|
40
|
+
* Middleware ordering follows the Redux convention: the **first** middleware in
|
|
41
|
+
* the array is the outermost layer — it sees the action first on the way in and
|
|
42
|
+
* last on the way out.
|
|
43
|
+
*
|
|
44
|
+
* Each middleware layer is wrapped in a try / catch. If a middleware throws, the
|
|
45
|
+
* error is logged and the action falls through to the next layer so a single
|
|
46
|
+
* broken middleware never tears down the entire dispatch chain. If the middleware
|
|
47
|
+
* already called `next` before throwing, the error boundary returns the current
|
|
48
|
+
* state instead of re-dispatching to prevent double-dispatch.
|
|
49
|
+
*/
|
|
50
|
+
export function applyMiddleware<S extends IGameState, A extends IAction>(
|
|
51
|
+
...middlewares: Middleware<S, A>[]
|
|
52
|
+
): (
|
|
53
|
+
reducer: GameReducer<S, A | InternalAction<S>>,
|
|
54
|
+
) => GameReducer<S, A | InternalAction<S>> {
|
|
55
|
+
return (reducer: GameReducer<S, A | InternalAction<S>>) => {
|
|
56
|
+
// Mutable ref so `getState()` always returns the latest state.
|
|
57
|
+
let currentState: S;
|
|
58
|
+
|
|
59
|
+
const api: MiddlewareAPI<S> = {
|
|
60
|
+
getState: () => currentState,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// The innermost dispatch simply runs the actual reducer and updates the
|
|
64
|
+
// mutable ref.
|
|
65
|
+
const baseDispatch: MiddlewareDispatch<S, A> = (action) => {
|
|
66
|
+
currentState = reducer(currentState, action);
|
|
67
|
+
return currentState;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// Build the chain by initialising each middleware with the API, then
|
|
71
|
+
// composing right-to-left so the first middleware is outermost.
|
|
72
|
+
const chain = middlewares.map((mw, index) => {
|
|
73
|
+
const layer = mw(api);
|
|
74
|
+
// Return a wrapper that adds an error boundary around each layer.
|
|
75
|
+
// We call `layer(safeNext)` inside the action handler so we can track
|
|
76
|
+
// whether `next` was already called — preventing double-dispatch.
|
|
77
|
+
return (next: MiddlewareDispatch<S, A>): MiddlewareDispatch<S, A> => {
|
|
78
|
+
return (action: A | InternalAction<S>): S => {
|
|
79
|
+
let nextCalled = false;
|
|
80
|
+
const safeNext: MiddlewareDispatch<S, A> = (a) => {
|
|
81
|
+
nextCalled = true;
|
|
82
|
+
return next(a);
|
|
83
|
+
};
|
|
84
|
+
const safeHandler = layer(safeNext);
|
|
85
|
+
try {
|
|
86
|
+
return safeHandler(action);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
console.error(
|
|
89
|
+
`[couch-kit] Middleware ${index} threw on action "${action.type}":`,
|
|
90
|
+
error,
|
|
91
|
+
);
|
|
92
|
+
return nextCalled ? api.getState() : next(action);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
};
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Compose right-to-left: last middleware wraps baseDispatch first.
|
|
99
|
+
const dispatch = chain.reduceRight<MiddlewareDispatch<S, A>>(
|
|
100
|
+
(next, layer) => layer(next),
|
|
101
|
+
baseDispatch,
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
// Return a reducer with the same signature that threads through the chain.
|
|
105
|
+
return (state: S, action: A | InternalAction<S>): S => {
|
|
106
|
+
currentState = state;
|
|
107
|
+
return dispatch(action);
|
|
108
|
+
};
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
// actionLogger
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
/** Options for the built-in action logger middleware. */
|
|
117
|
+
export interface ActionLoggerOptions {
|
|
118
|
+
/** Use `console.groupCollapsed` instead of `console.group`. Defaults to `true`. */
|
|
119
|
+
collapsed?: boolean;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Middleware that logs every dispatched action together with the previous and
|
|
124
|
+
* next state.
|
|
125
|
+
*
|
|
126
|
+
* ```ts
|
|
127
|
+
* createGameReducer(reducer, { middleware: [actionLogger()] });
|
|
128
|
+
* ```
|
|
129
|
+
*/
|
|
130
|
+
export function actionLogger<S extends IGameState, A extends IAction>(
|
|
131
|
+
options?: ActionLoggerOptions,
|
|
132
|
+
): Middleware<S, A> {
|
|
133
|
+
const collapsed = options?.collapsed ?? true;
|
|
134
|
+
|
|
135
|
+
return (api) => (next) => (action) => {
|
|
136
|
+
const prevState = api.getState();
|
|
137
|
+
const group = collapsed ? console.groupCollapsed : console.group;
|
|
138
|
+
|
|
139
|
+
group(`[couch-kit] action ${action.type}`);
|
|
140
|
+
console.log("prev state", prevState);
|
|
141
|
+
console.log("action", action);
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
const nextState = next(action);
|
|
145
|
+
console.log("next state", nextState);
|
|
146
|
+
return nextState;
|
|
147
|
+
} finally {
|
|
148
|
+
console.groupEnd();
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
// actionValidator
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
/** Lazily-initialised set of internal action type strings for fast lookup. */
|
|
158
|
+
let _internalTypes: Set<string> | undefined;
|
|
159
|
+
function getInternalTypes(): Set<string> {
|
|
160
|
+
if (!_internalTypes) {
|
|
161
|
+
_internalTypes = new Set<string>(Object.values(InternalActionTypes));
|
|
162
|
+
}
|
|
163
|
+
return _internalTypes;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* A map from action type strings to validator functions. Each validator receives
|
|
168
|
+
* the fully-typed action and returns `true` if the action is valid.
|
|
169
|
+
*/
|
|
170
|
+
export type ActionSchema<A extends IAction> = {
|
|
171
|
+
[K in A["type"]]?: (action: A & { type: K }) => boolean;
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Middleware that validates actions against a schema before they reach the
|
|
176
|
+
* reducer. Invalid actions are silently dropped (with a console warning).
|
|
177
|
+
*
|
|
178
|
+
* Internal actions are never validated — they always pass through.
|
|
179
|
+
*
|
|
180
|
+
* ```ts
|
|
181
|
+
* const validator = actionValidator<GameState, GameAction>({
|
|
182
|
+
* SCORE: (action) => action.payload > 0,
|
|
183
|
+
* });
|
|
184
|
+
* ```
|
|
185
|
+
*/
|
|
186
|
+
export function actionValidator<S extends IGameState, A extends IAction>(
|
|
187
|
+
schema: ActionSchema<A>,
|
|
188
|
+
): Middleware<S, A> {
|
|
189
|
+
return (_api) => (next) => (action) => {
|
|
190
|
+
// Internal actions always pass through — never validate them.
|
|
191
|
+
if (getInternalTypes().has(action.type)) return next(action);
|
|
192
|
+
|
|
193
|
+
const validate = schema[action.type as A["type"]];
|
|
194
|
+
|
|
195
|
+
if (validate && !validate(action as A & { type: A["type"] })) {
|
|
196
|
+
console.warn(
|
|
197
|
+
`[couch-kit] Action "${action.type}" failed validation, skipping`,
|
|
198
|
+
);
|
|
199
|
+
return _api.getState();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return next(action);
|
|
203
|
+
};
|
|
204
|
+
}
|
package/src/replay.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { GameReducer, IAction, IGameState } from "./types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A recorded action with timing metadata.
|
|
5
|
+
*/
|
|
6
|
+
export interface RecordedAction<A extends IAction = IAction> {
|
|
7
|
+
action: A;
|
|
8
|
+
timestamp: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A snapshot of state at a point in time during replay.
|
|
13
|
+
*/
|
|
14
|
+
export interface StateSnapshot<S extends IGameState = IGameState> {
|
|
15
|
+
state: S;
|
|
16
|
+
action: IAction;
|
|
17
|
+
timestamp: number;
|
|
18
|
+
index: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A recording that can be replayed against a reducer.
|
|
23
|
+
*/
|
|
24
|
+
export interface Recording<
|
|
25
|
+
S extends IGameState = IGameState,
|
|
26
|
+
A extends IAction = IAction,
|
|
27
|
+
> {
|
|
28
|
+
initialState: S;
|
|
29
|
+
actions: RecordedAction<A>[];
|
|
30
|
+
startTimestamp: number;
|
|
31
|
+
endTimestamp?: number;
|
|
32
|
+
metadata?: Record<string, unknown>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The result of replaying a recording.
|
|
37
|
+
*/
|
|
38
|
+
export interface ReplayResult<S extends IGameState = IGameState> {
|
|
39
|
+
finalState: S;
|
|
40
|
+
snapshots: StateSnapshot<S>[];
|
|
41
|
+
duration: number;
|
|
42
|
+
actionCount: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Replays a recording against a reducer, producing the final state
|
|
47
|
+
* and intermediate snapshots for each action applied.
|
|
48
|
+
*/
|
|
49
|
+
export function replayActions<
|
|
50
|
+
S extends IGameState = IGameState,
|
|
51
|
+
A extends IAction = IAction,
|
|
52
|
+
>(recording: Recording<S, A>, reducer: GameReducer<S, A>): ReplayResult<S> {
|
|
53
|
+
const snapshots: StateSnapshot<S>[] = [];
|
|
54
|
+
let currentState = recording.initialState;
|
|
55
|
+
|
|
56
|
+
for (let i = 0; i < recording.actions.length; i++) {
|
|
57
|
+
const { action, timestamp } = recording.actions[i];
|
|
58
|
+
currentState = reducer(currentState, action);
|
|
59
|
+
|
|
60
|
+
snapshots.push({
|
|
61
|
+
state: currentState,
|
|
62
|
+
action,
|
|
63
|
+
timestamp,
|
|
64
|
+
index: i,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const endTimestamp =
|
|
69
|
+
recording.endTimestamp ??
|
|
70
|
+
(recording.actions.length > 0
|
|
71
|
+
? recording.actions[recording.actions.length - 1].timestamp
|
|
72
|
+
: recording.startTimestamp);
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
finalState: currentState,
|
|
76
|
+
snapshots,
|
|
77
|
+
duration: endTimestamp - recording.startTimestamp,
|
|
78
|
+
actionCount: recording.actions.length,
|
|
79
|
+
};
|
|
80
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { applyMiddleware } from "./middleware";
|
|
2
|
+
import type { Middleware } from "./middleware";
|
|
3
|
+
|
|
1
4
|
/**
|
|
2
5
|
* Represents a player connected to the game session.
|
|
3
6
|
*
|
|
@@ -76,6 +79,22 @@ export type GameReducer<S extends IGameState, A extends IAction> = (
|
|
|
76
79
|
action: A,
|
|
77
80
|
) => S;
|
|
78
81
|
|
|
82
|
+
/** Options for `createGameReducer`. */
|
|
83
|
+
export interface CreateGameReducerOptions<
|
|
84
|
+
S extends IGameState,
|
|
85
|
+
A extends IAction,
|
|
86
|
+
> {
|
|
87
|
+
/**
|
|
88
|
+
* Optional middleware stack. Middlewares are applied in order — the first
|
|
89
|
+
* middleware in the array is the outermost layer and sees every action first.
|
|
90
|
+
*
|
|
91
|
+
* Middleware can observe **and** transform both user actions and internal
|
|
92
|
+
* actions. Custom middleware should avoid blocking internal actions (types
|
|
93
|
+
* prefixed with `__`) to prevent breaking framework behaviour.
|
|
94
|
+
*/
|
|
95
|
+
middleware?: Middleware<S, A>[];
|
|
96
|
+
}
|
|
97
|
+
|
|
79
98
|
/**
|
|
80
99
|
* Wraps a user-provided reducer with automatic handling of internal actions:
|
|
81
100
|
*
|
|
@@ -87,11 +106,19 @@ export type GameReducer<S extends IGameState, A extends IAction> = (
|
|
|
87
106
|
*
|
|
88
107
|
* The wrapped reducer accepts both `A` (user actions) and `InternalAction<S>`.
|
|
89
108
|
* User reducers only need to handle their own action types.
|
|
109
|
+
*
|
|
110
|
+
* When `options.middleware` is provided, the middleware stack wraps around the
|
|
111
|
+
* entire reducer (including internal-action handling). Middleware can observe
|
|
112
|
+
* all actions flowing through the reducer.
|
|
90
113
|
*/
|
|
91
114
|
export function createGameReducer<S extends IGameState, A extends IAction>(
|
|
92
115
|
reducer: GameReducer<S, A>,
|
|
116
|
+
options?: CreateGameReducerOptions<S, A>,
|
|
93
117
|
): GameReducer<S, A | InternalAction<S>> {
|
|
94
|
-
|
|
118
|
+
const baseReducer: GameReducer<S, A | InternalAction<S>> = (
|
|
119
|
+
state: S,
|
|
120
|
+
action: A | InternalAction<S>,
|
|
121
|
+
): S => {
|
|
95
122
|
switch (action.type) {
|
|
96
123
|
case InternalActionTypes.HYDRATE:
|
|
97
124
|
return (action as InternalAction<S> & { type: "__HYDRATE__" }).payload;
|
|
@@ -161,4 +188,10 @@ export function createGameReducer<S extends IGameState, A extends IAction>(
|
|
|
161
188
|
return reducer(state, action as A);
|
|
162
189
|
}
|
|
163
190
|
};
|
|
191
|
+
|
|
192
|
+
const middlewares = options?.middleware;
|
|
193
|
+
|
|
194
|
+
if (!middlewares || middlewares.length === 0) return baseReducer;
|
|
195
|
+
|
|
196
|
+
return applyMiddleware<S, A>(...middlewares)(baseReducer);
|
|
164
197
|
}
|