UNPKG

@lumino/messaging

Version:
517 lines (513 loc) 19.3 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@lumino/algorithm'), require('@lumino/collections')) : typeof define === 'function' && define.amd ? define(['exports', '@lumino/algorithm', '@lumino/collections'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.lumino_messaging = {}, global.lumino_algorithm, global.lumino_collections)); })(this, (function (exports, algorithm, collections) { 'use strict'; // Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. /*----------------------------------------------------------------------------- | Copyright (c) 2014-2017, PhosphorJS Contributors | | Distributed under the terms of the BSD 3-Clause License. | | The full license is in the file LICENSE, distributed with this software. |----------------------------------------------------------------------------*/ /** * @packageDocumentation * @module messaging */ /** * A message which can be delivered to a message handler. * * #### Notes * This class may be subclassed to create complex message types. */ class Message { /** * Construct a new message. * * @param type - The type of the message. */ constructor(type) { this.type = type; } /** * Test whether the message is conflatable. * * #### Notes * Message conflation is an advanced topic. Most message types will * not make use of this feature. * * If a conflatable message is posted to a handler while another * conflatable message of the same `type` has already been posted * to the handler, the `conflate()` method of the existing message * will be invoked. If that method returns `true`, the new message * will not be enqueued. This allows messages to be compressed, so * that only a single instance of the message type is processed per * cycle, no matter how many times messages of that type are posted. * * Custom message types may reimplement this property. * * The default implementation is always `false`. */ get isConflatable() { return false; } /** * Conflate this message with another message of the same `type`. * * @param other - A conflatable message of the same `type`. * * @returns `true` if the message was successfully conflated, or * `false` otherwise. * * #### Notes * Message conflation is an advanced topic. Most message types will * not make use of this feature. * * This method is called automatically by the message loop when the * given message is posted to the handler paired with this message. * This message will already be enqueued and conflatable, and the * given message will have the same `type` and also be conflatable. * * This method should merge the state of the other message into this * message as needed so that when this message is finally delivered * to the handler, it receives the most up-to-date information. * * If this method returns `true`, it signals that the other message * was successfully conflated and that message will not be enqueued. * * If this method returns `false`, the other message will be enqueued * for normal delivery. * * Custom message types may reimplement this method. * * The default implementation always returns `false`. */ conflate(other) { return false; } } /** * A convenience message class which conflates automatically. * * #### Notes * Message conflation is an advanced topic. Most user code will not * make use of this class. * * This message class is useful for creating message instances which * should be conflated, but which have no state other than `type`. * * If conflation of stateful messages is required, a custom `Message` * subclass should be created. */ class ConflatableMessage extends Message { /** * Test whether the message is conflatable. * * #### Notes * This property is always `true`. */ get isConflatable() { return true; } /** * Conflate this message with another message of the same `type`. * * #### Notes * This method always returns `true`. */ conflate(other) { return true; } } /** * The namespace for the global singleton message loop. */ exports.MessageLoop = void 0; (function (MessageLoop) { /** * A function that cancels the pending loop task; `null` if unavailable. */ let pending = null; /** * Schedules a function for invocation as soon as possible asynchronously. * * @param fn The function to invoke when called back. * * @returns An anonymous function that will unschedule invocation if possible. */ const schedule = (resolved => (fn) => { let rejected = false; resolved.then(() => !rejected && fn()); return () => { rejected = true; }; })(Promise.resolve()); /** * Send a message to a message handler to process immediately. * * @param handler - The handler which should process the message. * * @param msg - The message to deliver to the handler. * * #### Notes * The message will first be sent through any installed message hooks * for the handler. If the message passes all hooks, it will then be * delivered to the `processMessage` method of the handler. * * The message will not be conflated with pending posted messages. * * Exceptions in hooks and handlers will be caught and logged. */ function sendMessage(handler, msg) { // Lookup the message hooks for the handler. let hooks = messageHooks.get(handler); // Handle the common case of no installed hooks. if (!hooks || hooks.length === 0) { invokeHandler(handler, msg); return; } // Invoke the message hooks starting with the newest first. let passed = algorithm.every(algorithm.retro(hooks), hook => { return hook ? invokeHook(hook, handler, msg) : true; }); // Invoke the handler if the message passes all hooks. if (passed) { invokeHandler(handler, msg); } } MessageLoop.sendMessage = sendMessage; /** * Post a message to a message handler to process in the future. * * @param handler - The handler which should process the message. * * @param msg - The message to post to the handler. * * #### Notes * The message will be conflated with the pending posted messages for * the handler, if possible. If the message is not conflated, it will * be queued for normal delivery on the next cycle of the event loop. * * Exceptions in hooks and handlers will be caught and logged. */ function postMessage(handler, msg) { // Handle the common case of a non-conflatable message. if (!msg.isConflatable) { enqueueMessage(handler, msg); return; } // Conflate the message with an existing message if possible. let conflated = algorithm.some(messageQueue, posted => { if (posted.handler !== handler) { return false; } if (!posted.msg) { return false; } if (posted.msg.type !== msg.type) { return false; } if (!posted.msg.isConflatable) { return false; } return posted.msg.conflate(msg); }); // Enqueue the message if it was not conflated. if (!conflated) { enqueueMessage(handler, msg); } } MessageLoop.postMessage = postMessage; /** * Install a message hook for a message handler. * * @param handler - The message handler of interest. * * @param hook - The message hook to install. * * #### Notes * A message hook is invoked before a message is delivered to the * handler. If the hook returns `false`, no other hooks will be * invoked and the message will not be delivered to the handler. * * The most recently installed message hook is executed first. * * If the hook is already installed, this is a no-op. */ function installMessageHook(handler, hook) { // Look up the hooks for the handler. let hooks = messageHooks.get(handler); // Bail early if the hook is already installed. if (hooks && hooks.indexOf(hook) !== -1) { return; } // Add the hook to the end, so it will be the first to execute. if (!hooks) { messageHooks.set(handler, [hook]); } else { hooks.push(hook); } } MessageLoop.installMessageHook = installMessageHook; /** * Remove an installed message hook for a message handler. * * @param handler - The message handler of interest. * * @param hook - The message hook to remove. * * #### Notes * It is safe to call this function while the hook is executing. * * If the hook is not installed, this is a no-op. */ function removeMessageHook(handler, hook) { // Lookup the hooks for the handler. let hooks = messageHooks.get(handler); // Bail early if the hooks do not exist. if (!hooks) { return; } // Lookup the index of the hook and bail if not found. let i = hooks.indexOf(hook); if (i === -1) { return; } // Clear the hook and schedule a cleanup of the array. hooks[i] = null; scheduleCleanup(hooks); } MessageLoop.removeMessageHook = removeMessageHook; /** * Clear all message data associated with a message handler. * * @param handler - The message handler of interest. * * #### Notes * This will clear all posted messages and hooks for the handler. */ function clearData(handler) { // Lookup the hooks for the handler. let hooks = messageHooks.get(handler); // Clear all messsage hooks for the handler. if (hooks && hooks.length > 0) { algorithm.ArrayExt.fill(hooks, null); scheduleCleanup(hooks); } // Clear all posted messages for the handler. for (const posted of messageQueue) { if (posted.handler === handler) { posted.handler = null; posted.msg = null; } } } MessageLoop.clearData = clearData; /** * Process the pending posted messages in the queue immediately. * * #### Notes * This function is useful when posted messages must be processed immediately. * * This function should normally not be needed, but it may be * required to work around certain browser idiosyncrasies. * * Recursing into this function is a no-op. */ function flush() { // Bail if recursion is detected or if there is no pending task. if (flushGuard || pending === null) { return; } // Unschedule the pending loop task. pending(); pending = null; // Run the message loop within the recursion guard. flushGuard = true; runMessageLoop(); flushGuard = false; } MessageLoop.flush = flush; /** * Get the message loop exception handler. * * @returns The current exception handler. * * #### Notes * The default exception handler is `console.error`. */ function getExceptionHandler() { return exceptionHandler; } MessageLoop.getExceptionHandler = getExceptionHandler; /** * Set the message loop exception handler. * * @param handler - The function to use as the exception handler. * * @returns The old exception handler. * * #### Notes * The exception handler is invoked when a message handler or a * message hook throws an exception. */ function setExceptionHandler(handler) { let old = exceptionHandler; exceptionHandler = handler; return old; } MessageLoop.setExceptionHandler = setExceptionHandler; /** * The queue of posted message pairs. */ const messageQueue = new collections.LinkedList(); /** * A mapping of handler to array of installed message hooks. */ const messageHooks = new WeakMap(); /** * A set of message hook arrays which are pending cleanup. */ const dirtySet = new Set(); /** * The message loop exception handler. */ let exceptionHandler = (err) => { console.error(err); }; /** * A guard flag to prevent flush recursion. */ let flushGuard = false; /** * Invoke a message hook with the specified handler and message. * * Returns the result of the hook, or `true` if the hook throws. * * Exceptions in the hook will be caught and logged. */ function invokeHook(hook, handler, msg) { let result = true; try { if (typeof hook === 'function') { result = hook(handler, msg); } else { result = hook.messageHook(handler, msg); } } catch (err) { exceptionHandler(err); } return result; } /** * Invoke a message handler with the specified message. * * Exceptions in the handler will be caught and logged. */ function invokeHandler(handler, msg) { try { handler.processMessage(msg); } catch (err) { exceptionHandler(err); } } /** * Add a message to the end of the message queue. * * This will automatically schedule a run of the message loop. */ function enqueueMessage(handler, msg) { // Add the posted message to the queue. messageQueue.addLast({ handler, msg }); // Bail if a loop task is already pending. if (pending !== null) { return; } // Schedule a run of the message loop. pending = schedule(runMessageLoop); } /** * Run an iteration of the message loop. * * This will process all pending messages in the queue. If a message * is added to the queue while the message loop is running, it will * be processed on the next cycle of the loop. */ function runMessageLoop() { // Clear the task so the next loop can be scheduled. pending = null; // If the message queue is empty, there is nothing else to do. if (messageQueue.isEmpty) { return; } // Add a sentinel value to the end of the queue. The queue will // only be processed up to the sentinel. Messages posted during // this cycle will execute on the next cycle. let sentinel = { handler: null, msg: null }; messageQueue.addLast(sentinel); // Enter the message loop. // eslint-disable-next-line no-constant-condition while (true) { // Remove the first posted message in the queue. let posted = messageQueue.removeFirst(); // If the value is the sentinel, exit the loop. if (posted === sentinel) { return; } // Dispatch the message if it has not been cleared. if (posted.handler && posted.msg) { sendMessage(posted.handler, posted.msg); } } } /** * Schedule a cleanup of a message hooks array. * * This will add the array to the dirty set and schedule a deferred * cleanup of the array contents. On cleanup, any `null` hook will * be removed from the array. */ function scheduleCleanup(hooks) { if (dirtySet.size === 0) { schedule(cleanupDirtySet); } dirtySet.add(hooks); } /** * Cleanup the message hook arrays in the dirty set. * * This function should only be invoked asynchronously, when the * stack frame is guaranteed to not be on the path of user code. */ function cleanupDirtySet() { dirtySet.forEach(cleanupHooks); dirtySet.clear(); } /** * Cleanup the dirty hooks in a message hooks array. * * This will remove any `null` hook from the array. * * This function should only be invoked asynchronously, when the * stack frame is guaranteed to not be on the path of user code. */ function cleanupHooks(hooks) { algorithm.ArrayExt.removeAllWhere(hooks, isNull); } /** * Test whether a value is `null`. */ function isNull(value) { return value === null; } })(exports.MessageLoop || (exports.MessageLoop = {})); exports.ConflatableMessage = ConflatableMessage; exports.Message = Message; })); //# sourceMappingURL=index.js.map