@theia/core
Version:
Theia is a cloud & desktop IDE framework implemented in TypeScript.
386 lines • 13.9 kB
JavaScript
"use strict";
// *****************************************************************************
// Copyright (C) 2017 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
Object.defineProperty(exports, "__esModule", { value: true });
exports.AsyncEmitter = exports.WaitUntilEvent = exports.Emitter = exports.Event = void 0;
/* eslint-disable @typescript-eslint/no-explicit-any */
const disposable_1 = require("./disposable");
var Event;
(function (Event) {
const _disposable = { dispose() { } };
function getMaxListeners(event) {
const { maxListeners } = event;
return typeof maxListeners === 'number' ? maxListeners : 0;
}
Event.getMaxListeners = getMaxListeners;
function setMaxListeners(event, maxListeners) {
if (typeof event.maxListeners === 'number') {
return event.maxListeners = maxListeners;
}
return maxListeners;
}
Event.setMaxListeners = setMaxListeners;
function addMaxListeners(event, add) {
if (typeof event.maxListeners === 'number') {
return event.maxListeners += add;
}
return add;
}
Event.addMaxListeners = addMaxListeners;
Event.None = Object.assign(function () { return _disposable; }, {
get maxListeners() { return 0; },
set maxListeners(maxListeners) { }
});
/**
* Given an event, returns another event which only fires once.
*/
function once(event) {
return (listener, thisArgs = undefined, 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);
}, undefined, disposables);
if (didFire) {
result.dispose();
}
return result;
};
}
Event.once = once;
function toPromise(event) {
return new Promise(resolve => once(event)(resolve));
}
Event.toPromise = toPromise;
/**
* Given an event and a `map` function, returns another event which maps each element
* through the mapping function.
*/
function map(event, mapFunc) {
return Object.assign((listener, thisArgs, disposables) => event(i => listener.call(thisArgs, mapFunc(i)), undefined, disposables), {
get maxListeners() { return 0; },
set maxListeners(maxListeners) { }
});
}
Event.map = map;
function any(...events) {
return (listener, thisArgs = undefined, disposables) => new disposable_1.DisposableCollection(...events.map(event => event(e => listener.call(thisArgs, e), undefined, disposables)));
}
Event.any = any;
})(Event = exports.Event || (exports.Event = {}));
class CallbackList {
get length() {
return this._callbacks && this._callbacks.length || 0;
}
add(callback, context = undefined, bucket) {
if (!this._callbacks) {
this._callbacks = [];
this._contexts = [];
}
this._callbacks.push(callback);
this._contexts.push(context);
if (Array.isArray(bucket)) {
bucket.push({ dispose: () => this.remove(callback, context) });
}
}
remove(callback, context = undefined) {
if (!this._callbacks) {
return;
}
let foundCallbackWithDifferentContext = false;
for (let i = 0; i < this._callbacks.length; i++) {
if (this._callbacks[i] === callback) {
if (this._contexts[i] === context) {
// callback & context match => remove it
this._callbacks.splice(i, 1);
this._contexts.splice(i, 1);
return;
}
else {
foundCallbackWithDifferentContext = true;
}
}
}
if (foundCallbackWithDifferentContext) {
throw new Error('When adding a listener with a context, you should remove it with the same context');
}
}
// tslint:disable-next-line:typedef
[Symbol.iterator]() {
if (!this._callbacks) {
return [][Symbol.iterator]();
}
const callbacks = this._callbacks.slice(0);
const contexts = this._contexts.slice(0);
return callbacks.map((callback, i) => (...args) => callback.apply(contexts[i], args))[Symbol.iterator]();
}
invoke(...args) {
const ret = [];
for (const callback of this) {
try {
ret.push(callback(...args));
}
catch (e) {
console.error(e);
}
}
return ret;
}
isEmpty() {
return !this._callbacks || this._callbacks.length === 0;
}
dispose() {
this._callbacks = undefined;
this._contexts = undefined;
}
}
class Emitter {
constructor(_options) {
this._options = _options;
this._disposed = false;
this._leakWarnCountdown = 0;
}
/**
* For the public to allow to subscribe
* to events from this Emitter
*/
get event() {
if (!this._event) {
this._event = Object.assign((listener, thisArgs, disposables) => {
if (!this._callbacks) {
this._callbacks = new CallbackList();
}
if (this._options && this._options.onFirstListenerAdd && this._callbacks.isEmpty()) {
this._options.onFirstListenerAdd(this);
}
this._callbacks.add(listener, thisArgs);
const removeMaxListenersCheck = this.checkMaxListeners(Event.getMaxListeners(this._event));
const result = {
dispose: () => {
if (removeMaxListenersCheck) {
removeMaxListenersCheck();
}
result.dispose = Emitter._noop;
if (!this._disposed) {
this._callbacks.remove(listener, thisArgs);
result.dispose = Emitter._noop;
if (this._options && this._options.onLastListenerRemove && this._callbacks.isEmpty()) {
this._options.onLastListenerRemove(this);
}
}
}
};
if (disposable_1.DisposableGroup.canPush(disposables)) {
disposables.push(result);
}
else if (disposable_1.DisposableGroup.canAdd(disposables)) {
disposables.add(result);
}
return result;
}, {
maxListeners: Emitter.LEAK_WARNING_THRESHHOLD
});
}
return this._event;
}
checkMaxListeners(maxListeners) {
if (maxListeners === 0 || !this._callbacks) {
return undefined;
}
const listenerCount = this._callbacks.length;
if (listenerCount <= maxListeners) {
return undefined;
}
const popStack = this.pushLeakingStack();
this._leakWarnCountdown -= 1;
if (this._leakWarnCountdown <= 0) {
// only warn on first exceed and then every time the limit
// is exceeded by 50% again
this._leakWarnCountdown = maxListeners * 0.5;
let topStack;
let topCount = 0;
this._leakingStacks.forEach((stackCount, stack) => {
if (!topStack || topCount < stackCount) {
topStack = stack;
topCount = stackCount;
}
});
// eslint-disable-next-line max-len
console.warn(`Possible Emitter memory leak detected. ${listenerCount} listeners added. Use event.maxListeners to increase the limit (${maxListeners}). MOST frequent listener (${topCount}):`);
console.warn(topStack);
}
return popStack;
}
pushLeakingStack() {
if (!this._leakingStacks) {
this._leakingStacks = new Map();
}
const stack = new Error().stack.split('\n').slice(3).join('\n');
const count = (this._leakingStacks.get(stack) || 0);
this._leakingStacks.set(stack, count + 1);
return () => this.popLeakingStack(stack);
}
popLeakingStack(stack) {
if (!this._leakingStacks) {
return;
}
const count = (this._leakingStacks.get(stack) || 0);
this._leakingStacks.set(stack, count - 1);
}
/**
* To be kept private to fire an event to
* subscribers
*/
fire(event) {
if (this._callbacks) {
return this._callbacks.invoke(event);
}
}
/**
* Process each listener one by one.
* Return `false` to stop iterating over the listeners, `true` to continue.
*/
async sequence(processor) {
if (this._callbacks) {
for (const listener of this._callbacks) {
if (!await processor(listener)) {
break;
}
}
}
}
dispose() {
if (this._leakingStacks) {
this._leakingStacks.clear();
this._leakingStacks = undefined;
}
if (this._callbacks) {
this._callbacks.dispose();
this._callbacks = undefined;
}
this._disposed = true;
}
}
exports.Emitter = Emitter;
Emitter.LEAK_WARNING_THRESHHOLD = 175;
Emitter._noop = function () { };
var WaitUntilEvent;
(function (WaitUntilEvent) {
/**
* Fire all listeners in the same tick.
*
* Use `AsyncEmitter.fire` to fire listeners async one after another.
*/
async function fire(emitter, event, timeout, token = cancellation_1.CancellationToken.None) {
const waitables = [];
const asyncEvent = Object.assign(event, {
token,
waitUntil: (thenable) => {
if (Object.isFrozen(waitables)) {
throw new Error('waitUntil cannot be called asynchronously.');
}
waitables.push(thenable);
}
});
try {
emitter.fire(asyncEvent);
// Asynchronous calls to `waitUntil` should fail.
Object.freeze(waitables);
}
finally {
delete asyncEvent['waitUntil'];
}
if (!waitables.length) {
return;
}
if (timeout !== undefined) {
await Promise.race([Promise.all(waitables), new Promise(resolve => setTimeout(resolve, timeout))]);
}
else {
await Promise.all(waitables);
}
}
WaitUntilEvent.fire = fire;
})(WaitUntilEvent = exports.WaitUntilEvent || (exports.WaitUntilEvent = {}));
const cancellation_1 = require("./cancellation");
class AsyncEmitter extends Emitter {
/**
* Fire listeners async one after another.
*/
fire(event, token = cancellation_1.CancellationToken.None, promiseJoin) {
const callbacks = this._callbacks;
if (!callbacks) {
return Promise.resolve();
}
const listeners = [...callbacks];
if (this.deliveryQueue) {
return this.deliveryQueue = this.deliveryQueue.then(() => this.deliver(listeners, event, token, promiseJoin));
}
return this.deliveryQueue = this.deliver(listeners, event, token, promiseJoin);
}
async deliver(listeners, event, token, promiseJoin) {
for (const listener of listeners) {
if (token.isCancellationRequested) {
return;
}
const waitables = [];
const asyncEvent = Object.assign(event, {
token,
waitUntil: (thenable) => {
if (Object.isFrozen(waitables)) {
throw new Error('waitUntil cannot be called asynchronously.');
}
if (promiseJoin) {
thenable = promiseJoin(thenable, listener);
}
waitables.push(thenable);
}
});
try {
listener(event);
// Asynchronous calls to `waitUntil` should fail.
Object.freeze(waitables);
}
catch (e) {
console.error(e);
}
finally {
delete asyncEvent['waitUntil'];
}
if (!waitables.length) {
return;
}
try {
await Promise.all(waitables);
}
catch (e) {
console.error(e);
}
}
}
}
exports.AsyncEmitter = AsyncEmitter;
//# sourceMappingURL=event.js.map