UNPKG

@lumino/signaling

Version:
613 lines (609 loc) 22 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@lumino/algorithm'), require('@lumino/coreutils')) : typeof define === 'function' && define.amd ? define(['exports', '@lumino/algorithm', '@lumino/coreutils'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.lumino_signaling = {}, global.lumino_algorithm, global.lumino_coreutils)); })(this, (function (exports, algorithm, coreutils) { '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 signaling */ /** * A concrete implementation of `ISignal`. * * #### Example * ```typescript * import { ISignal, Signal } from '@lumino/signaling'; * * class SomeClass { * * constructor(name: string) { * this.name = name; * } * * readonly name: string; * * get valueChanged: ISignal<this, number> { * return this._valueChanged; * } * * get value(): number { * return this._value; * } * * set value(value: number) { * if (value === this._value) { * return; * } * this._value = value; * this._valueChanged.emit(value); * } * * private _value = 0; * private _valueChanged = new Signal<this, number>(this); * } * * function logger(sender: SomeClass, value: number): void { * console.log(sender.name, value); * } * * let m1 = new SomeClass('foo'); * let m2 = new SomeClass('bar'); * * m1.valueChanged.connect(logger); * m2.valueChanged.connect(logger); * * m1.value = 42; // logs: foo 42 * m2.value = 17; // logs: bar 17 * ``` */ class Signal { /** * Construct a new signal. * * @param sender - The sender which owns the signal. */ constructor(sender) { this.sender = sender; } /** * Connect a slot to the signal. * * @param slot - The slot to invoke when the signal is emitted. * * @param thisArg - The `this` context for the slot. If provided, * this must be a non-primitive object. * * @returns `true` if the connection succeeds, `false` otherwise. */ connect(slot, thisArg) { return Private.connect(this, slot, thisArg); } /** * Disconnect a slot from the signal. * * @param slot - The slot to disconnect from the signal. * * @param thisArg - The `this` context for the slot. If provided, * this must be a non-primitive object. * * @returns `true` if the connection is removed, `false` otherwise. */ disconnect(slot, thisArg) { return Private.disconnect(this, slot, thisArg); } /** * Emit the signal and invoke the connected slots. * * @param args - The args to pass to the connected slots. * * #### Notes * Slots are invoked synchronously in connection order. * * Exceptions thrown by connected slots will be caught and logged. */ emit(args) { Private.emit(this, args); } } /** * The namespace for the `Signal` class statics. */ (function (Signal) { /** * Remove all connections between a sender and receiver. * * @param sender - The sender object of interest. * * @param receiver - The receiver object of interest. * * #### Notes * If a `thisArg` is provided when connecting a signal, that object * is considered the receiver. Otherwise, the `slot` is considered * the receiver. */ function disconnectBetween(sender, receiver) { Private.disconnectBetween(sender, receiver); } Signal.disconnectBetween = disconnectBetween; /** * Remove all connections where the given object is the sender. * * @param sender - The sender object of interest. */ function disconnectSender(sender) { Private.disconnectSender(sender); } Signal.disconnectSender = disconnectSender; /** * Remove all connections where the given object is the receiver. * * @param receiver - The receiver object of interest. * * #### Notes * If a `thisArg` is provided when connecting a signal, that object * is considered the receiver. Otherwise, the `slot` is considered * the receiver. */ function disconnectReceiver(receiver) { Private.disconnectReceiver(receiver); } Signal.disconnectReceiver = disconnectReceiver; /** * Remove all connections where an object is the sender or receiver. * * @param object - The object of interest. * * #### Notes * If a `thisArg` is provided when connecting a signal, that object * is considered the receiver. Otherwise, the `slot` is considered * the receiver. */ function disconnectAll(object) { Private.disconnectAll(object); } Signal.disconnectAll = disconnectAll; /** * Clear all signal data associated with the given object. * * @param object - The object for which the data should be cleared. * * #### Notes * This removes all signal connections and any other signal data * associated with the object. */ function clearData(object) { Private.disconnectAll(object); } Signal.clearData = clearData; /** * Get the signal exception handler. * * @returns The current exception handler. * * #### Notes * The default exception handler is `console.error`. */ function getExceptionHandler() { return Private.exceptionHandler; } Signal.getExceptionHandler = getExceptionHandler; /** * Set the signal 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 slot throws an exception. */ function setExceptionHandler(handler) { let old = Private.exceptionHandler; Private.exceptionHandler = handler; return old; } Signal.setExceptionHandler = setExceptionHandler; })(Signal || (Signal = {})); /** * A concrete implementation of `IStream`. * * #### Example * ```typescript * import { IStream, Stream } from '@lumino/signaling'; * * class SomeClass { * * constructor(name: string) { * this.name = name; * } * * readonly name: string; * * get pings(): IStream<this, string> { * return this._pings; * } * * ping(value: string) { * this._pings.emit(value); * } * * private _pings = new Stream<this, string>(this); * } * * let m1 = new SomeClass('foo'); * * m1.pings.connect((_, value: string) => { * console.log('connect', value); * }); * * void (async () => { * for await (const ping of m1.pings) { * console.log('iterator', ping); * } * })(); * * m1.ping('alpha'); // logs: connect alpha * // logs: iterator alpha * m1.ping('beta'); // logs: connect beta * // logs: iterator beta * ``` */ class Stream extends Signal { constructor() { super(...arguments); this._pending = new coreutils.PromiseDelegate(); } /** * Return an async iterator that yields every emission. */ async *[Symbol.asyncIterator]() { let pending = this._pending; while (true) { try { const { args, next } = await pending.promise; pending = next; yield args; } catch (_) { return; // Any promise rejection stops the iterator. } } } /** * Emit the signal, invoke the connected slots, and yield the emission. * * @param args - The args to pass to the connected slots. */ emit(args) { const pending = this._pending; const next = (this._pending = new coreutils.PromiseDelegate()); pending.resolve({ args, next }); super.emit(args); } /** * Stop the stream's async iteration. */ stop() { this._pending.promise.catch(() => undefined); this._pending.reject('stop'); this._pending = new coreutils.PromiseDelegate(); } } /** * The namespace for the module implementation details. */ var Private; (function (Private) { /** * The signal exception handler function. */ Private.exceptionHandler = (err) => { console.error(err); }; /** * Connect a slot to a signal. * * @param signal - The signal of interest. * * @param slot - The slot to invoke when the signal is emitted. * * @param thisArg - The `this` context for the slot. If provided, * this must be a non-primitive object. * * @returns `true` if the connection succeeds, `false` otherwise. */ function connect(signal, slot, thisArg) { // Coerce a `null` `thisArg` to `undefined`. thisArg = thisArg || undefined; // Ensure the sender's array of receivers is created. let receivers = receiversForSender.get(signal.sender); if (!receivers) { receivers = []; receiversForSender.set(signal.sender, receivers); } // Bail if a matching connection already exists. if (findConnection(receivers, signal, slot, thisArg)) { return false; } // Choose the best object for the receiver. let receiver = thisArg || slot; // Ensure the receiver's array of senders is created. let senders = sendersForReceiver.get(receiver); if (!senders) { senders = []; sendersForReceiver.set(receiver, senders); } // Create a new connection and add it to the end of each array. let connection = { signal, slot, thisArg }; receivers.push(connection); senders.push(connection); // Indicate a successful connection. return true; } Private.connect = connect; /** * Disconnect a slot from a signal. * * @param signal - The signal of interest. * * @param slot - The slot to disconnect from the signal. * * @param thisArg - The `this` context for the slot. If provided, * this must be a non-primitive object. * * @returns `true` if the connection is removed, `false` otherwise. */ function disconnect(signal, slot, thisArg) { // Coerce a `null` `thisArg` to `undefined`. thisArg = thisArg || undefined; // Lookup the list of receivers, and bail if none exist. let receivers = receiversForSender.get(signal.sender); if (!receivers || receivers.length === 0) { return false; } // Bail if no matching connection exits. let connection = findConnection(receivers, signal, slot, thisArg); if (!connection) { return false; } // Choose the best object for the receiver. let receiver = thisArg || slot; // Lookup the array of senders, which is now known to exist. let senders = sendersForReceiver.get(receiver); // Clear the connection and schedule cleanup of the arrays. connection.signal = null; scheduleCleanup(receivers); scheduleCleanup(senders); // Indicate a successful disconnection. return true; } Private.disconnect = disconnect; /** * Remove all connections between a sender and receiver. * * @param sender - The sender object of interest. * * @param receiver - The receiver object of interest. */ function disconnectBetween(sender, receiver) { // If there are no receivers, there is nothing to do. let receivers = receiversForSender.get(sender); if (!receivers || receivers.length === 0) { return; } // If there are no senders, there is nothing to do. let senders = sendersForReceiver.get(receiver); if (!senders || senders.length === 0) { return; } // Clear each connection between the sender and receiver. for (const connection of senders) { // Skip connections which have already been cleared. if (!connection.signal) { continue; } // Clear the connection if it matches the sender. if (connection.signal.sender === sender) { connection.signal = null; } } // Schedule a cleanup of the senders and receivers. scheduleCleanup(receivers); scheduleCleanup(senders); } Private.disconnectBetween = disconnectBetween; /** * Remove all connections where the given object is the sender. * * @param sender - The sender object of interest. */ function disconnectSender(sender) { // If there are no receivers, there is nothing to do. let receivers = receiversForSender.get(sender); if (!receivers || receivers.length === 0) { return; } // Clear each receiver connection. for (const connection of receivers) { // Skip connections which have already been cleared. if (!connection.signal) { continue; } // Choose the best object for the receiver. let receiver = connection.thisArg || connection.slot; // Clear the connection. connection.signal = null; // Cleanup the array of senders, which is now known to exist. scheduleCleanup(sendersForReceiver.get(receiver)); } // Schedule a cleanup of the receivers. scheduleCleanup(receivers); } Private.disconnectSender = disconnectSender; /** * Remove all connections where the given object is the receiver. * * @param receiver - The receiver object of interest. */ function disconnectReceiver(receiver) { // If there are no senders, there is nothing to do. let senders = sendersForReceiver.get(receiver); if (!senders || senders.length === 0) { return; } // Clear each sender connection. for (const connection of senders) { // Skip connections which have already been cleared. if (!connection.signal) { continue; } // Lookup the sender for the connection. let sender = connection.signal.sender; // Clear the connection. connection.signal = null; // Cleanup the array of receivers, which is now known to exist. scheduleCleanup(receiversForSender.get(sender)); } // Schedule a cleanup of the list of senders. scheduleCleanup(senders); } Private.disconnectReceiver = disconnectReceiver; /** * Remove all connections where an object is the sender or receiver. * * @param object - The object of interest. */ function disconnectAll(object) { // Remove all connections where the given object is the sender. disconnectSender(object); // Remove all connections where the given object is the receiver. disconnectReceiver(object); } Private.disconnectAll = disconnectAll; /** * Emit a signal and invoke its connected slots. * * @param signal - The signal of interest. * * @param args - The args to pass to the connected slots. * * #### Notes * Slots are invoked synchronously in connection order. * * Exceptions thrown by connected slots will be caught and logged. */ function emit(signal, args) { // If there are no receivers, there is nothing to do. let receivers = receiversForSender.get(signal.sender); if (!receivers || receivers.length === 0) { return; } // Invoke the slots for connections with a matching signal. // Any connections added during emission are not invoked. for (let i = 0, n = receivers.length; i < n; ++i) { let connection = receivers[i]; if (connection.signal === signal) { invokeSlot(connection, args); } } } Private.emit = emit; /** * A weak mapping of sender to array of receiver connections. */ const receiversForSender = new WeakMap(); /** * A weak mapping of receiver to array of sender connections. */ const sendersForReceiver = new WeakMap(); /** * A set of connection arrays which are pending cleanup. */ const dirtySet = new Set(); /** * A function to schedule an event loop callback. */ const schedule = (() => { let ok = typeof requestAnimationFrame === 'function'; return ok ? requestAnimationFrame : setImmediate; })(); /** * Find a connection which matches the given parameters. */ function findConnection(connections, signal, slot, thisArg) { return algorithm.find(connections, connection => connection.signal === signal && connection.slot === slot && connection.thisArg === thisArg); } /** * Invoke a slot with the given parameters. * * The connection is assumed to be valid. * * Exceptions in the slot will be caught and logged. */ function invokeSlot(connection, args) { let { signal, slot, thisArg } = connection; try { slot.call(thisArg, signal.sender, args); } catch (err) { Private.exceptionHandler(err); } } /** * Schedule a cleanup of a connection array. * * This will add the array to the dirty set and schedule a deferred * cleanup of the array contents. On cleanup, any connection with a * `null` signal will be removed from the array. */ function scheduleCleanup(array) { if (dirtySet.size === 0) { schedule(cleanupDirtySet); } dirtySet.add(array); } /** * Cleanup the connection lists 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(cleanupConnections); dirtySet.clear(); } /** * Cleanup the dirty connections in a connections array. * * This will remove any connection with a `null` signal. * * This function should only be invoked asynchronously, when the * stack frame is guaranteed to not be on the path of user code. */ function cleanupConnections(connections) { algorithm.ArrayExt.removeAllWhere(connections, isDeadConnection); } /** * Test whether a connection is dead. * * A dead connection has a `null` signal. */ function isDeadConnection(connection) { return connection.signal === null; } })(Private || (Private = {})); exports.Signal = Signal; exports.Stream = Stream; })); //# sourceMappingURL=index.js.map