UNPKG

13.9 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-only 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, returns another event which only fires once.
49 */
50 function once(event) {
51 return (listener, thisArgs = undefined, disposables) => {
52 // we need this, in case the event fires during the listener call
53 let didFire = false;
54 let result = undefined;
55 result = event(e => {
56 if (didFire) {
57 return;
58 }
59 else if (result) {
60 result.dispose();
61 }
62 else {
63 didFire = true;
64 }
65 return listener.call(thisArgs, e);
66 }, undefined, disposables);
67 if (didFire) {
68 result.dispose();
69 }
70 return result;
71 };
72 }
73 Event.once = once;
74 function toPromise(event) {
75 return new Promise(resolve => once(event)(resolve));
76 }
77 Event.toPromise = toPromise;
78 /**
79 * Given an event and a `map` function, returns another event which maps each element
80 * through the mapping function.
81 */
82 function map(event, mapFunc) {
83 return Object.assign((listener, thisArgs, disposables) => event(i => listener.call(thisArgs, mapFunc(i)), undefined, disposables), {
84 get maxListeners() { return 0; },
85 set maxListeners(maxListeners) { }
86 });
87 }
88 Event.map = map;
89 function any(...events) {
90 return (listener, thisArgs = undefined, disposables) => new disposable_1.DisposableCollection(...events.map(event => event(e => listener.call(thisArgs, e), undefined, disposables)));
91 }
92 Event.any = any;
93})(Event = exports.Event || (exports.Event = {}));
94class CallbackList {
95 get length() {
96 return this._callbacks && this._callbacks.length || 0;
97 }
98 add(callback, context = undefined, bucket) {
99 if (!this._callbacks) {
100 this._callbacks = [];
101 this._contexts = [];
102 }
103 this._callbacks.push(callback);
104 this._contexts.push(context);
105 if (Array.isArray(bucket)) {
106 bucket.push({ dispose: () => this.remove(callback, context) });
107 }
108 }
109 remove(callback, context = undefined) {
110 if (!this._callbacks) {
111 return;
112 }
113 let foundCallbackWithDifferentContext = false;
114 for (let i = 0; i < this._callbacks.length; i++) {
115 if (this._callbacks[i] === callback) {
116 if (this._contexts[i] === context) {
117 // callback & context match => remove it
118 this._callbacks.splice(i, 1);
119 this._contexts.splice(i, 1);
120 return;
121 }
122 else {
123 foundCallbackWithDifferentContext = true;
124 }
125 }
126 }
127 if (foundCallbackWithDifferentContext) {
128 throw new Error('When adding a listener with a context, you should remove it with the same context');
129 }
130 }
131 // tslint:disable-next-line:typedef
132 [Symbol.iterator]() {
133 if (!this._callbacks) {
134 return [][Symbol.iterator]();
135 }
136 const callbacks = this._callbacks.slice(0);
137 const contexts = this._contexts.slice(0);
138 return callbacks.map((callback, i) => (...args) => callback.apply(contexts[i], args))[Symbol.iterator]();
139 }
140 invoke(...args) {
141 const ret = [];
142 for (const callback of this) {
143 try {
144 ret.push(callback(...args));
145 }
146 catch (e) {
147 console.error(e);
148 }
149 }
150 return ret;
151 }
152 isEmpty() {
153 return !this._callbacks || this._callbacks.length === 0;
154 }
155 dispose() {
156 this._callbacks = undefined;
157 this._contexts = undefined;
158 }
159}
160class Emitter {
161 constructor(_options) {
162 this._options = _options;
163 this._disposed = false;
164 this._leakWarnCountdown = 0;
165 }
166 /**
167 * For the public to allow to subscribe
168 * to events from this Emitter
169 */
170 get event() {
171 if (!this._event) {
172 this._event = Object.assign((listener, thisArgs, disposables) => {
173 if (!this._callbacks) {
174 this._callbacks = new CallbackList();
175 }
176 if (this._options && this._options.onFirstListenerAdd && this._callbacks.isEmpty()) {
177 this._options.onFirstListenerAdd(this);
178 }
179 this._callbacks.add(listener, thisArgs);
180 const removeMaxListenersCheck = this.checkMaxListeners(Event.getMaxListeners(this._event));
181 const result = {
182 dispose: () => {
183 if (removeMaxListenersCheck) {
184 removeMaxListenersCheck();
185 }
186 result.dispose = Emitter._noop;
187 if (!this._disposed) {
188 this._callbacks.remove(listener, thisArgs);
189 result.dispose = Emitter._noop;
190 if (this._options && this._options.onLastListenerRemove && this._callbacks.isEmpty()) {
191 this._options.onLastListenerRemove(this);
192 }
193 }
194 }
195 };
196 if (disposable_1.DisposableGroup.canPush(disposables)) {
197 disposables.push(result);
198 }
199 else if (disposable_1.DisposableGroup.canAdd(disposables)) {
200 disposables.add(result);
201 }
202 return result;
203 }, {
204 maxListeners: Emitter.LEAK_WARNING_THRESHHOLD
205 });
206 }
207 return this._event;
208 }
209 checkMaxListeners(maxListeners) {
210 if (maxListeners === 0 || !this._callbacks) {
211 return undefined;
212 }
213 const listenerCount = this._callbacks.length;
214 if (listenerCount <= maxListeners) {
215 return undefined;
216 }
217 const popStack = this.pushLeakingStack();
218 this._leakWarnCountdown -= 1;
219 if (this._leakWarnCountdown <= 0) {
220 // only warn on first exceed and then every time the limit
221 // is exceeded by 50% again
222 this._leakWarnCountdown = maxListeners * 0.5;
223 let topStack;
224 let topCount = 0;
225 this._leakingStacks.forEach((stackCount, stack) => {
226 if (!topStack || topCount < stackCount) {
227 topStack = stack;
228 topCount = stackCount;
229 }
230 });
231 // eslint-disable-next-line max-len
232 console.warn(`Possible Emitter memory leak detected. ${listenerCount} listeners added. Use event.maxListeners to increase the limit (${maxListeners}). MOST frequent listener (${topCount}):`);
233 console.warn(topStack);
234 }
235 return popStack;
236 }
237 pushLeakingStack() {
238 if (!this._leakingStacks) {
239 this._leakingStacks = new Map();
240 }
241 const stack = new Error().stack.split('\n').slice(3).join('\n');
242 const count = (this._leakingStacks.get(stack) || 0);
243 this._leakingStacks.set(stack, count + 1);
244 return () => this.popLeakingStack(stack);
245 }
246 popLeakingStack(stack) {
247 if (!this._leakingStacks) {
248 return;
249 }
250 const count = (this._leakingStacks.get(stack) || 0);
251 this._leakingStacks.set(stack, count - 1);
252 }
253 /**
254 * To be kept private to fire an event to
255 * subscribers
256 */
257 fire(event) {
258 if (this._callbacks) {
259 return this._callbacks.invoke(event);
260 }
261 }
262 /**
263 * Process each listener one by one.
264 * Return `false` to stop iterating over the listeners, `true` to continue.
265 */
266 async sequence(processor) {
267 if (this._callbacks) {
268 for (const listener of this._callbacks) {
269 if (!await processor(listener)) {
270 break;
271 }
272 }
273 }
274 }
275 dispose() {
276 if (this._leakingStacks) {
277 this._leakingStacks.clear();
278 this._leakingStacks = undefined;
279 }
280 if (this._callbacks) {
281 this._callbacks.dispose();
282 this._callbacks = undefined;
283 }
284 this._disposed = true;
285 }
286}
287exports.Emitter = Emitter;
288Emitter.LEAK_WARNING_THRESHHOLD = 175;
289Emitter._noop = function () { };
290var WaitUntilEvent;
291(function (WaitUntilEvent) {
292 /**
293 * Fire all listeners in the same tick.
294 *
295 * Use `AsyncEmitter.fire` to fire listeners async one after another.
296 */
297 async function fire(emitter, event, timeout, token = cancellation_1.CancellationToken.None) {
298 const waitables = [];
299 const asyncEvent = Object.assign(event, {
300 token,
301 waitUntil: (thenable) => {
302 if (Object.isFrozen(waitables)) {
303 throw new Error('waitUntil cannot be called asynchronously.');
304 }
305 waitables.push(thenable);
306 }
307 });
308 try {
309 emitter.fire(asyncEvent);
310 // Asynchronous calls to `waitUntil` should fail.
311 Object.freeze(waitables);
312 }
313 finally {
314 delete asyncEvent['waitUntil'];
315 }
316 if (!waitables.length) {
317 return;
318 }
319 if (timeout !== undefined) {
320 await Promise.race([Promise.all(waitables), new Promise(resolve => setTimeout(resolve, timeout))]);
321 }
322 else {
323 await Promise.all(waitables);
324 }
325 }
326 WaitUntilEvent.fire = fire;
327})(WaitUntilEvent = exports.WaitUntilEvent || (exports.WaitUntilEvent = {}));
328const cancellation_1 = require("./cancellation");
329class AsyncEmitter extends Emitter {
330 /**
331 * Fire listeners async one after another.
332 */
333 fire(event, token = cancellation_1.CancellationToken.None, promiseJoin) {
334 const callbacks = this._callbacks;
335 if (!callbacks) {
336 return Promise.resolve();
337 }
338 const listeners = [...callbacks];
339 if (this.deliveryQueue) {
340 return this.deliveryQueue = this.deliveryQueue.then(() => this.deliver(listeners, event, token, promiseJoin));
341 }
342 return this.deliveryQueue = this.deliver(listeners, event, token, promiseJoin);
343 }
344 async deliver(listeners, event, token, promiseJoin) {
345 for (const listener of listeners) {
346 if (token.isCancellationRequested) {
347 return;
348 }
349 const waitables = [];
350 const asyncEvent = Object.assign(event, {
351 token,
352 waitUntil: (thenable) => {
353 if (Object.isFrozen(waitables)) {
354 throw new Error('waitUntil cannot be called asynchronously.');
355 }
356 if (promiseJoin) {
357 thenable = promiseJoin(thenable, listener);
358 }
359 waitables.push(thenable);
360 }
361 });
362 try {
363 listener(event);
364 // Asynchronous calls to `waitUntil` should fail.
365 Object.freeze(waitables);
366 }
367 catch (e) {
368 console.error(e);
369 }
370 finally {
371 delete asyncEvent['waitUntil'];
372 }
373 if (!waitables.length) {
374 return;
375 }
376 try {
377 await Promise.all(waitables);
378 }
379 catch (e) {
380 console.error(e);
381 }
382 }
383 }
384}
385exports.AsyncEmitter = AsyncEmitter;
386//# sourceMappingURL=event.js.map
\No newline at end of file