Skip to content

Commit 1f33d43

Browse files
lehaSVV2009nicolodavis
authored andcommitted
Serve API and Game Server on same port with option to split (#343)
* Add option to use eigther one or two ports for socket and http api * Use deployable single port mode by default for server.js * Update docs and examples with single-port default server mode * edit some comments * fix TODO about koa mock not being called * wrap ports in Number()
1 parent d7d6b44 commit 1f33d43

8 files changed

Lines changed: 291 additions & 48 deletions

File tree

docs/api/API.md

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
# API
22

3-
The [Server](/api/Server) object hosts a REST API that can be used to
4-
create and join games. It is particularly useful when you want to
3+
The [Server](/api/Server) hosts a REST API that can be used to create and join games. It is particularly useful when you want to
54
authenticate clients to prove that they have the right to send
65
actions on behalf of a player.
76

@@ -11,10 +10,6 @@ A game that is authenticated will not accept moves from a client on behalf of a
1110

1211
Use the `create` API call to create a game that requires credential tokens. When you call the `join` API, you can retrieve the credential token for a particular player.
1312

14-
The API is available at server `port + 1` by default (i.e. if your
15-
server is at `localhost:8000`, then the API is hosted at
16-
`localhost:8001`.
17-
1813
### Creating a game
1914

2015
#### POST `/games/{name}/create`

docs/api/Server.md

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,26 +8,38 @@ broadcasts updates to those clients so that all browsers
88
that are connected to the same game are kept in sync in
99
realtime.
1010

11+
The server also hosts a REST [API](/api/API) that is used for creating
12+
and joining games. This is hosted on the same port, but can
13+
be configured to run on a separate port.
14+
1115
### Arguments
1216

13-
obj(_object_): A config object with the following options:
17+
A config object with the following options:
1418

15-
1. `games`: a list of game implementations
19+
1. `games` (_array_): a list of game implementations
1620
(each is the return value of [Game](/api/Game.md)).
1721

18-
2. `db`: the [database connector](/storage).
22+
2. `db` (_object_): the [database connector](/storage).
23+
If not provided, an in-memory implementation is used.
24+
25+
3. `transport` (_object_): the transport implementation.
26+
If not provided, socket.io is used.
1927

2028
### Returns
2129

2230
An object that contains:
2331

2432
1. run (_function_): A function to run the server.
25-
Signature: (port, callback) => {}
26-
2. app (_object_): The Koa app.
27-
3. db (_object_): The `db` implementation.
33+
_(portOrConfig, callback) => ({ apiServer, appServer })_
34+
2. kill (_function_): A function to stop the server.
35+
_({ apiServer, appServer }) => {}_
36+
3. app (_object_): The Koa app.
37+
4. db (_object_): The `db` implementation.
2838

2939
### Usage
3040

41+
##### Basic
42+
3143
```js
3244
const Server = require('boardgame.io/server').Server;
3345

@@ -39,3 +51,15 @@ const server = Server({
3951

4052
server.run(8000);
4153
```
54+
55+
##### With callback
56+
57+
```
58+
server.run(8000, () => console.log("server running..."));
59+
```
60+
61+
##### Running the API server on a separate port
62+
63+
```js
64+
server.run({ port: 8000, apiPort: 8001 });
65+
```

examples/react-web/src/lobby/lobby.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ const LobbyView = () => (
2929

3030
<Lobby
3131
gameServer="http://localhost:8000"
32-
lobbyServer="http://localhost:8001"
32+
lobbyServer="http://localhost:8000"
3333
gameComponents={importedGames}
3434
/>
3535
</div>

examples/react-web/src/tic-tac-toe/authenticated.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ class AuthenticatedClient extends React.Component {
4141
const PORT = 8000;
4242

4343
const newGame = await request
44-
.post(`http://localhost:${PORT + 1}/games/${gameName}/create`)
44+
.post(`http://localhost:${PORT}/games/${gameName}/create`)
4545
.send({ numPlayers: 2 });
4646

4747
const gameID = newGame.body.gameID;
@@ -50,7 +50,7 @@ class AuthenticatedClient extends React.Component {
5050

5151
for (let playerID of [0, 1]) {
5252
const player = await request
53-
.post(`http://localhost:${PORT + 1}/games/${gameName}/${gameID}/join`)
53+
.post(`http://localhost:${PORT}/games/${gameName}/${gameID}/join`)
5454
.send({
5555
gameName,
5656
playerID,

src/server/api.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,10 @@ export const CreateGame = async (db, game, numPlayers, setupData) => {
9595

9696
export const createApiServer = ({ db, games }) => {
9797
const app = new Koa();
98+
return addApiToServer({ app, db, games });
99+
};
100+
101+
export const addApiToServer = ({ app, db, games }) => {
98102
const router = new Router();
99103

100104
router.get('/games', async ctx => {

src/server/api.test.js

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@
88

99
import request from 'supertest';
1010

11-
import { isActionFromAuthenticPlayer, createApiServer } from './api';
11+
import {
12+
isActionFromAuthenticPlayer,
13+
addApiToServer,
14+
createApiServer,
15+
} from './api';
1216
import Game from '../core/game';
1317

1418
jest.setTimeout(2000000000);
@@ -637,3 +641,34 @@ describe('.createApiServer', () => {
637641
});
638642
});
639643
});
644+
645+
describe('.addApiToServer', () => {
646+
describe('when server app is provided', () => {
647+
let setSpy;
648+
let db;
649+
let server;
650+
let useChain;
651+
let games;
652+
653+
beforeEach(async () => {
654+
setSpy = jest.fn();
655+
useChain = jest.fn(() => ({ use: useChain }));
656+
server = { use: useChain };
657+
db = {
658+
set: async (id, state) => setSpy(id, state),
659+
};
660+
games = [
661+
Game({
662+
name: 'foo',
663+
setup: () => {},
664+
}),
665+
];
666+
667+
addApiToServer({ app: server, db, games });
668+
});
669+
670+
test('call .use method several times', async () => {
671+
expect(server.use.mock.calls.length).toBeGreaterThan(1);
672+
});
673+
});
674+
});

src/server/index.js

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,31 @@
88

99
const Koa = require('koa');
1010

11+
import { addApiToServer, createApiServer } from './api';
1112
import { DBFromEnv } from './db';
12-
import { createApiServer } from './api';
1313
import * as logger from '../core/logger';
1414
import { SocketIO } from './transport/socketio';
1515

16+
/**
17+
* Build config object from server run arguments.
18+
*
19+
* @param {number} portOrConfig - Either port or server config object. Optional.
20+
* @param {function} callback - Server run callback. Optional.
21+
*/
22+
export const createServerRunConfig = (portOrConfig, callback) => {
23+
const config = {};
24+
if (portOrConfig && typeof portOrConfig === 'object') {
25+
config.port = portOrConfig.port;
26+
config.callback = portOrConfig.callback || callback;
27+
config.apiPort = portOrConfig.apiPort;
28+
config.apiCallback = portOrConfig.apiCallback;
29+
} else {
30+
config.port = portOrConfig;
31+
config.callback = callback;
32+
}
33+
return config;
34+
};
35+
1636
/**
1737
* Instantiate a game server.
1838
*
@@ -33,23 +53,44 @@ export function Server({ games, db, transport }) {
3353
}
3454
transport.init(app, games);
3555

36-
const api = createApiServer({ db, games });
37-
3856
return {
3957
app,
40-
api,
4158
db,
4259

43-
run: async (port, callback) => {
60+
run: async (portOrConfig, callback) => {
61+
const serverRunConfig = createServerRunConfig(portOrConfig, callback);
62+
63+
// DB
4464
await db.connect();
45-
let apiServer = await api.listen(port + 1);
46-
let appServer = await app.listen(port, callback);
47-
logger.info('listening...');
65+
66+
// API
67+
let apiServer;
68+
if (!serverRunConfig.apiPort) {
69+
addApiToServer({ app, db, games });
70+
} else {
71+
// Run API in a separate Koa app.
72+
const api = createApiServer({ db, games });
73+
apiServer = await api.listen(
74+
Number(serverRunConfig.apiPort),
75+
serverRunConfig.apiCallback
76+
);
77+
logger.info(`API serving on ${apiServer.address().port}...`);
78+
}
79+
80+
// Run Game Server (+ API, if necessary).
81+
const appServer = await app.listen(
82+
Number(serverRunConfig.port),
83+
serverRunConfig.callback
84+
);
85+
logger.info(`App serving on ${appServer.address().port}...`);
86+
4887
return { apiServer, appServer };
4988
},
5089

5190
kill: ({ apiServer, appServer }) => {
52-
apiServer.close();
91+
if (apiServer) {
92+
apiServer.close();
93+
}
5394
appServer.close();
5495
},
5596
};

0 commit comments

Comments
 (0)