@pkerschbaum/code-oss-file-service
Version:
VS Code ([microsoft/vscode](https://github.com/microsoft/vscode)) includes a rich "`FileService`" and "`DiskFileSystemProvider`" abstraction built on top of Node.js core modules (`fs`, `path`) and Electron's `shell` module. This package allows to use that
950 lines (789 loc) • 27.6 kB
text/typescript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { CancellationToken } from 'vs/base/common/cancellation';
import { onUnexpectedError } from 'vs/base/common/errors';
import { once as onceFn } from 'vs/base/common/functional';
import { combinedDisposable, Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { LinkedList } from 'vs/base/common/linkedList';
import { StopWatch } from 'vs/base/common/stopwatch';
/**
* To an event a function with one or zero parameters
* can be subscribed. The event is the subscriber function itself.
*/
export interface Event<T> {
(listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[] | DisposableStore): IDisposable;
}
export namespace Event {
export const None: Event<any> = () => Disposable.None;
/**
* Given an event, returns another event which only fires once.
*/
export function once<T>(event: Event<T>): Event<T> {
return (listener, thisArgs = null, disposables?) => {
// we need this, in case the event fires during the listener call
let didFire = false;
let result: IDisposable;
result = event(e => {
if (didFire) {
return;
} else if (result) {
result.dispose();
} else {
didFire = true;
}
return listener.call(thisArgs, e);
}, null, disposables);
if (didFire) {
result.dispose();
}
return result;
};
}
/**
* @deprecated DO NOT use, this leaks memory
*/
export function map<I, O>(event: Event<I>, map: (i: I) => O): Event<O> {
return snapshot((listener, thisArgs = null, disposables?) => event(i => listener.call(thisArgs, map(i)), null, disposables));
}
/**
* @deprecated DO NOT use, this leaks memory
*/
export function forEach<I>(event: Event<I>, each: (i: I) => void): Event<I> {
return snapshot((listener, thisArgs = null, disposables?) => event(i => { each(i); listener.call(thisArgs, i); }, null, disposables));
}
/**
* @deprecated DO NOT use, this leaks memory
*/
export function filter<T, U>(event: Event<T | U>, filter: (e: T | U) => e is T): Event<T>;
export function filter<T>(event: Event<T>, filter: (e: T) => boolean): Event<T>;
export function filter<T, R>(event: Event<T | R>, filter: (e: T | R) => e is R): Event<R>;
export function filter<T>(event: Event<T>, filter: (e: T) => boolean): Event<T> {
return snapshot((listener, thisArgs = null, disposables?) => event(e => filter(e) && listener.call(thisArgs, e), null, disposables));
}
/**
* Given an event, returns the same event but typed as `Event<void>`.
*/
export function signal<T>(event: Event<T>): Event<void> {
return event as Event<any> as Event<void>;
}
/**
* Given a collection of events, returns a single event which emits
* whenever any of the provided events emit.
*/
export function any<T>(...events: Event<T>[]): Event<T>;
export function any(...events: Event<any>[]): Event<void>;
export function any<T>(...events: Event<T>[]): Event<T> {
return (listener, thisArgs = null, disposables?) => combinedDisposable(...events.map(event => event(e => listener.call(thisArgs, e), null, disposables)));
}
/**
* @deprecated DO NOT use, this leaks memory
*/
export function reduce<I, O>(event: Event<I>, merge: (last: O | undefined, event: I) => O, initial?: O): Event<O> {
let output: O | undefined = initial;
return map<I, O>(event, e => {
output = merge(output, e);
return output;
});
}
/**
* @deprecated DO NOT use, this leaks memory
*/
function snapshot<T>(event: Event<T>): Event<T> {
let listener: IDisposable;
const emitter = new Emitter<T>({
onFirstListenerAdd() {
listener = event(emitter.fire, emitter);
},
onLastListenerRemove() {
listener.dispose();
}
});
return emitter.event;
}
export function debouncedListener<T, O = T>(event: Event<T>, listener: (data: O) => any, merge: (last: O | undefined, event: T) => O, delay: number = 100, leading: boolean = false): IDisposable {
let output: O | undefined = undefined;
let handle: any = undefined;
let numDebouncedCalls = 0;
return event(cur => {
numDebouncedCalls++;
output = merge(output, cur);
if (leading && !handle) {
listener(output);
output = undefined;
}
clearTimeout(handle);
handle = setTimeout(() => {
const _output = output;
output = undefined;
handle = undefined;
if (!leading || numDebouncedCalls > 1) {
listener(_output!);
}
numDebouncedCalls = 0;
}, delay);
});
}
/**
* @deprecated this leaks memory, {@link debouncedListener} or {@link DebounceEmitter} instead
*/
export function debounce<T>(event: Event<T>, merge: (last: T | undefined, event: T) => T, delay?: number, leading?: boolean, leakWarningThreshold?: number): Event<T>;
/**
* @deprecated this leaks memory, {@link debouncedListener} or {@link DebounceEmitter} instead
*/
export function debounce<I, O>(event: Event<I>, merge: (last: O | undefined, event: I) => O, delay?: number, leading?: boolean, leakWarningThreshold?: number): Event<O>;
/**
* @deprecated this leaks memory, {@link debouncedListener} or {@link DebounceEmitter} instead
*/
export function debounce<I, O>(event: Event<I>, merge: (last: O | undefined, event: I) => O, delay: number = 100, leading = false, leakWarningThreshold?: number): Event<O> {
let subscription: IDisposable;
let output: O | undefined = undefined;
let handle: any = undefined;
let numDebouncedCalls = 0;
const emitter = new Emitter<O>({
leakWarningThreshold,
onFirstListenerAdd() {
subscription = event(cur => {
numDebouncedCalls++;
output = merge(output, cur);
if (leading && !handle) {
emitter.fire(output);
output = undefined;
}
clearTimeout(handle);
handle = setTimeout(() => {
const _output = output;
output = undefined;
handle = undefined;
if (!leading || numDebouncedCalls > 1) {
emitter.fire(_output!);
}
numDebouncedCalls = 0;
}, delay);
});
},
onLastListenerRemove() {
subscription.dispose();
}
});
return emitter.event;
}
/**
* @deprecated DO NOT use, this leaks memory
*/
export function latch<T>(event: Event<T>, equals: (a: T, b: T) => boolean = (a, b) => a === b): Event<T> {
let firstCall = true;
let cache: T;
return filter(event, value => {
const shouldEmit = firstCall || !equals(value, cache);
firstCall = false;
cache = value;
return shouldEmit;
});
}
/**
* @deprecated DO NOT use, this leaks memory
*/
export function split<T, U>(event: Event<T | U>, isT: (e: T | U) => e is T): [Event<T>, Event<U>] {
return [
Event.filter(event, isT),
Event.filter(event, e => !isT(e)) as Event<U>,
];
}
/**
* @deprecated DO NOT use, this leaks memory
*/
export function buffer<T>(event: Event<T>, flushAfterTimeout = false, _buffer: T[] = []): Event<T> {
let buffer: T[] | null = _buffer.slice();
let listener: IDisposable | null = event(e => {
if (buffer) {
buffer.push(e);
} else {
emitter.fire(e);
}
});
const flush = () => {
if (buffer) {
buffer.forEach(e => emitter.fire(e));
}
buffer = null;
};
const emitter = new Emitter<T>({
onFirstListenerAdd() {
if (!listener) {
listener = event(e => emitter.fire(e));
}
},
onFirstListenerDidAdd() {
if (buffer) {
if (flushAfterTimeout) {
setTimeout(flush);
} else {
flush();
}
}
},
onLastListenerRemove() {
if (listener) {
listener.dispose();
}
listener = null;
}
});
return emitter.event;
}
export interface IChainableEvent<T> {
event: Event<T>;
map<O>(fn: (i: T) => O): IChainableEvent<O>;
forEach(fn: (i: T) => void): IChainableEvent<T>;
filter(fn: (e: T) => boolean): IChainableEvent<T>;
filter<R>(fn: (e: T | R) => e is R): IChainableEvent<R>;
reduce<R>(merge: (last: R | undefined, event: T) => R, initial?: R): IChainableEvent<R>;
latch(): IChainableEvent<T>;
debounce(merge: (last: T | undefined, event: T) => T, delay?: number, leading?: boolean, leakWarningThreshold?: number): IChainableEvent<T>;
debounce<R>(merge: (last: R | undefined, event: T) => R, delay?: number, leading?: boolean, leakWarningThreshold?: number): IChainableEvent<R>;
on(listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[] | DisposableStore): IDisposable;
once(listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[]): IDisposable;
}
class ChainableEvent<T> implements IChainableEvent<T> {
constructor(readonly event: Event<T>) { }
map<O>(fn: (i: T) => O): IChainableEvent<O> {
return new ChainableEvent(map(this.event, fn));
}
forEach(fn: (i: T) => void): IChainableEvent<T> {
return new ChainableEvent(forEach(this.event, fn));
}
filter(fn: (e: T) => boolean): IChainableEvent<T>;
filter<R>(fn: (e: T | R) => e is R): IChainableEvent<R>;
filter(fn: (e: T) => boolean): IChainableEvent<T> {
return new ChainableEvent(filter(this.event, fn));
}
reduce<R>(merge: (last: R | undefined, event: T) => R, initial?: R): IChainableEvent<R> {
return new ChainableEvent(reduce(this.event, merge, initial));
}
latch(): IChainableEvent<T> {
return new ChainableEvent(latch(this.event));
}
debounce(merge: (last: T | undefined, event: T) => T, delay?: number, leading?: boolean, leakWarningThreshold?: number): IChainableEvent<T>;
debounce<R>(merge: (last: R | undefined, event: T) => R, delay?: number, leading?: boolean, leakWarningThreshold?: number): IChainableEvent<R>;
debounce<R>(merge: (last: R | undefined, event: T) => R, delay: number = 100, leading = false, leakWarningThreshold?: number): IChainableEvent<R> {
return new ChainableEvent(debounce(this.event, merge, delay, leading, leakWarningThreshold));
}
on(listener: (e: T) => any, thisArgs: any, disposables: IDisposable[] | DisposableStore) {
return this.event(listener, thisArgs, disposables);
}
once(listener: (e: T) => any, thisArgs: any, disposables: IDisposable[]) {
return once(this.event)(listener, thisArgs, disposables);
}
}
/**
* @deprecated DO NOT use, this leaks memory
*/
export function chain<T>(event: Event<T>): IChainableEvent<T> {
return new ChainableEvent(event);
}
export interface NodeEventEmitter {
on(event: string | symbol, listener: Function): unknown;
removeListener(event: string | symbol, listener: Function): unknown;
}
export function fromNodeEventEmitter<T>(emitter: NodeEventEmitter, eventName: string, map: (...args: any[]) => T = id => id): Event<T> {
const fn = (...args: any[]) => result.fire(map(...args));
const onFirstListenerAdd = () => emitter.on(eventName, fn);
const onLastListenerRemove = () => emitter.removeListener(eventName, fn);
const result = new Emitter<T>({ onFirstListenerAdd, onLastListenerRemove });
return result.event;
}
export interface DOMEventEmitter {
addEventListener(event: string | symbol, listener: Function): void;
removeEventListener(event: string | symbol, listener: Function): void;
}
export function fromDOMEventEmitter<T>(emitter: DOMEventEmitter, eventName: string, map: (...args: any[]) => T = id => id): Event<T> {
const fn = (...args: any[]) => result.fire(map(...args));
const onFirstListenerAdd = () => emitter.addEventListener(eventName, fn);
const onLastListenerRemove = () => emitter.removeEventListener(eventName, fn);
const result = new Emitter<T>({ onFirstListenerAdd, onLastListenerRemove });
return result.event;
}
export function toPromise<T>(event: Event<T>): Promise<T> {
return new Promise(resolve => once(event)(resolve));
}
export function runAndSubscribe<T>(event: Event<T>, handler: (e: T | undefined) => any): IDisposable {
handler(undefined);
return event(e => handler(e));
}
}
export type Listener<T> = [(e: T) => void, any] | ((e: T) => void);
export interface EmitterOptions {
onFirstListenerAdd?: Function;
onFirstListenerDidAdd?: Function;
onListenerDidAdd?: Function;
onLastListenerRemove?: Function;
leakWarningThreshold?: number;
/** ONLY enable this during development */
_profName?: string
}
class EventProfiling {
private static _idPool = 0;
private _name: string;
private _stopWatch?: StopWatch;
private _listenerCount: number = 0;
private _invocationCount = 0;
private _elapsedOverall = 0;
constructor(name: string) {
this._name = `${name}_${EventProfiling._idPool++}`;
}
start(listenerCount: number): void {
this._stopWatch = new StopWatch(true);
this._listenerCount = listenerCount;
}
stop(): void {
if (this._stopWatch) {
const elapsed = this._stopWatch.elapsed();
this._elapsedOverall += elapsed;
this._invocationCount += 1;
console.info(`did FIRE ${this._name}: elapsed_ms: ${elapsed.toFixed(5)}, listener: ${this._listenerCount} (elapsed_overall: ${this._elapsedOverall.toFixed(2)}, invocations: ${this._invocationCount})`);
this._stopWatch = undefined;
}
}
}
let _globalLeakWarningThreshold = -1;
export function setGlobalLeakWarningThreshold(n: number): IDisposable {
const oldValue = _globalLeakWarningThreshold;
_globalLeakWarningThreshold = n;
return {
dispose() {
_globalLeakWarningThreshold = oldValue;
}
};
}
class LeakageMonitor {
private _stacks: Map<string, number> | undefined;
private _warnCountdown: number = 0;
constructor(
readonly customThreshold?: number,
readonly name: string = Math.random().toString(18).slice(2, 5),
) { }
dispose(): void {
if (this._stacks) {
this._stacks.clear();
}
}
check(listenerCount: number): undefined | (() => void) {
let threshold = _globalLeakWarningThreshold;
if (typeof this.customThreshold === 'number') {
threshold = this.customThreshold;
}
if (threshold <= 0 || listenerCount < threshold) {
return undefined;
}
if (!this._stacks) {
this._stacks = new Map();
}
const stack = new Error().stack!.split('\n').slice(3).join('\n');
const count = (this._stacks.get(stack) || 0);
this._stacks.set(stack, count + 1);
this._warnCountdown -= 1;
if (this._warnCountdown <= 0) {
// only warn on first exceed and then every time the limit
// is exceeded by 50% again
this._warnCountdown = threshold * 0.5;
// find most frequent listener and print warning
let topStack: string | undefined;
let topCount: number = 0;
for (const [stack, count] of this._stacks) {
if (!topStack || topCount < count) {
topStack = stack;
topCount = count;
}
}
console.warn(`[${this.name}] potential listener LEAK detected, having ${listenerCount} listeners already. MOST frequent listener (${topCount}):`);
console.warn(topStack!);
}
return () => {
const count = (this._stacks!.get(stack) || 0);
this._stacks!.set(stack, count - 1);
};
}
}
/**
* The Emitter can be used to expose an Event to the public
* to fire it from the insides.
* Sample:
class Document {
private readonly _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<T> {
private readonly _options?: EmitterOptions;
private readonly _leakageMon?: LeakageMonitor;
private readonly _perfMon?: EventProfiling;
private _disposed: boolean = false;
private _event?: Event<T>;
private _deliveryQueue?: LinkedList<[Listener<T>, T]>;
protected _listeners?: LinkedList<Listener<T>>;
constructor(options?: EmitterOptions) {
this._options = options;
this._leakageMon = _globalLeakWarningThreshold > 0 ? new LeakageMonitor(this._options && this._options.leakWarningThreshold) : undefined;
this._perfMon = this._options?._profName ? new EventProfiling(this._options._profName) : undefined;
}
/**
* For the public to allow to subscribe
* to events from this Emitter
*/
get event(): Event<T> {
if (!this._event) {
this._event = (listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[] | DisposableStore) => {
if (!this._listeners) {
this._listeners = new LinkedList();
}
const firstListener = this._listeners.isEmpty();
if (firstListener && this._options && this._options.onFirstListenerAdd) {
this._options.onFirstListenerAdd(this);
}
const remove = this._listeners.push(!thisArgs ? listener : [listener, thisArgs]);
if (firstListener && this._options && this._options.onFirstListenerDidAdd) {
this._options.onFirstListenerDidAdd(this);
}
if (this._options && this._options.onListenerDidAdd) {
this._options.onListenerDidAdd(this, listener, thisArgs);
}
// check and record this emitter for potential leakage
const removeMonitor = this._leakageMon?.check(this._listeners.size);
const result = toDisposable(() => {
if (removeMonitor) {
removeMonitor();
}
if (!this._disposed) {
remove();
if (this._options && this._options.onLastListenerRemove) {
const hasListeners = (this._listeners && !this._listeners.isEmpty());
if (!hasListeners) {
this._options.onLastListenerRemove(this);
}
}
}
});
if (disposables instanceof DisposableStore) {
disposables.add(result);
} else 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): void {
if (this._listeners) {
// put all [listener,event]-pairs into delivery queue
// then emit all event. an inner/nested event might be
// the driver of this
if (!this._deliveryQueue) {
this._deliveryQueue = new LinkedList();
}
for (let listener of this._listeners) {
this._deliveryQueue.push([listener, event]);
}
// start/stop performance insight collection
this._perfMon?.start(this._deliveryQueue.size);
while (this._deliveryQueue.size > 0) {
const [listener, event] = this._deliveryQueue.shift()!;
try {
if (typeof listener === 'function') {
listener.call(undefined, event);
} else {
listener[0].call(listener[1], event);
}
} catch (e) {
onUnexpectedError(e);
}
}
this._perfMon?.stop();
}
}
dispose() {
if (!this._disposed) {
this._disposed = true;
this._listeners?.clear();
this._deliveryQueue?.clear();
this._options?.onLastListenerRemove?.();
this._leakageMon?.dispose();
}
}
}
export interface IWaitUntil {
token: CancellationToken;
waitUntil(thenable: Promise<unknown>): void;
}
export type IWaitUntilData<T> = Omit<Omit<T, 'waitUntil'>, 'token'>;
export class AsyncEmitter<T extends IWaitUntil> extends Emitter<T> {
private _asyncDeliveryQueue?: LinkedList<[Listener<T>, IWaitUntilData<T>]>;
async fireAsync(data: IWaitUntilData<T>, token: CancellationToken, promiseJoin?: (p: Promise<unknown>, listener: Function) => Promise<unknown>): Promise<void> {
if (!this._listeners) {
return;
}
if (!this._asyncDeliveryQueue) {
this._asyncDeliveryQueue = new LinkedList();
}
for (const listener of this._listeners) {
this._asyncDeliveryQueue.push([listener, data]);
}
while (this._asyncDeliveryQueue.size > 0 && !token.isCancellationRequested) {
const [listener, data] = this._asyncDeliveryQueue.shift()!;
const thenables: Promise<unknown>[] = [];
const event = <T>{
...data,
token,
waitUntil: (p: Promise<unknown>): void => {
if (Object.isFrozen(thenables)) {
throw new Error('waitUntil can NOT be called asynchronous');
}
if (promiseJoin) {
p = promiseJoin(p, typeof listener === 'function' ? listener : listener[0]);
}
thenables.push(p);
}
};
try {
if (typeof listener === 'function') {
listener.call(undefined, event);
} else {
listener[0].call(listener[1], event);
}
} catch (e) {
onUnexpectedError(e);
continue;
}
// freeze thenables-collection to enforce sync-calls to
// wait until and then wait for all thenables to resolve
Object.freeze(thenables);
await Promise.allSettled(thenables).then(values => {
for (const value of values) {
if (value.status === 'rejected') {
onUnexpectedError(value.reason);
}
}
});
}
}
}
export class PauseableEmitter<T> extends Emitter<T> {
private _isPaused = 0;
protected _eventQueue = new LinkedList<T>();
private _mergeFn?: (input: T[]) => T;
constructor(options?: EmitterOptions & { merge?: (input: T[]) => T }) {
super(options);
this._mergeFn = options?.merge;
}
pause(): void {
this._isPaused++;
}
resume(): void {
if (this._isPaused !== 0 && --this._isPaused === 0) {
if (this._mergeFn) {
// use the merge function to create a single composite
// event. make a copy in case firing pauses this emitter
const events = Array.from(this._eventQueue);
this._eventQueue.clear();
super.fire(this._mergeFn(events));
} else {
// no merging, fire each event individually and test
// that this emitter isn't paused halfway through
while (!this._isPaused && this._eventQueue.size !== 0) {
super.fire(this._eventQueue.shift()!);
}
}
}
}
override fire(event: T): void {
if (this._listeners) {
if (this._isPaused !== 0) {
this._eventQueue.push(event);
} else {
super.fire(event);
}
}
}
}
export class DebounceEmitter<T> extends PauseableEmitter<T> {
private readonly _delay: number;
private _handle: any | undefined;
constructor(options: EmitterOptions & { merge: (input: T[]) => T, delay?: number }) {
super(options);
this._delay = options.delay ?? 100;
}
override fire(event: T): void {
if (!this._handle) {
this.pause();
this._handle = setTimeout(() => {
this._handle = undefined;
this.resume();
}, this._delay);
}
super.fire(event);
}
}
/**
* An emitter which queue all events and then process them at the
* end of the event loop.
*/
export class MicrotaskEmitter<T> extends Emitter<T> {
private _queuedEvents: T[] = [];
private _mergeFn?: (input: T[]) => T;
constructor(options?: EmitterOptions & { merge?: (input: T[]) => T }) {
super(options);
this._mergeFn = options?.merge;
}
override fire(event: T): void {
this._queuedEvents.push(event);
if (this._queuedEvents.length === 1) {
queueMicrotask(() => {
if (this._mergeFn) {
super.fire(this._mergeFn(this._queuedEvents));
} else {
this._queuedEvents.forEach(e => super.fire(e));
}
this._queuedEvents = [];
});
}
}
}
export class EventMultiplexer<T> implements IDisposable {
private readonly emitter: Emitter<T>;
private hasListeners = false;
private events: { event: Event<T>; listener: IDisposable | null; }[] = [];
constructor() {
this.emitter = new Emitter<T>({
onFirstListenerAdd: () => this.onFirstListenerAdd(),
onLastListenerRemove: () => this.onLastListenerRemove()
});
}
get event(): Event<T> {
return this.emitter.event;
}
add(event: Event<T>): IDisposable {
const e = { event: event, listener: null };
this.events.push(e);
if (this.hasListeners) {
this.hook(e);
}
const dispose = () => {
if (this.hasListeners) {
this.unhook(e);
}
const idx = this.events.indexOf(e);
this.events.splice(idx, 1);
};
return toDisposable(onceFn(dispose));
}
private onFirstListenerAdd(): void {
this.hasListeners = true;
this.events.forEach(e => this.hook(e));
}
private onLastListenerRemove(): void {
this.hasListeners = false;
this.events.forEach(e => this.unhook(e));
}
private hook(e: { event: Event<T>; listener: IDisposable | null; }): void {
e.listener = e.event(r => this.emitter.fire(r));
}
private unhook(e: { event: Event<T>; listener: IDisposable | null; }): void {
if (e.listener) {
e.listener.dispose();
}
e.listener = null;
}
dispose(): void {
this.emitter.dispose();
}
}
/**
* The EventBufferer 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<T>(event: Event<T>): Event<T> {
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);
}
}, undefined, disposables);
};
}
bufferEvents<R = void>(fn: () => R): R {
const buffer: Array<() => R> = [];
this.buffers.push(buffer);
const r = fn();
this.buffers.pop();
buffer.forEach(flush => flush());
return r;
}
}
/**
* A Relay is an event forwarder which functions as a replugabble event pipe.
* Once created, you can connect an input event to it and it will simply forward
* events from that input event through its own `event` property. The `input`
* can be changed at any point in time.
*/
export class Relay<T> implements IDisposable {
private listening = false;
private inputEvent: Event<T> = Event.None;
private inputEventListener: IDisposable = Disposable.None;
private readonly emitter = new Emitter<T>({
onFirstListenerDidAdd: () => {
this.listening = true;
this.inputEventListener = this.inputEvent(this.emitter.fire, this.emitter);
},
onLastListenerRemove: () => {
this.listening = false;
this.inputEventListener.dispose();
}
});
readonly event: Event<T> = this.emitter.event;
set input(event: Event<T>) {
this.inputEvent = event;
if (this.listening) {
this.inputEventListener.dispose();
this.inputEventListener = event(this.emitter.fire, this.emitter);
}
}
dispose() {
this.inputEventListener.dispose();
this.emitter.dispose();
}
}