UNPKG

12.6 kBJavaScriptView Raw
1"use strict";
2// *****************************************************************************
3// Copyright (C) 2017 TypeFox and others.
4//
5// This program and the accompanying materials are made available under the
6// terms of the Eclipse Public License v. 2.0 which is available at
7// http://www.eclipse.org/legal/epl-2.0.
8//
9// This Source Code may also be made available under the following Secondary
10// Licenses when the conditions for such availability set forth in the Eclipse
11// Public License v. 2.0 are satisfied: GNU General Public License, version 2
12// with the GNU Classpath Exception which is available at
13// https://www.gnu.org/software/classpath/license.html.
14//
15// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
16// *****************************************************************************
17Object.defineProperty(exports, "__esModule", { value: true });
18exports.AsyncEmitter = exports.WaitUntilEvent = exports.Emitter = exports.Event = void 0;
19/* eslint-disable @typescript-eslint/no-explicit-any */
20const disposable_1 = require("./disposable");
21var Event;
22(function (Event) {
23 const _disposable = { dispose() { } };
24 function getMaxListeners(event) {
25 const { maxListeners } = event;
26 return typeof maxListeners === 'number' ? maxListeners : 0;
27 }
28 Event.getMaxListeners = getMaxListeners;
29 function setMaxListeners(event, maxListeners) {
30 if (typeof event.maxListeners === 'number') {
31 return event.maxListeners = maxListeners;
32 }
33 return maxListeners;
34 }
35 Event.setMaxListeners = setMaxListeners;
36 function addMaxListeners(event, add) {
37 if (typeof event.maxListeners === 'number') {
38 return event.maxListeners += add;
39 }
40 return add;
41 }
42 Event.addMaxListeners = addMaxListeners;
43 Event.None = Object.assign(function () { return _disposable; }, {
44 get maxListeners() { return 0; },
45 set maxListeners(maxListeners) { }
46 });
47 /**
48 * Given an event and a `map` function, returns another event which maps each element
49 * through the mapping function.
50 */
51 function map(event, mapFunc) {
52 return Object.assign((listener, thisArgs, disposables) => event(i => listener.call(thisArgs, mapFunc(i)), undefined, disposables), {
53 get maxListeners() { return 0; },
54 set maxListeners(maxListeners) { }
55 });
56 }
57 Event.map = map;
58})(Event = exports.Event || (exports.Event = {}));
59class CallbackList {
60 get length() {
61 return this._callbacks && this._callbacks.length || 0;
62 }
63 add(callback, context = undefined, bucket) {
64 if (!this._callbacks) {
65 this._callbacks = [];
66 this._contexts = [];
67 }
68 this._callbacks.push(callback);
69 this._contexts.push(context);
70 if (Array.isArray(bucket)) {
71 bucket.push({ dispose: () => this.remove(callback, context) });
72 }
73 }
74 remove(callback, context = undefined) {
75 if (!this._callbacks) {
76 return;
77 }
78 let foundCallbackWithDifferentContext = false;
79 for (let i = 0; i < this._callbacks.length; i++) {
80 if (this._callbacks[i] === callback) {
81 if (this._contexts[i] === context) {
82 // callback & context match => remove it
83 this._callbacks.splice(i, 1);
84 this._contexts.splice(i, 1);
85 return;
86 }
87 else {
88 foundCallbackWithDifferentContext = true;
89 }
90 }
91 }
92 if (foundCallbackWithDifferentContext) {
93 throw new Error('When adding a listener with a context, you should remove it with the same context');
94 }
95 }
96 // tslint:disable-next-line:typedef
97 [Symbol.iterator]() {
98 if (!this._callbacks) {
99 return [][Symbol.iterator]();
100 }
101 const callbacks = this._callbacks.slice(0);
102 const contexts = this._contexts.slice(0);
103 return callbacks.map((callback, i) => (...args) => callback.apply(contexts[i], args))[Symbol.iterator]();
104 }
105 invoke(...args) {
106 const ret = [];
107 for (const callback of this) {
108 try {
109 ret.push(callback(...args));
110 }
111 catch (e) {
112 console.error(e);
113 }
114 }
115 return ret;
116 }
117 isEmpty() {
118 return !this._callbacks || this._callbacks.length === 0;
119 }
120 dispose() {
121 this._callbacks = undefined;
122 this._contexts = undefined;
123 }
124}
125class Emitter {
126 constructor(_options) {
127 this._options = _options;
128 this._disposed = false;
129 this._leakWarnCountdown = 0;
130 }
131 /**
132 * For the public to allow to subscribe
133 * to events from this Emitter
134 */
135 get event() {
136 if (!this._event) {
137 this._event = Object.assign((listener, thisArgs, disposables) => {
138 if (!this._callbacks) {
139 this._callbacks = new CallbackList();
140 }
141 if (this._options && this._options.onFirstListenerAdd && this._callbacks.isEmpty()) {
142 this._options.onFirstListenerAdd(this);
143 }
144 this._callbacks.add(listener, thisArgs);
145 const removeMaxListenersCheck = this.checkMaxListeners(Event.getMaxListeners(this._event));
146 const result = {
147 dispose: () => {
148 if (removeMaxListenersCheck) {
149 removeMaxListenersCheck();
150 }
151 result.dispose = Emitter._noop;
152 if (!this._disposed) {
153 this._callbacks.remove(listener, thisArgs);
154 result.dispose = Emitter._noop;
155 if (this._options && this._options.onLastListenerRemove && this._callbacks.isEmpty()) {
156 this._options.onLastListenerRemove(this);
157 }
158 }
159 }
160 };
161 if (disposable_1.DisposableGroup.canPush(disposables)) {
162 disposables.push(result);
163 }
164 else if (disposable_1.DisposableGroup.canAdd(disposables)) {
165 disposables.add(result);
166 }
167 return result;
168 }, {
169 maxListeners: Emitter.LEAK_WARNING_THRESHHOLD
170 });
171 }
172 return this._event;
173 }
174 checkMaxListeners(maxListeners) {
175 if (maxListeners === 0 || !this._callbacks) {
176 return undefined;
177 }
178 const listenerCount = this._callbacks.length;
179 if (listenerCount <= maxListeners) {
180 return undefined;
181 }
182 const popStack = this.pushLeakingStack();
183 this._leakWarnCountdown -= 1;
184 if (this._leakWarnCountdown <= 0) {
185 // only warn on first exceed and then every time the limit
186 // is exceeded by 50% again
187 this._leakWarnCountdown = maxListeners * 0.5;
188 let topStack;
189 let topCount = 0;
190 this._leakingStacks.forEach((stackCount, stack) => {
191 if (!topStack || topCount < stackCount) {
192 topStack = stack;
193 topCount = stackCount;
194 }
195 });
196 // eslint-disable-next-line max-len
197 console.warn(`Possible Emitter memory leak detected. ${listenerCount} listeners added. Use event.maxListeners to increase the limit (${maxListeners}). MOST frequent listener (${topCount}):`);
198 console.warn(topStack);
199 }
200 return popStack;
201 }
202 pushLeakingStack() {
203 if (!this._leakingStacks) {
204 this._leakingStacks = new Map();
205 }
206 const stack = new Error().stack.split('\n').slice(3).join('\n');
207 const count = (this._leakingStacks.get(stack) || 0);
208 this._leakingStacks.set(stack, count + 1);
209 return () => this.popLeakingStack(stack);
210 }
211 popLeakingStack(stack) {
212 if (!this._leakingStacks) {
213 return;
214 }
215 const count = (this._leakingStacks.get(stack) || 0);
216 this._leakingStacks.set(stack, count - 1);
217 }
218 /**
219 * To be kept private to fire an event to
220 * subscribers
221 */
222 fire(event) {
223 if (this._callbacks) {
224 this._callbacks.invoke(event);
225 }
226 }
227 /**
228 * Process each listener one by one.
229 * Return `false` to stop iterating over the listeners, `true` to continue.
230 */
231 async sequence(processor) {
232 if (this._callbacks) {
233 for (const listener of this._callbacks) {
234 if (!await processor(listener)) {
235 break;
236 }
237 }
238 }
239 }
240 dispose() {
241 if (this._leakingStacks) {
242 this._leakingStacks.clear();
243 this._leakingStacks = undefined;
244 }
245 if (this._callbacks) {
246 this._callbacks.dispose();
247 this._callbacks = undefined;
248 }
249 this._disposed = true;
250 }
251}
252exports.Emitter = Emitter;
253Emitter.LEAK_WARNING_THRESHHOLD = 175;
254Emitter._noop = function () { };
255var WaitUntilEvent;
256(function (WaitUntilEvent) {
257 /**
258 * Fire all listeners in the same tick.
259 *
260 * Use `AsyncEmitter.fire` to fire listeners async one after another.
261 */
262 async function fire(emitter, event, timeout = undefined) {
263 const waitables = [];
264 const asyncEvent = Object.assign(event, {
265 waitUntil: (thenable) => {
266 if (Object.isFrozen(waitables)) {
267 throw new Error('waitUntil cannot be called asynchronously.');
268 }
269 waitables.push(thenable);
270 }
271 });
272 try {
273 emitter.fire(asyncEvent);
274 // Asynchronous calls to `waitUntil` should fail.
275 Object.freeze(waitables);
276 }
277 finally {
278 delete asyncEvent['waitUntil'];
279 }
280 if (!waitables.length) {
281 return;
282 }
283 if (timeout !== undefined) {
284 await Promise.race([Promise.all(waitables), new Promise(resolve => setTimeout(resolve, timeout))]);
285 }
286 else {
287 await Promise.all(waitables);
288 }
289 }
290 WaitUntilEvent.fire = fire;
291})(WaitUntilEvent = exports.WaitUntilEvent || (exports.WaitUntilEvent = {}));
292const cancellation_1 = require("./cancellation");
293class AsyncEmitter extends Emitter {
294 /**
295 * Fire listeners async one after another.
296 */
297 fire(event, token = cancellation_1.CancellationToken.None, promiseJoin) {
298 const callbacks = this._callbacks;
299 if (!callbacks) {
300 return Promise.resolve();
301 }
302 const listeners = [...callbacks];
303 if (this.deliveryQueue) {
304 return this.deliveryQueue = this.deliveryQueue.then(() => this.deliver(listeners, event, token, promiseJoin));
305 }
306 return this.deliveryQueue = this.deliver(listeners, event, token, promiseJoin);
307 }
308 async deliver(listeners, event, token, promiseJoin) {
309 for (const listener of listeners) {
310 if (token.isCancellationRequested) {
311 return;
312 }
313 const waitables = [];
314 const asyncEvent = Object.assign(event, {
315 waitUntil: (thenable) => {
316 if (Object.isFrozen(waitables)) {
317 throw new Error('waitUntil cannot be called asynchronously.');
318 }
319 if (promiseJoin) {
320 thenable = promiseJoin(thenable, listener);
321 }
322 waitables.push(thenable);
323 }
324 });
325 try {
326 listener(event);
327 // Asynchronous calls to `waitUntil` should fail.
328 Object.freeze(waitables);
329 }
330 catch (e) {
331 console.error(e);
332 }
333 finally {
334 delete asyncEvent['waitUntil'];
335 }
336 if (!waitables.length) {
337 return;
338 }
339 try {
340 await Promise.all(waitables);
341 }
342 catch (e) {
343 console.error(e);
344 }
345 }
346 }
347}
348exports.AsyncEmitter = AsyncEmitter;
349//# sourceMappingURL=event.js.map
\No newline at end of file