Add a built-in deck source and the CaH base deck.
This does a lot of the work needed for #120.
This commit is contained in:
@@ -18,6 +18,7 @@ import { LoadDeckSummary } from "../../task/load-deck-summary";
|
||||
import * as Handler from "../handler";
|
||||
import { Privileged } from "../privileged";
|
||||
import * as Validation from "../validation.validator";
|
||||
import { ServerState } from "../../server-state";
|
||||
|
||||
/**
|
||||
* An action to change the configuration of the lobby.
|
||||
@@ -80,6 +81,7 @@ function applyRules(
|
||||
}
|
||||
|
||||
function apply(
|
||||
server: ServerState,
|
||||
gameCode: GameCode,
|
||||
lobby: Lobby.Lobby,
|
||||
existing: Config.Config,
|
||||
@@ -92,7 +94,7 @@ function apply(
|
||||
);
|
||||
const allTasks = [...tasks];
|
||||
for (const deck of updated.decks) {
|
||||
const resolver = Sources.limitedResolver(deck.source);
|
||||
const resolver = server.sources.limitedResolver(deck.source);
|
||||
const matching = existing.decks.find((ed) => resolver.equals(ed.source));
|
||||
if (matching === undefined) {
|
||||
allTasks.push(new LoadDeckSummary(gameCode, deck.source));
|
||||
@@ -146,7 +148,8 @@ class ConfigureActions extends Actions.Implementation<
|
||||
protected handle: Handler.Custom<Configure, Lobby.Lobby> = (
|
||||
auth,
|
||||
lobby,
|
||||
action
|
||||
action,
|
||||
server
|
||||
) => {
|
||||
const version = lobby.config.version;
|
||||
for (const op of action.change) {
|
||||
@@ -163,6 +166,7 @@ class ConfigureActions extends Actions.Implementation<
|
||||
}
|
||||
validated = _validateConfig(patched);
|
||||
const { result, events, tasks } = apply(
|
||||
server,
|
||||
auth.gc,
|
||||
lobby,
|
||||
lobby.config,
|
||||
|
||||
@@ -141,6 +141,22 @@ export const Schema = {
|
||||
required: ["action", "token"],
|
||||
type: "object",
|
||||
},
|
||||
BuiltIn: {
|
||||
additionalProperties: false,
|
||||
defaultProperties: [],
|
||||
description: "A source for built-in decks..",
|
||||
properties: {
|
||||
id: {
|
||||
type: "string",
|
||||
},
|
||||
source: {
|
||||
enum: ["BuiltIn"],
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
required: ["id", "source"],
|
||||
type: "object",
|
||||
},
|
||||
Cardcast: {
|
||||
additionalProperties: false,
|
||||
defaultProperties: [],
|
||||
@@ -295,8 +311,15 @@ export const Schema = {
|
||||
type: "object",
|
||||
},
|
||||
External: {
|
||||
$ref: "#/definitions/Cardcast",
|
||||
description: "A source for Cardcast.",
|
||||
anyOf: [
|
||||
{
|
||||
$ref: "#/definitions/BuiltIn",
|
||||
},
|
||||
{
|
||||
$ref: "#/definitions/Cardcast",
|
||||
},
|
||||
],
|
||||
description: "An external source for a card or deck.",
|
||||
},
|
||||
FailReason: {
|
||||
description: "The reason a deck could not be loaded.",
|
||||
|
||||
+21
-18
@@ -40,13 +40,16 @@ export abstract class Cache {
|
||||
*/
|
||||
public abstract readonly config: ServerConfig.Cache;
|
||||
|
||||
private async get<Always extends Tagged, Sometimes extends Tagged, Result>(
|
||||
source: Source.Resolver,
|
||||
getCachedAlways: (
|
||||
source: Source.Resolver
|
||||
) => Promise<Aged<Always> | undefined>,
|
||||
cacheAlways: (source: Source.Resolver, value: Always) => void,
|
||||
cacheSometimes: (source: Source.Resolver, value: Sometimes) => void,
|
||||
private async get<
|
||||
Always extends Tagged,
|
||||
Sometimes extends Tagged,
|
||||
Resolver extends Source.Resolver<Source.External>,
|
||||
Result
|
||||
>(
|
||||
source: Resolver,
|
||||
getCachedAlways: (source: Resolver) => Promise<Aged<Always> | undefined>,
|
||||
cacheAlways: (source: Resolver, value: Always) => void,
|
||||
cacheSometimes: (source: Resolver, value: Sometimes) => void,
|
||||
extract: (result: Result) => [Always, Sometimes | undefined],
|
||||
miss: () => Promise<Result>
|
||||
): Promise<Always> {
|
||||
@@ -64,7 +67,7 @@ export abstract class Cache {
|
||||
}
|
||||
|
||||
private async cacheExpired(
|
||||
source: Source.Resolver,
|
||||
source: Source.Resolver<Source.External>,
|
||||
cached: Aged<Tagged>
|
||||
): Promise<boolean> {
|
||||
if (
|
||||
@@ -85,7 +88,7 @@ export abstract class Cache {
|
||||
* to do so.
|
||||
*/
|
||||
public async getSummary(
|
||||
source: Source.Resolver,
|
||||
source: Source.Resolver<Source.External>,
|
||||
miss: () => Promise<Source.AtLeastSummary>
|
||||
): Promise<Source.Summary> {
|
||||
return this.get(
|
||||
@@ -93,7 +96,7 @@ export abstract class Cache {
|
||||
this.getCachedSummary.bind(this),
|
||||
this.cacheSummaryBackground.bind(this),
|
||||
this.cacheTemplatesBackground.bind(this),
|
||||
result => [result.summary, result.templates],
|
||||
(result) => [result.summary, result.templates],
|
||||
miss
|
||||
);
|
||||
}
|
||||
@@ -107,7 +110,7 @@ export abstract class Cache {
|
||||
* to do so.
|
||||
*/
|
||||
public async getTemplates(
|
||||
source: Source.Resolver,
|
||||
source: Source.Resolver<Source.External>,
|
||||
miss: () => Promise<Source.AtLeastTemplates>
|
||||
): Promise<Decks.Templates> {
|
||||
return this.get(
|
||||
@@ -115,7 +118,7 @@ export abstract class Cache {
|
||||
this.getCachedTemplates.bind(this),
|
||||
this.cacheTemplatesBackground.bind(this),
|
||||
this.cacheSummaryBackground.bind(this),
|
||||
result => [result.templates, result.summary],
|
||||
(result) => [result.templates, result.summary],
|
||||
miss
|
||||
);
|
||||
}
|
||||
@@ -124,19 +127,19 @@ export abstract class Cache {
|
||||
* Get the given summary from the cache.
|
||||
*/
|
||||
public abstract async getCachedSummary(
|
||||
source: Source.Resolver
|
||||
source: Source.Resolver<Source.External>
|
||||
): Promise<Aged<Source.Summary> | undefined>;
|
||||
|
||||
/**
|
||||
* Store the given summary in the cache.
|
||||
*/
|
||||
public abstract async cacheSummary(
|
||||
source: Source.Resolver,
|
||||
source: Source.Resolver<Source.External>,
|
||||
summary: Source.Summary
|
||||
): Promise<void>;
|
||||
|
||||
public cacheSummaryBackground(
|
||||
source: Source.Resolver,
|
||||
source: Source.Resolver<Source.External>,
|
||||
summary: Source.Summary
|
||||
): void {
|
||||
this.cacheSummary(source, summary).catch(Cache.logError);
|
||||
@@ -146,19 +149,19 @@ export abstract class Cache {
|
||||
* Get the given deck templates from the cache.
|
||||
*/
|
||||
public abstract async getCachedTemplates(
|
||||
source: Source.Resolver
|
||||
source: Source.Resolver<Source.External>
|
||||
): Promise<Aged<Decks.Templates> | undefined>;
|
||||
|
||||
/**
|
||||
* Store the given deck templates in the cache.
|
||||
*/
|
||||
public abstract async cacheTemplates(
|
||||
source: Source.Resolver,
|
||||
source: Source.Resolver<Source.External>,
|
||||
templates: Decks.Templates
|
||||
): Promise<void>;
|
||||
|
||||
public cacheTemplatesBackground(
|
||||
source: Source.Resolver,
|
||||
source: Source.Resolver<Source.External>,
|
||||
templates: Decks.Templates
|
||||
): void {
|
||||
this.cacheTemplates(source, templates).catch(Cache.logError);
|
||||
|
||||
@@ -18,42 +18,44 @@ export class InMemoryCache extends Cache.Cache {
|
||||
this.config = config;
|
||||
this.cache = {
|
||||
summaries: new Map(),
|
||||
templates: new Map()
|
||||
templates: new Map(),
|
||||
};
|
||||
}
|
||||
|
||||
private static key(source: Source.Resolver): [string, string] {
|
||||
private static key(
|
||||
source: Source.Resolver<Source.External>
|
||||
): [string, string] {
|
||||
return [source.id(), source.deckId()];
|
||||
}
|
||||
|
||||
public async cacheSummary(
|
||||
source: Source.Resolver,
|
||||
source: Source.Resolver<Source.External>,
|
||||
summary: Source.Summary
|
||||
): Promise<void> {
|
||||
this.cache.summaries.set(InMemoryCache.key(source), {
|
||||
cached: summary,
|
||||
age: Date.now()
|
||||
age: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
public async cacheTemplates(
|
||||
source: Source.Resolver,
|
||||
source: Source.Resolver<Source.External>,
|
||||
templates: Decks.Templates
|
||||
): Promise<void> {
|
||||
this.cache.templates.set(InMemoryCache.key(source), {
|
||||
cached: templates,
|
||||
age: Date.now()
|
||||
age: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
public async getCachedSummary(
|
||||
source: Source.Resolver
|
||||
source: Source.Resolver<Source.External>
|
||||
): Promise<Cache.Aged<Source.Summary> | undefined> {
|
||||
return this.cache.summaries.get(InMemoryCache.key(source));
|
||||
}
|
||||
|
||||
public async getCachedTemplates(
|
||||
source: Source.Resolver
|
||||
source: Source.Resolver<Source.External>
|
||||
): Promise<Cache.Aged<Decks.Templates> | undefined> {
|
||||
return this.cache.templates.get(InMemoryCache.key(source));
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ export class PostgresCache extends Cache.Cache {
|
||||
}
|
||||
|
||||
public async cacheSummary(
|
||||
source: Source.Resolver,
|
||||
source: Source.Resolver<Source.External>,
|
||||
summary: Source.Summary
|
||||
): Promise<void> {
|
||||
await this.pg.inTransaction(async (client) => {
|
||||
@@ -133,7 +133,7 @@ export class PostgresCache extends Cache.Cache {
|
||||
}
|
||||
|
||||
public async cacheTemplates(
|
||||
source: Source.Resolver,
|
||||
source: Source.Resolver<Source.External>,
|
||||
templates: Decks.Templates
|
||||
): Promise<void> {
|
||||
await this.pg.inTransaction(async (client) => {
|
||||
@@ -174,7 +174,7 @@ export class PostgresCache extends Cache.Cache {
|
||||
}
|
||||
|
||||
public async getCachedSummary(
|
||||
source: Source.Resolver
|
||||
source: Source.Resolver<Source.External>
|
||||
): Promise<Cache.Aged<Source.Summary> | undefined> {
|
||||
return await this.pg.withClient(async (client) => {
|
||||
const result = await client.query(
|
||||
@@ -204,7 +204,7 @@ export class PostgresCache extends Cache.Cache {
|
||||
}
|
||||
|
||||
public async getCachedTemplates(
|
||||
source: Source.Resolver
|
||||
source: Source.Resolver<Source.External>
|
||||
): Promise<Cache.Aged<Decks.Templates> | undefined> {
|
||||
return await this.pg.withClient(async (client) => {
|
||||
const about = await client.query(
|
||||
|
||||
+36
-5
@@ -10,7 +10,7 @@ const environmental: (keyof EnvironmentalConfig)[] = [
|
||||
"listenOn",
|
||||
"basePath",
|
||||
"version",
|
||||
"touchOnStart"
|
||||
"touchOnStart",
|
||||
];
|
||||
|
||||
export interface EnvironmentalConfig {
|
||||
@@ -22,6 +22,7 @@ export interface EnvironmentalConfig {
|
||||
}
|
||||
|
||||
export interface Config<D extends Duration> extends EnvironmentalConfig {
|
||||
sources: BaseSources<D>;
|
||||
timeouts: Timeouts<D>;
|
||||
tasks: Tasks<D>;
|
||||
storage: BaseStorage<D>;
|
||||
@@ -42,6 +43,23 @@ type Tasks<D extends Duration> = {
|
||||
processTickFrequency: D;
|
||||
};
|
||||
|
||||
export interface BuiltIn {
|
||||
basePath: string;
|
||||
decks: string[];
|
||||
}
|
||||
|
||||
interface BaseCardcast<D extends Duration> {
|
||||
timeout: D;
|
||||
simultaneousConnections: number;
|
||||
}
|
||||
export type Cardcast = BaseCardcast<ParsedDuration>;
|
||||
|
||||
interface BaseSources<D extends Duration> {
|
||||
builtIn?: BuiltIn;
|
||||
cardcast?: BaseCardcast<D>;
|
||||
}
|
||||
export type Sources = BaseSources<ParsedDuration>;
|
||||
|
||||
type BaseStorage<D extends Duration> = BaseInMemory<D> | BasePostgreSQL<D>;
|
||||
export type Storage = BaseStorage<ParsedDuration>;
|
||||
|
||||
@@ -100,14 +118,14 @@ export const parseStorage = (
|
||||
): BaseStorage<ParsedDuration> => ({
|
||||
...storage,
|
||||
abandonedTime: parseDuration(storage.abandonedTime),
|
||||
garbageCollectionFrequency: parseDuration(storage.garbageCollectionFrequency)
|
||||
garbageCollectionFrequency: parseDuration(storage.garbageCollectionFrequency),
|
||||
});
|
||||
|
||||
export const parseCache = (
|
||||
cache: BaseCache<UnparsedDuration>
|
||||
): BaseCache<ParsedDuration> => ({
|
||||
...cache,
|
||||
checkAfter: parseDuration(cache.checkAfter)
|
||||
checkAfter: parseDuration(cache.checkAfter),
|
||||
});
|
||||
|
||||
export const parseTimeouts = (
|
||||
@@ -121,7 +139,19 @@ export const parseTasks = (
|
||||
tasks: Tasks<UnparsedDuration>
|
||||
): Tasks<ParsedDuration> => ({
|
||||
...tasks,
|
||||
processTickFrequency: parseDuration(tasks.processTickFrequency)
|
||||
processTickFrequency: parseDuration(tasks.processTickFrequency),
|
||||
});
|
||||
|
||||
const parseCardcast = (cardcast: BaseCardcast<UnparsedDuration>): Cardcast => ({
|
||||
...cardcast,
|
||||
timeout: parseDuration(cardcast.timeout),
|
||||
});
|
||||
|
||||
const parseSources = (sources: BaseSources<UnparsedDuration>): Sources => ({
|
||||
...(sources.builtIn !== undefined ? { builtIn: sources.builtIn } : {}),
|
||||
...(sources.cardcast !== undefined
|
||||
? { cardcast: parseCardcast(sources.cardcast) }
|
||||
: {}),
|
||||
});
|
||||
|
||||
export const pullFromEnvironment = (config: Parsed): Parsed => {
|
||||
@@ -141,8 +171,9 @@ export const pullFromEnvironment = (config: Parsed): Parsed => {
|
||||
export const parse = (config: Unparsed): Parsed =>
|
||||
pullFromEnvironment({
|
||||
...config,
|
||||
sources: parseSources(config.sources),
|
||||
timeouts: parseTimeouts(config.timeouts),
|
||||
tasks: parseTasks(config.tasks),
|
||||
storage: parseStorage(config.storage),
|
||||
cache: parseCache(config.cache)
|
||||
cache: parseCache(config.cache),
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ import * as Errors from "../errors";
|
||||
import * as Round from "../games/game/round";
|
||||
import * as Player from "../games/player";
|
||||
import * as User from "../user";
|
||||
import * as Source from "../games/cards/source";
|
||||
|
||||
abstract class ActionExecutionError extends Errors.MassiveDecksError<
|
||||
Errors.Details
|
||||
@@ -31,7 +32,7 @@ export class GameNotStartedError extends ActionExecutionError {
|
||||
}
|
||||
|
||||
public details = (): Errors.Details => ({
|
||||
error: "GameNotStarted"
|
||||
error: "GameNotStarted",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -49,7 +50,7 @@ export class UnprivilegedError extends ActionExecutionError {
|
||||
}
|
||||
|
||||
public details = (): Errors.Details => ({
|
||||
error: "Unprivileged"
|
||||
error: "Unprivileged",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -81,7 +82,7 @@ export class IncorrectPlayerRoleError extends ActionExecutionError {
|
||||
public details = (): IncorrectPlayerRoleDetails => ({
|
||||
error: "IncorrectPlayerRole",
|
||||
role: this.role,
|
||||
expected: this.expected
|
||||
expected: this.expected,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -109,7 +110,7 @@ export class IncorrectUserRoleError extends ActionExecutionError {
|
||||
public details = (): IncorrectUserRoleDetails => ({
|
||||
error: "IncorrectUserRole",
|
||||
role: this.role,
|
||||
expected: this.expected
|
||||
expected: this.expected,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -141,7 +142,7 @@ export class IncorrectRoundStageError extends ActionExecutionError {
|
||||
public details = (): IncorrectRoundStageDetails => ({
|
||||
error: "IncorrectRoundStage",
|
||||
stage: this.stage,
|
||||
expected: this.expected
|
||||
expected: this.expected,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -170,6 +171,48 @@ export class ConfigEditConflictError extends ActionExecutionError {
|
||||
public details = (): ConfigEditConflictDetails => ({
|
||||
error: "ConfigEditConflict",
|
||||
version: this.version,
|
||||
expected: this.expected
|
||||
expected: this.expected,
|
||||
});
|
||||
}
|
||||
|
||||
interface SourceErrorDetails extends Errors.Details {
|
||||
source: Source.External;
|
||||
}
|
||||
|
||||
// Happens if the user asks for a deck that doesn't exist.
|
||||
export class SourceNotFoundError extends ActionExecutionError {
|
||||
public readonly source: Source.External;
|
||||
|
||||
public constructor(source: Source.External) {
|
||||
super(
|
||||
`The given deck (${source}) was not found at the source.`,
|
||||
(undefined as unknown) as Action
|
||||
);
|
||||
this.source = source;
|
||||
Error.captureStackTrace(this, SourceNotFoundError);
|
||||
}
|
||||
|
||||
public details = (): SourceErrorDetails => ({
|
||||
error: "SourceServiceError",
|
||||
source: this.source,
|
||||
});
|
||||
}
|
||||
|
||||
// Happens if the deck service is down.
|
||||
export class SourceServiceError extends ActionExecutionError {
|
||||
public readonly source: Source.External;
|
||||
|
||||
public constructor(source: Source.External) {
|
||||
super(
|
||||
`The given deck source (${source.source}) was not available.`,
|
||||
(undefined as unknown) as Action
|
||||
);
|
||||
this.source = source;
|
||||
Error.captureStackTrace(this, SourceServiceError);
|
||||
}
|
||||
|
||||
public details = (): SourceErrorDetails => ({
|
||||
error: "SourceServiceError",
|
||||
source: this.source,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -50,18 +50,30 @@ export interface BaseCard {
|
||||
source: Source;
|
||||
}
|
||||
|
||||
export type Style = "Em" | "Strong";
|
||||
|
||||
/** An empty slot for responses to be played into.*/
|
||||
export interface Slot {
|
||||
/**
|
||||
* Defines a transformation over the content the slot is filled with.
|
||||
*/
|
||||
transform?: "UpperCase" | "Capitalize";
|
||||
style?: Style;
|
||||
}
|
||||
|
||||
export const isSlot = (part: Part): part is Slot => typeof part !== "string";
|
||||
export interface Styled {
|
||||
text: string;
|
||||
style?: Style;
|
||||
}
|
||||
|
||||
export const isSlot = (part: Part): part is Slot =>
|
||||
typeof part !== "string" && !part.hasOwnProperty("text");
|
||||
|
||||
export const isStyled = (part: Part): part is Styled =>
|
||||
typeof part !== "string" && part.hasOwnProperty("text");
|
||||
|
||||
/** Either text or a slot.*/
|
||||
export type Part = string | Slot;
|
||||
export type Part = string | Styled | Slot;
|
||||
|
||||
/**
|
||||
* Create a new user id.
|
||||
|
||||
@@ -2,6 +2,7 @@ import * as Cache from "../../cache";
|
||||
import * as Decks from "./decks";
|
||||
import { Cardcast } from "./sources/cardcast";
|
||||
import { Custom } from "./sources/custom";
|
||||
import { BuiltIn } from "./sources/builtIn";
|
||||
|
||||
/**
|
||||
* A source for a card or deck.
|
||||
@@ -11,7 +12,7 @@ export type Source = External | Custom;
|
||||
/**
|
||||
* An external source for a card or deck.
|
||||
*/
|
||||
export type External = Cardcast;
|
||||
export type External = BuiltIn | Cardcast;
|
||||
|
||||
/**
|
||||
* More information that can be looked up given a source.
|
||||
@@ -54,14 +55,26 @@ export interface AtLeastTemplates {
|
||||
summary?: Summary;
|
||||
}
|
||||
|
||||
/**
|
||||
* A resolver that only allows access to properties that don't require store
|
||||
* access.
|
||||
*/
|
||||
export interface LimitedResolver<S extends External> {
|
||||
id: () => string;
|
||||
deckId: () => string;
|
||||
loadingDetails: () => Details;
|
||||
equals: (source: External) => boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve information about the given source.
|
||||
*/
|
||||
export abstract class Resolver implements LimitedResolver {
|
||||
export abstract class Resolver<S extends External>
|
||||
implements LimitedResolver<S> {
|
||||
/**
|
||||
* The source in question.
|
||||
*/
|
||||
public abstract source: Source;
|
||||
public abstract source: S;
|
||||
|
||||
/**
|
||||
* A unique id for the source as a whole.
|
||||
@@ -133,31 +146,20 @@ export abstract class Resolver implements LimitedResolver {
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* A resolver that only allows access to properties that don't require store
|
||||
* access.
|
||||
*/
|
||||
export interface LimitedResolver {
|
||||
id: () => string;
|
||||
deckId: () => string;
|
||||
loadingDetails: () => Details;
|
||||
equals: (source: External) => boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* A resolver that caches expensive responses in the store.
|
||||
*/
|
||||
export class CachedResolver extends Resolver {
|
||||
private readonly resolver: Resolver;
|
||||
export class CachedResolver<S extends External> extends Resolver<S> {
|
||||
private readonly resolver: Resolver<S>;
|
||||
private readonly cache: Cache.Cache;
|
||||
|
||||
public constructor(cache: Cache.Cache, resolver: Resolver) {
|
||||
public constructor(cache: Cache.Cache, resolver: Resolver<S>) {
|
||||
super();
|
||||
this.cache = cache;
|
||||
this.resolver = resolver;
|
||||
}
|
||||
|
||||
public get source(): Source {
|
||||
public get source(): S {
|
||||
return this.resolver.source;
|
||||
}
|
||||
|
||||
@@ -225,3 +227,11 @@ export class CachedResolver extends Resolver {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get resolvers for the given source type.
|
||||
*/
|
||||
export interface MetaResolver<S extends External> {
|
||||
limitedResolver(source: S): LimitedResolver<S>;
|
||||
resolver(source: S): Resolver<S>;
|
||||
}
|
||||
|
||||
@@ -3,55 +3,124 @@ import * as Util from "../../util";
|
||||
import * as Source from "./source";
|
||||
import * as Cardcast from "./sources/cardcast";
|
||||
import * as Player from "./sources/custom";
|
||||
import * as Config from "../../config";
|
||||
import * as BuiltIn from "./sources/builtIn";
|
||||
import { SourceNotFoundError } from "../../errors/action-execution-error";
|
||||
|
||||
function uncachedResolver(source: Source.External): Source.Resolver {
|
||||
switch (source.source) {
|
||||
case "Cardcast":
|
||||
return new Cardcast.Resolver(source);
|
||||
|
||||
default:
|
||||
Util.assertNever(source.source);
|
||||
async function loadIfEnabled<Config, MetaResolver>(
|
||||
config: Config | undefined,
|
||||
load: (value: Config) => Promise<MetaResolver>
|
||||
): Promise<MetaResolver | undefined> {
|
||||
if (config === undefined) {
|
||||
return undefined;
|
||||
} else {
|
||||
return await load(config);
|
||||
}
|
||||
}
|
||||
|
||||
export class SourceNotFoundError extends Error {
|
||||
public constructor() {
|
||||
super("The given deck was not found at the source.");
|
||||
}
|
||||
}
|
||||
export class SourceServiceError extends Error {
|
||||
public constructor() {
|
||||
super("The given source was not available.");
|
||||
}
|
||||
export interface ClientInfo {
|
||||
builtIn?: BuiltIn.ClientInfo;
|
||||
cardcast?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the limited resolver for the given source.
|
||||
*/
|
||||
export const limitedResolver = (
|
||||
source: Source.External
|
||||
): Source.LimitedResolver => uncachedResolver(source);
|
||||
export class Sources {
|
||||
public readonly builtIn?: BuiltIn.MetaResolver;
|
||||
public readonly cardcast?: Cardcast.MetaResolver;
|
||||
|
||||
/**
|
||||
* Get the resolver for the given source.
|
||||
*/
|
||||
export const resolver = (
|
||||
cache: Cache,
|
||||
source: Source.External
|
||||
): Source.Resolver =>
|
||||
new Source.CachedResolver(cache, uncachedResolver(source));
|
||||
|
||||
/**
|
||||
* Get the details for the given source.
|
||||
*/
|
||||
export const details = async (
|
||||
cache: Cache,
|
||||
source: Source.Source
|
||||
): Promise<Source.Details> => {
|
||||
switch (source.source) {
|
||||
case "Custom":
|
||||
return Player.details(source);
|
||||
default:
|
||||
return await resolver(cache, source).details();
|
||||
public constructor(
|
||||
builtIn?: BuiltIn.MetaResolver,
|
||||
cardcast?: Cardcast.MetaResolver
|
||||
) {
|
||||
if (builtIn === undefined && cardcast === undefined) {
|
||||
throw new Error("At least one source must be enabled.");
|
||||
}
|
||||
this.builtIn = builtIn;
|
||||
this.cardcast = cardcast;
|
||||
}
|
||||
};
|
||||
|
||||
public clientInfo(): ClientInfo {
|
||||
return {
|
||||
...(this.builtIn !== undefined
|
||||
? {
|
||||
builtIn: this.builtIn.clientInfo(),
|
||||
}
|
||||
: {}),
|
||||
...(this.cardcast !== undefined ? { cardcast: true } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
private metaResolverIfConfigured(
|
||||
source: Source.External
|
||||
): Source.MetaResolver<Source.External> | undefined {
|
||||
switch (source.source) {
|
||||
case "BuiltIn":
|
||||
return this.builtIn;
|
||||
|
||||
case "Cardcast":
|
||||
return this.cardcast;
|
||||
|
||||
default:
|
||||
Util.assertNever(source);
|
||||
}
|
||||
}
|
||||
|
||||
private metaResolver(
|
||||
source: Source.External
|
||||
): Source.MetaResolver<Source.External> {
|
||||
const metaResolver = this.metaResolverIfConfigured(source);
|
||||
if (metaResolver === undefined) {
|
||||
throw new SourceNotFoundError(source);
|
||||
} else {
|
||||
return metaResolver;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the limited resolver for the given source.
|
||||
*/
|
||||
public limitedResolver(
|
||||
source: Source.External
|
||||
): Source.Resolver<Source.External> {
|
||||
return this.metaResolver(source).resolver(source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the resolver for the given source.
|
||||
*/
|
||||
public resolver(
|
||||
cache: Cache,
|
||||
source: Source.External
|
||||
): Source.Resolver<Source.External> {
|
||||
return new Source.CachedResolver(
|
||||
cache,
|
||||
this.metaResolver(source).resolver(source)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the details for the given source.
|
||||
*/
|
||||
details = async (
|
||||
cache: Cache,
|
||||
source: Source.Source
|
||||
): Promise<Source.Details> => {
|
||||
switch (source.source) {
|
||||
case "Custom":
|
||||
return Player.details(source);
|
||||
|
||||
default:
|
||||
return await this.resolver(cache, source).details();
|
||||
}
|
||||
};
|
||||
|
||||
public static async from(config: Config.Sources): Promise<Sources> {
|
||||
const [builtInMeta, cardcastMeta] = await Promise.all<
|
||||
BuiltIn.MetaResolver | undefined,
|
||||
Cardcast.MetaResolver | undefined
|
||||
>([
|
||||
loadIfEnabled(config.builtIn, BuiltIn.load),
|
||||
loadIfEnabled(config.cardcast, Cardcast.load),
|
||||
]);
|
||||
return new Sources(builtInMeta, cardcastMeta);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
import * as Source from "../source";
|
||||
import * as Decks from "../decks";
|
||||
import JSON5 from "json5";
|
||||
import { promises as fs } from "fs";
|
||||
import * as Config from "../../../config";
|
||||
import * as path from "path";
|
||||
import { Part } from "../card";
|
||||
import * as Card from "../card";
|
||||
import {
|
||||
SourceNotFoundError,
|
||||
SourceServiceError,
|
||||
} from "../../../errors/action-execution-error";
|
||||
|
||||
const extension = ".deck.json5";
|
||||
|
||||
interface BuiltInDeck {
|
||||
name: string;
|
||||
calls: Part[][][];
|
||||
responses: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* A source for built-in decks..
|
||||
*/
|
||||
export interface BuiltIn {
|
||||
source: "BuiltIn";
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface ClientInfo {
|
||||
decks: { name: string; id: string }[];
|
||||
}
|
||||
|
||||
export class Resolver extends Source.Resolver<BuiltIn> {
|
||||
public readonly source: BuiltIn;
|
||||
private config: Config.BuiltIn;
|
||||
/**
|
||||
* Can be undefined because we want to error out later if the deck doesn't exist for nicer errors.
|
||||
*/
|
||||
private readonly storedSummary?: Source.Summary;
|
||||
|
||||
public constructor(
|
||||
config: Config.BuiltIn,
|
||||
source: BuiltIn,
|
||||
summary?: Source.Summary
|
||||
) {
|
||||
super();
|
||||
this.config = config;
|
||||
this.source = source;
|
||||
this.storedSummary = summary;
|
||||
}
|
||||
|
||||
public id(): string {
|
||||
return "BuiltIn";
|
||||
}
|
||||
|
||||
public deckId(): string {
|
||||
return this.source.id;
|
||||
}
|
||||
|
||||
public loadingDetails(): Source.Details {
|
||||
if (this.storedSummary !== undefined) {
|
||||
return this.storedSummary.details;
|
||||
}
|
||||
return { name: "Deck Not Found" };
|
||||
}
|
||||
|
||||
public equals(source: Source.External): boolean {
|
||||
return source.source === "BuiltIn" && this.source.id == source.id;
|
||||
}
|
||||
|
||||
public async getTag(): Promise<string | undefined> {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public async atLeastSummary(): Promise<Source.AtLeastSummary> {
|
||||
if (this.storedSummary === undefined) {
|
||||
throw new SourceNotFoundError(this.source);
|
||||
}
|
||||
return {
|
||||
summary: this.storedSummary,
|
||||
};
|
||||
}
|
||||
|
||||
public async atLeastTemplates(): Promise<Source.AtLeastTemplates> {
|
||||
return this.summaryAndTemplates();
|
||||
}
|
||||
|
||||
public summaryAndTemplates = async (): Promise<{
|
||||
summary: Source.Summary;
|
||||
templates: Decks.Templates;
|
||||
}> => {
|
||||
if (this.storedSummary === undefined) {
|
||||
throw new SourceNotFoundError(this.source);
|
||||
}
|
||||
try {
|
||||
const rawDeck = JSON5.parse(
|
||||
(
|
||||
await fs.readFile(
|
||||
path.join(this.config.basePath, this.source.id + extension)
|
||||
)
|
||||
).toString()
|
||||
) as BuiltInDeck;
|
||||
return {
|
||||
summary: this.storedSummary,
|
||||
templates: {
|
||||
calls: new Set(rawDeck.calls.map(this.call)),
|
||||
responses: new Set(rawDeck.responses.map(this.response)),
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
throw new SourceServiceError(this.source);
|
||||
}
|
||||
};
|
||||
|
||||
private call = (call: Card.Part[][]): Card.Call => ({
|
||||
id: Card.id(),
|
||||
parts: call,
|
||||
source: this.source,
|
||||
});
|
||||
|
||||
private response = (response: string): Card.Response => ({
|
||||
id: Card.id(),
|
||||
text: response,
|
||||
source: this.source,
|
||||
});
|
||||
}
|
||||
|
||||
export class MetaResolver implements Source.MetaResolver<BuiltIn> {
|
||||
private readonly config: Config.BuiltIn;
|
||||
private readonly summaries: Map<string, Source.Summary>;
|
||||
|
||||
public constructor(
|
||||
config: Config.BuiltIn,
|
||||
summaries: Map<string, Source.Summary>
|
||||
) {
|
||||
this.config = config;
|
||||
this.summaries = summaries;
|
||||
}
|
||||
|
||||
limitedResolver(source: BuiltIn): Source.LimitedResolver<BuiltIn> {
|
||||
return this.resolver(source);
|
||||
}
|
||||
|
||||
resolver(source: BuiltIn): Resolver {
|
||||
const summary = this.summaries.get(source.id);
|
||||
return new Resolver(this.config, source, summary);
|
||||
}
|
||||
|
||||
public clientInfo(): ClientInfo {
|
||||
return {
|
||||
decks: this.config.decks.map((id) => ({
|
||||
name: (this.summaries.get(id) as Source.Summary).details.name,
|
||||
id,
|
||||
})),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function load(config: Config.BuiltIn): Promise<MetaResolver> {
|
||||
const summaries = new Map<string, Source.Summary>();
|
||||
|
||||
for (const id of config.decks) {
|
||||
const rawDeck = JSON5.parse(
|
||||
(await fs.readFile(path.join(config.basePath, id + extension))).toString()
|
||||
) as BuiltInDeck;
|
||||
summaries.set(id, {
|
||||
details: {
|
||||
name: rawDeck.name,
|
||||
},
|
||||
calls: rawDeck.calls.length,
|
||||
responses: rawDeck.responses.length,
|
||||
});
|
||||
}
|
||||
|
||||
return new MetaResolver(config, summaries);
|
||||
}
|
||||
@@ -1,11 +1,15 @@
|
||||
import http, { AxiosRequestConfig } from "axios";
|
||||
import http, { AxiosInstance, AxiosRequestConfig } from "axios";
|
||||
import genericPool from "generic-pool";
|
||||
import HttpStatus from "http-status-codes";
|
||||
import * as Card from "../card";
|
||||
import { Slot } from "../card";
|
||||
import * as Decks from "../decks";
|
||||
import * as Source from "../source";
|
||||
import { SourceNotFoundError, SourceServiceError } from "../sources";
|
||||
import * as Config from "../../../config";
|
||||
import {
|
||||
SourceNotFoundError,
|
||||
SourceServiceError,
|
||||
} from "../../../errors/action-execution-error";
|
||||
|
||||
interface CCSummary {
|
||||
name: string;
|
||||
@@ -38,27 +42,6 @@ interface CCCard {
|
||||
nsfw: boolean;
|
||||
}
|
||||
|
||||
const config: AxiosRequestConfig = {
|
||||
method: "GET",
|
||||
baseURL: "https://api.cardcastgame.com/v1/",
|
||||
timeout: 10000,
|
||||
responseType: "json"
|
||||
};
|
||||
|
||||
/**
|
||||
* We pool requests to cardcast to stop us hitting them too hard (on top of
|
||||
* caching). We only allow two simultaneous requests.
|
||||
*/
|
||||
const connectionPool = genericPool.createPool(
|
||||
{
|
||||
create: async () => http.create(config),
|
||||
destroy: async _ => {
|
||||
// Do nothing.
|
||||
}
|
||||
},
|
||||
{ max: 2 }
|
||||
);
|
||||
|
||||
const summaryUrl = (playCode: PlayCode): string => `decks/${playCode}`;
|
||||
const deckUrl = (playCode: PlayCode): string => `${summaryUrl(playCode)}/cards`;
|
||||
const humanViewUrl = (playCode: PlayCode): string =>
|
||||
@@ -88,7 +71,7 @@ const nextWordShouldBeCapitalized = (previously: string): boolean =>
|
||||
*/
|
||||
// TODO: We probably want to offer some control over these heuristics.
|
||||
function* parts(call: CCCard): Iterable<Card.Part> {
|
||||
const upper: Slot = call.text.every(text => text === text.toUpperCase())
|
||||
const upper: Slot = call.text.every((text) => text === text.toUpperCase())
|
||||
? { transform: "UpperCase" }
|
||||
: {};
|
||||
let first = true;
|
||||
@@ -114,21 +97,26 @@ function* parts(call: CCCard): Iterable<Card.Part> {
|
||||
const call = (source: Cardcast, call: CCCard): Card.Call => ({
|
||||
id: Card.id(),
|
||||
parts: [Array.from(parts(call))],
|
||||
source: source
|
||||
source: source,
|
||||
});
|
||||
|
||||
const response = (source: Cardcast, response: CCCard): Card.Response => ({
|
||||
id: Card.id(),
|
||||
text: response.text[0],
|
||||
source: source
|
||||
source: source,
|
||||
});
|
||||
|
||||
export class Resolver extends Source.Resolver {
|
||||
export class Resolver extends Source.Resolver<Cardcast> {
|
||||
public readonly source: Cardcast;
|
||||
private readonly connectionPool: genericPool.Pool<AxiosInstance>;
|
||||
|
||||
public constructor(source: Cardcast) {
|
||||
public constructor(
|
||||
source: Cardcast,
|
||||
connectionPool: genericPool.Pool<AxiosInstance>
|
||||
) {
|
||||
super();
|
||||
this.source = source;
|
||||
this.connectionPool = connectionPool;
|
||||
}
|
||||
|
||||
public id(): string {
|
||||
@@ -142,7 +130,7 @@ export class Resolver extends Source.Resolver {
|
||||
public loadingDetails(): Source.Details {
|
||||
return {
|
||||
name: `Cardcast ${this.source.playCode}`,
|
||||
url: humanViewUrl(this.source.playCode)
|
||||
url: humanViewUrl(this.source.playCode),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -158,49 +146,47 @@ export class Resolver extends Source.Resolver {
|
||||
}
|
||||
|
||||
public async atLeastSummary(): Promise<Source.AtLeastSummary> {
|
||||
const summary = await Resolver.get<CCSummary>(
|
||||
summaryUrl(this.source.playCode)
|
||||
);
|
||||
const summary = await this.get<CCSummary>(summaryUrl(this.source.playCode));
|
||||
return {
|
||||
summary: {
|
||||
details: {
|
||||
name: summary.name,
|
||||
url: humanViewUrl(this.source.playCode)
|
||||
url: humanViewUrl(this.source.playCode),
|
||||
},
|
||||
calls: Number.parseInt(summary.call_count, 10),
|
||||
responses: Number.parseInt(summary.response_count, 10),
|
||||
tag: summary.updated_at
|
||||
}
|
||||
tag: summary.updated_at,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public async atLeastTemplates(): Promise<Source.AtLeastTemplates> {
|
||||
const deck = await Resolver.get<CCDeck>(deckUrl(this.source.playCode));
|
||||
const deck = await this.get<CCDeck>(deckUrl(this.source.playCode));
|
||||
return {
|
||||
templates: {
|
||||
calls: new Set(deck.calls.map(c => call(this.source, c))),
|
||||
responses: new Set(deck.responses.map(r => response(this.source, r)))
|
||||
}
|
||||
calls: new Set(deck.calls.map((c) => call(this.source, c))),
|
||||
responses: new Set(deck.responses.map((r) => response(this.source, r))),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private static async get<T>(url: string): Promise<T> {
|
||||
const connection = await connectionPool.acquire();
|
||||
private async get<T>(url: string): Promise<T> {
|
||||
const connection = await this.connectionPool.acquire();
|
||||
try {
|
||||
return (await connection.get(url)).data;
|
||||
} catch (error) {
|
||||
if (error.response) {
|
||||
const response = error.response;
|
||||
if (response.status === HttpStatus.NOT_FOUND) {
|
||||
throw new SourceNotFoundError();
|
||||
throw new SourceNotFoundError(this.source);
|
||||
} else {
|
||||
throw new SourceServiceError();
|
||||
throw new SourceServiceError(this.source);
|
||||
}
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
} finally {
|
||||
await connectionPool.release(connection);
|
||||
await this.connectionPool.release(connection);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,6 +195,43 @@ export class Resolver extends Source.Resolver {
|
||||
templates: Decks.Templates;
|
||||
}> => ({
|
||||
summary: await this.summary(),
|
||||
templates: await this.templates()
|
||||
templates: await this.templates(),
|
||||
});
|
||||
}
|
||||
|
||||
export class MetaResolver implements Source.MetaResolver<Cardcast> {
|
||||
/**
|
||||
* We pool requests to cardcast to stop us hitting them too hard (on top of caching).
|
||||
*/
|
||||
private readonly connectionPool: genericPool.Pool<AxiosInstance>;
|
||||
|
||||
public constructor(config: Config.Cardcast) {
|
||||
const httpConfig: AxiosRequestConfig = {
|
||||
method: "GET",
|
||||
baseURL: "https://api.cardcastgame.com/v1/",
|
||||
timeout: config.timeout,
|
||||
responseType: "json",
|
||||
};
|
||||
|
||||
this.connectionPool = genericPool.createPool(
|
||||
{
|
||||
create: async () => http.create(httpConfig),
|
||||
destroy: async (_) => {
|
||||
// Do nothing.
|
||||
},
|
||||
},
|
||||
{ max: config.simultaneousConnections }
|
||||
);
|
||||
}
|
||||
|
||||
limitedResolver(source: Cardcast): Resolver {
|
||||
return this.resolver(source);
|
||||
}
|
||||
|
||||
resolver(source: Cardcast): Resolver {
|
||||
return new Resolver(source, this.connectionPool);
|
||||
}
|
||||
}
|
||||
|
||||
export const load = async (config: Config.Cardcast): Promise<MetaResolver> =>
|
||||
new MetaResolver(config);
|
||||
|
||||
+24
-19
@@ -3,12 +3,11 @@ import express, { NextFunction, Request, Response } from "express";
|
||||
import "express-async-errors";
|
||||
import expressWinston from "express-winston";
|
||||
import ws from "express-ws";
|
||||
import fs from "fs";
|
||||
import { promises as fs } from "fs";
|
||||
import helmet from "helmet";
|
||||
import HttpStatus from "http-status-codes";
|
||||
import JSON5 from "json5";
|
||||
import sourceMapSupport from "source-map-support";
|
||||
import { promisify } from "util";
|
||||
import wu from "wu";
|
||||
import * as CheckAlive from "./action/initial/check-alive";
|
||||
import * as CreateLobby from "./action/initial/create-lobby";
|
||||
@@ -31,11 +30,11 @@ import * as Token from "./user/token";
|
||||
|
||||
sourceMapSupport.install();
|
||||
|
||||
process.on("uncaughtException", function(error) {
|
||||
process.on("uncaughtException", function (error) {
|
||||
Logging.logException("Uncaught exception: ", error);
|
||||
});
|
||||
|
||||
process.on("unhandledRejection", function(reason, promise) {
|
||||
process.on("unhandledRejection", function (reason, promise) {
|
||||
if (reason instanceof Error) {
|
||||
Logging.logException(`Unhandled rejection for ${promise}.`, reason);
|
||||
} else {
|
||||
@@ -51,7 +50,7 @@ function getConfigFilePath(): string {
|
||||
async function main(): Promise<void> {
|
||||
const config = ServerConfig.parse(
|
||||
JSON5.parse(
|
||||
(await promisify(fs.readFile)(getConfigFilePath())).toString()
|
||||
(await fs.readFile(getConfigFilePath())).toString()
|
||||
) as ServerConfig.Unparsed
|
||||
);
|
||||
|
||||
@@ -75,7 +74,7 @@ async function main(): Promise<void> {
|
||||
|
||||
app.use(
|
||||
expressWinston.logger({
|
||||
winstonInstance: Logging.logger
|
||||
winstonInstance: Logging.logger,
|
||||
})
|
||||
);
|
||||
|
||||
@@ -123,12 +122,12 @@ async function main(): Promise<void> {
|
||||
|
||||
const registration = RegisterUser.validate(req.body);
|
||||
const newUser = User.create(registration);
|
||||
const id = await Change.applyAndReturn(state, gameCode, lobby => {
|
||||
const id = await Change.applyAndReturn(state, gameCode, (lobby) => {
|
||||
if (lobby.config.password !== registration.password) {
|
||||
throw new InvalidLobbyPasswordError();
|
||||
}
|
||||
if (
|
||||
wu(Object.values(lobby.users)).find(u => u.name === registration.name)
|
||||
wu(Object.values(lobby.users)).find((u) => u.name === registration.name)
|
||||
) {
|
||||
throw new UsernameAlreadyInUseError(registration.name);
|
||||
}
|
||||
@@ -155,22 +154,22 @@ async function main(): Promise<void> {
|
||||
lobby,
|
||||
events: [
|
||||
Event.targetAll(PresenceChanged.joined(id, newUser)),
|
||||
...(unpause.events !== undefined ? unpause.events : [])
|
||||
...(unpause.events !== undefined ? unpause.events : []),
|
||||
],
|
||||
timeouts: [
|
||||
{
|
||||
timeout: UserDisconnect.of(id),
|
||||
after: config.timeouts.disconnectionGracePeriod
|
||||
after: config.timeouts.disconnectionGracePeriod,
|
||||
},
|
||||
...(unpause.timeouts !== undefined ? unpause.timeouts : [])
|
||||
]
|
||||
...(unpause.timeouts !== undefined ? unpause.timeouts : []),
|
||||
],
|
||||
},
|
||||
returnValue: id
|
||||
returnValue: id,
|
||||
};
|
||||
});
|
||||
const claims: Token.Claims = {
|
||||
gc: gameCode,
|
||||
uid: id
|
||||
uid: id,
|
||||
};
|
||||
res.json(Token.create(claims, await state.store.id(), config.secret));
|
||||
});
|
||||
@@ -180,6 +179,10 @@ async function main(): Promise<void> {
|
||||
state.socketManager.add(state, gameCode, socket);
|
||||
});
|
||||
|
||||
app.get("/api/sources", async (req, res) => {
|
||||
res.json(state.sources.clientInfo());
|
||||
});
|
||||
|
||||
app.use((error: Error, req: Request, res: Response, next: NextFunction) => {
|
||||
if (res.headersSent) {
|
||||
next(error);
|
||||
@@ -195,7 +198,7 @@ async function main(): Promise<void> {
|
||||
app.use(
|
||||
expressWinston.errorLogger({
|
||||
winstonInstance: Logging.logger,
|
||||
msg: "{{err.message}}"
|
||||
msg: "{{err.message}}",
|
||||
})
|
||||
);
|
||||
|
||||
@@ -237,18 +240,20 @@ async function main(): Promise<void> {
|
||||
|
||||
state.tasks
|
||||
.loadFromStore(state)
|
||||
.catch(error => Logging.logException("Error running store tasks:", error));
|
||||
.catch((error) =>
|
||||
Logging.logException("Error running store tasks:", error)
|
||||
);
|
||||
|
||||
app.listen(config.listenOn, async () => {
|
||||
Logging.logger.info(`Listening on ${config.listenOn}.`);
|
||||
if (config.touchOnStart !== null) {
|
||||
const f = await promisify(fs.open)(config.touchOnStart, "w");
|
||||
await promisify(fs.close)(f);
|
||||
const f = await fs.open(config.touchOnStart, "w");
|
||||
await f.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
main().catch(error => {
|
||||
main().catch((error) => {
|
||||
Logging.logException("Application exception:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
@@ -5,9 +5,11 @@ import { SocketManager } from "./socket-manager";
|
||||
import { Store } from "./store";
|
||||
import * as Stores from "./store/stores";
|
||||
import * as Tasks from "./task/tasks";
|
||||
import { Sources } from "./games/cards/sources";
|
||||
|
||||
export interface ServerState {
|
||||
config: Config.Parsed;
|
||||
sources: Sources;
|
||||
store: Store;
|
||||
cache: Cache;
|
||||
socketManager: SocketManager;
|
||||
@@ -15,16 +17,18 @@ export interface ServerState {
|
||||
}
|
||||
|
||||
export async function create(config: Config.Parsed): Promise<ServerState> {
|
||||
const sources = await Sources.from(config.sources);
|
||||
const store = await Stores.from(config.storage);
|
||||
const cache = await caches.from(config.cache);
|
||||
const socketManager = new SocketManager();
|
||||
const tasks = new Tasks.Queue(config.tasks.rateLimit);
|
||||
|
||||
return {
|
||||
sources,
|
||||
config,
|
||||
store,
|
||||
cache,
|
||||
socketManager,
|
||||
tasks
|
||||
tasks,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,17 +2,16 @@ import Rfc6902 from "rfc6902";
|
||||
import * as Event from "../event";
|
||||
import * as Configured from "../events/lobby-event/configured";
|
||||
import * as Source from "../games/cards/source";
|
||||
import * as Sources from "../games/cards/sources";
|
||||
import {
|
||||
SourceNotFoundError,
|
||||
SourceServiceError
|
||||
} from "../games/cards/sources";
|
||||
import { Lobby } from "../lobby";
|
||||
import { Change } from "../lobby/change";
|
||||
import * as Config from "../lobby/config";
|
||||
import { GameCode } from "../lobby/game-code";
|
||||
import { ServerState } from "../server-state";
|
||||
import * as Task from "../task";
|
||||
import {
|
||||
SourceNotFoundError,
|
||||
SourceServiceError,
|
||||
} from "../errors/action-execution-error";
|
||||
|
||||
export class LoadDeckSummary extends Task.TaskBase<Source.Summary> {
|
||||
private readonly source: Source.External;
|
||||
@@ -23,7 +22,8 @@ export class LoadDeckSummary extends Task.TaskBase<Source.Summary> {
|
||||
}
|
||||
|
||||
protected async begin(server: ServerState): Promise<Source.Summary> {
|
||||
const loaded = await Sources.resolver(server.cache, this.source)
|
||||
const loaded = await server.sources
|
||||
.resolver(server.cache, this.source)
|
||||
// We are intentionally ensuring the templates get cached here in advance,
|
||||
// but don't actually need to do anything with them at this point.
|
||||
.summaryAndTemplates();
|
||||
@@ -32,35 +32,48 @@ export class LoadDeckSummary extends Task.TaskBase<Source.Summary> {
|
||||
|
||||
private resolveInternal(
|
||||
lobby: Lobby,
|
||||
modify: (source: Config.ConfiguredSource) => void
|
||||
modify: (source: Config.ConfiguredSource) => void,
|
||||
server: ServerState
|
||||
): Change {
|
||||
const lobbyConfig = lobby.config;
|
||||
const oldConfig = JSON.parse(JSON.stringify(Config.censor(lobby.config)));
|
||||
const decks = lobbyConfig.decks;
|
||||
const resolver = Sources.limitedResolver(this.source);
|
||||
const target = decks.find(deck => resolver.equals(deck.source));
|
||||
const resolver = server.sources.limitedResolver(this.source);
|
||||
const target = decks.find((deck) => resolver.equals(deck.source));
|
||||
if (target !== undefined) {
|
||||
modify(target);
|
||||
lobbyConfig.version += 1;
|
||||
const patch = Rfc6902.createPatch(oldConfig, Config.censor(lobbyConfig));
|
||||
return {
|
||||
lobby,
|
||||
events: [Event.targetAll(Configured.of(patch))]
|
||||
events: [Event.targetAll(Configured.of(patch))],
|
||||
};
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
protected resolve(lobby: Lobby, work: Source.Summary): Change {
|
||||
return this.resolveInternal(lobby, summarised => {
|
||||
if (!Config.isFailed(summarised)) {
|
||||
summarised.summary = { ...work, tag: undefined };
|
||||
}
|
||||
});
|
||||
protected resolve(
|
||||
lobby: Lobby,
|
||||
work: Source.Summary,
|
||||
server: ServerState
|
||||
): Change {
|
||||
return this.resolveInternal(
|
||||
lobby,
|
||||
(summarised) => {
|
||||
if (!Config.isFailed(summarised)) {
|
||||
summarised.summary = { ...work, tag: undefined };
|
||||
}
|
||||
},
|
||||
server
|
||||
);
|
||||
}
|
||||
|
||||
protected resolveError(lobby: Lobby, error: Error): Change {
|
||||
protected resolveError(
|
||||
lobby: Lobby,
|
||||
error: Error,
|
||||
server: ServerState
|
||||
): Change {
|
||||
let reason: Config.FailReason;
|
||||
if (error instanceof SourceNotFoundError) {
|
||||
reason = "NotFound";
|
||||
@@ -69,11 +82,15 @@ export class LoadDeckSummary extends Task.TaskBase<Source.Summary> {
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
return this.resolveInternal(lobby, failed => {
|
||||
if (!failed.hasOwnProperty("summary")) {
|
||||
(failed as Config.FailedSource).failure = reason;
|
||||
}
|
||||
});
|
||||
return this.resolveInternal(
|
||||
lobby,
|
||||
(failed) => {
|
||||
if (!failed.hasOwnProperty("summary")) {
|
||||
(failed as Config.FailedSource).failure = reason;
|
||||
}
|
||||
},
|
||||
server
|
||||
);
|
||||
}
|
||||
|
||||
public static *discover(
|
||||
|
||||
@@ -19,8 +19,8 @@ export class StartGame extends Task.TaskBase<Decks.Templates[]> {
|
||||
|
||||
protected async begin(server: ServerState): Promise<Decks.Templates[]> {
|
||||
return Promise.all(
|
||||
wu(this.decks).map(deck =>
|
||||
Sources.resolver(server.cache, deck).templates()
|
||||
wu(this.decks).map((deck) =>
|
||||
server.sources.resolver(server.cache, deck).templates()
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -42,7 +42,7 @@ export class StartGame extends Task.TaskBase<Decks.Templates[]> {
|
||||
return {
|
||||
lobby,
|
||||
events: atStartOfRound.events,
|
||||
timeouts: atStartOfRound.timeouts
|
||||
timeouts: atStartOfRound.timeouts,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user