Event Store Projections – Distributing events to other streams
Since the GameOver events are appended to the Game stream, we need a way to distribute them to all participating players’ streams. The GameOver event contains a list of participating players, and the distributor emits an event for each player. The event type emitted depends on how much the player has won or lost: if the amount is less than zero, a GameLost event is emitted, but if the amount is greater than zero, a GameWon event is emitted instead. (For simplicity’s sake, we’ll assume GameDraw events don’t exist.)
Note that we don’t technically have to split the event in two to implement our alarm system, but it’s useful for the purposes of this example.
This projection takes GameOver events from all Game streams as input. For each GameOver event, it will loop over the list of players and distribute (i.e. emit) GameLost and GameWon events to the players’ streams as appropriate.
JavaScript implementation
This projection module has a single public method, process, which processes the GameOver events by looping over the participating players and emitting the appropriate events:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | // <reference path="References\1Prelude.js"></reference> // <reference path="References\Projections.js"></reference> var gameOverToPlayerDistributor = function gameOverToPlayerDistributorConstuctor($eventServices) { var eventServices = !$eventServices ? { emit: emit } : $eventServices; var createEvent = function (playerId, gameId, amount, timestamp) { return { PlayerId: playerId, GameId: gameId, Amount: amount, Timestamp: timestamp }; }; var emitGameEvent = function (eventType, gameId, playerId, amount, timestamp) { var event = createEvent(playerId, gameId, amount, timestamp); eventServices.emit(playerId, eventType, event); }; var processPlayer = function (gameId, playerResult, timestamp) { if (playerResult.Amount > ) { emitGameEvent("GameWon", gameId, playerResult.PlayerId, playerResult.Amount, timestamp); } else if (playerResult.Amount < ) { emitGameEvent("GameLost", gameId, playerResult.PlayerId, playerResult.Amount, timestamp); } }; var process = function (state, event) { var gameId = event.body.GameId; var players = event.body.PlayerResults; var timestamp = event.body.Timestamp; for (var playerIndex = ; playerIndex < players.length; playerIndex++) { var player = players[playerIndex]; processPlayer(gameId, player, timestamp); } }; return { process: process }; }; |
This module is instantiated once globally and then referenced from the projection’s definition:
1 2 3 4 5 6 | var distributor = gameOverToPlayerDistributor(); fromCategory('Game') .when({ GameOver: distributor.process }); |
Testing the projection
To test this projection, we’ll define three scenarios. The first will arrange an event with a positive amount and assert that a GameWon event is emitted. The second will arrange an event with a negative amount and assert that a GameLost event is emitted. The last will check for a combination of both.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 | /// <reference path="../References/jasmine/jasmine.js"></reference> /// <reference path="../GameOverToPlayerDistributor.js"></reference> describe("when distributing GameOver event to to players", function () { var distributor; var projections; beforeEach(function () { projections = jasmine.createSpyObj('projections', ['emit']); distributor = gameOverToPlayerDistributor(projections); }); describe("when processing GameOver event", function () { describe("given amount is positive", function () { it("should emit a GameWon event to the player stream", function () { distributor.process({}, { body: { GameId: 'Game-abc', Timestamp: '2014-02-20T08:02:39.687Z', PlayerResults: [ { PlayerId: 'Player-p1', Amount: 100 } ] } }); expect(projections.emit) .wasCalledWith("Player-p1", "GameWon", { PlayerId: 'Player-p1', GameId: 'Game-abc', Amount: 100, Timestamp: '2014-02-20T08:02:39.687Z' }); }); }); describe("given amount is negative", function () { it("should emit a GameLost event to the player stream", function () { distributor.process({}, { body: { GameId: 'Game-abc', Timestamp: '2014-02-20T08:02:39.687Z', PlayerResults: [ { PlayerId: 'Player-p1', Amount: -100 } ] } }); expect(projections.emit) .wasCalledWith("Player-p1", "GameLost", { PlayerId: 'Player-p1', GameId: 'Game-abc', Amount: -100, Timestamp: '2014-02-20T08:02:39.687Z' }); }); }); describe("given multiple players", function () { beforeEach(function () { distributor.process({}, { body: { GameId: 'Game-abc', Timestamp: '2014-02-20T08:02:39.687Z', PlayerResults: [ { PlayerId: 'Player-p1', Amount: -100 }, { PlayerId: 'Player-p2', Amount: 80 }, { PlayerId: 'Player-p3', Amount: -40 }, { PlayerId: 'Player-p4', Amount: 20 } ] } }); }); it("should emit a GameWon event to the winning players stream", function () { expect(projections.emit) .wasCalledWith("Player-p2", "GameWon", { PlayerId: 'Player-p2', GameId: 'Game-abc', Amount: 80, Timestamp: '2014-02-20T08:02:39.687Z', }); expect(projections.emit) .wasCalledWith("Player-p4", "GameWon", { PlayerId: 'Player-p4', GameId: 'Game-abc', Amount: 20, Timestamp: '2014-02-20T08:02:39.687Z', }); }); it("should emit a GameLost event to the losing players stream", function () { expect(projections.emit) .wasCalledWith("Player-p1", "GameLost", { PlayerId: 'Player-p1', GameId: 'Game-abc', Amount: -100, Timestamp: '2014-02-20T08:02:39.687Z', }); expect(projections.emit) .wasCalledWith("Player-p3", "GameLost", { PlayerId: 'Player-p3', GameId: 'Game-abc', Amount: -40, Timestamp: '2014-02-20T08:02:39.687Z', }); }); }); }); }); |
Querying the results with the Client API
The results of this projection are used solely as input for another stream, so there’s no need to read them with the C# Client API.
Source code
A working project for this example can be found on github: https://github.com/tim-cools/EventStore-Examples
Event Store Projections by Example
This post is part of a series:
Leave a Reply
Want to join the discussion?Feel free to contribute!