From a0d5f36e74f611dfd28e0c06c865dd2ad00f8100 Mon Sep 17 00:00:00 2001 From: Jason Harrison Date: Wed, 14 Aug 2019 21:29:37 -0400 Subject: [PATCH] Surface game metadata and player nicknames in client / react props (#436) * Surface game metadata and player nicknames in client / react props * Add gameMetadata to Client documentation * Do not send credentials to clients in metadata, update documentation, add tests * remove unnecessary code from client.js * Address review --- docs/documentation/api/Client.md | 25 ++++++++++++++++++++++--- src/client/client.js | 5 +++++ src/client/client.test.js | 15 +++++++++++++-- src/client/react-native.js | 1 + src/client/react.js | 1 + src/client/transport/local.js | 2 ++ src/client/transport/socketio.js | 8 +++++++- src/master/master.js | 12 +++++++++--- src/master/master.test.js | 30 ++++++++++++++++++++++++++++++ 9 files changed, 90 insertions(+), 9 deletions(-) diff --git a/docs/documentation/api/Client.md b/docs/documentation/api/Client.md index 5ece4fda0..5def9e83f 100644 --- a/docs/documentation/api/Client.md +++ b/docs/documentation/api/Client.md @@ -108,9 +108,28 @@ The `Board` component will receive the following as `props`: 10. `playerID`: The player ID associated with the client. -11. `isActive`: `true` if the client is able to currently make +11. `gameMetadata`: An object containing the players that have joined the game from a [room](/api/Lobby.md). + +Example: + +````json +{ + "players": { + "0": { + "id": 0, + "name": "Alice" + }, + "1": { + "id": 1, + "name": "Bob" + } + } +}``` + +12. `isActive`: `true` if the client is able to currently make a move or interact with the game. -12. `isMultiplayer`: `true` if it is a multiplayer game. +13. `isMultiplayer`: `true` if it is a multiplayer game. -13. `isConnected`: `true` if connection to the server is active. +14. `isConnected`: `true` if connection to the server is active. +```` diff --git a/src/client/client.js b/src/client/client.js index 67a2a9f5d..1eaceffce 100644 --- a/src/client/client.js +++ b/src/client/client.js @@ -239,6 +239,7 @@ class _ClientImpl { isConnected: true, onAction: () => {}, subscribe: () => {}, + subscribeGameMetadata: _metadata => {}, // eslint-disable-line no-unused-vars connect: () => {}, updateGameID: () => {}, updatePlayerID: () => {}, @@ -286,6 +287,10 @@ class _ClientImpl { } this.createDispatchers(); + + this.transport.subscribeGameMetadata(metadata => { + this.gameMetadata = metadata; + }); } subscribe(fn) { diff --git a/src/client/client.test.js b/src/client/client.test.js index e41ccf940..0c46004e8 100644 --- a/src/client/client.test.js +++ b/src/client/client.test.js @@ -195,7 +195,13 @@ describe('multiplayer', () => { describe('custom transport', () => { class CustomTransport { - custom = true; + constructor() { + this.callback = null; + } + + subscribeGameMetadata(fn) { + this.callback = fn; + } } let client; @@ -209,7 +215,12 @@ describe('multiplayer', () => { test('correct transport used', () => { expect(client.transport).toBeInstanceOf(CustomTransport); - expect(client.transport.custom).toBe(true); + }); + + test('metadata callback', () => { + const metadata = { m: true }; + client.transport.callback(metadata); + expect(client.gameMetadata).toEqual(metadata); }); }); diff --git a/src/client/react-native.js b/src/client/react-native.js index 84803b273..741f19c62 100644 --- a/src/client/react-native.js +++ b/src/client/react-native.js @@ -113,6 +113,7 @@ export function Client(opts) { reset: this.client.reset, undo: this.client.undo, redo: this.client.redo, + gameMetadata: this.client.gameMetadata, }); } diff --git a/src/client/react.js b/src/client/react.js index 42b941e45..db55a2669 100644 --- a/src/client/react.js +++ b/src/client/react.js @@ -170,6 +170,7 @@ export function Client(opts) { reset: this.client.reset, undo: this.client.undo, redo: this.client.redo, + gameMetadata: this.client.gameMetadata, }); } diff --git a/src/client/transport/local.js b/src/client/transport/local.js index 4429f4cb9..38b0f4628 100644 --- a/src/client/transport/local.js +++ b/src/client/transport/local.js @@ -121,6 +121,8 @@ export class Local { */ subscribe() {} + subscribeGameMetadata(_metadata) {} // eslint-disable-line no-unused-vars + /** * Updates the game id. * @param {string} id - The new game id. diff --git a/src/client/transport/socketio.js b/src/client/transport/socketio.js index 3ae916471..536a9219f 100644 --- a/src/client/transport/socketio.js +++ b/src/client/transport/socketio.js @@ -46,6 +46,7 @@ export class SocketIO { this.gameID = this.gameName + ':' + this.gameID; this.isConnected = false; this.callback = () => {}; + this.gameMetadataCallback = () => {}; } /** @@ -96,10 +97,11 @@ export class SocketIO { // Called when the client first connects to the master // and requests the current game state. - this.socket.on('sync', (gameID, state, log) => { + this.socket.on('sync', (gameID, state, log, gameMetadata) => { if (gameID == this.gameID) { const action = ActionCreators.sync(state, log); this.store.dispatch(action); + this.gameMetadataCallback(gameMetadata); } }); @@ -124,6 +126,10 @@ export class SocketIO { this.callback = fn; } + subscribeGameMetadata(fn) { + this.gameMetadataCallback = fn; + } + /** * Updates the game id. * @param {string} id - The new game id. diff --git a/src/master/master.js b/src/master/master.js index b14910a22..912582047 100644 --- a/src/master/master.js +++ b/src/master/master.js @@ -259,14 +259,20 @@ export class Master { async onSync(gameID, playerID, numPlayers) { const key = gameID; - let state; + let state, gameMetadata, filteredGameMetadata; if (this.executeSynchronously) { state = this.storageAPI.get(key); + gameMetadata = this.storageAPI.get(GameMetadataKey(gameID)); } else { state = await this.storageAPI.get(key); + gameMetadata = await this.storageAPI.get(GameMetadataKey(gameID)); + } + if (gameMetadata) { + filteredGameMetadata = Object.values(gameMetadata.players).map(player => { + return { id: player.id, name: player.name }; + }); } - // If the game doesn't exist, then create one on demand. // TODO: Move this out of the sync call. if (state === undefined) { @@ -301,7 +307,7 @@ export class Master { this.transportAPI.send({ playerID, type: 'sync', - args: [gameID, filteredState, log], + args: [gameID, filteredState, log, filteredGameMetadata], }); return; diff --git a/src/master/master.test.js b/src/master/master.test.js index fb35c4ca7..7ac0c5fa2 100644 --- a/src/master/master.test.js +++ b/src/master/master.test.js @@ -44,6 +44,36 @@ describe('sync', () => { await master.onSync('gameID', '0', 2); expect(send).toHaveBeenCalled(); }); + + test('should not have metadata', async () => { + await master.onSync('gameID', '0', 2); + // [0][0] = first call, first argument + expect(send.mock.calls[0][0].args[3]).toBeUndefined(); + }); + + test('should have metadata', async () => { + const db = new InMemory(); + const dbMetadata = { + players: { + '0': { + id: 0, + credentials: 'qS2m4Ujb_', + name: 'Alice', + }, + '1': { + id: 1, + credentials: 'nIQtXFybDD', + name: 'Bob', + }, + }, + }; + db.set('gameID:metadata', dbMetadata); + const masterWithMetadata = new Master(game, db, TransportAPI(send)); + await masterWithMetadata.onSync('gameID', '0', 2); + + const expectedMetadata = [{ id: 0, name: 'Alice' }, { id: 1, name: 'Bob' }]; + expect(send.mock.calls[0][0].args[3]).toMatchObject(expectedMetadata); + }); }); describe('update', () => {