Player queues for parties

This commit is contained in:
Andrew Guibert 2018-05-03 11:05:07 -05:00
parent dd410f6b48
commit 5aacfcf659
19 changed files with 320 additions and 126 deletions

View File

@ -1,13 +1,15 @@
import { Component, OnInit } from '@angular/core';
import { Component, OnInit, OnDestroy, NgZone } from '@angular/core';
import { Router } from '@angular/router';
import { Meta } from '@angular/platform-browser';
import { GameService } from './game.service';
@Component({
selector: 'app-game',
templateUrl: './game.component.html',
styleUrls: ['./game.component.scss']
styleUrls: ['./game.component.scss'],
providers: [ GameService ],
})
export class GameComponent implements OnInit {
export class GameComponent implements OnInit, OnDestroy {
static readonly BOX_SIZE = 5;
roundId: string;
@ -16,7 +18,6 @@ export class GameComponent implements OnInit {
partyId: string;
showPartyId = false;
showLoader = false;
output: HTMLElement;
@ -24,14 +25,16 @@ export class GameComponent implements OnInit {
canvas: any;
context: CanvasRenderingContext2D;
constructor(private meta: Meta, private gameService: GameService) {
constructor(private meta: Meta,
private router: Router,
private ngZone: NgZone,
private gameService: GameService,
) {
gameService.messages.subscribe((msg) => {
const json = msg as any;
if (json.requeue) {
this.roundId = json.requeue;
sessionStorage.setItem('roundId', this.roundId);
location.reload();
this.processRequeue(json.requeue);
}
if (json.obstacles) {
for (let obstacle of json.obstacles) {
@ -66,7 +69,7 @@ export class GameComponent implements OnInit {
if (sessionStorage.getItem('isSpectator') === 'true') {
console.log('is a spectator... showing game id');
// Set the Round ID and make visible
// Set the Party ID and make visible
this.partyId = sessionStorage.getItem('partyId');
this.showPartyId = true;
this.gameService.send({'spectatorjoined': true});
@ -97,6 +100,10 @@ export class GameComponent implements OnInit {
}
};
}
ngOnDestroy() {
sessionStorage.removeItem('roundId');
}
// Game actions
startGame() {
@ -104,7 +111,14 @@ export class GameComponent implements OnInit {
}
requeue() {
this.gameService.send({ message: 'GAME_REQUEUE' });
if (sessionStorage.getItem('isSpectator') === 'true' ||
sessionStorage.getItem('partyId') === null) {
this.gameService.send({ message: 'GAME_REQUEUE' });
} else {
this.ngZone.run(() => {
this.router.navigate(['/login']);
});
}
}
moveUp() {
@ -122,6 +136,12 @@ export class GameComponent implements OnInit {
moveRight() {
this.gameService.send({ direction: 'RIGHT' });
}
processRequeue(newRoundId) {
this.roundId = newRoundId;
sessionStorage.setItem('roundId', this.roundId);
location.reload();
}
// Update display
drawPlayer(player) {

View File

@ -20,14 +20,12 @@ export class LeaderboardComponent implements OnInit {
async getLeaders() {
try {
console.log("Calling rank service");
let data = await this.http.get(`${environment.API_URL_RANKS}/top/10`).toPromise();
console.log(`Got leaders: ${JSON.stringify(data)}`);
const json = data as any;
const rankingsArr = new Array();
let i = 1;
for (let ranking of json) {
console.log(`Got rank ${JSON.stringify(ranking)}`);
rankingsArr.push(new Ranking(i++, ranking.name, ranking.stats.numWins, ranking.stats.totalGames, ranking.stats.rating));
}
this.ngZone.run(() => {

View File

@ -5,20 +5,19 @@ import { PlayersService } from './players.service';
@Component({
selector: 'app-player-list',
templateUrl: './playerlist.component.html',
styleUrls: ['./playerlist.component.scss']
styleUrls: ['./playerlist.component.scss'],
providers: [ PlayersService ],
})
export class PlayerListComponent implements OnInit {
players: Player[] = new Array();
constructor(private playersService: PlayersService, private ngZone: NgZone) {
playersService.messages.subscribe((msg) => {
console.log('Updating player list');
const json = msg as any;
if (json.playerlist) {
const newPlayers = new Array();
console.log(`Got ${JSON.stringify(json.playerlist)}`);
//console.log(`Got playerlist ${JSON.stringify(json.playerlist)}`);
for (let player of json.playerlist) {
console.log(`Adding player ${player.name}`);
newPlayers.push(new Player(player.name, player.status, player.color));
}
this.ngZone.run(() => {

View File

@ -11,7 +11,7 @@ export class PlayersService {
this.messages = <Subject<Object>>socketService.socket
.map((response: MessageEvent): any => {
console.log(`Players service handling message: ${response.data}`);
//console.log(`Players service handling message: ${response.data}`);
return JSON.parse(response.data);
});
}

View File

@ -64,7 +64,7 @@
<input type="text" id="roundid" name="roundid" [(ngModel)]="party" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false">
</div>
<div class="form-item">
<button type="button" (click)="joinRound()">Join Party</button>
<button type="button" (click)="joinParty()">Join Party</button>
</div>
</div>
<div class="form-group">
@ -73,6 +73,14 @@
</div>
</div>
</div>
<div queuePane>
<div class="form-item">
<h2>The party is full! Hang out a bit and you will automatically join the next round</h2>
<hr/>
<h2>You are number {{queuePosition}} in queue</h2>
</div>
</div>
</app-slider>
</div>
</div>

View File

@ -116,6 +116,10 @@ button {
border-color: #bbb;
}
h2 {
color: #fff;
}
.section-header {
width: 100%;
margin-bottom: 15px;

View File

@ -1,7 +1,9 @@
import { Component, OnInit, NgZone, HostBinding } from '@angular/core';
import { Component, OnInit, NgZone, HostBinding, Injectable, Output } from '@angular/core';
import { Meta } from '@angular/platform-browser';
import { Router, ActivatedRoute } from '@angular/router';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable } from 'rxjs';
import * as EventSource from 'eventsource';
import { trigger, animate, style, transition, group, query, stagger, state } from '@angular/animations';
import { environment } from './../../environments/environment';
import { PaneType } from '../slider/slider.component';
@ -16,6 +18,7 @@ export class LoginComponent implements OnInit {
pane: PaneType = sessionStorage.getItem('username') === null ? 'left' : 'right';
username: string;
party: string;
@Output() queuePosition: number;
player = new Player('PLAYER NAME HERE', 'none', '#FFFFFF');
constructor(
@ -28,7 +31,6 @@ export class LoginComponent implements OnInit {
ngOnInit() {
this.meta.removeTag('viewport');
let viewWidth = window.innerWidth;
let viewHeight = window.innerHeight;
@ -47,6 +49,13 @@ export class LoginComponent implements OnInit {
this.username = sessionStorage.getItem('username');
this.player.name = this.username;
}
if (sessionStorage.getItem('partyId') !== null &&
sessionStorage.getItem('isSpectator') !== 'true') {
this.party = sessionStorage.getItem('partyId');
console.log(`User already associated with party ${this.party}, entering queue`);
this.enterQueue();
}
}
loginGoogle() {
@ -60,9 +69,10 @@ export class LoginComponent implements OnInit {
this.joinRoundById(roundID);
}
async joinRound() {
async joinParty() {
let roundID: any = await this.http.get(`${environment.API_URL_PARTY}/${this.party}/round`, { responseType: 'text' }).toPromise();
console.log(`Got roundID=${roundID} for partyID=${this.party}`);
sessionStorage.setItem('partyId', this.party);
this.joinRoundById(roundID);
}
@ -90,16 +100,15 @@ export class LoginComponent implements OnInit {
alert('Game round does not exist!');
return;
}
if (data.gameState === 'FULL') {
alert('Game round is Full!');
return;
}
if (data.gameState === 'RUNNING') {
alert('Game round has already started!');
return;
}
if (data.gameState === 'FINISHED') {
alert('Game round has already finished!');
if (data.gameState === 'FULL' ||
data.gameState === 'RUNNING' ||
data.gameState === 'FINISHED') {
if (this.party === null) {
alert('Game has already begun! Try again later.');
} else {
this.enterQueue();
}
return;
}
@ -107,9 +116,7 @@ export class LoginComponent implements OnInit {
let response: any = await this.http.post(`${environment.API_URL_PLAYERS}/create?name=${this.username}&id=${id}`, '', {
responseType: 'text'
}).toPromise();
console.log(JSON.stringify(response));
console.log('Created player: ' + JSON.stringify(response));
// TEMP: to prevent a race condition, putting this code inside of the player create callback to ensure that
// userId is set in the session storage before proceeding to the game board
@ -157,19 +164,42 @@ export class LoginComponent implements OnInit {
showGuestLogin() {
this.pane = 'center';
}
enterQueue() {
console.log(`enering queue for party ${this.party}`);
let queueCallback = new EventSource(`${environment.API_URL_PARTY}/${this.party}/queue`);
queueCallback.onmessage = msg => {
let queueMsg = JSON.parse(msg.data);
if (queueMsg.queuePosition) {
this.ngZone.run(() => {
this.queuePosition = queueMsg.queuePosition;
console.log(`Still waiting in queue at position ${this.queuePosition}`);
this.pane = 'queue';
});
} else if (queueMsg.requeue) {
console.log(`ready to join game! Joining round ${queueMsg.requeue}`);
queueCallback.close();
this.joinRoundById(queueMsg.requeue);
} else {
console.log('Error: unrecognized message ' + msg.data);
}
}
queueCallback.onerror = msg => {
console.log('Error showing queue position: ' + JSON.stringify(msg.data));
}
}
loginAsGuest(username: string) {
username = username.trim();
console.log(`Username input: "${username}"`);
let usernameError = this.validateUsername(username);
if(usernameError !== null) {
alert(usernameError);
return;
alert(usernameError);
return;
}
this.player.name = username;
this.username = username;
this.player.name = username.trim();
this.username = username.trim();
sessionStorage.setItem('username', username);
this.pane = 'right';
}
@ -198,7 +228,7 @@ export class LoginComponent implements OnInit {
}
validateUsername(username: string) {
if (username.length < 1 || username.length > 20) {
if (username === undefined || username.trim().length < 1 || username.trim().length > 20) {
return 'Username must be between 1 and 20 chars';
}
let usernameRegex: RegExp = /^[a-zA-Z0-9 -]{1,20}$/;

View File

@ -2,4 +2,5 @@
<div class="left-pane" [@fade]="isActivePane('left')"><ng-content select="[leftPane]"></ng-content></div>
<div class="center-pane" [@fade]="isActivePane('center')"><ng-content select="[centerPane]"></ng-content></div>
<div class="right-pane" [@fade]="isActivePane('right')"><ng-content select="[rightPane]"></ng-content></div>
<div class="queue-pane" [@fade]="isActivePane('queue')"><ng-content select="[queuePane]"></ng-content></div>
</div>

View File

@ -5,7 +5,7 @@
.parent {
height: 100%;
width: 300%;
width: 400%;
display: flex;

View File

@ -9,8 +9,9 @@ import { trigger, state, style, transition, animate, query, group } from '@angul
animations: [
trigger('slide', [
state('left', style({ transform: 'translateX(0)' })),
state('center', style({ transform: 'translateX(-33.333%)' })),
state('right', style({ transform: 'translateX(-66.666%)' })),
state('center', style({ transform: 'translateX(-25%)' })),
state('right', style({ transform: 'translateX(-50%)' })),
state('queue', style({ transform: 'translateX(-75%)'})),
transition('void => *', animate(0)),
transition('* => *', animate(300))
]),
@ -29,4 +30,4 @@ export class SliderComponent {
}
}
export type PaneType = 'left' | 'center' | 'right';
export type PaneType = 'left' | 'center' | 'right' | 'queue';

View File

@ -15,7 +15,6 @@ import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import javax.enterprise.concurrent.LastExecution;
@ -100,7 +99,7 @@ public class GameRound implements Runnable {
public void addPlayer(Session s, String playerId, String playerName, Boolean hasGameBoard) {
// Front end should be preventing a player joining a full game but
// defensive programming
if (gameState != State.OPEN) {
if (!isOpen()) {
log("Cannot add player " + playerId + " to game because game has already started.");
return;
}
@ -124,7 +123,7 @@ public class GameRound implements Runnable {
}
public void addAI() {
if (gameState != State.OPEN) {
if (!isOpen()) {
return;
}
@ -182,7 +181,7 @@ public class GameRound implements Runnable {
gameState = State.OPEN;
}
if (gameState == State.OPEN) {
if (isOpen()) {
board.removePlayer(p);
} else if (gameState == State.RUNNING) {
checkForWinner();
@ -218,21 +217,7 @@ public class GameRound implements Runnable {
if (ticksFromGameEnd > DELAY_BETWEEN_ROUNDS)
gameRunning.set(false); // end the game if nobody can move anymore
}
runningGames.decrementAndGet();
log("<<< Finished round");
broadcastPlayerList();
long start = System.nanoTime();
updatePlayerStats();
// Wait for 5 seconds, but subtract the amount of time it took to update player stats
long nanoWait = TimeUnit.SECONDS.toNanos(5) - (System.nanoTime() - start);
delay(TimeUnit.NANOSECONDS.toMillis(nanoWait));
log("Clients flagged for auto-requeue will be redirected to the next round now");
GameRoundService gameSvc = CDI.current().select(GameRoundService.class).get();
for (Client c : clients.values())
if (c.autoRequeue)
GameRoundWebsocket.requeueClient(gameSvc, this, c.session);
endGame();
}
private void updatePlayerStats() {
@ -344,11 +329,16 @@ public class GameRound implements Runnable {
return gameState != State.OPEN && gameState != State.FULL;
}
@JsonbTransient
public boolean isOpen() {
return gameState == State.OPEN;
}
public void startGame() {
if (isStarted())
return;
while (gameState == State.OPEN) {
while (isOpen()) {
addAI();
}
@ -358,8 +348,6 @@ public class GameRound implements Runnable {
sendTextToClients(clients.keySet(), jsonb.toJson(new OutboundMessage.StartingCountdown(STARTING_COUNTDOWN)));
delay(TimeUnit.SECONDS.toMillis(STARTING_COUNTDOWN));
lifecycleCallbacks.forEach(c -> c.get());
paused.set(false);
for (Player p : getPlayers())
if (STATUS.Connected == p.getStatus())
@ -377,11 +365,34 @@ public class GameRound implements Runnable {
gameState = State.RUNNING;
}
private void endGame() {
runningGames.decrementAndGet();
log("<<< Finished round");
broadcastPlayerList();
long start = System.nanoTime();
updatePlayerStats();
lifecycleCallbacks.forEach(c -> c.gameEnding());
// Wait for 5 seconds, but subtract the amount of time it took to update player stats
long nanoWait = TimeUnit.SECONDS.toNanos(5) - (System.nanoTime() - start);
delay(TimeUnit.NANOSECONDS.toMillis(nanoWait));
log("Clients flagged for auto-requeue will be redirected to the next round now");
GameRoundService gameSvc = CDI.current().select(GameRoundService.class).get();
for (Client c : clients.values())
if (c.autoRequeue)
GameRoundWebsocket.requeueClient(gameSvc, this, c.session);
}
private void log(String msg) {
System.out.println("[GameRound-" + id + "] " + msg);
}
public interface LifecycleCallback extends Supplier<Void> {}
public interface LifecycleCallback {
public void gameEnding();
}
private class HeartbeatTrigger implements Trigger {
@ -398,8 +409,7 @@ public class GameRound implements Runnable {
if (round.clients.size() == 0) {
log("No clients remaining. Cancelling heartbeat.");
// Ensure that game state is closed off so that no other players can quick join while a round is marked for deletion
if (gameState == State.OPEN)
gameState = State.FINISHED;
gameState = State.FINISHED;
return null;
}
return Date.from(Instant.now().plusSeconds(HEARTBEAT_INTERVAL_SEC));

View File

@ -1,6 +1,3 @@
/**
*
*/
package org.libertybikes.game.core;
import java.util.Set;
@ -54,4 +51,13 @@ public class OutboundMessage {
public Heartbeat() {}
}
public static class QueuePosition {
@JsonbProperty("queuePosition")
public final int queuePosition;
public QueuePosition(int pos) {
queuePosition = pos;
}
}
}

View File

@ -1,12 +1,16 @@
package org.libertybikes.game.round.service;
package org.libertybikes.game.party;
import java.util.Random;
import javax.enterprise.context.Dependent;
import javax.inject.Inject;
import javax.json.bind.annotation.JsonbTransient;
import javax.ws.rs.sse.Sse;
import javax.ws.rs.sse.SseEventSink;
import org.libertybikes.game.core.GameRound;
import org.libertybikes.game.core.GameRound.LifecycleCallback;
import org.libertybikes.game.round.service.GameRoundService;
@Dependent
public class Party {
@ -20,7 +24,7 @@ public class Party {
GameRoundService roundService;
public final String id;
private final PartyQueue queue = new PartyQueue(this);
private volatile GameRound currentRound;
@Inject
@ -37,10 +41,38 @@ public class Party {
currentRound = roundService.getRound(roundService.createRound());
installCallback(currentRound);
}
log("Current round id=" + currentRound.id);
return this.currentRound;
}
public void enqueueClient(SseEventSink sink, Sse sse) {
queue.add(sink, sse);
}
public void close() {
queue.close();
}
public void log(String msg) {
System.out.println("[Party-" + id + "] " + msg);
}
// Installs a callback on the GameRound that updates this party's current round
private void installCallback(GameRound round) {
log("Install callback for round id=" + round.id);
LifecycleCallback callback = new LifecycleCallback() {
@Override
public void gameEnding() {
log("Updating current round from " + round.id + " -> " + round.nextRoundId);
currentRound = roundService.createRoundById(round.nextRoundId);
Party.this.installCallback(currentRound);
log("Processing next members in queue...");
queue.promoteClients();
}
};
round.addCallback(callback);
}
// Get a string of 4 random letters
private static String getRandomPartyID() {
char[] chars = new char[4];
@ -48,20 +80,4 @@ public class Party {
chars[i] = SAFE_CHARS[r.nextInt(SAFE_CHARS.length)];
return new String(chars);
}
private void log(String msg) {
System.out.println("[Party-" + id + "] " + msg);
}
// Installs a callback on the GameRound that updates this party's current round
private void installCallback(GameRound round) {
log("Install callback for round id=" + round.id);
round.addCallback(() -> {
log("Updating current round from " + round.id + " -> " + round.nextRoundId);
currentRound = roundService.createRoundById(round.nextRoundId);
this.installCallback(currentRound);
return null;
});
}
}

View File

@ -0,0 +1,93 @@
package org.libertybikes.game.party;
import java.util.concurrent.ConcurrentLinkedDeque;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.sse.OutboundSseEvent;
import javax.ws.rs.sse.Sse;
import javax.ws.rs.sse.SseEventSink;
import org.libertybikes.game.core.GameRound;
import org.libertybikes.game.core.OutboundMessage;
import org.libertybikes.game.core.Player;
public class PartyQueue {
private final ConcurrentLinkedDeque<QueuedClient> waitingPlayers = new ConcurrentLinkedDeque<>();
private final Party party;
private int queueCounter = 0, firstClient = 0;
public PartyQueue(Party p) {
this.party = p;
}
public void add(SseEventSink sink, Sse sse) {
QueuedClient client = new QueuedClient(sink, sse);
party.log("Adding client " + client.queueNumber + " into the queue in position " + client.queuePosition());
waitingPlayers.add(client);
if (party.getCurrentRound().isOpen())
promoteClients();
else
client.notifyPosition();
}
public void promoteClients() {
GameRound newRound = party.getCurrentRound();
int availableSpots = Player.MAX_PLAYERS - newRound.getPlayers().size();
for (int i = 0; i < availableSpots; i++) {
QueuedClient first = waitingPlayers.pollFirst();
if (first != null) {
first.promoteToGame(newRound.id);
firstClient++;
}
}
for (QueuedClient client : waitingPlayers)
client.notifyPosition();
}
public void close() {
party.log("Closing party queue");
for (QueuedClient client : waitingPlayers)
client.close();
}
private class QueuedClient {
private final int queueNumber;
private final SseEventSink sink;
private final Sse sse;
public QueuedClient(SseEventSink sink, Sse sse) {
this.sink = sink;
this.sse = sse;
this.queueNumber = PartyQueue.this.queueCounter++;
}
public int queuePosition() {
return this.queueNumber - PartyQueue.this.firstClient + 1;
}
public void notifyPosition() {
OutboundSseEvent event = sse.newEventBuilder()
.mediaType(MediaType.APPLICATION_JSON_TYPE)
.data(new OutboundMessage.QueuePosition(queuePosition()))
.build();
party.log("Notifying queued client " + queueNumber + " who is currently at position " + queuePosition());
sink.send(event);
}
public void promoteToGame(String roundId) {
OutboundSseEvent event = sse.newEventBuilder()
.mediaType(MediaType.APPLICATION_JSON_TYPE)
.data(new OutboundMessage.RequeueGame(roundId))
.build();
party.log("Promoting queued client " + queueNumber + " into round " + roundId);
sink.send(event);
close();
}
public void close() {
sink.close();
}
}
}

View File

@ -1,23 +0,0 @@
/**
*
*/
package org.libertybikes.game.round.service;
import java.io.IOException;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerResponseContext;
import javax.ws.rs.container.ContainerResponseFilter;
import javax.ws.rs.ext.Provider;
@Provider
public class CORSFilter implements ContainerResponseFilter {
@Override
public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) throws IOException {
responseContext.getHeaders().add("Access-Control-Allow-Origin", "*");
responseContext.getHeaders().add("Access-Control-Allow-Headers", "origin, content-type, accept, authorization");
responseContext.getHeaders().add("Access-Control-Allow-Credentials", "true");
responseContext.getHeaders().add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, HEAD");
responseContext.getHeaders().add("Access-Control-Max-Age", "1209600");
}
}

View File

@ -70,7 +70,7 @@ public class GameRoundService {
public String getAvailableRound() {
Optional<GameRound> availableRound = allRounds.values()
.stream()
.filter(r -> r.gameState == GameRound.State.OPEN)
.filter(r -> r.isOpen())
.findFirst();
if (availableRound.isPresent())
return availableRound.get().id;
@ -104,7 +104,7 @@ public class GameRoundService {
public void deleteRound(GameRound round) {
String roundId = round.id;
if (round.gameState == State.OPEN)
if (round.isOpen())
round.gameState = State.FINISHED;
System.out.println("Scheduling round id=" + roundId + " for deletion in 5 minutes");
// Do not immediately delete rounds in order to give players/spectators time to move along to the next game

View File

@ -4,6 +4,6 @@ import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;
@ApplicationPath("/")
public class GameRoundApp extends Application {
public class GameServiceApp extends Application {
}

View File

@ -4,9 +4,13 @@
package org.libertybikes.game.round.service;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import javax.annotation.Resource;
import javax.enterprise.concurrent.ManagedScheduledExecutorService;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.inject.spi.CDI;
import javax.ws.rs.GET;
@ -14,44 +18,70 @@ import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.sse.Sse;
import javax.ws.rs.sse.SseEventSink;
import org.libertybikes.game.party.Party;
@Path("/party")
@ApplicationScoped
@Produces(MediaType.APPLICATION_JSON)
public class PartyService {
// Map of PartyID to current RoundID
private final Map<String, Party> allParties = new ConcurrentHashMap<>();
@Resource
private ManagedScheduledExecutorService exec;
@GET
@Produces(MediaType.APPLICATION_JSON)
public Collection<Party> listParties() {
return allParties.values();
return Collections.unmodifiableCollection(allParties.values());
}
@POST
@Path("/create")
@Produces(MediaType.APPLICATION_JSON)
public Party createParty() {
Party p = CDI.current().select(Party.class).get();
allParties.put(p.id, p);
// Put a max lifetime of 12 hours on a party
exec.schedule(() -> this.deleteParty(p.id), 12, TimeUnit.HOURS);
return p;
}
@GET
@Path("/{partyId}")
@Produces(MediaType.APPLICATION_JSON)
public Party getParty(@PathParam("partyId") String partyId) {
if (partyId == null)
if (partyId == null) {
System.out.println("WARN: got null partyId request");
return null;
}
return allParties.get(partyId.toUpperCase());
}
public void deleteParty(String partyId) {
if (allParties.remove(partyId) != null)
System.out.println("Deleted party " + partyId);
}
@GET
@Path("/{partyId}/round")
public String getCurrentRound(@PathParam("partyId") String partyId) {
if (partyId == null)
return null;
Party p = getParty(partyId.toUpperCase());
Party p = getParty(partyId);
return p == null ? null : p.getCurrentRound().id;
}
@GET
@Path("/{partyId}/queue")
@Produces(MediaType.SERVER_SENT_EVENTS)
public void joinQueue(@PathParam("partyId") String partyId, @Context SseEventSink sink, @Context Sse sse) {
Party p = getParty(partyId);
if (p != null)
p.enqueueClient(sink, sse);
}
}

View File

@ -6,6 +6,7 @@
<feature>jaxrs-2.1</feature>
<feature>jsonb-1.0</feature>
<feature>concurrent-1.0</feature>
<feature>apiDiscovery-1.0</feature>
</featureManager>
<httpEndpoint id="defaultHttpEndpoint" host="*" httpPort="${httpPort}" httpsPort="${httpsPort}" />
@ -27,10 +28,10 @@
<!-- This configuration allows cross-origin HTTP requests, such
as those from the front-end component (different port). -->
<cors domain="/round"
<cors domain="/"
allowedOrigins="*"
allowedMethods="GET, DELETE, POST, PUT"
allowedHeaders="Accept, Content-Type, Authorization"
allowedHeaders="origin, content-type, accept, authorization, cache-control"
maxAge="3600" />
<logging traceSpecification="*=info:com.ibm.ws.security.*=all:Authentication=all:com.ibm.ws.container.service.security.internal.*=all" />