@sussudio/base
Version:
Internal APIs for VS Code's utilities and user interface building blocks.
1,036 lines (1,031 loc) • 30.3 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { onUnexpectedError } from './errors.mjs';
import { once as onceFn } from './functional.mjs';
import { combinedDisposable, Disposable, DisposableStore, SafeDisposable, toDisposable } from './lifecycle.mjs';
import { LinkedList } from './linkedList.mjs';
import { StopWatch } from './stopwatch.mjs';
// -----------------------------------------------------------------------------------------------------------------------
// Uncomment the next line to print warnings whenever an emitter with listeners is disposed. That is a sign of code smell.
// -----------------------------------------------------------------------------------------------------------------------
const _enableDisposeWithListenerWarning = false;
// _enableDisposeWithListenerWarning = Boolean("TRUE"); // causes a linter warning so that it cannot be pushed
// -----------------------------------------------------------------------------------------------------------------------
// Uncomment the next line to print warnings whenever a snapshotted event is used repeatedly without cleanup.
// See https://github.com/microsoft/vscode/issues/142851
// -----------------------------------------------------------------------------------------------------------------------
const _enableSnapshotPotentialLeakWarning = false;
export var Event;
(function (Event) {
Event.None = () => Disposable.None;
function _addLeakageTraceLogic(options) {
if (_enableSnapshotPotentialLeakWarning) {
const { onDidAddListener: origListenerDidAdd } = options;
const stack = Stacktrace.create();
let count = 0;
options.onDidAddListener = () => {
if (++count === 2) {
console.warn(
'snapshotted emitter LIKELY used public and SHOULD HAVE BEEN created with DisposableStore. snapshotted here',
);
stack.print();
}
origListenerDidAdd?.();
};
}
}
/**
* Given an event, returns another event which debounces calls and defers the listeners to a later task via a shared
* `setTimeout`. The event is converted into a signal (`Event<void>`) to avoid additional object creation as a
* result of merging events and to try prevent race conditions that could arise when using related deferred and
* non-deferred events.
*
* This is useful for deferring non-critical work (eg. general UI updates) to ensure it does not block critical work
* (eg. latency of keypress to text rendered).
*
* *NOTE* that this function returns an `Event` and it MUST be called with a `DisposableStore` whenever the returned
* event is accessible to "third parties", e.g the event is a public property. Otherwise a leaked listener on the
* returned event causes this utility to leak a listener on the original event.
*
* @param event The event source for the new event.
* @param disposable A disposable store to add the new EventEmitter to.
*/
function defer(event, disposable) {
return debounce(event, () => void 0, 0, undefined, undefined, disposable);
}
Event.defer = defer;
/**
* Given an event, returns another event which only fires once.
*
* @param event The event source for the new event.
*/
function once(event) {
return (listener, thisArgs = null, disposables) => {
// we need this, in case the event fires during the listener call
let didFire = false;
let result = undefined;
result = event(
(e) => {
if (didFire) {
return;
} else if (result) {
result.dispose();
} else {
didFire = true;
}
return listener.call(thisArgs, e);
},
null,
disposables,
);
if (didFire) {
result.dispose();
}
return result;
};
}
Event.once = once;
/**
* Maps an event of one type into an event of another type using a mapping function, similar to how
* `Array.prototype.map` works.
*
* *NOTE* that this function returns an `Event` and it MUST be called with a `DisposableStore` whenever the returned
* event is accessible to "third parties", e.g the event is a public property. Otherwise a leaked listener on the
* returned event causes this utility to leak a listener on the original event.
*
* @param event The event source for the new event.
* @param map The mapping function.
* @param disposable A disposable store to add the new EventEmitter to.
*/
function map(event, map, disposable) {
return snapshot(
(listener, thisArgs = null, disposables) => event((i) => listener.call(thisArgs, map(i)), null, disposables),
disposable,
);
}
Event.map = map;
/**
* *NOTE* that this function returns an `Event` and it MUST be called with a `DisposableStore` whenever the returned
* event is accessible to "third parties", e.g the event is a public property. Otherwise a leaked listener on the
* returned event causes this utility to leak a listener on the original event.
*/
function forEach(event, each, disposable) {
return snapshot(
(listener, thisArgs = null, disposables) =>
event(
(i) => {
each(i);
listener.call(thisArgs, i);
},
null,
disposables,
),
disposable,
);
}
Event.forEach = forEach;
function filter(event, filter, disposable) {
return snapshot(
(listener, thisArgs = null, disposables) =>
event((e) => filter(e) && listener.call(thisArgs, e), null, disposables),
disposable,
);
}
Event.filter = filter;
/**
* Given an event, returns the same event but typed as `Event<void>`.
*/
function signal(event) {
return event;
}
Event.signal = signal;
function any(...events) {
return (listener, thisArgs = null, disposables) =>
combinedDisposable(...events.map((event) => event((e) => listener.call(thisArgs, e), null, disposables)));
}
Event.any = any;
/**
* *NOTE* that this function returns an `Event` and it MUST be called with a `DisposableStore` whenever the returned
* event is accessible to "third parties", e.g the event is a public property. Otherwise a leaked listener on the
* returned event causes this utility to leak a listener on the original event.
*/
function reduce(event, merge, initial, disposable) {
let output = initial;
return map(
event,
(e) => {
output = merge(output, e);
return output;
},
disposable,
);
}
Event.reduce = reduce;
function snapshot(event, disposable) {
let listener;
const options = {
onWillAddFirstListener() {
listener = event(emitter.fire, emitter);
},
onDidRemoveLastListener() {
listener?.dispose();
},
};
if (!disposable) {
_addLeakageTraceLogic(options);
}
const emitter = new Emitter(options);
disposable?.add(emitter);
return emitter.event;
}
function debounce(event, merge, delay = 100, leading = false, leakWarningThreshold, disposable) {
let subscription;
let output = undefined;
let handle = undefined;
let numDebouncedCalls = 0;
const options = {
leakWarningThreshold,
onWillAddFirstListener() {
subscription = event((cur) => {
numDebouncedCalls++;
output = merge(output, cur);
if (leading && !handle) {
emitter.fire(output);
output = undefined;
}
clearTimeout(handle);
handle = setTimeout(() => {
const _output = output;
output = undefined;
handle = undefined;
if (!leading || numDebouncedCalls > 1) {
emitter.fire(_output);
}
numDebouncedCalls = 0;
}, delay);
});
},
onDidRemoveLastListener() {
subscription.dispose();
},
};
if (!disposable) {
_addLeakageTraceLogic(options);
}
const emitter = new Emitter(options);
disposable?.add(emitter);
return emitter.event;
}
Event.debounce = debounce;
/**
* Debounces an event, firing after some delay (default=0) with an array of all event original objects.
*
* *NOTE* that this function returns an `Event` and it MUST be called with a `DisposableStore` whenever the returned
* event is accessible to "third parties", e.g the event is a public property. Otherwise a leaked listener on the
* returned event causes this utility to leak a listener on the original event.
*/
function accumulate(event, delay = 0, disposable) {
return Event.debounce(
event,
(last, e) => {
if (!last) {
return [e];
}
last.push(e);
return last;
},
delay,
undefined,
undefined,
disposable,
);
}
Event.accumulate = accumulate;
/**
* *NOTE* that this function returns an `Event` and it MUST be called with a `DisposableStore` whenever the returned
* event is accessible to "third parties", e.g the event is a public property. Otherwise a leaked listener on the
* returned event causes this utility to leak a listener on the original event.
*/
function latch(event, equals = (a, b) => a === b, disposable) {
let firstCall = true;
let cache;
return filter(
event,
(value) => {
const shouldEmit = firstCall || !equals(value, cache);
firstCall = false;
cache = value;
return shouldEmit;
},
disposable,
);
}
Event.latch = latch;
/**
* Splits an event whose parameter is a union type into 2 separate events for each type in the union.
*
* *NOTE* that this function returns an `Event` and it MUST be called with a `DisposableStore` whenever the returned
* event is accessible to "third parties", e.g the event is a public property. Otherwise a leaked listener on the
* returned event causes this utility to leak a listener on the original event.
*
* @example
* ```
* const event = new EventEmitter<number | undefined>().event;
* const [numberEvent, undefinedEvent] = Event.split(event, isUndefined);
* ```
*
* @param event The event source for the new event.
* @param isT A function that determines what event is of the first type.
* @param disposable A disposable store to add the new EventEmitter to.
*/
function split(event, isT, disposable) {
return [Event.filter(event, isT, disposable), Event.filter(event, (e) => !isT(e), disposable)];
}
Event.split = split;
/**
* *NOTE* that this function returns an `Event` and it MUST be called with a `DisposableStore` whenever the returned
* event is accessible to "third parties", e.g the event is a public property. Otherwise a leaked listener on the
* returned event causes this utility to leak a listener on the original event.
*/
function buffer(event, flushAfterTimeout = false, _buffer = []) {
let buffer = _buffer.slice();
let listener = event((e) => {
if (buffer) {
buffer.push(e);
} else {
emitter.fire(e);
}
});
const flush = () => {
buffer?.forEach((e) => emitter.fire(e));
buffer = null;
};
const emitter = new Emitter({
onWillAddFirstListener() {
if (!listener) {
listener = event((e) => emitter.fire(e));
}
},
onDidAddFirstListener() {
if (buffer) {
if (flushAfterTimeout) {
setTimeout(flush);
} else {
flush();
}
}
},
onDidRemoveLastListener() {
if (listener) {
listener.dispose();
}
listener = null;
},
});
return emitter.event;
}
Event.buffer = buffer;
class ChainableEvent {
event;
disposables = new DisposableStore();
constructor(event) {
this.event = event;
}
map(fn) {
return new ChainableEvent(map(this.event, fn, this.disposables));
}
forEach(fn) {
return new ChainableEvent(forEach(this.event, fn, this.disposables));
}
filter(fn) {
return new ChainableEvent(filter(this.event, fn, this.disposables));
}
reduce(merge, initial) {
return new ChainableEvent(reduce(this.event, merge, initial, this.disposables));
}
latch() {
return new ChainableEvent(latch(this.event, undefined, this.disposables));
}
debounce(merge, delay = 100, leading = false, leakWarningThreshold) {
return new ChainableEvent(debounce(this.event, merge, delay, leading, leakWarningThreshold, this.disposables));
}
on(listener, thisArgs, disposables) {
return this.event(listener, thisArgs, disposables);
}
once(listener, thisArgs, disposables) {
return once(this.event)(listener, thisArgs, disposables);
}
dispose() {
this.disposables.dispose();
}
}
function chain(event) {
return new ChainableEvent(event);
}
Event.chain = chain;
function fromNodeEventEmitter(emitter, eventName, map = (id) => id) {
const fn = (...args) => result.fire(map(...args));
const onFirstListenerAdd = () => emitter.on(eventName, fn);
const onLastListenerRemove = () => emitter.removeListener(eventName, fn);
const result = new Emitter({
onWillAddFirstListener: onFirstListenerAdd,
onDidRemoveLastListener: onLastListenerRemove,
});
return result.event;
}
Event.fromNodeEventEmitter = fromNodeEventEmitter;
function fromDOMEventEmitter(emitter, eventName, map = (id) => id) {
const fn = (...args) => result.fire(map(...args));
const onFirstListenerAdd = () => emitter.addEventListener(eventName, fn);
const onLastListenerRemove = () => emitter.removeEventListener(eventName, fn);
const result = new Emitter({
onWillAddFirstListener: onFirstListenerAdd,
onDidRemoveLastListener: onLastListenerRemove,
});
return result.event;
}
Event.fromDOMEventEmitter = fromDOMEventEmitter;
function toPromise(event) {
return new Promise((resolve) => once(event)(resolve));
}
Event.toPromise = toPromise;
function runAndSubscribe(event, handler) {
handler(undefined);
return event((e) => handler(e));
}
Event.runAndSubscribe = runAndSubscribe;
function runAndSubscribeWithStore(event, handler) {
let store = null;
function run(e) {
store?.dispose();
store = new DisposableStore();
handler(e, store);
}
run(undefined);
const disposable = event((e) => run(e));
return toDisposable(() => {
disposable.dispose();
store?.dispose();
});
}
Event.runAndSubscribeWithStore = runAndSubscribeWithStore;
class EmitterObserver {
obs;
emitter;
_counter = 0;
_hasChanged = false;
constructor(obs, store) {
this.obs = obs;
const options = {
onWillAddFirstListener: () => {
obs.addObserver(this);
},
onDidRemoveLastListener: () => {
obs.removeObserver(this);
},
};
if (!store) {
_addLeakageTraceLogic(options);
}
this.emitter = new Emitter(options);
if (store) {
store.add(this.emitter);
}
}
beginUpdate(_observable) {
// console.assert(_observable === this.obs);
this._counter++;
}
handleChange(_observable, _change) {
this._hasChanged = true;
}
endUpdate(_observable) {
if (--this._counter === 0) {
if (this._hasChanged) {
this._hasChanged = false;
this.emitter.fire(this.obs.get());
}
}
}
}
function fromObservable(obs, store) {
const observer = new EmitterObserver(obs, store);
return observer.emitter.event;
}
Event.fromObservable = fromObservable;
})(Event || (Event = {}));
export class EventProfiling {
static all = new Set();
static _idPool = 0;
name;
listenerCount = 0;
invocationCount = 0;
elapsedOverall = 0;
durations = [];
_stopWatch;
constructor(name) {
this.name = `${name}_${EventProfiling._idPool++}`;
EventProfiling.all.add(this);
}
start(listenerCount) {
this._stopWatch = new StopWatch(true);
this.listenerCount = listenerCount;
}
stop() {
if (this._stopWatch) {
const elapsed = this._stopWatch.elapsed();
this.durations.push(elapsed);
this.elapsedOverall += elapsed;
this.invocationCount += 1;
this._stopWatch = undefined;
}
}
}
let _globalLeakWarningThreshold = -1;
export function setGlobalLeakWarningThreshold(n) {
const oldValue = _globalLeakWarningThreshold;
_globalLeakWarningThreshold = n;
return {
dispose() {
_globalLeakWarningThreshold = oldValue;
},
};
}
class LeakageMonitor {
threshold;
name;
_stacks;
_warnCountdown = 0;
constructor(threshold, name = Math.random().toString(18).slice(2, 5)) {
this.threshold = threshold;
this.name = name;
}
dispose() {
this._stacks?.clear();
}
check(stack, listenerCount) {
const threshold = this.threshold;
if (threshold <= 0 || listenerCount < threshold) {
return undefined;
}
if (!this._stacks) {
this._stacks = new Map();
}
const count = this._stacks.get(stack.value) || 0;
this._stacks.set(stack.value, count + 1);
this._warnCountdown -= 1;
if (this._warnCountdown <= 0) {
// only warn on first exceed and then every time the limit
// is exceeded by 50% again
this._warnCountdown = threshold * 0.5;
// find most frequent listener and print warning
let topStack;
let topCount = 0;
for (const [stack, count] of this._stacks) {
if (!topStack || topCount < count) {
topStack = stack;
topCount = count;
}
}
console.warn(
`[${this.name}] potential listener LEAK detected, having ${listenerCount} listeners already. MOST frequent listener (${topCount}):`,
);
console.warn(topStack);
}
return () => {
const count = this._stacks.get(stack.value) || 0;
this._stacks.set(stack.value, count - 1);
};
}
}
class Stacktrace {
value;
static create() {
return new Stacktrace(new Error().stack ?? '');
}
constructor(value) {
this.value = value;
}
print() {
console.warn(this.value.split('\n').slice(2).join('\n'));
}
}
class Listener {
callback;
callbackThis;
stack;
subscription = new SafeDisposable();
constructor(callback, callbackThis, stack) {
this.callback = callback;
this.callbackThis = callbackThis;
this.stack = stack;
}
invoke(e) {
this.callback.call(this.callbackThis, e);
}
}
/**
* The Emitter can be used to expose an Event to the public
* to fire it from the insides.
* Sample:
class Document {
private readonly _onDidChange = new Emitter<(value:string)=>any>();
public onDidChange = this._onDidChange.event;
// getter-style
// get onDidChange(): Event<(value:string)=>any> {
// return this._onDidChange.event;
// }
private _doIt() {
//...
this._onDidChange.fire(value);
}
}
*/
export class Emitter {
_options;
_leakageMon;
_perfMon;
_disposed = false;
_event;
_deliveryQueue;
_listeners;
constructor(options) {
this._options = options;
this._leakageMon =
_globalLeakWarningThreshold > 0 || this._options?.leakWarningThreshold
? new LeakageMonitor(this._options?.leakWarningThreshold ?? _globalLeakWarningThreshold)
: undefined;
this._perfMon = this._options?._profName ? new EventProfiling(this._options._profName) : undefined;
this._deliveryQueue = this._options?.deliveryQueue;
}
dispose() {
if (!this._disposed) {
this._disposed = true;
// It is bad to have listeners at the time of disposing an emitter, it is worst to have listeners keep the emitter
// alive via the reference that's embedded in their disposables. Therefore we loop over all remaining listeners and
// unset their subscriptions/disposables. Looping and blaming remaining listeners is done on next tick because the
// the following programming pattern is very popular:
//
// const someModel = this._disposables.add(new ModelObject()); // (1) create and register model
// this._disposables.add(someModel.onDidChange(() => { ... }); // (2) subscribe and register model-event listener
// ...later...
// this._disposables.dispose(); disposes (1) then (2): don't warn after (1) but after the "overall dispose" is done
if (this._listeners) {
if (_enableDisposeWithListenerWarning) {
const listeners = Array.from(this._listeners);
queueMicrotask(() => {
for (const listener of listeners) {
if (listener.subscription.isset()) {
listener.subscription.unset();
listener.stack?.print();
}
}
});
}
this._listeners.clear();
}
this._deliveryQueue?.clear(this);
this._options?.onDidRemoveLastListener?.();
this._leakageMon?.dispose();
}
}
/**
* For the public to allow to subscribe
* to events from this Emitter
*/
get event() {
if (!this._event) {
this._event = (callback, thisArgs, disposables) => {
if (!this._listeners) {
this._listeners = new LinkedList();
}
if (this._leakageMon && this._listeners.size > this._leakageMon.threshold * 3) {
console.warn(
`[${this._leakageMon.name}] REFUSES to accept new listeners because it exceeded its threshold by far`,
);
return Disposable.None;
}
const firstListener = this._listeners.isEmpty();
if (firstListener && this._options?.onWillAddFirstListener) {
this._options.onWillAddFirstListener(this);
}
let removeMonitor;
let stack;
if (this._leakageMon && this._listeners.size >= Math.ceil(this._leakageMon.threshold * 0.2)) {
// check and record this emitter for potential leakage
stack = Stacktrace.create();
removeMonitor = this._leakageMon.check(stack, this._listeners.size + 1);
}
if (_enableDisposeWithListenerWarning) {
stack = stack ?? Stacktrace.create();
}
const listener = new Listener(callback, thisArgs, stack);
const removeListener = this._listeners.push(listener);
if (firstListener && this._options?.onDidAddFirstListener) {
this._options.onDidAddFirstListener(this);
}
if (this._options?.onDidAddListener) {
this._options.onDidAddListener(this, callback, thisArgs);
}
const result = listener.subscription.set(() => {
removeMonitor?.();
if (!this._disposed) {
removeListener();
if (this._options && this._options.onDidRemoveLastListener) {
const hasListeners = this._listeners && !this._listeners.isEmpty();
if (!hasListeners) {
this._options.onDidRemoveLastListener(this);
}
}
}
});
if (disposables instanceof DisposableStore) {
disposables.add(result);
} else if (Array.isArray(disposables)) {
disposables.push(result);
}
return result;
};
}
return this._event;
}
/**
* To be kept private to fire an event to
* subscribers
*/
fire(event) {
if (this._listeners) {
// put all [listener,event]-pairs into delivery queue
// then emit all event. an inner/nested event might be
// the driver of this
if (!this._deliveryQueue) {
this._deliveryQueue = new PrivateEventDeliveryQueue();
}
for (const listener of this._listeners) {
this._deliveryQueue.push(this, listener, event);
}
// start/stop performance insight collection
this._perfMon?.start(this._deliveryQueue.size);
this._deliveryQueue.deliver();
this._perfMon?.stop();
}
}
hasListeners() {
if (!this._listeners) {
return false;
}
return !this._listeners.isEmpty();
}
}
export class EventDeliveryQueue {
_queue = new LinkedList();
get size() {
return this._queue.size;
}
push(emitter, listener, event) {
this._queue.push(new EventDeliveryQueueElement(emitter, listener, event));
}
clear(emitter) {
const newQueue = new LinkedList();
for (const element of this._queue) {
if (element.emitter !== emitter) {
newQueue.push(element);
}
}
this._queue = newQueue;
}
deliver() {
while (this._queue.size > 0) {
const element = this._queue.shift();
try {
element.listener.invoke(element.event);
} catch (e) {
onUnexpectedError(e);
}
}
}
}
/**
* An `EventDeliveryQueue` that is guaranteed to be used by a single `Emitter`.
*/
class PrivateEventDeliveryQueue extends EventDeliveryQueue {
clear(emitter) {
// Here we can just clear the entire linked list because
// all elements are guaranteed to belong to this emitter
this._queue.clear();
}
}
class EventDeliveryQueueElement {
emitter;
listener;
event;
constructor(emitter, listener, event) {
this.emitter = emitter;
this.listener = listener;
this.event = event;
}
}
export class AsyncEmitter extends Emitter {
_asyncDeliveryQueue;
async fireAsync(data, token, promiseJoin) {
if (!this._listeners) {
return;
}
if (!this._asyncDeliveryQueue) {
this._asyncDeliveryQueue = new LinkedList();
}
for (const listener of this._listeners) {
this._asyncDeliveryQueue.push([listener, data]);
}
while (this._asyncDeliveryQueue.size > 0 && !token.isCancellationRequested) {
const [listener, data] = this._asyncDeliveryQueue.shift();
const thenables = [];
const event = {
...data,
token,
waitUntil: (p) => {
if (Object.isFrozen(thenables)) {
throw new Error('waitUntil can NOT be called asynchronous');
}
if (promiseJoin) {
p = promiseJoin(p, listener.callback);
}
thenables.push(p);
},
};
try {
listener.invoke(event);
} catch (e) {
onUnexpectedError(e);
continue;
}
// freeze thenables-collection to enforce sync-calls to
// wait until and then wait for all thenables to resolve
Object.freeze(thenables);
await Promise.allSettled(thenables).then((values) => {
for (const value of values) {
if (value.status === 'rejected') {
onUnexpectedError(value.reason);
}
}
});
}
}
}
export class PauseableEmitter extends Emitter {
_isPaused = 0;
_eventQueue = new LinkedList();
_mergeFn;
constructor(options) {
super(options);
this._mergeFn = options?.merge;
}
pause() {
this._isPaused++;
}
resume() {
if (this._isPaused !== 0 && --this._isPaused === 0) {
if (this._mergeFn) {
// use the merge function to create a single composite
// event. make a copy in case firing pauses this emitter
if (this._eventQueue.size > 0) {
const events = Array.from(this._eventQueue);
this._eventQueue.clear();
super.fire(this._mergeFn(events));
}
} else {
// no merging, fire each event individually and test
// that this emitter isn't paused halfway through
while (!this._isPaused && this._eventQueue.size !== 0) {
super.fire(this._eventQueue.shift());
}
}
}
}
fire(event) {
if (this._listeners) {
if (this._isPaused !== 0) {
this._eventQueue.push(event);
} else {
super.fire(event);
}
}
}
}
export class DebounceEmitter extends PauseableEmitter {
_delay;
_handle;
constructor(options) {
super(options);
this._delay = options.delay ?? 100;
}
fire(event) {
if (!this._handle) {
this.pause();
this._handle = setTimeout(() => {
this._handle = undefined;
this.resume();
}, this._delay);
}
super.fire(event);
}
}
/**
* An emitter which queue all events and then process them at the
* end of the event loop.
*/
export class MicrotaskEmitter extends Emitter {
_queuedEvents = [];
_mergeFn;
constructor(options) {
super(options);
this._mergeFn = options?.merge;
}
fire(event) {
if (!this.hasListeners()) {
return;
}
this._queuedEvents.push(event);
if (this._queuedEvents.length === 1) {
queueMicrotask(() => {
if (this._mergeFn) {
super.fire(this._mergeFn(this._queuedEvents));
} else {
this._queuedEvents.forEach((e) => super.fire(e));
}
this._queuedEvents = [];
});
}
}
}
export class EventMultiplexer {
emitter;
hasListeners = false;
events = [];
constructor() {
this.emitter = new Emitter({
onWillAddFirstListener: () => this.onFirstListenerAdd(),
onDidRemoveLastListener: () => this.onLastListenerRemove(),
});
}
get event() {
return this.emitter.event;
}
add(event) {
const e = { event: event, listener: null };
this.events.push(e);
if (this.hasListeners) {
this.hook(e);
}
const dispose = () => {
if (this.hasListeners) {
this.unhook(e);
}
const idx = this.events.indexOf(e);
this.events.splice(idx, 1);
};
return toDisposable(onceFn(dispose));
}
onFirstListenerAdd() {
this.hasListeners = true;
this.events.forEach((e) => this.hook(e));
}
onLastListenerRemove() {
this.hasListeners = false;
this.events.forEach((e) => this.unhook(e));
}
hook(e) {
e.listener = e.event((r) => this.emitter.fire(r));
}
unhook(e) {
if (e.listener) {
e.listener.dispose();
}
e.listener = null;
}
dispose() {
this.emitter.dispose();
}
}
/**
* The EventBufferer is useful in situations in which you want
* to delay firing your events during some code.
* You can wrap that code and be sure that the event will not
* be fired during that wrap.
*
* ```
* const emitter: Emitter;
* const delayer = new EventDelayer();
* const delayedEvent = delayer.wrapEvent(emitter.event);
*
* delayedEvent(console.log);
*
* delayer.bufferEvents(() => {
* emitter.fire(); // event will not be fired yet
* });
*
* // event will only be fired at this point
* ```
*/
export class EventBufferer {
buffers = [];
wrapEvent(event) {
return (listener, thisArgs, disposables) => {
return event(
(i) => {
const buffer = this.buffers[this.buffers.length - 1];
if (buffer) {
buffer.push(() => listener.call(thisArgs, i));
} else {
listener.call(thisArgs, i);
}
},
undefined,
disposables,
);
};
}
bufferEvents(fn) {
const buffer = [];
this.buffers.push(buffer);
const r = fn();
this.buffers.pop();
buffer.forEach((flush) => flush());
return r;
}
}
/**
* A Relay is an event forwarder which functions as a replugabble event pipe.
* Once created, you can connect an input event to it and it will simply forward
* events from that input event through its own `event` property. The `input`
* can be changed at any point in time.
*/
export class Relay {
listening = false;
inputEvent = Event.None;
inputEventListener = Disposable.None;
emitter = new Emitter({
onDidAddFirstListener: () => {
this.listening = true;
this.inputEventListener = this.inputEvent(this.emitter.fire, this.emitter);
},
onDidRemoveLastListener: () => {
this.listening = false;
this.inputEventListener.dispose();
},
});
event = this.emitter.event;
set input(event) {
this.inputEvent = event;
if (this.listening) {
this.inputEventListener.dispose();
this.inputEventListener = event(this.emitter.fire, this.emitter);
}
}
dispose() {
this.inputEventListener.dispose();
this.emitter.dispose();
}
}