1146 lines
43 KiB
JavaScript
1146 lines
43 KiB
JavaScript
import { onUnexpectedError } from './errors.js';
|
|
import { once as onceFn } from './functional.js';
|
|
import { combinedDisposable, Disposable, DisposableStore, toDisposable } from './lifecycle.js';
|
|
import { LinkedList } from './linkedList.js';
|
|
import { StopWatch } from './stopwatch.js';
|
|
// -----------------------------------------------------------------------------------------------------------------------
|
|
// 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 === null || origListenerDidAdd === void 0 ? void 0 : 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, true, 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;
|
|
/**
|
|
* Wraps an event in another event that performs some function on the event object before firing.
|
|
*
|
|
* *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 each The function to perform on the event object.
|
|
* @param disposable A disposable store to add the new EventEmitter to.
|
|
*/
|
|
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 === null || listener === void 0 ? void 0 : listener.dispose();
|
|
}
|
|
};
|
|
if (!disposable) {
|
|
_addLeakageTraceLogic(options);
|
|
}
|
|
const emitter = new Emitter(options);
|
|
disposable === null || disposable === void 0 ? void 0 : disposable.add(emitter);
|
|
return emitter.event;
|
|
}
|
|
function debounce(event, merge, delay = 100, leading = false, flushOnListenerRemove = false, leakWarningThreshold, disposable) {
|
|
let subscription;
|
|
let output = undefined;
|
|
let handle = undefined;
|
|
let numDebouncedCalls = 0;
|
|
let doFire;
|
|
const options = {
|
|
leakWarningThreshold,
|
|
onWillAddFirstListener() {
|
|
subscription = event(cur => {
|
|
numDebouncedCalls++;
|
|
output = merge(output, cur);
|
|
if (leading && !handle) {
|
|
emitter.fire(output);
|
|
output = undefined;
|
|
}
|
|
doFire = () => {
|
|
const _output = output;
|
|
output = undefined;
|
|
handle = undefined;
|
|
if (!leading || numDebouncedCalls > 1) {
|
|
emitter.fire(_output);
|
|
}
|
|
numDebouncedCalls = 0;
|
|
};
|
|
if (typeof delay === 'number') {
|
|
clearTimeout(handle);
|
|
handle = setTimeout(doFire, delay);
|
|
}
|
|
else {
|
|
if (handle === undefined) {
|
|
handle = 0;
|
|
queueMicrotask(doFire);
|
|
}
|
|
}
|
|
});
|
|
},
|
|
onWillRemoveListener() {
|
|
if (flushOnListenerRemove && numDebouncedCalls > 0) {
|
|
doFire === null || doFire === void 0 ? void 0 : doFire();
|
|
}
|
|
},
|
|
onDidRemoveLastListener() {
|
|
doFire = undefined;
|
|
subscription.dispose();
|
|
}
|
|
};
|
|
if (!disposable) {
|
|
_addLeakageTraceLogic(options);
|
|
}
|
|
const emitter = new Emitter(options);
|
|
disposable === null || disposable === void 0 ? void 0 : 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, true, undefined, disposable);
|
|
}
|
|
Event.accumulate = accumulate;
|
|
/**
|
|
* Filters an event such that some condition is _not_ met more than once in a row, effectively ensuring duplicate
|
|
* event objects from different sources do not fire the same event object.
|
|
*
|
|
* *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 equals The equality condition.
|
|
* @param disposable A disposable store to add the new EventEmitter to.
|
|
*
|
|
* @example
|
|
* ```
|
|
* // Fire only one time when a single window is opened or focused
|
|
* Event.latch(Event.any(onDidOpenWindow, onDidFocusWindow))
|
|
* ```
|
|
*/
|
|
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;
|
|
/**
|
|
* Buffers an event until it has a listener attached.
|
|
*
|
|
* *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 flushAfterTimeout Determines whether to flush the buffer after a timeout immediately or after a
|
|
* `setTimeout` when the first event listener is added.
|
|
* @param _buffer Internal: A source event array used for tests.
|
|
*
|
|
* @example
|
|
* ```
|
|
* // Start accumulating events, when the first listener is attached, flush
|
|
* // the event after a timeout such that multiple listeners attached before
|
|
* // the timeout would receive the event
|
|
* this.onInstallExtension = Event.buffer(service.onInstallExtension, true);
|
|
* ```
|
|
*/
|
|
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 === null || buffer === void 0 ? void 0 : 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 {
|
|
constructor(event) {
|
|
this.event = event;
|
|
this.disposables = new DisposableStore();
|
|
}
|
|
/** @see {@link Event.map} */
|
|
map(fn) {
|
|
return new ChainableEvent(map(this.event, fn, this.disposables));
|
|
}
|
|
/** @see {@link Event.forEach} */
|
|
forEach(fn) {
|
|
return new ChainableEvent(forEach(this.event, fn, this.disposables));
|
|
}
|
|
filter(fn) {
|
|
return new ChainableEvent(filter(this.event, fn, this.disposables));
|
|
}
|
|
/** @see {@link Event.reduce} */
|
|
reduce(merge, initial) {
|
|
return new ChainableEvent(reduce(this.event, merge, initial, this.disposables));
|
|
}
|
|
/** @see {@link Event.reduce} */
|
|
latch() {
|
|
return new ChainableEvent(latch(this.event, undefined, this.disposables));
|
|
}
|
|
debounce(merge, delay = 100, leading = false, flushOnListenerRemove = false, leakWarningThreshold) {
|
|
return new ChainableEvent(debounce(this.event, merge, delay, leading, flushOnListenerRemove, leakWarningThreshold, this.disposables));
|
|
}
|
|
/**
|
|
* Attach a listener to the event.
|
|
*/
|
|
on(listener, thisArgs, disposables) {
|
|
return this.event(listener, thisArgs, disposables);
|
|
}
|
|
/** @see {@link Event.once} */
|
|
once(listener, thisArgs, disposables) {
|
|
return once(this.event)(listener, thisArgs, disposables);
|
|
}
|
|
dispose() {
|
|
this.disposables.dispose();
|
|
}
|
|
}
|
|
/**
|
|
* Wraps the event in an {@link IChainableEvent}, allowing a more functional programming style.
|
|
*
|
|
* @example
|
|
* ```
|
|
* // Normal
|
|
* const onEnterPressNormal = Event.filter(
|
|
* Event.map(onKeyPress.event, e => new StandardKeyboardEvent(e)),
|
|
* e.keyCode === KeyCode.Enter
|
|
* ).event;
|
|
*
|
|
* // Using chain
|
|
* const onEnterPressChain = Event.chain(onKeyPress.event)
|
|
* .map(e => new StandardKeyboardEvent(e))
|
|
* .filter(e => e.keyCode === KeyCode.Enter)
|
|
* .event;
|
|
* ```
|
|
*/
|
|
function chain(event) {
|
|
return new ChainableEvent(event);
|
|
}
|
|
Event.chain = chain;
|
|
/**
|
|
* Creates an {@link Event} from a node event emitter.
|
|
*/
|
|
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;
|
|
/**
|
|
* Creates an {@link Event} from a DOM event emitter.
|
|
*/
|
|
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;
|
|
/**
|
|
* Creates a promise out of an event, using the {@link Event.once} helper.
|
|
*/
|
|
function toPromise(event) {
|
|
return new Promise(resolve => once(event)(resolve));
|
|
}
|
|
Event.toPromise = toPromise;
|
|
/**
|
|
* Creates an event out of a promise that fires once when the promise is
|
|
* resolved with the result of the promise or `undefined`.
|
|
*/
|
|
function fromPromise(promise) {
|
|
const result = new Emitter();
|
|
promise.then(res => {
|
|
result.fire(res);
|
|
}, () => {
|
|
result.fire(undefined);
|
|
}).finally(() => {
|
|
result.dispose();
|
|
});
|
|
return result.event;
|
|
}
|
|
Event.fromPromise = fromPromise;
|
|
/**
|
|
* Adds a listener to an event and calls the listener immediately with undefined as the event object.
|
|
*
|
|
* @example
|
|
* ```
|
|
* // Initialize the UI and update it when dataChangeEvent fires
|
|
* runAndSubscribe(dataChangeEvent, () => this._updateUI());
|
|
* ```
|
|
*/
|
|
function runAndSubscribe(event, handler) {
|
|
handler(undefined);
|
|
return event(e => handler(e));
|
|
}
|
|
Event.runAndSubscribe = runAndSubscribe;
|
|
/**
|
|
* Adds a listener to an event and calls the listener immediately with undefined as the event object. A new
|
|
* {@link DisposableStore} is passed to the listener which is disposed when the returned disposable is disposed.
|
|
*/
|
|
function runAndSubscribeWithStore(event, handler) {
|
|
let store = null;
|
|
function run(e) {
|
|
store === null || store === void 0 ? void 0 : store.dispose();
|
|
store = new DisposableStore();
|
|
handler(e, store);
|
|
}
|
|
run(undefined);
|
|
const disposable = event(e => run(e));
|
|
return toDisposable(() => {
|
|
disposable.dispose();
|
|
store === null || store === void 0 ? void 0 : store.dispose();
|
|
});
|
|
}
|
|
Event.runAndSubscribeWithStore = runAndSubscribeWithStore;
|
|
class EmitterObserver {
|
|
constructor(_observable, store) {
|
|
this._observable = _observable;
|
|
this._counter = 0;
|
|
this._hasChanged = false;
|
|
const options = {
|
|
onWillAddFirstListener: () => {
|
|
_observable.addObserver(this);
|
|
},
|
|
onDidRemoveLastListener: () => {
|
|
_observable.removeObserver(this);
|
|
}
|
|
};
|
|
if (!store) {
|
|
_addLeakageTraceLogic(options);
|
|
}
|
|
this.emitter = new Emitter(options);
|
|
if (store) {
|
|
store.add(this.emitter);
|
|
}
|
|
}
|
|
beginUpdate(_observable) {
|
|
// assert(_observable === this.obs);
|
|
this._counter++;
|
|
}
|
|
handlePossibleChange(_observable) {
|
|
// assert(_observable === this.obs);
|
|
}
|
|
handleChange(_observable, _change) {
|
|
// assert(_observable === this.obs);
|
|
this._hasChanged = true;
|
|
}
|
|
endUpdate(_observable) {
|
|
// assert(_observable === this.obs);
|
|
this._counter--;
|
|
if (this._counter === 0) {
|
|
this._observable.reportChanges();
|
|
if (this._hasChanged) {
|
|
this._hasChanged = false;
|
|
this.emitter.fire(this._observable.get());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Creates an event emitter that is fired when the observable changes.
|
|
* Each listeners subscribes to the emitter.
|
|
*/
|
|
function fromObservable(obs, store) {
|
|
const observer = new EmitterObserver(obs, store);
|
|
return observer.emitter.event;
|
|
}
|
|
Event.fromObservable = fromObservable;
|
|
/**
|
|
* Each listener is attached to the observable directly.
|
|
*/
|
|
function fromObservableLight(observable) {
|
|
return (listener) => {
|
|
let count = 0;
|
|
let didChange = false;
|
|
const observer = {
|
|
beginUpdate() {
|
|
count++;
|
|
},
|
|
endUpdate() {
|
|
count--;
|
|
if (count === 0) {
|
|
observable.reportChanges();
|
|
if (didChange) {
|
|
didChange = false;
|
|
listener();
|
|
}
|
|
}
|
|
},
|
|
handlePossibleChange() {
|
|
// noop
|
|
},
|
|
handleChange() {
|
|
didChange = true;
|
|
}
|
|
};
|
|
observable.addObserver(observer);
|
|
observable.reportChanges();
|
|
return {
|
|
dispose() {
|
|
observable.removeObserver(observer);
|
|
}
|
|
};
|
|
};
|
|
}
|
|
Event.fromObservableLight = fromObservableLight;
|
|
})(Event || (Event = {}));
|
|
export class EventProfiling {
|
|
constructor(name) {
|
|
this.listenerCount = 0;
|
|
this.invocationCount = 0;
|
|
this.elapsedOverall = 0;
|
|
this.durations = [];
|
|
this.name = `${name}_${EventProfiling._idPool++}`;
|
|
EventProfiling.all.add(this);
|
|
}
|
|
start(listenerCount) {
|
|
this._stopWatch = new StopWatch();
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
EventProfiling.all = new Set();
|
|
EventProfiling._idPool = 0;
|
|
let _globalLeakWarningThreshold = -1;
|
|
class LeakageMonitor {
|
|
constructor(threshold, name = Math.random().toString(18).slice(2, 5)) {
|
|
this.threshold = threshold;
|
|
this.name = name;
|
|
this._warnCountdown = 0;
|
|
}
|
|
dispose() {
|
|
var _a;
|
|
(_a = this._stacks) === null || _a === void 0 ? void 0 : _a.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 {
|
|
static create() {
|
|
var _a;
|
|
return new Stacktrace((_a = new Error().stack) !== null && _a !== void 0 ? _a : '');
|
|
}
|
|
constructor(value) {
|
|
this.value = value;
|
|
}
|
|
print() {
|
|
console.warn(this.value.split('\n').slice(2).join('\n'));
|
|
}
|
|
}
|
|
class UniqueContainer {
|
|
constructor(value) {
|
|
this.value = value;
|
|
}
|
|
}
|
|
const compactionThreshold = 2;
|
|
const forEachListener = (listeners, fn) => {
|
|
if (listeners instanceof UniqueContainer) {
|
|
fn(listeners);
|
|
}
|
|
else {
|
|
for (let i = 0; i < listeners.length; i++) {
|
|
const l = listeners[i];
|
|
if (l) {
|
|
fn(l);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
/**
|
|
* 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 {
|
|
constructor(options) {
|
|
var _a, _b, _c, _d, _e;
|
|
this._size = 0;
|
|
this._options = options;
|
|
this._leakageMon = _globalLeakWarningThreshold > 0 || ((_a = this._options) === null || _a === void 0 ? void 0 : _a.leakWarningThreshold) ? new LeakageMonitor((_c = (_b = this._options) === null || _b === void 0 ? void 0 : _b.leakWarningThreshold) !== null && _c !== void 0 ? _c : _globalLeakWarningThreshold) : undefined;
|
|
this._perfMon = ((_d = this._options) === null || _d === void 0 ? void 0 : _d._profName) ? new EventProfiling(this._options._profName) : undefined;
|
|
this._deliveryQueue = (_e = this._options) === null || _e === void 0 ? void 0 : _e.deliveryQueue;
|
|
}
|
|
dispose() {
|
|
var _a, _b, _c, _d;
|
|
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 (((_a = this._deliveryQueue) === null || _a === void 0 ? void 0 : _a.current) === this) {
|
|
this._deliveryQueue.reset();
|
|
}
|
|
if (this._listeners) {
|
|
if (_enableDisposeWithListenerWarning) {
|
|
const listeners = this._listeners;
|
|
queueMicrotask(() => {
|
|
forEachListener(listeners, l => { var _a; return (_a = l.stack) === null || _a === void 0 ? void 0 : _a.print(); });
|
|
});
|
|
}
|
|
this._listeners = undefined;
|
|
this._size = 0;
|
|
}
|
|
(_c = (_b = this._options) === null || _b === void 0 ? void 0 : _b.onDidRemoveLastListener) === null || _c === void 0 ? void 0 : _c.call(_b);
|
|
(_d = this._leakageMon) === null || _d === void 0 ? void 0 : _d.dispose();
|
|
}
|
|
}
|
|
/**
|
|
* For the public to allow to subscribe
|
|
* to events from this Emitter
|
|
*/
|
|
get event() {
|
|
var _a;
|
|
(_a = this._event) !== null && _a !== void 0 ? _a : (this._event = (callback, thisArgs, disposables) => {
|
|
var _a, _b, _c, _d, _e;
|
|
if (this._leakageMon && this._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;
|
|
}
|
|
if (this._disposed) {
|
|
// todo: should we warn if a listener is added to a disposed emitter? This happens often
|
|
return Disposable.None;
|
|
}
|
|
if (thisArgs) {
|
|
callback = callback.bind(thisArgs);
|
|
}
|
|
const contained = new UniqueContainer(callback);
|
|
let removeMonitor;
|
|
let stack;
|
|
if (this._leakageMon && this._size >= Math.ceil(this._leakageMon.threshold * 0.2)) {
|
|
// check and record this emitter for potential leakage
|
|
contained.stack = Stacktrace.create();
|
|
removeMonitor = this._leakageMon.check(contained.stack, this._size + 1);
|
|
}
|
|
if (_enableDisposeWithListenerWarning) {
|
|
contained.stack = stack !== null && stack !== void 0 ? stack : Stacktrace.create();
|
|
}
|
|
if (!this._listeners) {
|
|
(_b = (_a = this._options) === null || _a === void 0 ? void 0 : _a.onWillAddFirstListener) === null || _b === void 0 ? void 0 : _b.call(_a, this);
|
|
this._listeners = contained;
|
|
(_d = (_c = this._options) === null || _c === void 0 ? void 0 : _c.onDidAddFirstListener) === null || _d === void 0 ? void 0 : _d.call(_c, this);
|
|
}
|
|
else if (this._listeners instanceof UniqueContainer) {
|
|
(_e = this._deliveryQueue) !== null && _e !== void 0 ? _e : (this._deliveryQueue = new EventDeliveryQueuePrivate());
|
|
this._listeners = [this._listeners, contained];
|
|
}
|
|
else {
|
|
this._listeners.push(contained);
|
|
}
|
|
this._size++;
|
|
const result = toDisposable(() => { removeMonitor === null || removeMonitor === void 0 ? void 0 : removeMonitor(); this._removeListener(contained); });
|
|
if (disposables instanceof DisposableStore) {
|
|
disposables.add(result);
|
|
}
|
|
else if (Array.isArray(disposables)) {
|
|
disposables.push(result);
|
|
}
|
|
return result;
|
|
});
|
|
return this._event;
|
|
}
|
|
_removeListener(listener) {
|
|
var _a, _b, _c, _d;
|
|
(_b = (_a = this._options) === null || _a === void 0 ? void 0 : _a.onWillRemoveListener) === null || _b === void 0 ? void 0 : _b.call(_a, this);
|
|
if (!this._listeners) {
|
|
return; // expected if a listener gets disposed
|
|
}
|
|
if (this._size === 1) {
|
|
this._listeners = undefined;
|
|
(_d = (_c = this._options) === null || _c === void 0 ? void 0 : _c.onDidRemoveLastListener) === null || _d === void 0 ? void 0 : _d.call(_c, this);
|
|
this._size = 0;
|
|
return;
|
|
}
|
|
// size > 1 which requires that listeners be a list:
|
|
const listeners = this._listeners;
|
|
const index = listeners.indexOf(listener);
|
|
if (index === -1) {
|
|
console.log('disposed?', this._disposed);
|
|
console.log('size?', this._size);
|
|
console.log('arr?', JSON.stringify(this._listeners));
|
|
throw new Error('Attempted to dispose unknown listener');
|
|
}
|
|
this._size--;
|
|
listeners[index] = undefined;
|
|
const adjustDeliveryQueue = this._deliveryQueue.current === this;
|
|
if (this._size * compactionThreshold <= listeners.length) {
|
|
let n = 0;
|
|
for (let i = 0; i < listeners.length; i++) {
|
|
if (listeners[i]) {
|
|
listeners[n++] = listeners[i];
|
|
}
|
|
else if (adjustDeliveryQueue) {
|
|
this._deliveryQueue.end--;
|
|
if (n < this._deliveryQueue.i) {
|
|
this._deliveryQueue.i--;
|
|
}
|
|
}
|
|
}
|
|
listeners.length = n;
|
|
}
|
|
}
|
|
_deliver(listener, value) {
|
|
var _a;
|
|
if (!listener) {
|
|
return;
|
|
}
|
|
const errorHandler = ((_a = this._options) === null || _a === void 0 ? void 0 : _a.onListenerError) || onUnexpectedError;
|
|
if (!errorHandler) {
|
|
listener.value(value);
|
|
return;
|
|
}
|
|
try {
|
|
listener.value(value);
|
|
}
|
|
catch (e) {
|
|
errorHandler(e);
|
|
}
|
|
}
|
|
/** Delivers items in the queue. Assumes the queue is ready to go. */
|
|
_deliverQueue(dq) {
|
|
const listeners = dq.current._listeners;
|
|
while (dq.i < dq.end) {
|
|
// important: dq.i is incremented before calling deliver() because it might reenter deliverQueue()
|
|
this._deliver(listeners[dq.i++], dq.value);
|
|
}
|
|
dq.reset();
|
|
}
|
|
/**
|
|
* To be kept private to fire an event to
|
|
* subscribers
|
|
*/
|
|
fire(event) {
|
|
var _a, _b, _c, _d;
|
|
if ((_a = this._deliveryQueue) === null || _a === void 0 ? void 0 : _a.current) {
|
|
this._deliverQueue(this._deliveryQueue);
|
|
(_b = this._perfMon) === null || _b === void 0 ? void 0 : _b.stop(); // last fire() will have starting perfmon, stop it before starting the next dispatch
|
|
}
|
|
(_c = this._perfMon) === null || _c === void 0 ? void 0 : _c.start(this._size);
|
|
if (!this._listeners) {
|
|
// no-op
|
|
}
|
|
else if (this._listeners instanceof UniqueContainer) {
|
|
this._deliver(this._listeners, event);
|
|
}
|
|
else {
|
|
const dq = this._deliveryQueue;
|
|
dq.enqueue(this, event, this._listeners.length);
|
|
this._deliverQueue(dq);
|
|
}
|
|
(_d = this._perfMon) === null || _d === void 0 ? void 0 : _d.stop();
|
|
}
|
|
hasListeners() {
|
|
return this._size > 0;
|
|
}
|
|
}
|
|
export const createEventDeliveryQueue = () => new EventDeliveryQueuePrivate();
|
|
class EventDeliveryQueuePrivate {
|
|
constructor() {
|
|
/**
|
|
* Index in current's listener list.
|
|
*/
|
|
this.i = -1;
|
|
/**
|
|
* The last index in the listener's list to deliver.
|
|
*/
|
|
this.end = 0;
|
|
}
|
|
enqueue(emitter, value, end) {
|
|
this.i = 0;
|
|
this.end = end;
|
|
this.current = emitter;
|
|
this.value = value;
|
|
}
|
|
reset() {
|
|
this.i = this.end; // force any current emission loop to stop, mainly for during dispose
|
|
this.current = undefined;
|
|
this.value = undefined;
|
|
}
|
|
}
|
|
export class PauseableEmitter extends Emitter {
|
|
constructor(options) {
|
|
super(options);
|
|
this._isPaused = 0;
|
|
this._eventQueue = new LinkedList();
|
|
this._mergeFn = options === null || options === void 0 ? void 0 : 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._size) {
|
|
if (this._isPaused !== 0) {
|
|
this._eventQueue.push(event);
|
|
}
|
|
else {
|
|
super.fire(event);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
export class DebounceEmitter extends PauseableEmitter {
|
|
constructor(options) {
|
|
var _a;
|
|
super(options);
|
|
this._delay = (_a = options.delay) !== null && _a !== void 0 ? _a : 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 {
|
|
constructor(options) {
|
|
super(options);
|
|
this._queuedEvents = [];
|
|
this._mergeFn = options === null || options === void 0 ? void 0 : 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 = [];
|
|
});
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* An event emitter that multiplexes many events into a single event.
|
|
*
|
|
* @example Listen to the `onData` event of all `Thing`s, dynamically adding and removing `Thing`s
|
|
* to the multiplexer as needed.
|
|
*
|
|
* ```typescript
|
|
* const anythingDataMultiplexer = new EventMultiplexer<{ data: string }>();
|
|
*
|
|
* const thingListeners = DisposableMap<Thing, IDisposable>();
|
|
*
|
|
* thingService.onDidAddThing(thing => {
|
|
* thingListeners.set(thing, anythingDataMultiplexer.add(thing.onData);
|
|
* });
|
|
* thingService.onDidRemoveThing(thing => {
|
|
* thingListeners.deleteAndDispose(thing);
|
|
* });
|
|
*
|
|
* anythingDataMultiplexer.event(e => {
|
|
* console.log('Something fired data ' + e.data)
|
|
* });
|
|
* ```
|
|
*/
|
|
export class EventMultiplexer {
|
|
constructor() {
|
|
this.hasListeners = false;
|
|
this.events = [];
|
|
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 {
|
|
constructor() {
|
|
this.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 {
|
|
constructor() {
|
|
this.listening = false;
|
|
this.inputEvent = Event.None;
|
|
this.inputEventListener = Disposable.None;
|
|
this.emitter = new Emitter({
|
|
onDidAddFirstListener: () => {
|
|
this.listening = true;
|
|
this.inputEventListener = this.inputEvent(this.emitter.fire, this.emitter);
|
|
},
|
|
onDidRemoveLastListener: () => {
|
|
this.listening = false;
|
|
this.inputEventListener.dispose();
|
|
}
|
|
});
|
|
this.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();
|
|
}
|
|
}
|