UNPKG

@towercg2/server

Version:

The server runtime for the TowerCG2 video graphics system.

131 lines (130 loc) 5.52 kB
"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;