Skip to content

Commit 01c522c

Browse files
vdfdevflamecoalsdelucis
authored
feat: Throw error if non-serializable state is used in a move (#896)
* Adds check to throw error if non-serialiable state is used * Ignore line that cant be tested * More lines ignored for coverage * More efficiently ignoring coverage * Address review * Removes unnecessary diff * Fix integration test * Fix lint warnings * Update src/plugins/plugin-serializable.ts Co-authored-by: Chris Swithinbank <[email protected]> Co-authored-by: flamecoals <[email protected]> Co-authored-by: Chris Swithinbank <[email protected]>
1 parent ccc9ada commit 01c522c

6 files changed

Lines changed: 103 additions & 3 deletions

File tree

integration/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"version": "0.1.0",
44
"private": true,
55
"dependencies": {
6+
"boardgame.io": "file:boardgame.io-0.43.2.tgz",
67
"react": "^16.7.0",
78
"react-dom": "^16.7.0",
89
"react-scripts": "^2.1.3"

package-lock.json

Lines changed: 1 addition & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@
156156
"koa-body": "^4.1.0",
157157
"koa-router": "^7.2.1",
158158
"koa-socket-2": "^1.0.17",
159+
"lodash.isplainobject": "^4.0.6",
159160
"nanoid": "^3.1.20",
160161
"p-queue": "^6.6.2",
161162
"prop-types": "^15.5.10",

src/plugins/main.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import PluginImmer from './plugin-immer';
1010
import PluginRandom from './plugin-random';
1111
import PluginEvents from './plugin-events';
1212
import PluginLog from './plugin-log';
13+
import PluginSerializable from './plugin-serializable';
1314
import type {
1415
AnyFn,
1516
PartialGameState,
@@ -29,7 +30,13 @@ interface PluginOpts {
2930
/**
3031
* List of plugins that are always added.
3132
*/
32-
const DEFAULT_PLUGINS = [PluginImmer, PluginRandom, PluginEvents, PluginLog];
33+
const DEFAULT_PLUGINS = [
34+
PluginImmer,
35+
PluginRandom,
36+
PluginEvents,
37+
PluginLog,
38+
PluginSerializable,
39+
];
3340

3441
/**
3542
* Allow plugins to intercept actions and process them.
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { Client } from '../client/client';
2+
3+
describe('plugin-serializable', () => {
4+
let client;
5+
6+
beforeAll(() => {
7+
const game = {
8+
moves: {
9+
serializable: () => {
10+
return { hello: 'world' };
11+
},
12+
13+
nonSerializable: () => {
14+
class Foo {
15+
a: number;
16+
constructor(a: number) {
17+
this.a = a;
18+
}
19+
}
20+
return { hello: new Foo(1) };
21+
},
22+
},
23+
};
24+
25+
client = Client({ game });
26+
});
27+
28+
test('does not throw for serializable move', () => {
29+
expect(() => {
30+
client.moves.serializable();
31+
}).not.toThrow();
32+
});
33+
34+
test('throws for non-serializable move', () => {
35+
expect(() => {
36+
client.moves.nonSerializable();
37+
}).toThrow();
38+
});
39+
});

src/plugins/plugin-serializable.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import type { Plugin, AnyFn, Ctx } from '../types';
2+
import isPlainObject from 'lodash.isplainobject';
3+
4+
/**
5+
* Check if a value can be serialized (e.g. using `JSON.stringify`).
6+
* Adapted from: https://stackoverflow.com/a/30712764/3829557
7+
*/
8+
function isSerializable(value: any) {
9+
// Primitives are OK.
10+
if (
11+
value === undefined ||
12+
value === null ||
13+
typeof value === 'boolean' ||
14+
typeof value === 'number' ||
15+
typeof value === 'string'
16+
) {
17+
return true;
18+
}
19+
20+
// A non-primitive value that is neither a POJO or an array cannot be serialized.
21+
if (!isPlainObject(value) && !Array.isArray(value)) {
22+
return false;
23+
}
24+
25+
// Recurse entries if the value is an object or array.
26+
for (const key in value) {
27+
if (!isSerializable(value[key])) return false;
28+
}
29+
30+
return true;
31+
}
32+
33+
/**
34+
* Plugin that checks whether state is serializable, in order to avoid
35+
* network serialization bugs.
36+
*/
37+
const SerializablePlugin: Plugin = {
38+
name: 'plugin-serializable',
39+
40+
fnWrap: (move: AnyFn) => (G: unknown, ctx: Ctx, ...args: any[]) => {
41+
const result = move(G, ctx, ...args);
42+
// Check state in non-production environments.
43+
if (process.env.NODE_ENV !== 'production' && !isSerializable(result)) {
44+
throw new Error(
45+
'Move state is not JSON-serialiazable.\n' +
46+
'See https://boardgame.io/documentation/#/?id=state for more information.'
47+
);
48+
}
49+
return result;
50+
},
51+
};
52+
53+
export default SerializablePlugin;

0 commit comments

Comments
 (0)