UNPKG

derby

Version:

MVC framework making it easy to write realtime, collaborative applications that run in both Node.js and browsers.

335 lines (334 loc) 14.1 kB
/* * App.server.js * * Application level functionality that is * only applicable to the server. * */ var __extends = (this && this.__extends) || (function () { var extendStatics = function (d, b) { extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; return extendStatics(d, b); }; return function (d, b) { if (typeof b !== "function" && b !== null) throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.AppForServer = void 0; var racer = require("racer"); var App_1 = require("./App"); var parsing = require("./parsing"); var derbyTemplates = require("./templates"); var util = racer.util; // Avoid Browserifying these dependencies var chokidar, files, fs, path; if (module.require) { chokidar = module.require('chokidar'); files = module.require('./files'); fs = module.require('fs'); path = module.require('path'); } var STYLE_EXTENSIONS = ['.css']; var VIEW_EXTENSIONS = ['.html']; var COMPILERS = { '.css': cssCompiler, '.html': htmlCompiler }; function cssCompiler(file, filename, _options) { return { css: file, files: [filename] }; } function htmlCompiler(file) { return file; } function watchOnce(filenames, callback) { var watcher = chokidar.watch(filenames); var closed = false; watcher.on('change', function () { if (closed) return; closed = true; // HACK: chokidar 3.1.1 crashes when you synchronously call close // in the change event. Delaying appears to prevent the crash process.nextTick(function () { watcher.close(); }); callback(); }); } var AppForServer = /** @class */ (function (_super) { __extends(AppForServer, _super); function AppForServer(derby, name, filename, options) { var _this = _super.call(this, derby, name, filename, options) || this; _this._init(options); return _this; } AppForServer.prototype._init = function (options) { this._initBundle(options); this._initRefresh(); this._initLoad(); this._initViews(); }; AppForServer.prototype._initBundle = function (options) { this.scriptFilename = null; this.scriptMapFilename = null; this.scriptBaseUrl = (options && options.scriptBaseUrl) || ''; this.scriptMapBaseUrl = (options && options.scriptMapBaseUrl) || ''; this.scriptCrossOrigin = (options && options.scriptCrossOrigin) || false; this.scriptUrl = null; this.scriptMapUrl = null; }; AppForServer.prototype._initRefresh = function () { this.watchFiles = !util.isProduction; this.agents = null; }; AppForServer.prototype._initLoad = function () { this.styleExtensions = STYLE_EXTENSIONS.slice(); this.viewExtensions = VIEW_EXTENSIONS.slice(); this.compilers = util.copyObject(COMPILERS); }; AppForServer.prototype._initViews = function () { this.serializedDir = path.dirname(this.filename || '') + '/derby-serialized'; this.serializedBase = this.serializedDir + '/' + this.name; if (fs.existsSync(this.serializedBase + '.json')) { this.deserialize(); this.loadViews = function (_filename, _namespace) { return this; }; this.loadStyles = function (_filename, _options) { return this; }; return; } this.views.register('Page', '<!DOCTYPE html>' + '<meta charset="utf-8">' + '<view is="{{$render.prefix}}TitleElement"></view>' + '<view is="{{$render.prefix}}Styles"></view>' + '<view is="{{$render.prefix}}Head"></view>' + '<view is="{{$render.prefix}}BodyElement"></view>', { serverOnly: true }); this.views.register('TitleElement', '<title><view is="{{$render.prefix}}Title"></view></title>'); this.views.register('BodyElement', '<body class="{{$bodyClass($render.ns)}}">' + '<view is="{{$render.prefix}}Body"></view>'); this.views.register('Title', 'Derby App'); this.views.register('Styles', '', { serverOnly: true }); this.views.register('Head', '', { serverOnly: true }); this.views.register('Body', ''); this.views.register('Tail', ''); }; // overload w different signatures, but different use cases AppForServer.prototype.createPage = function (req, res, next) { var model = req.model || racer.createModel(); this.emit('model', model); var Page = this.Page; var page = new Page(this, model, req, res); if (next) { model.on('error', function (err) { model.hasErrored = true; next(err); }); page.on('error', next); } this.emit('page', page); return page; }; // @DEPRECATED AppForServer.prototype.bundle = function (_backend, _options, _cb) { throw new Error('bundle implementation missing; use racer-bundler for implementation, or remove call to this method and use another bundler'); }; // @DEPRECATED AppForServer.prototype.writeScripts = function (_backend, _dir, _options, _cb) { throw new Error('writeScripts implementation missing; use racer-bundler for implementation, or remove call to this method and use another bundler'); }; AppForServer.prototype._viewsSource = function (options) { return "/*DERBY_SERIALIZED_VIEWS ".concat(this.name, "*/\n") + 'module.exports = ' + this.views.serialize(options) + ';\n' + "/*DERBY_SERIALIZED_VIEWS_END ".concat(this.name, "*/\n"); }; AppForServer.prototype.serialize = function () { if (!fs.existsSync(this.serializedDir)) { fs.mkdirSync(this.serializedDir); } // Don't minify the views (which doesn't include template source), since this // is for use on the server var viewsSource = this._viewsSource({ server: true, minify: true }); fs.writeFileSync(this.serializedBase + '.views.js', viewsSource, 'utf8'); var scriptUrl = (this.scriptUrl.indexOf(this.scriptBaseUrl) === 0) ? this.scriptUrl.slice(this.scriptBaseUrl.length) : this.scriptUrl; var scriptMapUrl = (this.scriptMapUrl.indexOf(this.scriptMapBaseUrl) === 0) ? this.scriptMapUrl.slice(this.scriptMapBaseUrl.length) : this.scriptMapUrl; var serialized = JSON.stringify({ scriptBaseUrl: this.scriptBaseUrl, scriptMapBaseUrl: this.scriptMapBaseUrl, scriptUrl: scriptUrl, scriptMapUrl: scriptMapUrl }); fs.writeFileSync(this.serializedBase + '.json', serialized, 'utf8'); }; AppForServer.prototype.deserialize = function () { var serializedViews = module.require(this.serializedBase + '.views.js'); var serialized = module.require(this.serializedBase + '.json'); serializedViews(derbyTemplates, this.views); this.scriptUrl = (this.scriptBaseUrl || serialized.scriptBaseUrl) + serialized.scriptUrl; this.scriptMapUrl = (this.scriptMapBaseUrl || serialized.scriptMapBaseUrl) + serialized.scriptMapUrl; }; AppForServer.prototype.loadViews = function (filename, namespace) { var data = files.loadViewsSync(this, filename, namespace); parsing.registerParsedViews(this, data.views); if (this.watchFiles) this._watchViews(data.files, filename, namespace); // Make chainable return this; }; AppForServer.prototype.loadStyles = function (filename, options) { this._loadStyles(filename, options); var stylesView = this.views.find('Styles'); stylesView.source += '<view is="' + filename + '"></view>'; // Make chainable return this; }; AppForServer.prototype._loadStyles = function (filename, options) { var styles = files.loadStylesSync(this, filename, options); var filepath = ''; if (this.watchFiles) { /** * Mark the path to file as an attribute * Used in development to add event watchers and autorefreshing of styles * SEE: local file, method this._watchStyles * SEE: file ./App.js, method App._autoRefresh() */ filepath = ' data-filename="' + filename + '"'; } var source = '<style' + filepath + '>' + styles.css + '</style>'; this.views.register(filename, source, { serverOnly: true }); if (this.watchFiles) { this._watchStyles(styles.files, filename, options); } return styles; }; AppForServer.prototype._watchViews = function (filenames, filename, namespace) { var _this = this; watchOnce(filenames, function () { _this.loadViews(filename, namespace); _this._updateScriptViews(); _this._refreshClients(); }); }; AppForServer.prototype._watchStyles = function (filenames, filename, options) { var _this = this; watchOnce(filenames, function () { var styles = _this._loadStyles(filename, options); _this._updateScriptViews(); _this._refreshStyles(filename, styles); }); }; AppForServer.prototype._watchBundle = function (filenames) { if (!process.send) return; watchOnce(filenames, function () { process.send({ type: 'reload' }); }); }; AppForServer.prototype._updateScriptViews = function () { if (!this.scriptFilename) return; var script = fs.readFileSync(this.scriptFilename, 'utf8'); var startIndex = script.indexOf('/*DERBY_SERIALIZED_VIEWS*/'); var before = script.slice(0, startIndex); var endIndex = script.indexOf('/*DERBY_SERIALIZED_VIEWS_END*/'); var after = script.slice(endIndex + 30); var viewsSource = this._viewsSource(); fs.writeFileSync(this.scriptFilename, before + viewsSource + after, 'utf8'); }; AppForServer.prototype._autoRefresh = function (backend) { var _this = this; // already been setup if agents is defined if (this.agents) return; this.agents = {}; // Auto-refresh is implemented on top of ShareDB's messaging layer. // // However, ShareDB wasn't originally designed to support custom message types, so ShareDB's // Agent class will log out "Invalid or unknown message" warnings if it encounters a message // it doesn't recognize. // // A workaround is to register a "receive" middleware, which fires when a ShareDB server // receives a message from a client. If the message is Derby-related, the middleware will // "exit" the middleware chain early by not calling `next()`. That way, the custom message never // gets to the ShareDB Agent and won't result in warnings. // // However, multiple Derby apps can run together on the same ShareDB backend, each adding a // "receive" middleware, and they all need to be notified of incoming Derby messages. This // solution combines the exit-early approach with a custom event to accomplish that. backend.use('receive', function (request, next) { var data = request.data; if (data.derby) { // Derby-related message, emit custom event and "exit" middleware chain early. backend.emit('derby:_messageReceived', request.agent, data.derby, data); return; } else { // Not a Derby-related message, pass to next middleware. next(); } }); backend.on('derby:_messageReceived', function (agent, action, message) { _this._handleMessageServer(agent, action, message); }); }; AppForServer.prototype._handleMessageServer = function (agent, action, message) { if (action === 'app') { if (message.name !== this.name) { return; } if (message.hash !== this.scriptHash) { return agent.send({ derby: 'reload' }); } this._addAgent(agent); } }; AppForServer.prototype._addAgent = function (agent) { var _this = this; this.agents[agent.clientId] = agent; agent.stream.once('end', function () { delete _this.agents[agent.clientId]; }); }; AppForServer.prototype._refreshClients = function () { if (!this.agents) return; var views = this.views.serialize({ minify: true }); var message = { derby: 'refreshViews', views: views }; for (var id in this.agents) { this.agents[id].send(message); } }; AppForServer.prototype._refreshStyles = function (filename, styles) { if (!this.agents) return; var message = { derby: 'refreshStyles', filename: filename, css: styles.css }; for (var id in this.agents) { this.agents[id].send(message); } }; AppForServer.prototype.middleware = function (backend) { return [backend.modelMiddware(), this.router()]; }; AppForServer.prototype.initAutoRefresh = function (backend) { this._autoRefresh(backend); }; return AppForServer; }(App_1.App)); exports.AppForServer = AppForServer;