Skip to content

Commit 3d2131e

Browse files
authored
fix(master): Use createMatch for implicit match creation (#821)
* refactor(server): Factor out metadata factory function * refactor(master): Set default numPlayers for master#onSync * fix(master): Use `createGame` for implicit match creation Closes #722 Uses Storage API’s `createGame` method when implicitly creating a match from the master’s `onSync` handler to ensure `initialState` gets stored as well as any other initialisation code an implementation may include in `createGame`. * refactor(master): Simplify fetch logic & tidy up code style * test(master): Fix master sync tests * test(master): Add tests for updating a match with no metadata * fix(master): Use `createMatch` for compatibility with #806
1 parent f74f953 commit 3d2131e

4 files changed

Lines changed: 112 additions & 64 deletions

File tree

src/master/master.test.ts

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
*/
88

99
import * as ActionCreators from '../core/action-creators';
10+
import { InitializeGame } from '../core/initialize';
1011
import { InMemory } from '../server/db/inmemory';
1112
import {
1213
Master,
@@ -16,7 +17,7 @@ import {
1617
isActionFromAuthenticPlayer,
1718
} from './master';
1819
import { error } from '../core/logger';
19-
import { Server } from '../types';
20+
import { Server, State } from '../types';
2021
import * as StorageAPI from '../server/db/base';
2122
import * as dateMock from 'jest-date-mock';
2223

@@ -43,7 +44,8 @@ function TransportAPI(send = jest.fn(), sendAll = jest.fn()) {
4344

4445
describe('sync', () => {
4546
const send = jest.fn();
46-
const master = new Master(game, new InMemory(), TransportAPI(send));
47+
const db = new InMemory();
48+
const master = new Master(game, db, TransportAPI(send));
4749

4850
beforeEach(() => {
4951
jest.clearAllMocks();
@@ -59,19 +61,21 @@ describe('sync', () => {
5961
});
6062

6163
test('sync a second time does not create a game', async () => {
64+
const fetchResult = db.fetch('matchID', { metadata: true });
6265
await master.onSync('matchID', '0', 2);
63-
expect(send).toHaveBeenCalled();
66+
expect(db.fetch('matchID', { metadata: true })).toMatchObject(fetchResult);
6467
});
6568

6669
test('should not have metadata', async () => {
67-
await master.onSync('matchID', '0', 2);
70+
db.setState('oldGameID', {} as State);
71+
await master.onSync('oldGameID', '0');
6872
// [0][0] = first call, first argument
69-
expect(send.mock.calls[0][0].args[3]).toBeUndefined();
73+
expect(send.mock.calls[0][0].args[1].filteredMetadata).toBeUndefined();
7074
});
7175

7276
test('should have metadata', async () => {
7377
const db = new InMemory();
74-
const dbMetadata = {
78+
const metadata = {
7579
gameName: 'tic-tac-toe',
7680
setupData: {},
7781
players: {
@@ -89,7 +93,7 @@ describe('sync', () => {
8993
createdAt: 0,
9094
updatedAt: 0,
9195
};
92-
db.setMetadata('matchID', dbMetadata);
96+
db.createMatch('matchID', { metadata, initialState: {} as State });
9397
const masterWithMetadata = new Master(game, db, TransportAPI(send));
9498
await masterWithMetadata.onSync('matchID', '0', 2);
9599

@@ -279,6 +283,42 @@ describe('update', () => {
279283
const { metadata } = db.fetch(id, { metadata: true });
280284
expect(metadata.updatedAt).toEqual(updatedAt.getTime());
281285
});
286+
287+
test('processes update if there is no metadata', async () => {
288+
const id = 'gameWithoutMetadata';
289+
const db = new InMemory();
290+
const masterWithoutMetadata = new Master(game, db, TransportAPI(send));
291+
// Store state manually to bypass automatic metadata initialization on sync.
292+
let state = InitializeGame({ game });
293+
expect(state.ctx.turn).toBe(1);
294+
db.setState(id, state);
295+
// Dispatch update to end the turn.
296+
const event = ActionCreators.gameEvent('endTurn', null, '0');
297+
await masterWithoutMetadata.onUpdate(event, 0, id, '0');
298+
// Confirm the turn ended.
299+
let metadata: undefined | Server.MatchData;
300+
({ state, metadata } = db.fetch(id, { state: true, metadata: true }));
301+
expect(state.ctx.turn).toBe(2);
302+
expect(metadata).toBeUndefined();
303+
});
304+
305+
test('processes update if there is no metadata with async DB', async () => {
306+
const id = 'gameWithoutMetadata';
307+
const db = new InMemoryAsync();
308+
const masterWithoutMetadata = new Master(game, db, TransportAPI(send));
309+
// Store state manually to bypass automatic metadata initialization on sync.
310+
let state = InitializeGame({ game });
311+
expect(state.ctx.turn).toBe(1);
312+
db.setState(id, state);
313+
// Dispatch update to end the turn.
314+
const event = ActionCreators.gameEvent('endTurn', null, '0');
315+
await masterWithoutMetadata.onUpdate(event, 0, id, '0');
316+
// Confirm the turn ended.
317+
let metadata: undefined | Server.MatchData;
318+
({ state, metadata } = db.fetch(id, { state: true, metadata: true }));
319+
expect(state.ctx.turn).toBe(2);
320+
expect(metadata).toBeUndefined();
321+
});
282322
});
283323

284324
describe('playerView', () => {

src/master/master.ts

Lines changed: 31 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
LogEntry,
2424
PlayerID,
2525
} from '../types';
26+
import { createMetadata } from '../server/util';
2627
import * as StorageAPI from '../server/db/base';
2728

2829
export const getPlayerMetadata = (
@@ -216,13 +217,11 @@ export class Master {
216217
const key = matchID;
217218

218219
let state: State;
219-
let result: StorageAPI.FetchResult<{ state: true }>;
220220
if (IsSynchronous(this.storageAPI)) {
221-
result = this.storageAPI.fetch(key, { state: true });
221+
({ state } = this.storageAPI.fetch(key, { state: true }));
222222
} else {
223-
result = await this.storageAPI.fetch(key, { state: true });
223+
({ state } = await this.storageAPI.fetch(key, { state: true }));
224224
}
225-
state = result.state;
226225

227226
if (state === undefined) {
228227
logging.error(`game not found, matchID=[${key}]`);
@@ -334,66 +333,53 @@ export class Master {
334333
* Called when the client connects / reconnects.
335334
* Returns the latest game state and the entire log.
336335
*/
337-
async onSync(matchID: string, playerID: string, numPlayers: number) {
336+
async onSync(matchID: string, playerID: string, numPlayers = 2) {
338337
const key = matchID;
339338

340-
let state: State;
341-
let initialState: State;
342-
let log: LogEntry[];
343-
let matchData: Server.MatchData;
344-
let filteredMetadata: FilteredMetadata;
345-
let result: StorageAPI.FetchResult<{
346-
state: true;
347-
metadata: true;
348-
log: true;
349-
initialState: true;
350-
}>;
339+
const fetchOpts = {
340+
state: true,
341+
metadata: true,
342+
log: true,
343+
initialState: true,
344+
} as const;
345+
346+
let fetchResult: StorageAPI.FetchResult<typeof fetchOpts>;
351347

352348
if (IsSynchronous(this.storageAPI)) {
353-
result = this.storageAPI.fetch(key, {
354-
state: true,
355-
metadata: true,
356-
log: true,
357-
initialState: true,
358-
});
349+
fetchResult = this.storageAPI.fetch(key, fetchOpts);
359350
} else {
360-
result = await this.storageAPI.fetch(key, {
361-
state: true,
362-
metadata: true,
363-
log: true,
364-
initialState: true,
365-
});
351+
fetchResult = await this.storageAPI.fetch(key, fetchOpts);
366352
}
367353

368-
state = result.state;
369-
initialState = result.initialState;
370-
log = result.log;
371-
matchData = result.metadata;
372-
373-
if (matchData) {
374-
filteredMetadata = Object.values(matchData.players).map(player => {
375-
const { credentials, ...filteredData } = player;
376-
return filteredData;
377-
});
378-
}
354+
let { state, initialState, log, metadata } = fetchResult;
379355

380356
// If the game doesn't exist, then create one on demand.
381357
// TODO: Move this out of the sync call.
382358
if (state === undefined) {
383359
initialState = state = InitializeGame({ game: this.game, numPlayers });
384-
385-
this.subscribeCallback({
386-
state,
387-
matchID,
360+
metadata = createMetadata({
361+
game: this.game,
362+
unlisted: true,
363+
numPlayers,
388364
});
389365

366+
this.subscribeCallback({ state, matchID });
367+
390368
if (IsSynchronous(this.storageAPI)) {
391-
this.storageAPI.setState(key, state);
369+
this.storageAPI.createMatch(key, { initialState, metadata });
392370
} else {
393-
await this.storageAPI.setState(key, state);
371+
await this.storageAPI.createMatch(key, { initialState, metadata });
394372
}
395373
}
396374

375+
let filteredMetadata: FilteredMetadata;
376+
if (metadata) {
377+
filteredMetadata = Object.values(metadata.players).map(player => {
378+
const { credentials, ...filteredData } = player;
379+
return filteredData;
380+
});
381+
}
382+
397383
const filteredState = {
398384
...state,
399385
G: this.game.playerView(state.G, state.ctx, playerID),

src/server/api.ts

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import cors from '@koa/cors';
1515
import { InitializeGame } from '../core/initialize';
1616
import * as StorageAPI from './db/base';
1717
import { Server, LobbyAPI, Game } from '../types';
18+
import { createMetadata } from './util';
1819

1920
/**
2021
* Creates a new match.
@@ -44,18 +45,7 @@ export const CreateMatch = async ({
4445
}) => {
4546
if (!numPlayers || typeof numPlayers !== 'number') numPlayers = 2;
4647

47-
const metadata: Server.MatchData = {
48-
gameName: game.name,
49-
unlisted: !!unlisted,
50-
players: {},
51-
createdAt: Date.now(),
52-
updatedAt: Date.now(),
53-
};
54-
if (setupData !== undefined) metadata.setupData = setupData;
55-
for (let playerIndex = 0; playerIndex < numPlayers; playerIndex++) {
56-
metadata.players[playerIndex] = { id: playerIndex };
57-
}
58-
48+
const metadata = createMetadata({ game, numPlayers, setupData, unlisted });
5949
const matchID = uuid();
6050
const initialState = InitializeGame({ game, numPlayers, setupData });
6151

src/server/util.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { Server, Game } from '../types';
2+
3+
/**
4+
* Creates a new match metadata object.
5+
*/
6+
export const createMetadata = ({
7+
game,
8+
unlisted,
9+
setupData,
10+
numPlayers,
11+
}: {
12+
game: Game;
13+
numPlayers: number;
14+
setupData?: any;
15+
unlisted?: boolean;
16+
}): Server.MatchData => {
17+
const metadata: Server.MatchData = {
18+
gameName: game.name,
19+
unlisted: !!unlisted,
20+
players: {},
21+
createdAt: Date.now(),
22+
updatedAt: Date.now(),
23+
};
24+
25+
if (setupData !== undefined) metadata.setupData = setupData;
26+
27+
for (let playerIndex = 0; playerIndex < numPlayers; playerIndex++) {
28+
metadata.players[playerIndex] = { id: playerIndex };
29+
}
30+
31+
return metadata;
32+
};

0 commit comments

Comments
 (0)