UNPKG

alchemymvc

Version:
593 lines (488 loc) 12.4 kB
const QUEUE = []; let queue_check_id, _total_postponement_counter = 0; /** * The Postponement Class represents requests that will be handled later. * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.1 * @version 1.3.2 * * @param {Conduit} conduit The original conduit */ const Postponement = Function.inherits('Alchemy.Base', 'Alchemy.Conduit', function Postponement(conduit, id, options) { // The original conduit instance this.original_conduit = conduit; // The original response object of the conduit this.original_response = conduit.response; // Get the session this.session = conduit.getSession(); // The identifier of this postponement this.id = id; // The original path string this.original_path = conduit.path; // The URL where to get postponement info this.url = '/alchemy/postponed/' + id; // The last known position in the queue this.last_queue_position = null; // Postponement options this.options = options || {}; // When did this postponement start this.started = Date.now(); // When did this postponement end? this.ended = null; // Has this postponement expired? this.expired = false; // Has this postponement been released yet? this.released = false; // When was the last check made from the client? this.last_check = this.started; // Also attach this postponement to the conduit conduit.postponement = this; // Keep track of the total amount of postponements ever _total_postponement_counter++; }); /** * The queued postponements * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.1 * @version 1.3.1 * * @return {Postponement[]} */ Postponement.setStatic('queue', QUEUE); /** * Get the total amount of postponements ever * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.1 * @version 1.3.1 * * @return {number} */ Postponement.setStaticProperty(function total_postponement_counter() { return _total_postponement_counter; }); /** * The current queue length * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.1 * @version 1.3.1 * * @return {number} */ Postponement.queue_length = 0; /** * How long has this been waiting? * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.1 * @version 1.3.1 * * @return {number} */ Postponement.setProperty(function time_waited() { if (!this.ended) { return Date.now() - this.started; } return this.ended - this.started; }); /** * How long has this postponement been left unchecked? * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.1 * @version 1.3.1 * * @return {number} */ Postponement.setProperty(function time_unchecked() { return Date.now() - this.last_check; }); /** * Has this postponement been abandoned? * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.1 * @version 1.3.2 * * @return {number} */ Postponement.setProperty(function has_been_abandoned() { // Postponements that haven't been checked in 3 minutes // are considered abandoned if (this.time_unchecked > 180_000) { return true; } return false; }); /** * Get the current position in the queue * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.1 * @version 1.3.1 * * @return {number} */ Postponement.setProperty(function position_in_queue() { if (this.last_queue_position == null) { return null; } let index = QUEUE.indexOf(this); if (index == -1) { index = null; } this.last_queue_position = index; return index; }); /** * Schedule a check of the queue * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.1 * @version 1.3.1 * * @param {Conduit} conduit The new conduit */ Postponement.setStatic(function scheduleQueueCheck() { if (queue_check_id) { return; } queue_check_id = setTimeout(() => { queue_check_id = null; Postponement.checkQueue(); }, 5000); }); /** * Check the (top of the) queue * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.1 * @version 1.3.1 * * @param {Conduit} conduit The new conduit */ Postponement.setStatic(function checkQueue() { let length = QUEUE.length; this.queue_length = length; if (!length) { return; } let to_remove = [], postponement, max = length, i; if (max > 20) { max = 20; } for (i = 0; i < max; i++) { postponement = QUEUE[i]; if (postponement.has_been_abandoned) { to_remove.push(postponement); } } for (i = 0; i < to_remove.length; i++) { postponement = to_remove[i]; postponement.expire(); } Postponement.scheduleQueueCheck(); }); /** * Handle a request * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.1 * @version 1.3.2 * * @param {Conduit} conduit The new conduit */ Postponement.setMethod(function handleRequest(conduit) { if (conduit.ajax) { let check_queue = conduit.param('check_queue'); if (check_queue) { let data = { position : this.position_in_queue, allowed : false, location : null, }; if (this.released) { data.allowed = true; data.location = this.url; } else if (this.expired) { // If the request has expired, send them to the original url again data.allowed = true; data.location = this.original_path; } else if (this.attemptUnlock()) { data.allowed = true; data.location = this.url; } else if (data.position == null) { // Something else has gone wrong? // Send them to the url anyway data.allowed = true; data.location = this.url; } conduit.end(data); return; } } let resumed = this.attemptResume(conduit); if (!resumed) { this.showPostponementMessage(conduit); } }); /** * Attempt to unlock * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.1 * @version 1.3.2 * * @return {boolean} True if the request is being resumed */ Postponement.setMethod(function attemptUnlock() { if (this.released) { return true; } this.last_check = Date.now(); Postponement.scheduleQueueCheck(); if (this.position_in_queue > 5) { return false; } if (alchemy.lagInMs() > 100) { return false; } this.released = true; return this.released; }); /** * Attempt to resume this postponement. * If it's in a queue and it's not our turn yet, do nothing. * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.1 * @version 1.3.1 * * @param {Conduit} conduit The new conduit * * @return {boolean} True if the request is being resumed */ Postponement.setMethod(function attemptResume(conduit) { if (!this.attemptUnlock()) { return false; } // Let the conduit know the response is being requested now // (Certain postponements also delay the processing of the request) this.original_conduit.emit('get-postponed-response'); // Once we're sure the postponed end has been reached, // actually send that to the browser this.original_conduit.afterOnce('after-postponed-end', () => { this.original_conduit.response = conduit.response; this.original_conduit._end(...this.original_conduit._end_arguments); this.remove(); }); return true; }); /** * Show the postponement message to the given conduit * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.1 * @version 1.3.1 * * @param {Conduit} conduit */ Postponement.setMethod(function showPostponementMessage(conduit) { if (!conduit) { conduit = this.original_conduit; } let response = conduit?.response || this.original_response; if (!response) { throw new Error('Failed to find a response instance, unable to show postponement message'); } response.setHeader('X-Robots-Tag', 'none'); let position_in_queue = this.position_in_queue; // Already set the cookies if (conduit.new_cookie_header.length) { response.setHeader('set-cookie', conduit.new_cookie_header); } // Set the location header where the client should look at later response.setHeader('Location', this.url); response.setHeader('Content-Type', 'text/html'); if (this.options.expected_duration) { response.setHeader('Expected-Duration', Number(this.options.expected_duration / 1000).toFixed(2)); } // Write the headers & status response.writeHead(this.options.status || 202); let end_message = this.options.end_message; // End the response if wanted if (end_message !== false) { if (!end_message) { if (position_in_queue != null) { end_message = this.getQueueHTML(); } else { end_message = 'The response has been postponed, you can find it at <a href="' + this.url + '">' + this.url + '</a>'; } } } else { end_message = ''; } response.end(end_message); }); /** * Remove this postponement * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.1 * @version 1.3.1 * * @param {boolean} expired True if this is due to an expired session */ Postponement.setMethod(function remove(expired) { if (!expired) { const session = this.session; this.ended = Date.now(); session.postponements.remove(this.id); session.addFinishedQueueDuration(this.time_waited); } let index = this.position_in_queue; if (index != null) { QUEUE.splice(index, 1); } }); /** * Called when the session or the postponement expires * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.1 * @version 1.3.1 */ Postponement.setMethod(function expire() { this.remove(true); this.expired = true; }); /** * Put this postponement in a queue * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.1 * @version 1.3.1 */ Postponement.setMethod(function putInQueue() { if (this.last_queue_position != null) { return; } let new_length = QUEUE.push(this) - 1; this.last_queue_position = new_length - 1; // Update the queue length Postponement.queue_length = new_length; return this.last_queue_position; }); /** * Get the HTML message for in the queue * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.1 * @version 1.3.1 */ Postponement.setMethod(function getQueueHTML() { let position = this.last_queue_position + 1; let html = `<!DOCTYPE html> <html> <head> <title>Please Wait...</title> <style> body { background-color: #F5F5F5; display: flex; justify-content: center; align-items: center; height: 100vh; font-family: 'Open Sans', sans-serif; } .main-logo { max-width: 50vw; max-height: 50vw; object-fit: contain; min-width: 150px; max-width: 150px; } .container { text-align: center; background: #F0F8FF; padding: 20px; border-radius: 10px; box-shadow: 5px 5px 10px #B0C4DE; } h1 { font-size: 3em; margin-bottom: 20px; } p { font-size: 1.5em; margin-bottom: 20px; } #queue { font-size: 1.2em; margin-bottom: 20px; } .loading { display: flex; justify-content: center; align-items: center; margin: 0 auto; border: 6px solid #F5F5F5; border-top: 6px solid #3498DB; border-radius: 50%; width: 60px; height: 60px; animation: spin 2s linear infinite; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } </style> </head> <body> <div class="container"> ` if (alchemy.settings.frontend.ui.main_logo) { html += `<img src="${alchemy.settings.frontend.ui.main_logo}" class="main-logo">\n`; } html += ` <h1>Server is busy</h1> <p id="queue">You are #${ position } in the queue.</p> <div class="loading"> <div class="spinner"></div> </div> </div> <script> function checkQueue() { let xhr = new XMLHttpRequest(); xhr.open('GET', '${ this.url }?check_queue=1', true); xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest"); xhr.onreadystatechange = function() { if (xhr.readyState === 4) { let data = JSON.parse(xhr.responseText); let element = document.getElementById("queue"); if (data.allowed && data.location) { element.innerHTML = "Redirecting!"; window.location.href = data.location; return; } let queue = data.position; element.innerHTML = "You are #" + queue + " in the queue"; setTimeout(checkQueue, 15000); } }; xhr.send(); }; setTimeout(checkQueue, 5000); </script> </body> </html>`; return html; });