diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a3ae9c0..c5879b4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## 1.4.0 (WIP) * Implement new settings system +* Replace old `sputnik` dependency with a new stages system ## 1.3.22 (2023-12-21) diff --git a/lib/app/helper_model/model.js b/lib/app/helper_model/model.js index cc22168a..a2d17fdb 100644 --- a/lib/app/helper_model/model.js +++ b/lib/app/helper_model/model.js @@ -308,7 +308,7 @@ Model.setStatic(function getClass(model_name, allow_create, parent) { * * @author Jelle De Loecker * @since 1.0.0 - * @version 1.1.2 + * @version 1.4.0 */ Model.setStatic(function setDocumentMethod(name, fnc, on_server) { @@ -321,7 +321,7 @@ Model.setStatic(function setDocumentMethod(name, fnc, on_server) { } if (Blast.isNode) { - alchemy.sputnik.after('plugins', whenLoaded); + STAGES.afterStages('load_app.plugins', whenLoaded); } else { Blast.loaded(whenLoaded); } @@ -336,7 +336,7 @@ Model.setStatic(function setDocumentMethod(name, fnc, on_server) { * * @author Jelle De Loecker * @since 1.0.0 - * @version 1.1.2 + * @version 1.4.0 */ Model.setStatic(function setDocumentProperty(key, getter, setter, on_server) { @@ -344,7 +344,7 @@ Model.setStatic(function setDocumentProperty(key, getter, setter, on_server) { args = arguments; if (Blast.isNode) { - alchemy.sputnik.after('plugins', whenLoaded); + STAGES.afterStages('load_app.plugins', whenLoaded); } else { Blast.loaded(whenLoaded); } diff --git a/lib/bootstrap.js b/lib/bootstrap.js index 0ff64452..4c0f0831 100644 --- a/lib/bootstrap.js +++ b/lib/bootstrap.js @@ -1,8 +1,5 @@ 'use strict'; -const libpath = require('path'); -let starting; - /** * Load Protoblast in the prototype-modifying mode. * This is the backbone of Alchemy. @@ -10,31 +7,9 @@ let starting; require('protoblast')(true); /** - * Resolve a core path - * - * @author Jelle De Loecker - * @since 1.4.0 - * @version 1.4.0 - */ -function resolveCorePath(...args) { - return libpath.resolve(PATH_CORE, ...args); -} - -/** - * Require a core path - * - * @author Jelle De Loecker - * @since 1.4.0 - * @version 1.4.0 - */ -function requireCorePath(...args) { - return require(resolveCorePath(...args)); -} - -/** - * Define DEFINE and constants + * Define global constants and require methods */ -require('./init/constants'); +require('./init_scripts/constants'); /** * Alchemy's Base class (from which all other classes inherit) @@ -47,224 +22,24 @@ requireCorePath('core', 'base'); requireCorePath('core', 'client_base'); /** - * Load the setting class - */ -requireCorePath('core', 'setting.js'); - -/** - * Load the actual settings - */ -requireCorePath('init', 'settings.js'); - -/** - * Define alchemy class and instance - */ -requireCorePath('core', 'alchemy'); - -/** - * Get all the languages by their locale - */ -requireCorePath('init', 'languages'); - -/** - * Require basic functions - */ -requireCorePath('init', 'functions'); - -/** - * Require load functions - */ -requireCorePath('init', 'load_functions'); - -/** - * Pre-load basic requirements - */ -requireCorePath('init', 'preload_modules'); - -/** - * Set up file change watchers for development - */ -requireCorePath('init', 'devwatch'); - -/** - * The migration class - */ -requireCorePath('class', 'migration'); - -const CLIENT_HAWKEJS_OPTIONS = { - - // Do not load on the server - server : false, - - // Turn it into a commonjs load - make_commonjs: true, - - // The arguments to add to the wrapper function - arguments : 'hawkejs' -}; - -const SERVER_HAWKEJS_OPTIONS = { - - // Also load on the server - server : true, - - // Turn it into a commonjs load - make_commonjs: true, - - // The arguments to add to the wrapper function - arguments : 'hawkejs' -}; - -/** - * Require the base class on the client side too - * - * @author Jelle De Loecker - * @since 0.3.0 - * @version 1.1.0 - */ -alchemy.hawkejs.load(resolveCorePath('core', 'base.js'), CLIENT_HAWKEJS_OPTIONS); - -/** - * Require the client_base class on the client side too - * - * @author Jelle De Loecker - * @since 1.0.0 - * @version 1.1.0 - */ -alchemy.hawkejs.load(resolveCorePath('core', 'client_base.js'), CLIENT_HAWKEJS_OPTIONS); - -/** - * Require the error class - * - * @author Jelle De Loecker - * @since 1.1.0 - * @version 1.1.0 - */ -alchemy.hawkejs.load(resolveCorePath('class', 'error.js'), SERVER_HAWKEJS_OPTIONS); - -/** - * Require the client_alchemy class on the client side - * - * @author Jelle De Loecker - * @since 1.0.5 - * @version 1.1.0 - */ -alchemy.hawkejs.load(resolveCorePath('core', 'client_alchemy.js'), SERVER_HAWKEJS_OPTIONS); - -/** - * Require the path_evaluator class - * - * @author Jelle De Loecker - * @since 1.1.0 - * @version 1.1.0 - */ -alchemy.hawkejs.load(resolveCorePath('class', 'path_evaluator.js'), SERVER_HAWKEJS_OPTIONS); - -/** - * Require the field_value class - * - * @author Jelle De Loecker - * @since 1.1.0 - * @version 1.1.0 - */ -alchemy.hawkejs.load(resolveCorePath('class', 'field_value.js'), SERVER_HAWKEJS_OPTIONS); - -/** - * Require the path_definition class on the client side too - * - * @author Jelle De Loecker - * @since 1.0.0 - * @version 1.1.0 + * Load the stages classes */ -alchemy.hawkejs.load(resolveCorePath('class', 'path_definition.js'), CLIENT_HAWKEJS_OPTIONS); -alchemy.hawkejs.load(resolveCorePath('class', 'path_param_definition.js'), CLIENT_HAWKEJS_OPTIONS); +requireCorePath('core', 'stages.js'); /** - * Require the element class on the client side too + * Define the Stages instance * * @author Jelle De Loecker - * @since 1.0.0 - * @version 1.1.0 - */ -alchemy.hawkejs.load(resolveCorePath('class', 'element.js'), CLIENT_HAWKEJS_OPTIONS); - -/** - * Require the helper class on the client side too - * - * @author Jelle De Loecker - * @since 1.0.0 - * @version 1.1.0 - */ -alchemy.hawkejs.load(resolveCorePath('class', 'helper.js'), CLIENT_HAWKEJS_OPTIONS); - -/** - * Require the datasource class on the client side too - * - * @author Jelle De Loecker - * @since 1.1.0 - * @version 1.1.0 - */ -alchemy.hawkejs.load(resolveCorePath('class', 'datasource.js'), CLIENT_HAWKEJS_OPTIONS); - -/** - * Require the field class on the client side too - * - * @author Jelle De Loecker - * @since 1.1.0 - * @version 1.1.0 - */ -alchemy.hawkejs.load(resolveCorePath('class', 'field.js'), CLIENT_HAWKEJS_OPTIONS); - -/** - * Require the schema_client class on the client side too + * @since 1.4.0 + * @version 1.4.0 * - * @author Jelle De Loecker - * @since 1.1.0 - * @version 1.1.0 - */ -alchemy.hawkejs.load(resolveCorePath('class', 'schema_client.js'), CLIENT_HAWKEJS_OPTIONS); - -/** - * Set up routing functions - */ -alchemy.useOnce(resolveCorePath('core', 'routing.js')); - -/** - * Set up middleware functions - */ -alchemy.useOnce(resolveCorePath('core', 'middleware.js')); - -/** - * Load socket.io code - */ -alchemy.useOnce(resolveCorePath('core', 'socket.js')); - -/** - * Load discovery code - */ -alchemy.useOnce(resolveCorePath('core', 'discovery.js')); - -/** - * Load inode classes + * @type {Alchemy.Stages.Stage} */ -alchemy.useOnce(resolveCorePath('class', 'inode.js')); -alchemy.useOnce(resolveCorePath('class', 'inode_file.js')); -alchemy.useOnce(resolveCorePath('class', 'inode_dir.js')); -alchemy.useOnce(resolveCorePath('class', 'inode_list.js')); +DEFINE('STAGES', new Classes.Alchemy.Stages.Stage('root')); /** - * Load in all classes + * Start the stages script */ -alchemy.usePath(libpath.resolve(__dirname, 'class'), {modular: false}); +requireCorePath('init_scripts', 'stages.js'); -// Load the base bootstrap file -try { - alchemy.useOnce(libpath.resolve(PATH_ROOT, 'app', 'config', 'bootstrap.js')); -} catch (err) { - if (err.message.indexOf('Cannot find') === -1) { - alchemy.printLog(alchemy.WARNING, 'Could not load app bootstrap file'); - throw err; - } else { - alchemy.printLog(alchemy.SEVERE, 'Could not load config bootstrap file', {err: err}); - } -} +module.exports = STAGES; \ No newline at end of file diff --git a/lib/class/schema.js b/lib/class/schema.js index bcc4859b..51d521a0 100644 --- a/lib/class/schema.js +++ b/lib/class/schema.js @@ -202,7 +202,7 @@ Schema.setMethod(function eachAlternateIndex(data, fnc) { * * @author Jelle De Loecker * @since 1.1.0 - * @version 1.2.0 + * @version 1.4.0 * * @return {Pledge} */ @@ -214,7 +214,7 @@ Schema.setMethod(function getDatasource() { that.afterOnce('has_model_class', next); }, function waitForDatasources(next) { - alchemy.sputnik.after('datasources', function afterDs() { + STAGES.afterStages('datasource', function afterDs() { let datasource; diff --git a/lib/core/alchemy.js b/lib/core/alchemy.js index b490cc4f..07a5ee05 100644 --- a/lib/core/alchemy.js +++ b/lib/core/alchemy.js @@ -144,6 +144,12 @@ global.Alchemy = Function.inherits('Alchemy.Base', function Alchemy() { console.warn('Failed to start Janeway:', err); } } + + this.any_body = this.use('body/any'); + this.text_body = this.use('body'); + this.formidable = this.use('formidable'); + this.body_parser = this.use('body-parser'); + this.url_form_body = this.body_parser.urlencoded({extended: true}); }); /** @@ -234,6 +240,19 @@ Alchemy.setProperty(function environment() { return this.settings.environment; }); +/** + * Add a getter for the total amount of http requests + * + * @author Jelle De Loecker + * @since 1.3.1 + * @version 1.3.1 + * + * @type {number} + */ +Alchemy.setProperty(function http_request_counter() { + return 0; +}); + /** * Get the current lag in ms * @@ -914,7 +933,7 @@ Alchemy.setMethod(function setStatus(name, value) { * * @author Jelle De Loecker * @since 0.0.1 - * @version 1.1.2 + * @version 1.4.0 * * @param {Function} callback The function to execute * @@ -922,20 +941,17 @@ Alchemy.setMethod(function setStatus(name, value) { */ Alchemy.setMethod(function ready(callback) { - var that = this, - pledge = new Pledge(); + let pledge = new Pledge(); pledge.done(callback); - if (!this.sputnik) { - Blast.loaded(function hasLoaded() { - pledge.resolve(that.ready()); - }); - } else { - this.sputnik.after(['start_server', 'datasources', 'listening'], function afterReady() { - pledge.resolve(); - }); - } + STAGES.afterStages([ + 'server.start', + 'datasource', + 'server.listening', + ], function hasLoaded() { + pledge.resolve(); + }); return pledge; }); @@ -1776,12 +1792,14 @@ Alchemy.setMethod(function parseRequestBody(req, res, callback) { return callback(null, req.body); } + const that = this; + let content_type = req.headers['content-type']; // Multipart data is handled by "formidable" if (content_type && content_type.startsWith('multipart/form-data')) { - let form = new formidable.IncomingForm({ + let form = new this.formidable.IncomingForm({ multiples : true, hashAlgorithm : this.settings.data_management.file_hash_algorithm || 'sha1', }); @@ -1835,7 +1853,7 @@ Alchemy.setMethod(function parseRequestBody(req, res, callback) { // Regular form-encoded data if (content_type && content_type.indexOf('form-urlencoded') > -1) { - urlFormBody(req, res, function parsedBody(err) { + this.url_form_body(req, res, function parsedBody(err) { if (err && req.conduit && req.conduit.aborted) { return callback(null); @@ -1862,7 +1880,7 @@ Alchemy.setMethod(function parseRequestBody(req, res, callback) { } // Any other encoded data (like JSON) - anyBody(req, function parsedBody(err, body) { + this.any_body(req, function parsedBody(err, body) { function handleResponse(err, body) { if (err && req.conduit && req.conduit.aborted) { @@ -1888,7 +1906,7 @@ Alchemy.setMethod(function parseRequestBody(req, res, callback) { if (err?.type == 'invalid.content.type') { if (!content_type || content_type.startsWith('text/')) { - textBody(req, handleResponse); + that.text_body(req, handleResponse); return; } } @@ -2349,7 +2367,12 @@ Alchemy.setMethod(function start(options, callback) { starting = true; // Start the stages - alchemy.useOnce(libpath.resolve(PATH_CORE, 'stages.js')); + STAGES.launch([ + 'load_app', + 'datasource', + 'routes', + 'server', + ]); // Make sure Blast has executed everything that's still waiting Blast.doLoaded(); @@ -2407,63 +2430,4 @@ function initializeErrorHandler() { this.printLog('error', [message, String(error), error], {err: error, level: -2}); }, 1); -} - -/** - * The alchemy global, where everything will be stored - * - * @author Jelle De Loecker - * @since 0.0.1 - * @version 0.4.0 - * - * @type {Alchemy} - */ -DEFINE('alchemy', new Alchemy()); - -/** - * Define the log function - * - * @author Jelle De Loecker - * @since 0.0.1 - * @version 0.4.0 - * - * @type {Function} - */ -DEFINE('log', alchemy.log); - -for (let key in alchemy.Janeway.LEVELS) { - let name = key.toLowerCase(); - let val = alchemy.Janeway.LEVELS[key]; - - log[name] = function(...args) { - return alchemy.printLog(val, args, {level: 2}); - }; -} - -log.warn = log.warning; - -/** - * Define the todo log function - * - * @author Jelle De Loecker - * @since 0.2.0 - * @version 0.4.0 - * - * @type {Function} - */ -log.todo = function todo(...args) { - - var options = { - gutter: alchemy.Janeway.esc(91) + '\u2620 Todo:' + alchemy.Janeway.esc(39), - level: 2 - }; - - return alchemy.printLog(alchemy.TODO, args, options); -}; - -const anyBody = alchemy.use('body/any'), - formBody = alchemy.use('body/form'), - textBody = alchemy.use('body'), - formidable = alchemy.use('formidable'), - bodyParser = alchemy.use('body-parser'), - urlFormBody = bodyParser.urlencoded({extended: true}); \ No newline at end of file +} \ No newline at end of file diff --git a/lib/init/functions.js b/lib/core/alchemy_functions.js similarity index 98% rename from lib/init/functions.js rename to lib/core/alchemy_functions.js index 3a692139..d5f3f369 100644 --- a/lib/init/functions.js +++ b/lib/core/alchemy_functions.js @@ -1631,4 +1631,22 @@ Alchemy.setMethod(function readDir(path, options) { }); return pledge; +}); + +/** + * Connect to another alchemy instance + * + * @author Jelle De Loecker + * @since 0.1.0 + * @version 0.4.0 + */ +Alchemy.setMethod(function callServer(address, data, callback) { + + var server = new Classes.ClientSocket(); + + server.reconnect = false; + + server.connect(address, data, callback); + + return server; }); \ No newline at end of file diff --git a/lib/init/load_functions.js b/lib/core/alchemy_load_functions.js similarity index 93% rename from lib/init/load_functions.js rename to lib/core/alchemy_load_functions.js index 06c585a6..e9d2b255 100644 --- a/lib/init/load_functions.js +++ b/lib/core/alchemy_load_functions.js @@ -2,7 +2,7 @@ // Get some modules from the cache var fs = alchemy.use('fs'), - path = alchemy.use('path'), + libpath = alchemy.use('path'), LOADED = Symbol('LOADED'), module = require('module'), original_wrap = module.wrap, @@ -77,43 +77,43 @@ Alchemy.setMethod(function usePath(dirPath, options) { // Let Hawkejs now about this view directory if (options.views !== false) { - alchemy.addViewDirectory(path.resolve(dirPath, 'view'), options.weight); + alchemy.addViewDirectory(libpath.resolve(dirPath, 'view'), options.weight); } // Main asset directory - alchemy.addAssetDirectory(path.resolve(dirPath, 'assets'), options.weight); + alchemy.addAssetDirectory(libpath.resolve(dirPath, 'assets'), options.weight); // Asset scripts if (options.scripts !== false) { - alchemy.addScriptDirectory(path.resolve(dirPath, 'assets', 'scripts'), options.weight); + alchemy.addScriptDirectory(libpath.resolve(dirPath, 'assets', 'scripts'), options.weight); } // Asset stylesheets if (options.less !== false) { - alchemy.addStylesheetDirectory(path.resolve(dirPath, 'assets', 'stylesheets'), options.weight); + alchemy.addStylesheetDirectory(libpath.resolve(dirPath, 'assets', 'stylesheets'), options.weight); // Also add the public folder, so less files in there can also be compiled - alchemy.addStylesheetDirectory(path.resolve(dirPath, 'public'), options.weight); + alchemy.addStylesheetDirectory(libpath.resolve(dirPath, 'public'), options.weight); } // Fonts if (options.fonts !== false) { - alchemy.addFontDirectory(path.resolve(dirPath, 'assets', 'fonts'), options.weight); + alchemy.addFontDirectory(libpath.resolve(dirPath, 'assets', 'fonts'), options.weight); } // Images if (options.images !== false) { - alchemy.addImageDirectory(path.resolve(dirPath, 'assets', 'images'), options.weight); + alchemy.addImageDirectory(libpath.resolve(dirPath, 'assets', 'images'), options.weight); } // public folders (going to /public/) if (options.public !== false) { - alchemy.addPublicDirectory(path.resolve(dirPath, 'public'), options.weight); + alchemy.addPublicDirectory(libpath.resolve(dirPath, 'public'), options.weight); } // Root folders (/) if (options.root !== false) { - alchemy.addRootDirectory(path.resolve(dirPath, 'root'), options.weight); + alchemy.addRootDirectory(libpath.resolve(dirPath, 'root'), options.weight); } modularRegex.push(/^view$|^helper$|^helper_datasource$|^helper_error$|^helper_field$|^helper_document$|^helper_model$|^helper_controller$|^helper_component$|^helper_validator$|^element$|^plugins$|^assets$/); @@ -249,7 +249,7 @@ Alchemy.setMethod(function _usePath(dir_path, options) { } // Resolve the entire path - file_path = path.resolve(dir_path, file_name); + file_path = libpath.resolve(dir_path, file_name); // Get information on this entry file_stat = fs.lstatSync(file_path); @@ -267,6 +267,13 @@ Alchemy.setMethod(function _usePath(dir_path, options) { continue; } + // Get the extension of the file + let extension = libpath.extname(file_path); + + if (!extension || (extension != '.js' && extension != '.mjs' && extension != '.cjs')) { + continue; + } + alchemy.useOnce(file_path); } @@ -468,7 +475,7 @@ Alchemy.setMethod(function addViewDirectory(dirPath, weight) { prive.loadBootstrap = function loadBootstrap(dirPath) { // If a bootstrap.js file exists inside the directory, load it first - var bootstrapPath = path.resolve(dirPath, 'bootstrap.js'); + var bootstrapPath = libpath.resolve(dirPath, 'bootstrap.js'); if (typeof _duplicateCheck[bootstrapPath] == 'undefined') { @@ -518,7 +525,7 @@ prive.loadRegexFile = function loadRegexFile (dirPath, pattern, multiple) { // Continue to the next file if the patternd ooesn't match if (!pattern.exec(fileName)) continue; - filePath = path.resolve(dirPath, fileName); + filePath = libpath.resolve(dirPath, fileName); fileStat = fs.lstatSync(filePath); // Skip directories @@ -579,7 +586,7 @@ prive.loadHelpers = function loadHelpers(...path_pieces) { } try { - filePath = path.resolve(dir_path, name); + filePath = libpath.resolve(dir_path, name); if (fs.lstatSync(filePath).isDirectory()) { dirs.push(filePath); @@ -819,7 +826,7 @@ Alchemy.setMethod(function startPlugins(names) { * * @author Jelle De Loecker * @since 0.0.1 - * @version 1.2.2 + * @version 1.4.0 * * @param {string|Array} names * @param {boolean} attempt_require @@ -850,7 +857,7 @@ Alchemy.setMethod(function requirePlugin(names, attempt_require) { temp = alchemy.usePlugin(name); if (temp) { - let plugin_stage = alchemy.sputnik.get('plugins'); + let plugin_stage = STAGES.getStage('load_app.plugins'); if (!plugin_stage || plugin_stage.started) { // If the plugin stage has already started, @@ -890,11 +897,6 @@ Alchemy.setMethod(function useOnce(dirPath, options) { dirPath = alchemy.pathResolve.apply(null, arguments); - if (dirPath.indexOf('.js') === -1) { - //log.verbose('Skipped non JS file: ' + dirPath.split('/').pop()); - return false; - } - if (typeof _duplicateCheck[dirPath] === 'undefined') { // Mainly used for tidying up the unit tests @@ -925,9 +927,9 @@ Alchemy.setMethod(function useOnce(dirPath, options) { let yellow = __Janeway.esc('103;91'); alchemy.printLog('error', [yellow + '========================='], {err: err, level: -2}); alchemy.printLog('error', [yellow + ' Failed to load file: '], {err: err, level: -2}); - alchemy.printLog('error', [yellow + ' »', dirPath.split(path.sep).last()], {err: err, level: -2}); + alchemy.printLog('error', [yellow + ' »', dirPath.split(libpath.sep).last()], {err: err, level: -2}); alchemy.printLog('error', [yellow + ' In directory: '], {err: err, level: -2}); - alchemy.printLog('error', [yellow + ' »', dirPath.split(path.sep).slice(0, -1).join(path.sep)], {err: err, level: -2}); + alchemy.printLog('error', [yellow + ' »', dirPath.split(libpath.sep).slice(0, -1).join(libpath.sep)], {err: err, level: -2}); alchemy.printLog('error', [yellow + ' With error: '], {err: err, level: -2}); alchemy.printLog('error', [yellow + ' »', err], {err: err, level: -2}); alchemy.printLog('error', [yellow + '========================='], {err: err, level: -2}); diff --git a/lib/core/client_base.js b/lib/core/client_base.js index ea91976d..2ec36dfb 100644 --- a/lib/core/client_base.js +++ b/lib/core/client_base.js @@ -45,7 +45,7 @@ ClientBase.setStatic(function getServerClass() { * * @author Jelle De Loecker * @since 1.1.2 - * @version 1.1.2 + * @version 1.4.0 * * @param {Function} */ @@ -63,7 +63,7 @@ ClientBase.setStatic(function callbackWithServerClass(callback) { const that = this; - alchemy.sputnik.after('plugins', function loadedPlugins() { + STAGES.afterStages('load_app.plugins', function loadedPlugins() { server_class = that.getServerClass(); diff --git a/lib/core/socket.js b/lib/core/socket.js deleted file mode 100644 index 45ebbb00..00000000 --- a/lib/core/socket.js +++ /dev/null @@ -1,173 +0,0 @@ -'use strict'; - -let msgpack_parser, - iostream, - types = alchemy.shared('Socket.types'), - path = alchemy.use('path'), - fs = alchemy.use('fs'); - -/** - * The "socket" stage: - * - * Create the socket.io listener - * - * @author Jelle De Loecker - * @since 0.1.0 - * @version 1.3.5 - */ -alchemy.sputnik.add(function socket() { - - const websockets = alchemy.settings.network.use_websockets; - - if (!websockets || websockets === 'never') { - log.info('Websockets have been disabled'); - return; - } else { - if (websockets == 'optional') { - log.info('Websockets have been enabled optionally'); - } else { - log.info('Websockets have been enabled, clients will automatically connect'); - } - } - - const socket_io = alchemy.use('socket.io'); - - if (!socket_io) { - return log.error('Could not load socket.io!'); - } - - iostream = alchemy.use('socket.io-stream'); - msgpack_parser = alchemy.use('socket.io-msgpack-parser'); - - let socket_io_options = { - serveClient : false, - }; - - if (msgpack_parser) { - socket_io_options.parser = msgpack_parser; - } - - // Create the Socket.io listener - alchemy.io = socket_io(alchemy.server, socket_io_options); - - // Get the core client path - let client_path = alchemy.findModule('socket.io-client').module_dir; - - if (msgpack_parser) { - client_path = path.join(client_path, 'dist', 'socket.io.msgpack.min.js'); - } else { - client_path = path.join(client_path, 'dist', 'socket.io.min.js'); - } - - // Get the stream client path - let stream_path = path.dirname(alchemy.findModule('@11ways/socket.io-stream').module_path); - stream_path = path.resolve(stream_path, 'socket.io-stream.js'); - - // Serve the socket io core file - Router.use('/scripts/socket.io.js', function getSocketIo(req, res, next) { - alchemy.minifyScript(client_path, function gotMinifiedPath(err, mpath) { - req.conduit.serveFile(mpath || client_path); - }); - }); - - // Serve the socket io stream file - Router.use('/scripts/socket.io-stream.js', function getSocketStream(req, res, next) { - alchemy.minifyScript(stream_path, function gotMinifiedPath(err, mpath) { - req.conduit.serveFile(mpath || stream_path); - }); - }); - - /** - * Handle connections - * - * @author Jelle De Loecker - * @since 0.0.1 - * @version 0.2.0 - */ - alchemy.io.sockets.on('connection', function onConnect(socket){ - - var syncs = {}, - latencies = [], - latency_avg = 2, - offset = 0; - - socket.on('timesync', function gotTimesyncRequest(data) { - - var received = Date.now(), - latency; - - // This is the initial request - if (data.count == null) { - data.count = 0; - data.latency_trip = 0; - - // Reset the latencies array - latencies.length = 0; - } - - // Do 8 round trips to determine latency - if (data.latency_trip <= 8) { - - if (data.last_sent) { - - // Latency is the timestamp when we received the response - // minus the timestamp when we sent the request - latency = received - data.last_sent; - latencies.push(latency); - } - - // Wait before responding - setTimeout(function waitForLatency() { - data.last_sent = Date.now(); - socket.emit('timesync', data); - }, 100 + (data.latency_trip * 150)); - - data.latency_trip++; - } else if (data.latency_trip) { - - latency_avg = ~~(Math.median(latencies) / 2); - offset = (received - latency_avg) - data.client_time; - - socket.emit('timesync', {offset: offset, latency: latency_avg}); - } - }); - - // Wait for the announcement - socket.once('announce', function gotAnnouncement(data) { - - var SocketClass, - class_name; - - // Try getting the socket class of this type - if (typeof data.type == 'string') { - class_name = data.type.classify(); - SocketClass = Classes.Alchemy.Conduit[class_name + 'Socket']; - } - - // If no socket class was found get the regular class - if (!SocketClass) { - SocketClass = Classes.Alchemy.Conduit.Socket; - } - - new SocketClass(socket, data); - }); - }); -}); - -/** - * Connect to another alchemy instance - * - * @author Jelle De Loecker - * @since 0.1.0 - * @version 0.4.0 - */ -Alchemy.setMethod(function callServer(address, data, callback) { - - var server = new Classes.ClientSocket(); - - server.reconnect = false; - - server.connect(address, data, callback); - - return server; -}); \ No newline at end of file diff --git a/lib/core/stages.js b/lib/core/stages.js new file mode 100644 index 00000000..25ee7b9d --- /dev/null +++ b/lib/core/stages.js @@ -0,0 +1,598 @@ +const STATUS = Symbol('status'), + STATUS_PLEDGE = Symbol('status_pledge'), + MAIN_PLEDGE = Symbol('main_pledge'), + PRE_STATUS = 'pre', + MAIN_STATUS = 'main', + CHILD_STATUS = 'children', + POST_STATUS = 'post'; + +/** + * The Stage class + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @param {string} name + * @param {Alchemy.Stages.Stages} parent + */ +const Stage = Function.inherits('Alchemy.Base', 'Alchemy.Stages', function Stage(name, parent) { + + // The name of this stage + this.name = name; + + // The path + this.id = parent ? parent.id + '.' + name : name; + + // The parent Stage (if any) + this.parent = parent; + + // The root stage + this.root_stage = parent ? parent.root_stage : this; + + // The current status + this[STATUS] = null; + + // The main pledge + this[MAIN_PLEDGE] = new Pledge.Swift(); + + // Pre-tasks + this.pre_tasks = new Map(); + + // Main tasks + this.main_tasks = new Map(); + + // Child stages + this.child_stages = new Map(); + + // Post-tasks + this.post_tasks = new Map(); + + // When this started + this.started = null; + + // When this ended + this.ended = null; +}); + +/** + * Get the main pledge + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + */ +Stage.setProperty(function pledge() { + return this[MAIN_PLEDGE]; +}); + +/** + * Add a new child stage + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @param {string} name Name of the stage + * @param {Function} fnc The function to execute as a main task + * + * @return {Alchemy.Stages.Stage} + */ +Stage.setMethod(function createStage(name, fnc) { + + if (this.child_stages.has(name)) { + throw new Error('Stage "' + name + '" already exists'); + } + + let stage = new Stage(name, this); + + this.child_stages.set(name, stage); + + if (fnc) { + stage.addMainTask(fnc); + } + + return stage; +}); + +/** + * Add a certain task + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @param {string} type + * @param {Function} fnc + */ +Stage.setMethod(function _addTask(type, fnc) { + + let task_map = this[type]; + + // First see if this type has already been started + let pledge = task_map[STATUS_PLEDGE]; + + // If it has, we can already start the task, + // but we have to set a new pledge. + if (pledge) { + let new_pledge = new Pledge.Swift(); + task_map[STATUS_PLEDGE] = new_pledge; + + let task_pledge; + + try { + task_pledge = fnc(); + } catch (err) { + new_pledge.reject(err); + } + + pledge.then(async () => { + try { + await fnc(); + new_pledge.resolve(); + } catch (err) { + new_pledge.reject(err); + } + }); + } else { + task_map.set(fnc, null); + } +}); + +/** + * Do the given type of tasks + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @param {string} type + */ +Stage.setMethod(function _doTasks(type) { + + let task_map = this[type]; + + let pledge = new Pledge.Swift(); + task_map[STATUS_PLEDGE] = pledge; + + let tasks = []; + let errors = []; + + for (let [fnc, value] of task_map) { + + // It already has a value: it has already been executed + if (value) { + if (Pledge.isThenable(value)) { + tasks.push(value); + } + + continue; + } + + // If needs to be executed + try { + value = fnc(); + + if (!value) { + value = true; + } else if (Pledge.isThenable(value)) { + tasks.push(value); + + value.done((err, result) => { + task_map.set(fnc, result || err || true); + }); + } + + task_map.set(fnc, value); + + } catch (err) { + errors.push(err); + task_map.set(fnc, err); + } + } + + if (!tasks.length && !errors.length) { + pledge.resolve(true); + return pledge; + } + + if (errors.length) { + pledge.reject(errors[0]); + return pledge; + } + + Function.parallel(tasks).done((err) => { + if (err) { + pledge.reject(err); + } else { + pledge.resolve(this._doTasks(type)); + } + }); + + return pledge; +}); + +/** + * Add a pre-task to this stage: + * This task will be performed before the main tasks of this stage. + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @param {Function} fnc + */ +Stage.setMethod(function addPreTask(fnc) { + this._addTask('pre_tasks', fnc); +}); + +/** + * Add a main task to this stage: + * This task will be performed after the pre-tasks of this stage. + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @param {Function} fnc + */ +Stage.setMethod(function addMainTask(fnc) { + this._addTask('main_tasks', fnc); +}); + +/** + * Add a post task to this stage: + * This task will be performed after the main-tasks + * and the child stages of this stage. + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @param {Function} fnc + */ +Stage.setMethod(function addPostTask(fnc) { + this._addTask('post_tasks', fnc); +}); + +/** + * Have all the child stages finished? + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + */ +Stage.setMethod(function hasFinishedAllChildStages() { + + if (this.child_stages.size == 0) { + return true; + } + + for (let [name, stage] of this.child_stages) { + if (!stage.ended) { + return false; + } + } + + return true; +}); + +/** + * Get a child stage by its path/id + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @param {string} id + */ +Stage.setMethod(function getStage(id) { + + if (!id) { + throw new Error('Unable to get stage without id'); + } + + let parts = id.split('.'); + + if (parts[0] == this.name) { + parts.shift(); + } + + let current = this; + + while (parts.length) { + let part = parts.shift(); + + current = current.child_stages.get(part); + + if (!current) { + return; + } + } + + return current; +}); + +/** + * Wait for the given child stages (without starting them) + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @param {string[]} stages + * @param {Function} callback + */ +Stage.setMethod(function afterStages(stages, callback) { + + stages = Array.cast(stages); + + let tasks = []; + + for (let id of stages) { + let stage = this.getStage(id); + + if (!stage) { + throw new Error('Child stage "' + id + '" not found'); + } + + if (stage.ended) { + continue; + } + + tasks.push(async (next) => { + await stage.pledge; + next(); + }); + } + + return Function.series(tasks, callback); +}); + +/** + * Launch this stage and all the given child stages. + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @param {string[]} child_stages The child stages to launch + */ +Stage.setMethod(function launch(child_stages) { + + if (child_stages == null) { + throw new Error('Unable to launch a stage without allowed child stages'); + } + + if (!this.started) { + this.started = Date.now(); + + this.emit('launching', this); + + if (this !== this.root_stage) { + this.root_stage.emit('launching', this); + } + } + + return this._launch(child_stages); +}); + +/** + * Actually launch this stage and all the given child stages + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @param {string[]} child_stages The child stages to launch + */ +Stage.setMethod(async function _launch(child_stages) { + + if (!this[STATUS]) { + this[STATUS] = PRE_STATUS; + } + + await this._doTasks('pre_tasks'); + + if (this[STATUS] == PRE_STATUS) { + this[STATUS] = MAIN_STATUS; + } + + await this._doTasks('main_tasks'); + + if (child_stages === true) { + child_stages = []; + + for (let [name, stage] of this.child_stages) { + child_stages.push(name); + } + } else if (typeof child_stages == 'string') { + child_stages = [child_stages]; + } + + if (child_stages.length) { + await this.pre_tasks[STATUS_PLEDGE]; + await this.main_tasks[STATUS_PLEDGE]; + + if (this[STATUS] != POST_STATUS) { + this[STATUS] = CHILD_STATUS; + } + + let stage_tasks = []; + + for (let name of child_stages) { + let stage = this.child_stages.get(name); + + if (!stage) { + throw new Error('Child stage "' + name + '" not found'); + } + + stage_tasks.push(async (next) => { + await stage.launch(true); + next(); + }); + } + + await Function.series(stage_tasks); + } + + this[STATUS] = POST_STATUS; + + if (this.hasFinishedAllChildStages()) { + await this._doTasks('post_tasks'); + this.refreshStatus(); + } +}); + + +/** + * Check if everything is finished + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + */ +Stage.setMethod(function refreshStatus() { + + if (!this.hasFinishedAllChildStages()) { + return; + } + + if (!this.ended) { + this.ended = Date.now(); + } + + this.pledge.resolve(); + + if (this.parent) { + this.parent.refreshStatus(); + } +}); + +/** + * Create a sputnik shim + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @param {Object} mapping + * + * @return {Alchemy.Stages.SputnikShim} + */ +Stage.setMethod(function createSputnikShim(mapping) { + return new SputnikShim(this, mapping); +}); + +/** + * The SputnikShim class + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @param {Alchemy.Stages.Stages} stage + * @param {Object} mapping + */ +const SputnikShim = Function.inherits('Alchemy.Base', 'Alchemy.Stages', function SputnikShim(stage, mapping) { + this.stage = stage; + this.mapping = mapping; +}); + +/** + * Get a stage by its old name + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @param {string} name + * + * @return {Alchemy.Stages.Stage} + */ +SputnikShim.setMethod(function getStage(name) { + + if (!name) { + throw new Error('Unable to get stage without name'); + } + + let id = this.mapping[name]; + + if (!id) { + id = name; + } + + return this.stage.getStage(id); +}); + +/** + * Do something before the given stage + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @param {string[]} names + * @param {Function} callback + */ +SputnikShim.setMethod(function before(names, callback) { + + let pledges = [], + stage; + + names = Array.cast(names); + + for (let name of names) { + stage = this.getStage(name); + + if (!stage) { + throw new Error('Stage "' + name + '" not found'); + } + + let pledge = new Pledge.Swift(); + + stage.addPreTask(() => { + pledge.resolve(); + }); + + pledges.push(pledge); + } + + return Function.parallel(pledges).then(callback); +}); + +/** + * Do something after the given stage + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @param {string[]} names + * @param {Function} callback + */ +SputnikShim.setMethod(function after(names, callback) { + + let pledges = [], + stage; + + names = Array.cast(names); + + for (let name of names) { + stage = this.getStage(name); + + if (!stage) { + throw new Error('Stage "' + name + '" not found'); + } + + let pledge = new Pledge.Swift(); + + stage.addPostTask(() => { + pledge.resolve(); + }); + + pledges.push(pledge); + } + + return Function.parallel(pledges).then(callback); +}); \ No newline at end of file diff --git a/lib/init_scripts/alchemy.js b/lib/init_scripts/alchemy.js new file mode 100644 index 00000000..58bc68a8 --- /dev/null +++ b/lib/init_scripts/alchemy.js @@ -0,0 +1,51 @@ +/** + * The alchemy global, where everything will be stored + * + * @author Jelle De Loecker + * @since 0.0.1 + * @version 0.4.0 + * + * @type {Alchemy} + */ +DEFINE('alchemy', new Alchemy()); + +/** + * Define the log function + * + * @author Jelle De Loecker + * @since 0.0.1 + * @version 0.4.0 + * + * @type {Function} + */ +DEFINE('log', alchemy.log); + +for (let key in alchemy.Janeway.LEVELS) { + let name = key.toLowerCase(); + let val = alchemy.Janeway.LEVELS[key]; + + log[name] = function(...args) { + return alchemy.printLog(val, args, {level: 2}); + }; +} + +log.warn = log.warning; + +/** + * Define the todo log function + * + * @author Jelle De Loecker + * @since 0.2.0 + * @version 0.4.0 + * + * @type {Function} + */ +log.todo = function todo(...args) { + + var options = { + gutter: alchemy.Janeway.esc(91) + '\u2620 Todo:' + alchemy.Janeway.esc(39), + level: 2 + }; + + return alchemy.printLog(alchemy.TODO, args, options); +}; \ No newline at end of file diff --git a/lib/init/constants.js b/lib/init_scripts/constants.js similarity index 76% rename from lib/init/constants.js rename to lib/init_scripts/constants.js index ceda4db7..856b89df 100644 --- a/lib/init/constants.js +++ b/lib/init_scripts/constants.js @@ -1,17 +1,24 @@ 'use strict'; -const libpath = require('path'); +const libpath = require('path'), + libfs = require('fs'); /** * Function to define global constants * * @author Jelle De Loecker * @since 0.4.0 - * @version 0.4.0 + * @version 1.4.0 * * @type {Function} */ function DEFINE(name, value) { + + if (typeof name == 'function') { + value = name; + name = value.name; + } + Object.defineProperty(global, name, {value: value}); }; @@ -126,6 +133,48 @@ DEFINE('PATH_TEMP', libpath.resolve(PATH_ROOT, 'temp')); */ DEFINE('PATH_CORE', libpath.resolve(__dirname, '..')); +/** + * Resolve a core path + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + */ +DEFINE(function resolveCorePath(...args) { + return libpath.resolve(PATH_CORE, ...args); +}); + +/** + * Require a core path + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + */ +DEFINE(function requireCorePath(...args) { + return require(resolveCorePath(...args)); +}); + +/** + * Require all files in a directory + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + */ +DEFINE(function requireCorePathAll(...args) { + + let path = resolveCorePath(...args), + result = {}; + + for (let filename of libfs.readdirSync(path)) { + let name = filename.beforeLast('.'); + result[name] = require(libpath.resolve(path, filename)); + } + + return result; +}); + /** * Debug value * Actually not a constant, will be changed later diff --git a/lib/init/devwatch.js b/lib/init_scripts/devwatch.js similarity index 100% rename from lib/init/devwatch.js rename to lib/init_scripts/devwatch.js diff --git a/lib/init/languages.js b/lib/init_scripts/languages.js similarity index 100% rename from lib/init/languages.js rename to lib/init_scripts/languages.js diff --git a/lib/init/preload_modules.js b/lib/init_scripts/preload_modules.js similarity index 79% rename from lib/init/preload_modules.js rename to lib/init_scripts/preload_modules.js index d239c6de..5ffeb5e4 100644 --- a/lib/init/preload_modules.js +++ b/lib/init_scripts/preload_modules.js @@ -58,9 +58,22 @@ alchemy.hawkejs = Classes.Hawkejs.Hawkejs.getInstance(); alchemy.toobusy = alchemy.use('toobusy-js', 'toobusy'); /** - * Load Sputnik, the stage-based launcher + * Compatibility for old Sputnik stage system */ -alchemy.sputnik = new (alchemy.use('sputnik', 'sputnik'))(); +alchemy.sputnik = STAGES.createSputnikShim({ + http : 'server.create_http', + core_app : 'load_app.core_app', + plugins : 'load_app.plugins', + base_app : 'load_app.main_app', + middleware : 'routes.middleware', + datasources : 'datasource', + define_debug : 'server.warn_debug', + socket : 'server.websocket', + hawkejs_setup : 'routes.hawkejs', + routes : 'routes', + start_server : 'server.start', + listening : 'server.listening', +}); /** * Real-time apps made cross-browser & easy with a WebSocket-like API. diff --git a/lib/init/settings.js b/lib/init_scripts/settings.js similarity index 100% rename from lib/init/settings.js rename to lib/init_scripts/settings.js diff --git a/lib/init_scripts/stages.js b/lib/init_scripts/stages.js new file mode 100644 index 00000000..9c6e0a56 --- /dev/null +++ b/lib/init_scripts/stages.js @@ -0,0 +1,55 @@ +/** + * Load all the stages + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + */ +requireCorePathAll('stages'); + +/** + * Add the launching event listener + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @param {Alchemy.Stages.Stage} stage + */ +STAGES.on('launching', function onLaunch(stage) { + + if (typeof alchemy == 'undefined') { + return; + } + + if (!alchemy.getSetting('debugging.debug')) { + return; + } + + let id = stage.id.after('root.'); + + let colored_name = alchemy.colors.fg.getRgb(0, 5, 5) + id + alchemy.colors.reset; + + let args = ['Launching', colored_name, 'stage…']; + + let line = alchemy.printLog(alchemy.INFO, args, {level: 1}); + + if (line && line.args) { + stage.pledge.then(function finished() { + line.args.push('Done in', stage.ended - stage.started, 'ms'); + line.dissect(); + line.render(); + }); + } +}); + +/** + * Start all the stages + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + */ +STAGES.launch([ + 'load_core', +]); \ No newline at end of file diff --git a/lib/stages.js b/lib/stages.js deleted file mode 100644 index f65d3ce8..00000000 --- a/lib/stages.js +++ /dev/null @@ -1,521 +0,0 @@ -/** - * This file is loaded after the main 'init' & 'core' folder files. - * Its main purpose is to launch the server in several stages and - * allow the app-specific logic to hook into them. - * - * Alchemy: Node.js MVC Framework - * Copyright 2013-2018 - * - * Licensed under The MIT License - * Redistributions of files must retain the above copyright notice. - * - * @copyright Copyright 2013-2018 - * @since 0.0.1 - * @version 1.1.0 - */ -let path = alchemy.modules.path, - http = alchemy.modules.http, - hawkejs = alchemy.hawkejs, - fs = alchemy.use('fs'), - total_http_requests = 0; - -if (alchemy.getSetting('debugging.debug')) { - alchemy.sputnik.on('launching', function onLaunch(stage) { - - let colored_name = alchemy.colors.fg.getRgb(0, 5, 5) + stage.name + alchemy.colors.reset; - - let args = ['Launching', colored_name, 'stage…']; - - let line = alchemy.printLog(alchemy.INFO, args, {level: 1}); - - if (line && line.args) { - stage.pledge.then(function finished() { - line.args.push('Done in', stage.ended - stage.started, 'ms'); - line.dissect(); - line.render(); - }); - } - }); -} - -/** - * Add a getter for the total amount of http requests - * - * @author Jelle De Loecker - * @since 1.3.1 - * @version 1.3.1 - * - * @type {number} - */ -Alchemy.setProperty(function http_request_counter() { - return total_http_requests; -}); - -/** - * The "http" stage: - * Create the server and listen to requests - * - * @author Jelle De Loecker - * @since 0.0.1 - * @version 1.3.1 - */ -alchemy.sputnik.add(function http() { - - // Create the server - alchemy.server = alchemy.modules.http.createServer(); - - // Listen for requests - alchemy.server.on('request', function onRequest(request, response) { - Router.resolve(request, response); - total_http_requests++; - }); -}); - -/** - * The "coreApp" stage: - * Load in Alchemy's main 'app' folder. - * - * @author Jelle De Loecker - * @since 0.0.1 - * @version 1.1.0 - */ -alchemy.sputnik.add(function core_app() { - alchemy.usePath(path.resolve(PATH_CORE, 'app'), {weight: 1}); -}); - -/** - * The "datasources" stage: - * Make a connection to all the datasources. - * - * @author Jelle De Loecker - * @since 0.0.1 - * @version 1.4.0 - */ -alchemy.sputnik.add(function datasources() { - - var tasks = []; - - // Force Blast to load - try { - Blast.doLoaded(); - } catch (err) { - alchemy.printLog('error', ['Failed to load application:', err.message], {err: err, level: 1}); - return - } - - let environment = alchemy.getSetting('environment'); - - // Require the environment datasources configuration - try { - require(path.resolve(PATH_ROOT, 'app', 'config', environment, 'database')); - } catch (err) { - - if (err.code == 'MODULE_NOT_FOUND') { - if (!alchemy.getSetting('client_mode')) { - // Only output a warning when not in client mode - log.warn('Could not find ' + environment + ' database settings'); - } - } else { - log.warn('Could not load ' + environment + ' database settings:', err); - } - - return; - } - - // Get all available datasources - Object.each(Datasource.get(), function eachDatasource(datasource, key) { - tasks.push(datasource.setup()); - }); - - return Function.parallel(tasks); -}); - -/** - * The "plugins" stage: - * Initialize the defined plugins. - * - * @author Jelle De Loecker - * @since 0.0.1 - * @version 1.2.7 - */ -alchemy.sputnik.add(function plugins() { - // Load in the plugins - try { - alchemy.startPlugins(); - } catch (err) { - // Constitutors sometimes throw errors during this stage. - // Not sure yet why they don't get caught by sputnik - // @TODO: refactor! - log.error('Caught error during "plugins" stage:', err); - throw err; - } -}); - -/** - * The "baseApp" stage: - * Load all the files in the user-defined 'app' folder - * - * @author Jelle De Loecker - * @since 0.0.1 - * @version 1.3.22 - */ -alchemy.sputnik.add(function base_app() { - // Load in the app - alchemy.usePath(PATH_APP, {weight: 20, skip: ['routes']}); -}); - -/** - * The "defineDebug" stage: - * Setup some debug settings - * - * @author Jelle De Loecker - * @since 0.0.1 - * @version 1.4.0 - */ -alchemy.sputnik.add(function define_debug() { - // See if we want to enable debugging - if (alchemy.getSetting('debugging.debug')) { - log.info('Hawkejs debugging has been ENABLED'); - alchemy.hawkejs._debug = true; - } -}); - -/** - * The "hawkejsSetup" stage: - * Initialize Hawkejs - * - * @author Jelle De Loecker - * @since 0.0.1 - * @version 1.3.22 - */ -alchemy.sputnik.add(function hawkejs_setup() { - - // Set the correct asset paths - alchemy.hawkejs.style_path = 'stylesheets/'; - alchemy.hawkejs.script_path = 'scripts/'; - - // Serve the hawkejs file - Router.use('/hawkejs/hawkejs-client.js', function getHawkejs(req, res, next) { - - var retries = 0; - - Blast.getClientPath({ - modify_prototypes : true, - ua : req.conduit.headers.useragent, - create_source_map : alchemy.getSetting('debugging.create_source_map'), - enable_coverage : !!global.__coverage__, - debug : alchemy.getSetting('debugging.debug'), - }).done(gotClientFile); - - function gotClientFile(err, path) { - - if (err) { - return retryFnc(err); - } - - let options = {}; - - if (req.conduit && req.conduit.supports('async') === false) { - options.add_async_support = true; - } - - alchemy.minifyScript(path, options, function gotMinifiedPath(err, mpath) { - - var options; - - if (!retries) { - options = { - onError: retryFnc - } - } - - req.conduit.serveFile(mpath || path, options); - }); - } - - function retryFnc(err) { - - if (retries > 0) { - return req.conduit.error(new Error('Failed to serve client file')); - } - - retries++; - - Blast.getClientPath({ - refresh : true, - modify_prototypes : true, - ua : req.conduit.headers.useragent, - create_source_map : alchemy.getSetting('debugging.create_source_map'), - debug : alchemy.getSetting('debugging.debug'), - }).done(gotClientFile); - } - }); - - // Serve the static file with exposed variables - Router.use('/hawkejs/static.js', function getHawkejs(req, res, next) { - alchemy.hawkejs.getStaticExposedPath((err, path) => { - - if (err) { - return req.conduit.error(err); - } - - req.conduit.serveFile(path); - }); - }); - - // Serve multiple template files - Router.use('/hawkejs/templates', function onGetTemplates(req, res) { - - var names = req.conduit.param('name'); - - if (!names) { - return req.conduit.error(new Error('No template names have been given')); - } - - alchemy.hawkejs.getFirstAvailableSource(names, function gotResult(err, result) { - - if (err) { - return req.conduit.error(err); - } - - if (!result || !result.name) { - return req.conduit.notFound('Could not find any of the given templates'); - } - - req.conduit.setHeader('cache-control', 'public, max-age=3600, must-revalidate'); - - // Don't use json dry, hawkejs expects regular json - req.conduit.json_dry = false; - - req.conduit.end(result); - }); - }, {methods: ['get'], weight: 19}); - - // Serve single template files - Router.use('/hawkejs/template', function onGetTemplate(req, res) { - - var name = req.conduit.param('name'); - - if (!name) { - return req.conduit.error(new Error('No template name has been given')); - } - - alchemy.hawkejs.getTemplatePath(name, function gotTemplate(err, path) { - - if (err) { - return req.conduit.error(err); - } - - if (!path) { - req.conduit.notFound('Could not find ' + name); - } else { - req.conduit.serveFile(path); - } - }); - }, {methods: ['get'], weight: 19}); -}); - -/** - * The "middleware" stage: - * Setup middleware - * - * @author Jelle De Loecker - * @since 0.0.1 - * @version 1.4.0 - */ -alchemy.sputnik.add(function middleware() { - - // Serve public files - Router.use('/public/', alchemy.publicMiddleware, 50); - - // Serve stylesheets - Router.use('/stylesheets/', alchemy.styleMiddleware, 50); - - // Serve scripts - Router.use('/scripts/', alchemy.scriptMiddleware, 50); - - // Serve fonts - Router.use('/fonts/', alchemy.fontMiddleware, 50); - - // Serve root files - Router.use('/', alchemy.rootMiddleware, 49); - - if (alchemy.getSetting('debugging.debug')) { - // Serve sourcemap files - Router.use('/_sourcemaps/', alchemy.sourcemapMiddleware, 50); - } - - // Parse body (form-data & json, no multipart) - // @todo: not all routes require body parsing - Router.use(function parseBody(req, res, next) { - - // Don't re-check internal redirects, they always should have a body set - if (req.original.body != null || (req.conduit && req.conduit instanceof Classes.Alchemy.Conduit.Loopback)) { - return next(); - } - - alchemy.parseRequestBody(req, res, next); - - }, {methods: ['post'], weight: 99999}); -}); - -/** - * The "routes" stage: - * Initialize all the routes - * - * @author Jelle De Loecker - * @since 0.0.1 - * @version 1.4.0 - */ -alchemy.sputnik.add(function routes() { - try { - alchemy.useOnce(path.resolve(PATH_APP, 'config', 'routes.js')); - } catch (err) { - // Only output warning when not in client mode - if (!alchemy.getSetting('client_mode')) { - log.warn('No app routes were found:', err); - } - } -}); - -/** - * The "startServer" stage: - * Actually start the server - * - * @author Jelle De Loecker - * @since 0.0.1 - * @version 1.4.0 - */ -alchemy.sputnik.add(function start_server() { - - if (process.send) { - // Create a connection to the hohenheim parent - alchemy.hohenheim = new Classes.Alchemy.Reciprocal(process, 'hohenheim'); - } - - if (alchemy.getSetting('client_mode')) { - return alchemy.sputnik.launch('listening'); - } - - alchemy.exposeDefaultStaticVariables(); - - let port = alchemy.getSetting('network.port'), - socket = alchemy.getSetting('network.socket'); - - // If a falsy (non-null) port is given (and no socket file), do nothing - if (!port && port !== null && !socket) { - return; - } - - let listen_target; - - // Are we using a socket file? - if (typeof socket == 'string') { - let stat; - - try { - stat = fs.statSync(socket); - } catch (err) { - // File not found, so it's safe to use - } - - if (stat) { - log.info('Found existing socketfile at', socket, ', need to remove it'); - fs.unlinkSync(socket); - } - - listen_target = socket; - } - - if (!listen_target && port) { - listen_target = port; - } - - // Start listening on the given port - // The actual `requests` listener is defined in the 'http' stage - alchemy.server.listen(listen_target, function areListening(){ - - let address = alchemy.server.address(); - let url = alchemy.getSetting('network.main_url'); - - if (typeof address == 'string') { - alchemy.setSetting('network.socket', address); - log.info('HTTP server listening on socket file', address); - - const set_socketfile_chmod = alchemy.getSetting('network.socketfile_chmod'); - - // Make readable by everyone - if (set_socketfile_chmod) { - fs.chmodSync(address, set_socketfile_chmod); - } - } else { - // Get the actual server port - alchemy.setSetting('network.port', address.port); - log.info('HTTP server listening on port', address.port); - - if (!url) { - url = 'http://localhost:' + address.port; - } - } - - if (url) { - let pretty_url = alchemy.colors.bg.getRgb(1, 0, 1) + alchemy.colors.fg.getRgb(5, 3, 0) + ' ' + url + ' ' + alchemy.colors.reset; - log.info('Served at »»', pretty_url, '««'); - } - - // If this process is a child, tell the parent we're ready - if (process.send) { - log.info('Letting the parent know we\'re ready!'); - process.send({alchemy: {ready: true}}); - - process.on('disconnect', function onParentExit() { - log.info('Parent exited, closing down'); - process.exit(); - }); - } - - alchemy.sputnik.launch('listening'); - }); - - // Listen for errors (like EADDRINUSE) - alchemy.server.on('error', function onError(err) { - - if (process.send) { - process.send({alchemy: {error: err}}); - return process.exit(); - } - - throw err; - }); -}); - -/** - * Launch the "startServer" stage after datasources & socket - * - * @author Jelle De Loecker - * @since 0.0.1 - * @version 0.5.0 - */ -alchemy.sputnik.after(['datasources', 'socket'], function scheduleServerStart() { - - // Need to wait for all classes to load - Blast.loaded(function hasLoaded() { - - alchemy.sputnik.launch('start_server'); - - // Indicate the server has started - alchemy.started = true; - }); -}); - -alchemy.sputnik.launch([ - 'http', - 'core_app', - 'plugins', - 'base_app', - 'middleware', - 'datasources', - 'define_debug', - 'socket', - 'hawkejs_setup', - 'routes']); \ No newline at end of file diff --git a/lib/stages/00-load_core.js b/lib/stages/00-load_core.js new file mode 100644 index 00000000..293a5ddf --- /dev/null +++ b/lib/stages/00-load_core.js @@ -0,0 +1,333 @@ +const libpath = require('path'); + +/** + * The "load_core" stage + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @type {Alchemy.Stages.Stage} + */ +const load_core = STAGES.createStage('load_core'); + +/** + * The "load_core.init_settings" stage + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @type {Alchemy.Stages.Stage} + */ +const init_settings = load_core.createStage('init_settings', () => { + /** + * Load the setting class + */ + requireCorePath('core', 'setting'); + + /** + * Load the actual settings + */ + requireCorePath('init_scripts', 'settings'); +}); + +/** + * The "load_core.init_alchemy" stage + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @type {Alchemy.Stages.Stage} + */ +const init_alchemy = load_core.createStage('init_alchemy', () => { + /** + * Load the setting class + */ + requireCorePath('core', 'alchemy'); + + /** + * Load the actual settings + */ + requireCorePath('init_scripts', 'alchemy'); + + /** + * Require basic functions + */ + requireCorePath('core', 'alchemy_functions'); + + /** + * Require load functions + */ + requireCorePath('core', 'alchemy_load_functions'); +}); + +/** + * The "load_core.init_languages" stage + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @type {Alchemy.Stages.Stage} + */ +const init_languages = load_core.createStage('init_languages', () => { + /** + * Get all the languages by their locale + */ + requireCorePath('init_scripts', 'languages'); +}); + +/** + * The "load_core.preload_modules" stage + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @type {Alchemy.Stages.Stage} + */ +const preload_modules = load_core.createStage('preload_modules', () => { + /** + * Pre-load basic requirements + */ + requireCorePath('init_scripts', 'preload_modules'); +}); + +/** + * The "load_core.devwatch" stage + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @type {Alchemy.Stages.Stage} + */ +const devwatch = load_core.createStage('devwatch', () => { + /** + * Set up file change watchers for development + */ + requireCorePath('init_scripts', 'devwatch'); +}); + +/** + * The "load_core.migration_class" stage + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @type {Alchemy.Stages.Stage} + */ +const migration_class = load_core.createStage('migration_class', () => { + /** + * The migration class + */ + requireCorePath('class', 'migration'); +}); + +/** + * The "load_core.core_classes" stage + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @type {Alchemy.Stages.Stage} + */ +const core_classes = load_core.createStage('core_classes', () => { + + const CLIENT_HAWKEJS_OPTIONS = { + + // Do not load on the server + server : false, + + // Turn it into a commonjs load + make_commonjs: true, + + // The arguments to add to the wrapper function + arguments : 'hawkejs' + }; + + const SERVER_HAWKEJS_OPTIONS = { + + // Also load on the server + server : true, + + // Turn it into a commonjs load + make_commonjs: true, + + // The arguments to add to the wrapper function + arguments : 'hawkejs' + }; + + /** + * Require the base class on the client side too + * + * @author Jelle De Loecker + * @since 0.3.0 + * @version 1.1.0 + */ + alchemy.hawkejs.load(resolveCorePath('core', 'base'), CLIENT_HAWKEJS_OPTIONS); + + /** + * Require the client_base class on the client side too + * + * @author Jelle De Loecker + * @since 1.0.0 + * @version 1.1.0 + */ + alchemy.hawkejs.load(resolveCorePath('core', 'client_base'), CLIENT_HAWKEJS_OPTIONS); + + /** + * Require the error class + * + * @author Jelle De Loecker + * @since 1.1.0 + * @version 1.1.0 + */ + alchemy.hawkejs.load(resolveCorePath('class', 'error'), SERVER_HAWKEJS_OPTIONS); + + /** + * Require the client_alchemy class on the client side + * + * @author Jelle De Loecker + * @since 1.0.5 + * @version 1.1.0 + */ + alchemy.hawkejs.load(resolveCorePath('core', 'client_alchemy'), SERVER_HAWKEJS_OPTIONS); + + /** + * Require the path_evaluator class + * + * @author Jelle De Loecker + * @since 1.1.0 + * @version 1.1.0 + */ + alchemy.hawkejs.load(resolveCorePath('class', 'path_evaluator'), SERVER_HAWKEJS_OPTIONS); + + /** + * Require the field_value class + * + * @author Jelle De Loecker + * @since 1.1.0 + * @version 1.1.0 + */ + alchemy.hawkejs.load(resolveCorePath('class', 'field_value'), SERVER_HAWKEJS_OPTIONS); + + /** + * Require the path_definition class on the client side too + * + * @author Jelle De Loecker + * @since 1.0.0 + * @version 1.1.0 + */ + alchemy.hawkejs.load(resolveCorePath('class', 'path_definition'), CLIENT_HAWKEJS_OPTIONS); + alchemy.hawkejs.load(resolveCorePath('class', 'path_param_definition'), CLIENT_HAWKEJS_OPTIONS); + + /** + * Require the element class on the client side too + * + * @author Jelle De Loecker + * @since 1.0.0 + * @version 1.1.0 + */ + alchemy.hawkejs.load(resolveCorePath('class', 'element'), CLIENT_HAWKEJS_OPTIONS); + + /** + * Require the helper class on the client side too + * + * @author Jelle De Loecker + * @since 1.0.0 + * @version 1.1.0 + */ + alchemy.hawkejs.load(resolveCorePath('class', 'helper'), CLIENT_HAWKEJS_OPTIONS); + + /** + * Require the datasource class on the client side too + * + * @author Jelle De Loecker + * @since 1.1.0 + * @version 1.1.0 + */ + alchemy.hawkejs.load(resolveCorePath('class', 'datasource'), CLIENT_HAWKEJS_OPTIONS); + + /** + * Require the field class on the client side too + * + * @author Jelle De Loecker + * @since 1.1.0 + * @version 1.1.0 + */ + alchemy.hawkejs.load(resolveCorePath('class', 'field'), CLIENT_HAWKEJS_OPTIONS); + + /** + * Require the schema_client class on the client side too + * + * @author Jelle De Loecker + * @since 1.1.0 + * @version 1.1.0 + */ + alchemy.hawkejs.load(resolveCorePath('class', 'schema_client'), CLIENT_HAWKEJS_OPTIONS); + + /** + * Set up routing functions + */ + alchemy.useOnce(resolveCorePath('core', 'routing')); + + /** + * Set up middleware functions + */ + alchemy.useOnce(resolveCorePath('core', 'middleware')); + + /** + * Load discovery code + */ + alchemy.useOnce(resolveCorePath('core', 'discovery')); + + /** + * Load inode classes + */ + alchemy.useOnce(resolveCorePath('class', 'inode')); + alchemy.useOnce(resolveCorePath('class', 'inode_file')); + alchemy.useOnce(resolveCorePath('class', 'inode_dir')); + alchemy.useOnce(resolveCorePath('class', 'inode_list')); +}); + +/** + * The "load_core.main_classes" stage + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @type {Alchemy.Stages.Stage} + */ +const main_classes = load_core.createStage('main_classes', () => { + // Load in all main classes + alchemy.usePath(resolveCorePath('class'), {modular: false}); +}); + +/** + * The "load_core.app_bootstrap" stage + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @type {Alchemy.Stages.Stage} + */ +const app_bootstrap = load_core.createStage('app_bootstrap', () => { + // Load the base bootstrap file + try { + alchemy.useOnce(libpath.resolve(PATH_ROOT, 'app', 'config', 'bootstrap')); + } catch (err) { + if (err.message.indexOf('Cannot find') === -1) { + alchemy.printLog(alchemy.WARNING, 'Could not load app bootstrap file'); + throw err; + } else { + alchemy.printLog(alchemy.SEVERE, 'Could not load config bootstrap file', {err: err}); + } + } +}); \ No newline at end of file diff --git a/lib/stages/05-load_app.js b/lib/stages/05-load_app.js new file mode 100644 index 00000000..f0bcc6fa --- /dev/null +++ b/lib/stages/05-load_app.js @@ -0,0 +1,59 @@ +const libpath = require('path'); + +/** + * The "load_app" stage + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @type {Alchemy.Stages.Stage} + */ +const load_app = STAGES.createStage('load_app'); + +/** + * The "load_app.core_app" stage + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @type {Alchemy.Stages.Stage} + */ +const core_app = load_app.createStage('core_app', () => { + alchemy.usePath(libpath.resolve(PATH_CORE, 'app'), {weight: 1}); +}); + +/** + * The "load_app.plugins" stage + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @type {Alchemy.Stages.Stage} + */ +const plugins = load_app.createStage('plugins', () => { + // Load in the plugins + try { + alchemy.startPlugins(); + } catch (err) { + // Constitutors sometimes throw errors during this stage + log.error('Caught error during "plugins" stage:', err); + throw err; + } +}); + +/** + * The "load_app.main_app" stage + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @type {Alchemy.Stages.Stage} + */ +const main_app = load_app.createStage('main_app', () => { + // Load in the app + alchemy.usePath(PATH_APP, {weight: 20, skip: ['routes']}); +}); \ No newline at end of file diff --git a/lib/stages/10-datasource.js b/lib/stages/10-datasource.js new file mode 100644 index 00000000..3d61bfaa --- /dev/null +++ b/lib/stages/10-datasource.js @@ -0,0 +1,75 @@ +const libpath = require('path'); + +/** + * The "datasource" stage + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @type {Alchemy.Stages.Stage} + */ +const datasource = STAGES.createStage('datasource'); + +/** + * The "datasource.connect" stage: + * Make a connection to all the datasources. + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @type {Alchemy.Stages.Stage} + */ +const connect = datasource.createStage('connect', () => { + + // Force Blast to load + try { + Blast.doLoaded(); + } catch (err) { + alchemy.printLog('error', ['Failed to load application:', err.message], {err: err, level: 1}); + return + } + + let environment = alchemy.getSetting('environment'); + + // Require the environment datasources configuration + try { + require(libpath.resolve(PATH_ROOT, 'app', 'config', environment, 'database')); + } catch (err) { + + if (err.code == 'MODULE_NOT_FOUND') { + if (!alchemy.getSetting('client_mode')) { + // Only output a warning when not in client mode + log.warn('Could not find ' + environment + ' database settings'); + } + } else { + log.warn('Could not load ' + environment + ' database settings:', err); + } + + return; + } + + let tasks = []; + + // Get all available datasources + Object.each(Datasource.get(), function eachDatasource(datasource, key) { + tasks.push(datasource.setup()); + }); + + return Function.parallel(tasks); +}); + +/** + * "datasource.connect.load_settings" + * Load any possible settings in the database + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @type {Alchemy.Stages.Stage} + */ +const load_settings = connect.createStage('load_settings', () => { + console.log('Should load settings..') +}); \ No newline at end of file diff --git a/lib/stages/20-routes.js b/lib/stages/20-routes.js new file mode 100644 index 00000000..1d93d0dc --- /dev/null +++ b/lib/stages/20-routes.js @@ -0,0 +1,218 @@ +const libpath = require('path'); + +/** + * The "routes" stage + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @type {Alchemy.Stages.Stage} + */ +const routes = STAGES.createStage('routes'); + +/** + * The "routes.hawkejs" stage: + * Setup the Hawkejs routes + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @type {Alchemy.Stages.Stage} + */ +const hawkejs = routes.createStage('hawkejs', () => { + + // Set the correct asset paths + alchemy.hawkejs.style_path = 'stylesheets/'; + alchemy.hawkejs.script_path = 'scripts/'; + + // Serve the hawkejs file + Router.use('/hawkejs/hawkejs-client.js', function getHawkejs(req, res, next) { + + var retries = 0; + + Blast.getClientPath({ + modify_prototypes : true, + ua : req.conduit.headers.useragent, + create_source_map : alchemy.getSetting('debugging.create_source_map'), + enable_coverage : !!global.__coverage__, + debug : alchemy.getSetting('debugging.debug'), + }).done(gotClientFile); + + function gotClientFile(err, path) { + + if (err) { + console.log(err) + return retryFnc(err); + } + + let options = {}; + + if (req.conduit && req.conduit.supports('async') === false) { + options.add_async_support = true; + } + + alchemy.minifyScript(path, options, function gotMinifiedPath(err, mpath) { + + var options; + + if (!retries) { + options = { + onError: retryFnc + } + } + + req.conduit.serveFile(mpath || path, options); + }); + } + + function retryFnc(err) { + + if (retries > 0) { + return req.conduit.error(new Error('Failed to serve client file')); + } + + retries++; + + Blast.getClientPath({ + refresh : true, + modify_prototypes : true, + ua : req.conduit.headers.useragent, + create_source_map : alchemy.getSetting('debugging.create_source_map'), + debug : alchemy.getSetting('debugging.debug'), + }).done(gotClientFile); + } + }); + + // Serve the static file with exposed variables + Router.use('/hawkejs/static.js', function getHawkejs(req, res, next) { + alchemy.hawkejs.getStaticExposedPath((err, path) => { + + if (err) { + return req.conduit.error(err); + } + + req.conduit.serveFile(path); + }); + }); + + // Serve multiple template files + Router.use('/hawkejs/templates', function onGetTemplates(req, res) { + + var names = req.conduit.param('name'); + + if (!names) { + return req.conduit.error(new Error('No template names have been given')); + } + + alchemy.hawkejs.getFirstAvailableSource(names, function gotResult(err, result) { + + if (err) { + return req.conduit.error(err); + } + + if (!result || !result.name) { + return req.conduit.notFound('Could not find any of the given templates'); + } + + req.conduit.setHeader('cache-control', 'public, max-age=3600, must-revalidate'); + + // Don't use json dry, hawkejs expects regular json + req.conduit.json_dry = false; + + req.conduit.end(result); + }); + }, {methods: ['get'], weight: 19}); + + // Serve single template files + Router.use('/hawkejs/template', function onGetTemplate(req, res) { + + var name = req.conduit.param('name'); + + if (!name) { + return req.conduit.error(new Error('No template name has been given')); + } + + alchemy.hawkejs.getTemplatePath(name, function gotTemplate(err, path) { + + if (err) { + return req.conduit.error(err); + } + + if (!path) { + req.conduit.notFound('Could not find ' + name); + } else { + req.conduit.serveFile(path); + } + }); + }, {methods: ['get'], weight: 19}); +}); + +/** + * The "routes.middleware" stage: + * Setup all the middleware routes + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @type {Alchemy.Stages.Stage} + */ +const middleware = routes.createStage('middleware', () => { + + // Serve public files + Router.use('/public/', alchemy.publicMiddleware, 50); + + // Serve stylesheets + Router.use('/stylesheets/', alchemy.styleMiddleware, 50); + + // Serve scripts + Router.use('/scripts/', alchemy.scriptMiddleware, 50); + + // Serve fonts + Router.use('/fonts/', alchemy.fontMiddleware, 50); + + // Serve root files + Router.use('/', alchemy.rootMiddleware, 49); + + if (alchemy.getSetting('debugging.debug')) { + // Serve sourcemap files + Router.use('/_sourcemaps/', alchemy.sourcemapMiddleware, 50); + } + + // Parse body (form-data & json, no multipart) + // @todo: not all routes require body parsing + Router.use(function parseBody(req, res, next) { + + // Don't re-check internal redirects, they always should have a body set + if (req.original.body != null || (req.conduit && req.conduit instanceof Classes.Alchemy.Conduit.Loopback)) { + return next(); + } + + alchemy.parseRequestBody(req, res, next); + + }, {methods: ['post'], weight: 99999}); + +}); + +/** + * The "routes.app_routes" stage: + * Setup the routes of the main app. + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @type {Alchemy.Stages.Stage} + */ +const app_routes = routes.createStage('app_routes', () => { + try { + alchemy.useOnce(libpath.resolve(PATH_APP, 'config', 'routes.js')); + } catch (err) { + // Only output warning when not in client mode + if (!alchemy.getSetting('client_mode')) { + log.warn('No app routes were found:', err); + } + } +}); \ No newline at end of file diff --git a/lib/stages/30-server.js b/lib/stages/30-server.js new file mode 100644 index 00000000..71f85ec6 --- /dev/null +++ b/lib/stages/30-server.js @@ -0,0 +1,352 @@ +const libfs = require('fs'); + +/** + * The "server" stage + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @type {Alchemy.Stages.Stage} + */ +const server = STAGES.createStage('server'); + +/** + * The "server.create_http" stage: + * Create the server instance + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @type {Alchemy.Stages.Stage} + */ +const create_http = server.createStage('create_http', () => { + + // Create the server + alchemy.server = alchemy.modules.http.createServer(); + + // Listen for requests + alchemy.server.on('request', (request, response) => Router.resolve(request, response)); +}); + +/** + * The "server.websocket" stage: + * Setup the websocket system + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @type {Alchemy.Stages.Stage} + */ +const websocket = server.createStage('websocket', () => { + + let msgpack_parser, + iostream, + types = alchemy.shared('Socket.types'), + path = alchemy.use('path'), + fs = alchemy.use('fs'); + + const websockets = alchemy.settings.network.use_websockets; + + if (!websockets || websockets === 'never') { + log.info('Websockets have been disabled'); + return; + } else { + if (websockets == 'optional') { + log.info('Websockets have been enabled optionally'); + } else { + log.info('Websockets have been enabled, clients will automatically connect'); + } + } + + const socket_io = alchemy.use('socket.io'); + + if (!socket_io) { + return log.error('Could not load socket.io!'); + } + + iostream = alchemy.use('socket.io-stream'); + msgpack_parser = alchemy.use('socket.io-msgpack-parser'); + + let socket_io_options = { + serveClient : false, + }; + + if (msgpack_parser) { + socket_io_options.parser = msgpack_parser; + } + + if (!alchemy.server) { + throw new Error('No server has been created yet, unable to start socket.io'); + } + + // Create the Socket.io listener + alchemy.io = socket_io(alchemy.server, socket_io_options); + + // Get the core client path + let client_path = alchemy.findModule('socket.io-client').module_dir; + + if (msgpack_parser) { + client_path = path.join(client_path, 'dist', 'socket.io.msgpack.min.js'); + } else { + client_path = path.join(client_path, 'dist', 'socket.io.min.js'); + } + + // Get the stream client path + let stream_path = path.dirname(alchemy.findModule('@11ways/socket.io-stream').module_path); + stream_path = path.resolve(stream_path, 'socket.io-stream.js'); + + // Serve the socket io core file + Router.use('/scripts/socket.io.js', function getSocketIo(req, res, next) { + alchemy.minifyScript(client_path, function gotMinifiedPath(err, mpath) { + req.conduit.serveFile(mpath || client_path); + }); + }); + + // Serve the socket io stream file + Router.use('/scripts/socket.io-stream.js', function getSocketStream(req, res, next) { + alchemy.minifyScript(stream_path, function gotMinifiedPath(err, mpath) { + req.conduit.serveFile(mpath || stream_path); + }); + }); + + /** + * Handle connections + * + * @author Jelle De Loecker + * @since 0.0.1 + * @version 0.2.0 + */ + alchemy.io.sockets.on('connection', function onConnect(socket){ + + var syncs = {}, + latencies = [], + latency_avg = 2, + offset = 0; + + socket.on('timesync', function gotTimesyncRequest(data) { + + var received = Date.now(), + latency; + + // This is the initial request + if (data.count == null) { + data.count = 0; + data.latency_trip = 0; + + // Reset the latencies array + latencies.length = 0; + } + + // Do 8 round trips to determine latency + if (data.latency_trip <= 8) { + + if (data.last_sent) { + + // Latency is the timestamp when we received the response + // minus the timestamp when we sent the request + latency = received - data.last_sent; + latencies.push(latency); + } + + // Wait before responding + setTimeout(function waitForLatency() { + data.last_sent = Date.now(); + socket.emit('timesync', data); + }, 100 + (data.latency_trip * 150)); + + data.latency_trip++; + } else if (data.latency_trip) { + + latency_avg = ~~(Math.median(latencies) / 2); + offset = (received - latency_avg) - data.client_time; + + socket.emit('timesync', {offset: offset, latency: latency_avg}); + } + }); + + // Wait for the announcement + socket.once('announce', function gotAnnouncement(data) { + + var SocketClass, + class_name; + + // Try getting the socket class of this type + if (typeof data.type == 'string') { + class_name = data.type.classify(); + SocketClass = Classes.Alchemy.Conduit[class_name + 'Socket']; + } + + // If no socket class was found get the regular class + if (!SocketClass) { + SocketClass = Classes.Alchemy.Conduit.Socket; + } + + new SocketClass(socket, data); + }); + }); + +}); + +/** + * The "server.warn_debug" stage: + * Start the server + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @type {Alchemy.Stages.Stage} + */ +const warn_debug = server.createStage('warn_debug', () => { + // See if we want to enable debugging + if (alchemy.getSetting('debugging.debug')) { + log.info('Hawkejs debugging has been ENABLED'); + alchemy.hawkejs._debug = true; + } +}); + +/** + * The "server.start" stage: + * Start the server + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @type {Alchemy.Stages.Stage} + */ +const start = server.createStage('start', () => { + + if (process.send) { + // Create a connection to the hohenheim parent + alchemy.hohenheim = new Classes.Alchemy.Reciprocal(process, 'hohenheim'); + } + + if (alchemy.getSetting('client_mode')) { + return server.launch(true); + } + + alchemy.exposeDefaultStaticVariables(); + + let port = alchemy.getSetting('network.port'), + socket = alchemy.getSetting('network.socket'); + + // If a falsy (non-null) port is given (and no socket file), do nothing + if (!port && port !== null && !socket) { + return; + } + + let listen_target; + + // Are we using a socket file? + if (typeof socket == 'string') { + let stat; + + try { + stat = libfs.statSync(socket); + } catch (err) { + // File not found, so it's safe to use + console.log(err); + } + + if (stat) { + log.info('Found existing socketfile at', socket, ', need to remove it'); + libfs.unlinkSync(socket); + } + + listen_target = socket; + } + + if (!listen_target && port) { + listen_target = port; + } + + // Start listening on the given port + // The actual `requests` listener is defined in the 'http' stage + alchemy.server.listen(listen_target, function areListening(){ + + let address = alchemy.server.address(); + let url = alchemy.getSetting('network.main_url'); + + if (typeof address == 'string') { + alchemy.setSetting('network.socket', address); + log.info('HTTP server listening on socket file', address); + + const set_socketfile_chmod = alchemy.getSetting('network.socketfile_chmod'); + + // Make readable by everyone + if (set_socketfile_chmod) { + libfs.chmodSync(address, set_socketfile_chmod); + } + } else { + // Get the actual server port + alchemy.setSetting('network.port', address.port); + log.info('HTTP server listening on port', address.port); + + if (!url) { + url = 'http://localhost:' + address.port; + } + } + + if (url) { + let pretty_url = alchemy.colors.bg.getRgb(1, 0, 1) + alchemy.colors.fg.getRgb(5, 3, 0) + ' ' + url + ' ' + alchemy.colors.reset; + log.info('Served at »»', pretty_url, '««'); + } + + // If this process is a child, tell the parent we're ready + if (process.send) { + log.info('Letting the parent know we\'re ready!'); + process.send({alchemy: {ready: true}}); + + process.on('disconnect', function onParentExit() { + log.info('Parent exited, closing down'); + process.exit(); + }); + } + + server.launch(true); + }); + + // Listen for errors (like EADDRINUSE) + alchemy.server.on('error', function onError(err) { + + if (process.send) { + process.send({alchemy: {error: err}}); + return process.exit(); + } + + throw err; + }); +}); + +/** + * The "server.listening" stage: + * At this point the server is listening for incoming requests + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @type {Alchemy.Stages.Stage} + */ +const listening = server.createStage('listening', () => { + +}); + +STAGES.afterStages(['datasource', 'server.websocket'], () => { + + // Need to wait for all classes to load + Blast.loaded(function hasLoaded() { + server.launch([ + 'create_http', + 'warn_debug', + 'start' + ]); + + // Indicate the server has started + alchemy.started = true; + }); +}); \ No newline at end of file diff --git a/package.json b/package.json index 71ef68c8..ca984eb6 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,6 @@ "semver" : "~7.5.4", "socket.io" : "~4.7.2", "@11ways/socket.io-stream" : "~0.9.2", - "sputnik" : "~0.1.0", "terser" : "~5.21.0", "toobusy-js" : "~0.5.1", "useragent" : "~2.3.0"