/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ 'use strict'; import {IDisposable, dispose} from 'vs/base/common/lifecycle'; import CallbackList from 'vs/base/common/callbackList'; import {EventEmitter} from 'vs/base/common/eventEmitter'; import {TPromise} from 'vs/base/common/winjs.base'; /** * To an event a function with one or zero parameters * can be subscribed. The event is the subscriber function itself. */ interface Event { (listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[]): IDisposable; } namespace Event { const _disposable = { dispose() { } }; export const None: Event = function() { return _disposable; }; } export default Event; export interface EmitterOptions { onFirstListenerAdd?: Function; onFirstListenerDidAdd?: Function; onLastListenerRemove?: Function; } /** * The Emitter can be used to expose an Event to the public * to fire it from the insides. * Sample: class Document { private _onDidChange = new Emitter<(value:string)=>any>(); public onDidChange = this._onDidChange.event; // getter-style // get onDidChange(): Event<(value:string)=>any> { // return this._onDidChange.event; // } private _doIt() { //... this._onDidChange.fire(value); } } */ export class Emitter { private static _noop = function () { }; private _event: Event; private _callbacks: CallbackList; private _disposed: boolean; constructor(private _options?: EmitterOptions) { } /** * For the public to allow to subscribe * to events from this Emitter */ get event(): Event { if (!this._event) { this._event = (listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[]) => { if (!this._callbacks) { this._callbacks = new CallbackList(); } const firstListener = this._callbacks.isEmpty(); if (firstListener && this._options && this._options.onFirstListenerAdd) { this._options.onFirstListenerAdd(this); } this._callbacks.add(listener, thisArgs); if (firstListener && this._options && this._options.onFirstListenerDidAdd) { this._options.onFirstListenerDidAdd(this); } let result: IDisposable; result = { dispose: () => { result.dispose = Emitter._noop; if (!this._disposed) { this._callbacks.remove(listener, thisArgs); if(this._options && this._options.onLastListenerRemove && this._callbacks.isEmpty()) { this._options.onLastListenerRemove(this); } } } }; if(Array.isArray(disposables)) { disposables.push(result); } return result; }; } return this._event; } /** * To be kept private to fire an event to * subscribers */ fire(event?: T): any { if (this._callbacks) { this._callbacks.invoke.call(this._callbacks, event); } } dispose() { if(this._callbacks) { this._callbacks.dispose(); this._callbacks = undefined; this._disposed = true; } } } /** * Creates an Event which is backed-up by the event emitter. This allows * to use the existing eventing pattern and is likely using less memory. * Sample: * * class Document { * * private _eventbus = new EventEmitter(); * * public onDidChange = fromEventEmitter(this._eventbus, 'changed'); * * // getter-style * // get onDidChange(): Event<(value:string)=>any> { * // cache fromEventEmitter result and return * // } * * private _doIt() { * // ... * this._eventbus.emit('changed', value) * } * } */ export function fromEventEmitter(emitter: EventEmitter, eventType: string): Event { return function (listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[]): IDisposable { const result = emitter.addListener2(eventType, function () { listener.apply(thisArgs, arguments); }); if(Array.isArray(disposables)) { disposables.push(result); } return result; }; } export function fromPromise(promise: TPromise): Event { const emitter = new Emitter(); let shouldEmit = false; promise .then(null, () => null) .then(() => { if (!shouldEmit) { setTimeout(() => emitter.fire(), 0); } else { emitter.fire(); } }); shouldEmit = true; return emitter.event; } export function delayed(promise: TPromise>): Event { let toCancel: TPromise = null; let listener: IDisposable = null; const emitter = new Emitter({ onFirstListenerAdd() { toCancel = promise.then( event => listener = event(e => emitter.fire(e)), () => null ); }, onLastListenerRemove() { if (toCancel) { toCancel.cancel(); toCancel = null; } if (listener) { listener.dispose(); listener = null; } } }); return emitter.event; } export function once(event: Event): Event { return (listener, thisArgs = null, disposables?) => { const result = event(e => { result.dispose(); return listener.call(thisArgs, e); }, null, disposables); return result; }; } export function any(...events: Event[]): Event { let listeners = []; const emitter = new Emitter({ onFirstListenerAdd() { listeners = events.map(e => e(() => emitter.fire(), null)); }, onLastListenerRemove() { listeners = dispose(listeners); } }); return emitter.event; } export function debounceEvent(event: Event, merger: (last: O, event: I) => O, delay: number = 100): Event { let subscription: IDisposable; let output: O; let handle: number; const emitter = new Emitter({ onFirstListenerAdd() { subscription = event(cur => { output = merger(output, cur); clearTimeout(handle); handle = setTimeout(() => { let _output = output; output = undefined; emitter.fire(_output); }, delay); }); }, onLastListenerRemove() { subscription.dispose(); } }); return emitter.event; } enum EventDelayerState { Idle, Running } /** * The EventDelayer is useful in situations in which you want * to delay firing your events during some code. * You can wrap that code and be sure that the event will not * be fired during that wrap. * * ``` * const emitter: Emitter; * const delayer = new EventDelayer(); * const delayedEvent = delayer.wrapEvent(emitter.event); * * delayedEvent(console.log); * * delayer.bufferEvents(() => { * emitter.fire(); // event will not be fired yet * }); * * // event will only be fired at this point * ``` */ export class EventBufferer { private buffers: Function[][] = []; wrapEvent(event: Event): Event { return (listener, thisArgs?, disposables?) => { return event(i => { const buffer = this.buffers[this.buffers.length - 1]; if (buffer) { buffer.push(() => listener.call(thisArgs, i)); } else { listener.call(thisArgs, i); } }, void 0, disposables); }; } bufferEvents(fn: () => void): void { const buffer = []; this.buffers.push(buffer); fn(); this.buffers.pop(); buffer.forEach(flush => flush()); } } export interface IChainableEvent { event: Event; map(fn: (i: T) => O): IChainableEvent; filter(fn: (e: T) => boolean): IChainableEvent; on(listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[]): IDisposable; } export function mapEvent(event: Event, map: (i:I)=>O): Event { return (listener, thisArgs = null, disposables?) => event(i => listener.call(thisArgs, map(i)), null, disposables); } export function filterEvent(event: Event, filter: (e:T)=>boolean): Event { return (listener, thisArgs = null, disposables?) => event(e => filter(e) && listener.call(thisArgs, e), null, disposables); } class ChainableEvent implements IChainableEvent { get event(): Event { return this._event; } constructor(private _event: Event) {} map(fn) { return new ChainableEvent(mapEvent(this._event, fn)); } filter(fn) { return new ChainableEvent(filterEvent(this._event, fn)); } on(listener, thisArgs, disposables) { return this._event(listener, thisArgs, disposables); } } export function chain(event: Event): IChainableEvent { return new ChainableEvent(event); } export function stopwatch(event: Event): Event { const start = new Date().getTime(); return mapEvent(once(event), _ => new Date().getTime() - start); } /** * Buffers the provided event until a first listener comes * along, at which point fire all the events at once and * pipe the event from then on. * * ```typescript * const emitter = new Emitter(); * const event = emitter.event; * const bufferedEvent = buffer(event); * * emitter.fire(1); * emitter.fire(2); * emitter.fire(3); * // nothing... * * const listener = bufferedEvent(num => console.log(num)); * // 1, 2, 3 * * emitter.fire(4); * // 4 * ``` */ export function buffer(event: Event, nextTick = false): Event { let buffer: T[] = []; let listener = event(e => { if (buffer) { buffer.push(e); } else { emitter.fire(e); } }); const flush = () => { buffer.forEach(e => emitter.fire(e)); buffer = null; }; const emitter = new Emitter({ onFirstListenerAdd() { if (!listener) { listener = event(e => emitter.fire(e)); } }, onFirstListenerDidAdd() { if (buffer) { if (nextTick) { setTimeout(flush); } else { flush(); } } }, onLastListenerRemove() { listener.dispose(); listener = null; } }); return emitter.event; }