@lumino/signaling
Version:
Lumino Signals and Slots
613 lines (609 loc) • 22 kB
JavaScript
(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