UNPKG

open-collaboration-protocol

Version:

Open Collaboration Protocol implementation, part of the Open Collaboration Tools project

280 lines 9.92 kB
// ****************************************************************************** // Copyright 2024 TypeFox GmbH // This program and the accompanying materials are made available under the // terms of the MIT License, which is available in the project root. // ****************************************************************************** import { DisposableGroup, DisposableCollection } from './disposable.js'; export 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 DisposableCollection(...events.map(event => event(e => listener.call(thisArgs, e), undefined, disposables))); } Event.any = any; })(Event || (Event = {})); class CallbackList { _callbacks; _contexts; 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; } } export class Emitter { _options; static LEAK_WARNING_THRESHHOLD = 175; static _noop = function () { }; _event; _callbacks; _disposed = false; _leakingStacks; _leakWarnCountdown = 0; constructor(_options) { this._options = _options; } /** * 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 (DisposableGroup.canPush(disposables)) { disposables.push(result); } else if (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; } }); 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; } } //# sourceMappingURL=event.js.map