+
+ /* Suport for game meta-data.
+ *
+ * What we want here is an effectively static field that is
+ * accessible through either the class name (SomeGame.meta) or an
+ * instance (some_game.meta). To pull this off we do keep two copies
+ * of the data. But the game classes can just set SomeGame.meta once
+ * and then reference it either way.
+ */
+ static set meta(data) {
+ /* This allows class access (SomeGame.meta) via the get method below. */
+ this._meta = data;
+
+ /* While this allows access via an instance (some_game.meta). */
+ this.prototype.meta = data;
+ }
+
+ static get meta() {
+ return this._meta;
+ }
+
+ add_client(response) {
+ const id = this.next_client_id;
+ this.clients.push({id: id,
+ response: response});
+ this.next_client_id++;
+
+ return id;
+ }
+
+ remove_client(id) {
+ this.clients = this.clients.filter(client => client.id !== id);
+ }
+
+ /* Send a string to all clients */
+ broadcast_string(str) {
+ this.clients.forEach(client => client.response.write(str + '\n'));
+ }
+
+ /* Send an event to all clients.
+ *
+ * An event has both a declared type and a separate data block.
+ * It also ends with two newlines (to mark the end of the event).
+ */
+ broadcast_event(type, data) {
+ this.broadcast_string(`event: ${type}\ndata: ${data}\n`);
+ }
+
+ handle_events(request, response) {
+ /* These headers will keep the connection open so we can stream events. */
+ const headers = {
+ "Content-type": "text/event-stream",
+ "Connection": "keep-alive",
+ "Cache-Control": "no-cache"
+ };
+ response.writeHead(200, headers);
+
+ /* Add this new client to our list of clients. */
+ const id = this.add_client(response);
+
+ /* And queue up cleanup to be triggered on client close. */
+ request.on('close', () => {
+ this.remove_client(id);
+ });
+ }
+