@itwin/core-bentley
Version:
Bentley JavaScript core components
256 lines • 10.5 kB
JavaScript
"use strict";
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
* @module Events
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.BeEventList = exports.BeUnorderedUiEvent = exports.BeUnorderedEvent = exports.BeUiEvent = exports.BeEvent = void 0;
const UnexpectedErrors_1 = require("./UnexpectedErrors");
/**
* Manages a set of *listeners* for a particular event and notifies them when the event is raised.
* This class is usually instantiated inside of a container class and
* exposed as a property for others to *subscribe* via [[BeEvent.addListener]].
* @public
*/
class BeEvent {
_listeners = [];
_emitDepth = 0;
/** The number of listeners currently subscribed to the event. */
get numberOfListeners() { return this._listeners.length; }
/**
* Registers a Listener to be executed whenever this event is raised.
* @param listener The function to be executed when the event is raised.
* @param scope An optional object scope to serve as the 'this' pointer when listener is invoked.
* @returns A function that will remove this event listener.
* @see [[BeEvent.raiseEvent]], [[BeEvent.removeListener]]
*/
addListener(listener, scope) {
this._listeners.push({ listener, scope, once: false });
return () => this.removeListener(listener, scope);
}
/**
* Registers a callback function to be executed *only once* when the event is raised.
* @param listener The function to be executed once when the event is raised.
* @param scope An optional object scope to serve as the `this` pointer in which the listener function will execute.
* @returns A function that will remove this event listener.
* @see [[BeEvent.raiseEvent]], [[BeEvent.removeListener]]
*/
addOnce(listener, scope) {
this._listeners.push({ listener, scope, once: true });
return () => this.removeListener(listener, scope);
}
/**
* Un-register a previously registered listener.
* @param listener The listener to be unregistered.
* @param scope The scope that was originally passed to addListener.
* @returns 'true' if the listener was removed; 'false' if the listener and scope are not registered with the event.
* @see [[BeEvent.raiseEvent]], [[BeEvent.addListener]]
*/
removeListener(listener, scope) {
const listeners = this._listeners;
for (let i = 0; i < listeners.length; ++i) {
const context = listeners[i];
if (context.listener === listener && context.scope === scope) {
if (this._emitDepth > 0) {
context.listener = undefined;
}
else {
listeners.splice(i, 1);
}
return true;
}
}
return false;
}
/**
* Raises the event by calling each registered listener with the supplied arguments.
* @param args This method takes any number of parameters and passes them through to the listeners.
* @see [[BeEvent.removeListener]], [[BeEvent.addListener]]
*/
raiseEvent(...args) {
this._emitDepth++;
const listeners = this._listeners;
const length = listeners.length;
let dropped = false;
for (let i = 0; i < length; ++i) {
const context = listeners[i];
if (!context.listener) {
dropped = true;
}
else {
try {
context.listener.apply(context.scope, args);
}
catch (e) {
UnexpectedErrors_1.UnexpectedErrors.handle(e);
}
if (!context.listener) {
// listener was removed during its own callback
dropped = true;
}
else if (context.once) {
context.listener = undefined;
dropped = true;
}
}
}
this._emitDepth--;
// Only sweep tombstoned entries when the outermost emit completes,
// so nested raiseEvent calls never mutate the array mid-iteration.
if (dropped && this._emitDepth === 0)
this._listeners = this._listeners.filter((ctx) => ctx.listener !== undefined);
}
/** Determine whether this BeEvent has a specified listener registered.
* @param listener The listener to check.
* @param scope optional scope argument to match call to addListener
*/
has(listener, scope) {
for (const ctx of this._listeners) {
if (ctx.listener === listener && ctx.scope === scope) {
return true;
}
}
return false;
}
/** Clear all Listeners from this BeEvent. */
clear() { this._listeners.length = 0; }
}
exports.BeEvent = BeEvent;
/** Specialization of BeEvent for events that take a single strongly typed argument, primarily used for UI events.
* @public
*/
class BeUiEvent extends BeEvent {
/** Raises event with single strongly typed argument. */
emit(args) { this.raiseEvent(args); }
}
exports.BeUiEvent = BeUiEvent;
/**
* Manages a set of *listeners* for a particular event and notifies them when the event is raised.
* Unlike [[BeEvent]], this class uses a `Set` internally to support safe concurrent modification
* during emit. When a listener is removed during emit, it is marked for deferred removal instead
* of mutating the set immediately.
*
* Listeners are managed exclusively through the disposal closure returned by [[addListener]] and
* [[addOnce]]. There is no `removeListener` or `has` method — callers must capture the returned
* closure to unsubscribe.
* @beta
*/
class BeUnorderedEvent {
_listeners = new Set();
_emitDepth = 0;
_hasTombstones = false;
/** The number of listeners currently subscribed to the event.
* @note During `raiseEvent()`, this may include listeners that have been removed or are one-shot
* but are awaiting deferred cleanup. The count is accurate outside of `raiseEvent()`.
*/
get numberOfListeners() { return this._listeners.size; }
/**
* Registers a Listener to be executed whenever this event is raised.
* @param listener The function to be executed when the event is raised.
* @param scope An optional object scope to serve as the 'this' pointer when listener is invoked.
* @returns A function that will remove this event listener in O(1).
*/
addListener(listener, scope) {
const ctx = { listener, scope, once: false };
this._listeners.add(ctx);
return () => this._removeCtx(ctx);
}
/**
* Registers a callback function to be executed *only once* when the event is raised.
* @param listener The function to be executed once when the event is raised.
* @param scope An optional object scope to serve as the `this` pointer in which the listener function will execute.
* @returns A function that will remove this event listener in O(1).
*/
addOnce(listener, scope) {
const ctx = { listener, scope, once: true };
this._listeners.add(ctx);
return () => this._removeCtx(ctx);
}
/** Remove a specific context entry, deferring during emit. */
_removeCtx(ctx) {
if (this._emitDepth > 0) {
ctx.listener = undefined; // tombstone for deferred cleanup
this._hasTombstones = true;
}
else {
this._listeners.delete(ctx);
}
}
/**
* Raises the event by calling each registered listener with the supplied arguments.
* @param args This method takes any number of parameters and passes them through to the listeners.
*/
raiseEvent(...args) {
this._emitDepth++;
for (const ctx of this._listeners) {
if (!ctx.listener) {
continue;
}
try {
ctx.listener.apply(ctx.scope, args);
}
catch (e) {
UnexpectedErrors_1.UnexpectedErrors.handle(e);
}
if (ctx.listener && ctx.once) {
ctx.listener = undefined;
this._hasTombstones = true;
}
}
this._emitDepth--;
// Only clean up tombstoned entries when we're back at the outermost emit
if (this._hasTombstones && this._emitDepth === 0) {
this._hasTombstones = false;
for (const ctx of this._listeners) {
if (ctx.listener === undefined)
this._listeners.delete(ctx);
}
}
}
/** Clear all listeners from this BeUnorderedEvent.
* @note If called during `raiseEvent`, remaining listeners in the current iteration are skipped
* immediately rather than being tombstoned.
*/
clear() { this._listeners.clear(); }
}
exports.BeUnorderedEvent = BeUnorderedEvent;
/** Specialization of BeUnorderedEvent for events that take a single strongly typed argument, primarily used for UI events.
* @beta
*/
class BeUnorderedUiEvent extends BeUnorderedEvent {
/** Raises event with single strongly typed argument. */
emit(args) { this.raiseEvent(args); }
}
exports.BeUnorderedUiEvent = BeUnorderedUiEvent;
/**
* A list of BeEvent objects, accessible by an event name.
* This class may be used instead of explicitly declaring each BeEvent as a member of a containing class.
* @public
*/
class BeEventList {
_events = {};
/**
* Gets the event associated with the specified name, creating the event if it does not already exist.
* @param name The name of the event.
*/
get(name) {
let event = this._events[name];
if (event)
return event;
event = new BeEvent();
this._events[name] = event;
return event;
}
/**
* Removes the event associated with a name.
* @param name The name of the event.
*/
remove(name) {
this._events[name] = undefined;
}
}
exports.BeEventList = BeEventList;
//# sourceMappingURL=BeEvent.js.map