Skip to content

Commit

Permalink
feat(*): wait for gameserver to start (#5)
Browse files Browse the repository at this point in the history
  • Loading branch information
garrappachc authored Jan 3, 2024
1 parent 6e2fee1 commit 5aad086
Show file tree
Hide file tree
Showing 9 changed files with 184 additions and 1 deletion.
8 changes: 8 additions & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ module.exports = {
'plugin:@typescript-eslint/stylistic-type-checked',
'plugin:prettier/recommended',
],
rules: {
'@typescript-eslint/no-misused-promises': [
'error',
{
checksVoidReturn: false,
},
],
},
env: {
node: true,
},
Expand Down
7 changes: 7 additions & 0 deletions examples/create-reservation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@ const reserveServer = async () => {
});

console.log(`reservation created: ${reservation.id}`);

await reservation.waitForStarted();
console.log(`server started`);
console.log(
`connect string: connect ${reservation.server.ip_and_port}; password ${reservation.password}`,
);
console.log(`rcon password: ${reservation.rcon}`);
};

reserveServer().catch(error => {
Expand Down
2 changes: 2 additions & 0 deletions src/errors/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export { HttpClientError } from './http-client.error';
export { InvalidReservationStatusError } from './invalid-reservation-state.error';
export { ServemeTfApiError } from './serveme-tf-api.error';
export { TimeoutError } from './timeout.error';
7 changes: 7 additions & 0 deletions src/errors/invalid-reservation-state.error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { ReservationStatus } from '../types/reservation-status';

export class InvalidReservationStatusError extends Error {
constructor(public readonly reservationStatus: ReservationStatus) {
super(`invalid reservation status: ${reservationStatus}`);
}
}
5 changes: 5 additions & 0 deletions src/errors/timeout.error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export class TimeoutError extends Error {
constructor(public readonly timeoutMs: number) {
super(`timed out after ${timeoutMs}ms`);
}
}
88 changes: 87 additions & 1 deletion src/reservation.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { beforeEach, expect, vi, describe, it } from 'vitest';
import { beforeEach, expect, vi, describe, it, afterEach } from 'vitest';
import { Reservation } from './reservation';
import { Client } from './client';
import { Response } from './types/serveme-tf-responses';
import { ReservationStatus } from './types/reservation-status';
import { secondsToMilliseconds } from 'date-fns';
import { TimeoutError } from './errors/timeout.error';

const mockReservationResponse: Response.ActiveReservation = vi.hoisted(() => ({
status: 'Starting',
Expand Down Expand Up @@ -131,4 +133,88 @@ describe('Reservation', () => {
expect(reservation.status).toEqual(ReservationStatus.ending);
});
});

describe('#waitForStarted()', () => {
let refreshNo = 0;

beforeEach(() => {
refreshNo = 0;
vi.mocked(client.httpClient).get.mockImplementation(() => {
switch (refreshNo++) {
case 0:
return Promise.resolve({
reservation: {
...mockReservationResponse,
status: 'Waiting to start',
},
});

case 1:
return Promise.resolve({
reservation: {
...mockReservationResponse,
status: 'Starting',
},
});

default:
return Promise.resolve({
reservation: {
...mockReservationResponse,
status: 'Ready',
},
});
}
});
});

afterEach(() => {
vi.useRealTimers();
});

it('should wait for reservation to start', () =>
new Promise<void>((resolve, reject) => {
vi.useFakeTimers();
reservation
.waitForStarted()
.then(() => {
expect(reservation.status).toEqual(ReservationStatus.ready);
resolve();
})
.catch(reject);
vi.advanceTimersByTime(secondsToMilliseconds(20));
}));

describe('when the reservation has ended', () => {
beforeEach(() => {
vi.mocked(client.httpClient).get.mockResolvedValue({
reservation: {
...mockReservationResponse,
status: 'Ended',
},
});
});

it('should throw', () =>
new Promise<void>((resolve, reject) => {
vi.useFakeTimers();
reservation
.waitForStarted()
.then(reject)
.catch(error => {
expect(error.message).toEqual(
'invalid reservation status: Ended',
);
resolve();
});
vi.advanceTimersByTime(secondsToMilliseconds(20));
}));
});

it('should timeout', async () => {
await expect(reservation.waitForStarted(100)).rejects.toThrow(
TimeoutError,
);
});
});
});
32 changes: 32 additions & 0 deletions src/reservation.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { Client } from './client';
import { Response } from './types/serveme-tf-responses';
import { ReservationDetails } from './reservation-details';
import { ReservationStatus } from './types/reservation-status';
import { InvalidReservationStatusError } from './errors/invalid-reservation-state.error';
import { secondsToMilliseconds } from 'date-fns';
import { withTimeout } from './with-timeout';

export class Reservation extends ReservationDetails {
constructor(
Expand Down Expand Up @@ -29,4 +33,32 @@ export class Reservation extends ReservationDetails {
this.setResponse(response.reservation);
return this;
}

async waitForStarted(timeoutMs = secondsToMilliseconds(30)): Promise<this> {
let interval: ReturnType<typeof setInterval>;
return withTimeout(
new Promise<this>((resolve, reject) => {
interval = setInterval(async () => {
await this.refresh();

if (
[ReservationStatus.ending, ReservationStatus.ended].includes(
this.status,
)
) {
return reject(new InvalidReservationStatusError(this.status));
}

if (
[ReservationStatus.ready, ReservationStatus.sdrReady].includes(
this.status,
)
) {
return resolve(this);
}
}, secondsToMilliseconds(5));
}),
timeoutMs,
).finally(() => clearInterval(interval));
}
}
20 changes: 20 additions & 0 deletions src/with-timeout.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { describe, it, expect } from 'vitest';
import { withTimeout } from './with-timeout';
import { TimeoutError } from './errors/timeout.error';

describe('when fn does not timeout', () => {
it('should return fn result', async () => {
const result = await withTimeout(
new Promise(resolve => setTimeout(() => resolve('foo'), 50)),
100,
);
expect(result).toEqual('foo');
});
});

describe('when fn times out', () => {
it('should throw', async () => {
const promise = new Promise(resolve => setTimeout(resolve, 100));
await expect(withTimeout(promise, 50)).rejects.toThrow(TimeoutError);
});
});
16 changes: 16 additions & 0 deletions src/with-timeout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { TimeoutError } from './errors/timeout.error';

export const withTimeout = async <T>(
promise: Promise<T>,
timeoutMs: number,
): Promise<T> => {
let timeout: ReturnType<typeof setTimeout>;
return Promise.race([
promise,
new Promise<never>((_resolve, reject) => {
timeout = setTimeout(() => {
reject(new TimeoutError(timeoutMs));
}, timeoutMs);
}),
]).finally(() => clearTimeout(timeout));
};

0 comments on commit 5aad086

Please sign in to comment.