@malagu/core
Version:
226 lines • 7.98 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.AsyncEmitter = exports.WaitUntilEvent = exports.Emitter = void 0;
const event_1 = require("./event");
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 event_1.CallbackList();
}
if (this._options && this._options.onFirstListenerAdd && this._callbacks.isEmpty()) {
this._options.onFirstListenerAdd(this);
}
this._callbacks.add(listener, thisArgs);
const removeMaxListenersCheck = this.checkMaxListeners(this._event.maxListeners);
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 (Array.isArray(disposables)) {
disposables.push(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) {
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 = undefined) {
const waitables = [];
const asyncEvent = Object.assign(event, {
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, {
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=emitter.js.map