@towercg2/server
Version:
The server runtime for the TowerCG2 video graphics system.
131 lines (130 loc) • 5.52 kB
JavaScript
"use strict";
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
result["default"] = mod;
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
const Express = __importStar(require("express"));
const lodash = __importStar(require("lodash"));
const path = __importStar(require("path"));
const fsx = __importStar(require("fs-extra"));
const client_1 = require("@towercg2/client");
const store_1 = require("./store");
;
class ServerPluginBase {
constructor(server) {
this.server = server;
this._client = null;
this.name = this.constructor.pluginName;
this.logger = server.baseLogger.child({ plugin: this.constructor.name, pluginName: this.name });
this.behaviors = this.behaviors = lodash.merge({}, ServerPluginBase.DEFAULT_BEHAVIORS, this.buildPluginBehaviors());
}
get client() {
if (!this._client) {
throw new Error("Attempted to get client before instantiation.");
}
return this._client;
}
buildPluginBehaviors() { return {}; }
get dataDirectory() { return path.resolve(this.server.config.dataDirectory, this.name); }
bindHttpEndpoints(app) { }
bindSocketIOHandlers(socket) { }
doSetClientConnection(localUri, clientId, clientSecret) {
if (this._client) {
throw new Error("Cannot reset client.");
}
this._client = new client_1.Client(localUri, { clientId, clientSecret }, this.logger.child({ component: "TowerCG2Client" }));
}
eventName(eventName) {
return `${this.name}:${eventName}`;
}
}
ServerPluginBase.DEFAULT_BEHAVIORS = {
enableFileServing: true,
storeJsonReviver: null,
storeJsonReplacer: null
};
exports.ServerPluginBase = ServerPluginBase;
class ServerPlugin extends ServerPluginBase {
constructor(config, server) {
super(server);
fsx.mkdirpSync(this.dataDirectory);
this.config = lodash.merge({}, this.buildDefaultConfig(), config);
this.store = store_1.buildStore(this.logger, this.buildReducer(), this.buildDefaultState(), this.behaviors.storeJsonReviver, this.behaviors.storeJsonReplacer, path.resolve(this.dataDirectory, "store.json"));
this.store.subscribe(() => {
this.logger.debug("Store updated; pushing out.");
this.server.messageToAuthenticated(`${this.name}:stateUpdated`, this.store.getState());
});
}
get state() {
return lodash.cloneDeep(this.store.getState());
}
async doInitialize(http) {
await this.initialize(http);
this.store.subscribe(() => {
this.server.messageToAuthenticated(`${this.name}:stateUpdated`, this.store.getState());
});
this.client.connect();
}
doBindHttpEndpoints(app) {
if (this.behaviors.enableFileServing) {
const fileDirectory = path.resolve(this.dataDirectory, "files");
this.logger.debug({ fileDirectory }, "Enabling express-static for plugin.");
app.use("/files", Express.static(fileDirectory));
}
app.use("/state", (req, res) => {
const json = JSON.stringify(this.store.getState(), this.behaviors.storeJsonReplacer || undefined, 2);
res.send(json);
});
this.bindHttpEndpoints(app);
}
doBindSocketIOHandlers(socket) {
this.bindSocketIOHandlers(socket);
}
// PROBLEM: this method does accept poorly defined events despite its signature!
// Ideally this method would infer TEventType and TPayload when passed a generic
// event interface, i.e. `this.emit<TickEvent>(event)`. But it can't. So we need
// higher-kinded types. (And people say category theory isn't useful.)
//
// The current workaround is to create the event and strictly define its type,
// then pass it in, because you can't create a misnamed event without TypeScript
// throwing an error.
//
// This is issue #1213 in TypeScript.
broadcastEvent(event) {
this.logger.trace({ event }, "Emitting event.");
this.server.messageToAuthenticated(event.type, event);
}
handleMessage(messageType, client, fn) {
client.on(messageType, async (message) => {
const { payload } = message;
try {
this.logger.trace({ messageType }, "Message received.");
await fn(payload, client);
}
catch (error) {
// as events are fire-and-forget, we do not return an error.
this.logger.error({ messageType, payload, error }, "Message when handling event.");
}
});
}
handleRequest(requestType, client, fn) {
client.on(requestType, async (request) => {
const { requestId, payload } = request;
const responseName = client_1.responseEventName(requestId);
try {
this.logger.trace({ requestType, requestId }, "Request received.");
const data = await fn(payload, client);
client.emit(responseName, data);
}
catch (error) {
const errorResponse = { error: error.message || "No message in error." };
client.emit(responseName, errorResponse);
}
});
}
}
exports.ServerPlugin = ServerPlugin;