Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions integration/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"boardgame.io": "file:boardgame.io-0.43.2.tgz",
"react": "^16.7.0",
"react-dom": "^16.7.0",
"react-scripts": "^2.1.3"
Expand Down
3 changes: 1 addition & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@
"koa-body": "^4.1.0",
"koa-router": "^7.2.1",
"koa-socket-2": "^1.0.17",
"lodash.isplainobject": "^4.0.6",
"nanoid": "^3.1.20",
"p-queue": "^6.6.2",
"prop-types": "^15.5.10",
Expand Down
9 changes: 8 additions & 1 deletion src/plugins/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import PluginImmer from './plugin-immer';
import PluginRandom from './plugin-random';
import PluginEvents from './plugin-events';
import PluginLog from './plugin-log';
import PluginSerializable from './plugin-serializable';
import type {
AnyFn,
PartialGameState,
Expand All @@ -29,7 +30,13 @@ interface PluginOpts {
/**
* List of plugins that are always added.
*/
const DEFAULT_PLUGINS = [PluginImmer, PluginRandom, PluginEvents, PluginLog];
const DEFAULT_PLUGINS = [
PluginImmer,
PluginRandom,
PluginEvents,
PluginLog,
PluginSerializable,
];

/**
* Allow plugins to intercept actions and process them.
Expand Down
39 changes: 39 additions & 0 deletions src/plugins/plugin-serializable.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Client } from '../client/client';

describe('plugin-serializable', () => {
let client;

beforeAll(() => {
const game = {
moves: {
serializable: () => {
return { hello: 'world' };
},

nonSerializable: () => {
class Foo {
a: number;
constructor(a: number) {
this.a = a;
}
}
return { hello: new Foo(1) };
},
},
};

client = Client({ game });
});

test('does not throw for serializable move', () => {
expect(() => {
client.moves.serializable();
}).not.toThrow();
});

test('throws for non-serializable move', () => {
expect(() => {
client.moves.nonSerializable();
}).toThrow();
});
});
55 changes: 55 additions & 0 deletions src/plugins/plugin-serializable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import type { Plugin, AnyFn, Ctx } from '../types';
import isPlainObject from 'lodash.isplainobject';

/**
* Check if a value can be serialized (e.g. using `JSON.stringify`).
* Adapted from: https://stackoverflow.com/a/30712764/3829557
*/
function isSerializable(value: any) {
// Primitives are OK.
if (
value === undefined ||
value === null ||
typeof value === 'boolean' ||
typeof value === 'number' ||
typeof value === 'string'
) {
return true;
}

// A non-primitive value that is neither a POJO or an array cannot be serialized.
if (!isPlainObject(value) && !Array.isArray(value)) {
return false;
}

// Recurse entries if the value is an object or array.
for (const key in value) {
if (!isSerializable(value[key])) return false;
}

return true;
}

/**
* Plugin that checks whether state is serializable, in order to avoid
* network serialization bugs.
*/
const SerializablePlugin: Plugin = {
name: 'plugin-serializable',

fnWrap: (move: AnyFn) => (G: unknown, ctx: Ctx, ...args: any[]) => {
const result = move(G, ctx, ...args);
/* istanbul ignore if */
if (process.env.NODE_ENV === 'production') {
// Do not perform this check in production.
return result;
}
if (!isSerializable(result)) {
throw new Error(`Move state is not JSON-serialiazable. See
https://boardgame.io/documentation/#/?id=state for more information.`);
}
Comment thread
vdfdev marked this conversation as resolved.
Outdated
return result;
},
};

export default SerializablePlugin;