UNPKG

@theia/core

Version:

Theia is a cloud & desktop IDE framework implemented in TypeScript.

494 lines (433 loc) • 17.1 kB
// ***************************************************************************** // 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 // ***************************************************************************** /* eslint-disable @typescript-eslint/no-explicit-any */ import { Disposable, DisposableGroup, DisposableCollection } from './disposable'; import { MaybePromise } from './types'; /** * Represents a typed event. */ export interface Event<T> { /** * * @param listener The listener function will be call when the event happens. * @param thisArgs The 'this' which will be used when calling the event listener. * @param disposables An array to which a {{IDisposable}} will be added. * @return a disposable to remove the listener again. */ (listener: (e: T) => any, thisArgs?: any, disposables?: DisposableGroup): Disposable; } export namespace Event { const _disposable = { dispose(): void { } }; export function getMaxListeners(event: Event<unknown>): number { const { maxListeners } = event as any; return typeof maxListeners === 'number' ? maxListeners : 0; } export function setMaxListeners<N extends number>(event: Event<unknown>, maxListeners: N): N { if (typeof (event as any).maxListeners === 'number') { return (event as any).maxListeners = maxListeners; } return maxListeners; } export function addMaxListeners(event: Event<unknown>, add: number): number { if (typeof (event as any).maxListeners === 'number') { return (event as any).maxListeners += add; } return add; } export const None: Event<any> = Object.assign(function (): { dispose(): void } { return _disposable; }, { get maxListeners(): number { return 0; }, set maxListeners(maxListeners: number) { } }); /** * Given an event, returns another event which only fires once. */ export function once<T>(event: Event<T>): Event<T> { return (listener, thisArgs = undefined, disposables?) => { // we need this, in case the event fires during the listener call let didFire = false; let result: Disposable | undefined = 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; }; } export function toPromise<T>(event: Event<T>): Promise<T> { return new Promise(resolve => once(event)(resolve)); } export function filter<T>(event: Event<T>, predicate: (e: T) => unknown): Event<T>; export function filter<T, S extends T>(event: Event<T>, predicate: (e: T) => e is S): Event<S>; export function filter<T>(event: Event<T>, predicate: (e: T) => unknown): Event<T> { return (listener, thisArg, disposables) => event(e => predicate(e) && listener.call(thisArg, e), undefined, disposables); } /** * Given an event and a `map` function, returns another event which maps each element * through the mapping function. */ export function map<I, O>(event: Event<I>, mapFunc: (i: I) => O): Event<O> { return Object.assign((listener: (e: O) => any, thisArgs?: any, disposables?: Disposable[]) => event(i => listener.call(thisArgs, mapFunc(i)), undefined, disposables), { get maxListeners(): number { return 0; }, set maxListeners(maxListeners: number) { } }); } /** * Given a collection of events, returns a single event which emits whenever any of the provided events emit. */ export function any<T>(...events: Event<T>[]): Event<T>; export function any(...events: Event<any>[]): Event<void>; export function any<T>(...events: Event<T>[]): Event<T> { return (listener, thisArgs = undefined, disposables?: Disposable[]) => new DisposableCollection(...events.map(event => event(e => listener.call(thisArgs, e), undefined, disposables))); } } type Callback = (...args: any[]) => any; class CallbackList implements Iterable<Callback> { private _callbacks: Function[] | undefined; private _contexts: any[] | undefined; get length(): number { return this._callbacks && this._callbacks.length || 0; } public add(callback: Function, context: any = undefined, bucket?: Disposable[]): void { 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) }); } } public remove(callback: Function, context: any = undefined): void { 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 public [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: any[]) => callback.apply(contexts[i], args) )[Symbol.iterator](); } public invoke(...args: any[]): any[] { const ret: any[] = []; for (const callback of this) { try { ret.push(callback(...args)); } catch (e) { console.error(e); } } return ret; } public isEmpty(): boolean { return !this._callbacks || this._callbacks.length === 0; } public dispose(): void { this._callbacks = undefined; this._contexts = undefined; } } export interface EmitterOptions { onFirstListenerAdd?: Function; onLastListenerRemove?: Function; } export class Emitter<T = any> { private static LEAK_WARNING_THRESHHOLD = 175; private static _noop = function (): void { }; private _event: Event<T>; protected _callbacks: CallbackList | undefined; private _disposed = false; private _leakingStacks: Map<string, number> | undefined; private _leakWarnCountdown = 0; constructor( private _options?: EmitterOptions ) { } /** * For the public to allow to subscribe * to events from this Emitter */ get event(): Event<T> { if (!this._event) { this._event = Object.assign((listener: (e: T) => any, thisArgs?: any, disposables?: DisposableGroup) => { 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: Disposable = { 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 (DisposableGroup.canPush(disposables)) { disposables.push(result); } else if (DisposableGroup.canAdd(disposables)) { disposables.add(result); } return result; }, { maxListeners: Emitter.LEAK_WARNING_THRESHHOLD }); } return this._event; } protected checkMaxListeners(maxListeners: number): (() => void) | undefined { 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: string; 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; } protected pushLeakingStack(): () => void { 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); } protected popLeakingStack(stack: string): void { 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: T): any { 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: (listener: (e: T) => any) => MaybePromise<boolean>): Promise<void> { if (this._callbacks) { for (const listener of this._callbacks) { if (!await processor(listener)) { break; } } } } dispose(): void { if (this._leakingStacks) { this._leakingStacks.clear(); this._leakingStacks = undefined; } if (this._callbacks) { this._callbacks.dispose(); this._callbacks = undefined; } this._disposed = true; } } export type WaitUntilData<T> = Omit<T, 'waitUntil' | 'token'>; export interface WaitUntilEvent { /** * A cancellation token. */ token: CancellationToken; /** * Allows to pause the event loop until the provided thenable resolved. * * *Note:* It can only be called during event dispatch and not in an asynchronous manner * * @param thenable A thenable that delays execution. */ waitUntil(thenable: Promise<any>): void; } export namespace WaitUntilEvent { /** * Fire all listeners in the same tick. * * Use `AsyncEmitter.fire` to fire listeners async one after another. */ export async function fire<T extends WaitUntilEvent>( emitter: Emitter<T>, event: WaitUntilData<T>, timeout?: number, token = CancellationToken.None ): Promise<void> { const waitables: Promise<void>[] = []; const asyncEvent = Object.assign(event, { token, waitUntil: (thenable: Promise<any>) => { if (Object.isFrozen(waitables)) { throw new Error('waitUntil cannot be called asynchronously.'); } waitables.push(thenable); } }) as T; try { emitter.fire(asyncEvent); // Asynchronous calls to `waitUntil` should fail. Object.freeze(waitables); } finally { delete (asyncEvent as any)['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); } } } import { CancellationToken } from './cancellation'; export class AsyncEmitter<T extends WaitUntilEvent> extends Emitter<T> { protected deliveryQueue: Promise<void> | undefined; /** * Fire listeners async one after another. */ override fire(event: WaitUntilData<T>, token: CancellationToken = CancellationToken.None, promiseJoin?: (p: Promise<any>, listener: Function) => Promise<any>): Promise<void> { 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); } protected async deliver(listeners: Callback[], event: WaitUntilData<T>, token: CancellationToken, promiseJoin?: (p: Promise<any>, listener: Function) => Promise<any>): Promise<void> { for (const listener of listeners) { if (token.isCancellationRequested) { return; } const waitables: Promise<void>[] = []; const asyncEvent = Object.assign(event, { token, waitUntil: (thenable: Promise<any>) => { if (Object.isFrozen(waitables)) { throw new Error('waitUntil cannot be called asynchronously.'); } if (promiseJoin) { thenable = promiseJoin(thenable, listener); } waitables.push(thenable); } }) as T; try { listener(event); // Asynchronous calls to `waitUntil` should fail. Object.freeze(waitables); } catch (e) { console.error(e); } finally { delete (asyncEvent as any)['waitUntil']; } if (!waitables.length) { continue; } try { await Promise.all(waitables); } catch (e) { console.error(e); } } } } export class QueueableEmitter<T> extends Emitter<T[]> { currentQueue?: T[]; queue(...arg: T[]): void { if (!this.currentQueue) { this.currentQueue = []; } this.currentQueue.push(...arg); } override fire(): void { super.fire(this.currentQueue || []); this.currentQueue = undefined; } }