Skip to content

Commit fa17b3e

Browse files
committed
EventMultiplexer
1 parent f68bd90 commit fa17b3e

8 files changed

Lines changed: 250 additions & 26 deletions

File tree

src/vs/base/common/async.ts

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -422,23 +422,6 @@ export function first<T>(promiseFactories: ITask<TPromise<T>>[], shouldStop: (t:
422422
return loop();
423423
}
424424

425-
export function once<T extends Function>(fn: T): T {
426-
const _this = this;
427-
let didCall = false;
428-
let result: any;
429-
430-
return function () {
431-
if (didCall) {
432-
return result;
433-
}
434-
435-
didCall = true;
436-
result = fn.apply(_this, arguments);
437-
438-
return result;
439-
} as any as T;
440-
}
441-
442425
interface ILimitedTaskFactory {
443426
factory: ITask<Promise>;
444427
c: ValueCallback;

src/vs/base/common/event.ts

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@
44
*--------------------------------------------------------------------------------------------*/
55
'use strict';
66

7-
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
7+
import { IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle';
88
import CallbackList from 'vs/base/common/callbackList';
99
import { EventEmitter } from 'vs/base/common/eventEmitter';
1010
import { TPromise } from 'vs/base/common/winjs.base';
11+
import { once as onceFn } from 'vs/base/common/functional';
1112

1213
/**
1314
* To an event a function with one or zero parameters
@@ -127,6 +128,67 @@ export class Emitter<T> {
127128
}
128129
}
129130

131+
export class EventMultiplexer<T> implements IDisposable {
132+
133+
private emitter: Emitter<T>;
134+
private hasListeners = false;
135+
private events: { event: Event<T>; listener: IDisposable; }[] = [];
136+
137+
constructor() {
138+
this.emitter = new Emitter<T>({
139+
onFirstListenerAdd: () => this.onFirstListenerAdd(),
140+
onLastListenerRemove: () => this.onLastListenerRemove()
141+
});
142+
}
143+
144+
get event(): Event<T> {
145+
return this.emitter.event;
146+
}
147+
148+
add(event: Event<T>): IDisposable {
149+
const e = { event: event, listener: null };
150+
this.events.push(e);
151+
152+
if (this.hasListeners) {
153+
this.hook(e);
154+
}
155+
156+
const dispose = () => {
157+
if (this.hasListeners) {
158+
this.unhook(e);
159+
}
160+
161+
const idx = this.events.indexOf(e);
162+
this.events.splice(idx, 1);
163+
};
164+
165+
return toDisposable(onceFn(dispose));
166+
}
167+
168+
private onFirstListenerAdd(): void {
169+
this.hasListeners = true;
170+
this.events.forEach(e => this.hook(e));
171+
}
172+
173+
private onLastListenerRemove(): void {
174+
this.hasListeners = false;
175+
this.events.forEach(e => this.unhook(e));
176+
}
177+
178+
private hook(e: { event: Event<T>; listener: IDisposable; }): void {
179+
e.listener = e.event(r => this.emitter.fire(r));
180+
}
181+
182+
private unhook(e: { event: Event<T>; listener: IDisposable; }): void {
183+
e.listener.dispose();
184+
e.listener = null;
185+
}
186+
187+
dispose(): void {
188+
this.emitter.dispose();
189+
}
190+
}
191+
130192
/**
131193
* Creates an Event which is backed-up by the event emitter. This allows
132194
* to use the existing eventing pattern and is likely using less memory.

src/vs/base/common/functional.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,21 @@
88
export function not<A>(fn: (a: A) => boolean): (a: A) => boolean;
99
export function not(fn: Function): Function {
1010
return (...args) => !fn(...args);
11+
}
12+
13+
export function once<T extends Function>(fn: T): T {
14+
const _this = this;
15+
let didCall = false;
16+
let result: any;
17+
18+
return function () {
19+
if (didCall) {
20+
return result;
21+
}
22+
23+
didCall = true;
24+
result = fn.apply(_this, arguments);
25+
26+
return result;
27+
} as any as T;
1128
}

src/vs/base/node/crypto.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import * as fs from 'fs';
99
import * as crypto from 'crypto';
1010
import * as stream from 'stream';
1111
import { TPromise } from 'vs/base/common/winjs.base';
12-
import { once } from 'vs/base/common/async';
12+
import { once } from 'vs/base/common/functional';
1313

1414
export function checksum(path: string, sha1hash: string): TPromise<void> {
1515
const promise = new TPromise<string>((c, e) => {

src/vs/base/test/common/event.test.ts

Lines changed: 163 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
'use strict';
66

77
import * as assert from 'assert';
8-
import Event, { Emitter, fromEventEmitter, debounceEvent, EventBufferer, once, fromPromise, stopwatch, buffer } from 'vs/base/common/event';
8+
import Event, { Emitter, fromEventEmitter, debounceEvent, EventBufferer, once, fromPromise, stopwatch, buffer, EventMultiplexer } from 'vs/base/common/event';
99
import { IDisposable } from 'vs/base/common/lifecycle';
1010
import { EventEmitter } from 'vs/base/common/eventEmitter';
1111
import Errors = require('vs/base/common/errors');
@@ -419,4 +419,166 @@ suite('Event utils', () => {
419419
});
420420
});
421421

422+
suite('EventMultiplexer', () => {
423+
424+
test('works', () => {
425+
const result = [];
426+
const m = new EventMultiplexer<number>();
427+
m.event(r => result.push(r));
428+
429+
const e1 = new Emitter<number>();
430+
m.add(e1.event);
431+
432+
assert.deepEqual(result, []);
433+
434+
e1.fire(0);
435+
assert.deepEqual(result, [0]);
436+
});
437+
438+
test('multiplexer dispose works', () => {
439+
const result = [];
440+
const m = new EventMultiplexer<number>();
441+
m.event(r => result.push(r));
442+
443+
const e1 = new Emitter<number>();
444+
m.add(e1.event);
445+
446+
assert.deepEqual(result, []);
447+
448+
e1.fire(0);
449+
assert.deepEqual(result, [0]);
450+
451+
m.dispose();
452+
assert.deepEqual(result, [0]);
453+
454+
e1.fire(0);
455+
assert.deepEqual(result, [0]);
456+
});
457+
458+
test('event dispose works', () => {
459+
const result = [];
460+
const m = new EventMultiplexer<number>();
461+
m.event(r => result.push(r));
462+
463+
const e1 = new Emitter<number>();
464+
m.add(e1.event);
465+
466+
assert.deepEqual(result, []);
467+
468+
e1.fire(0);
469+
assert.deepEqual(result, [0]);
470+
471+
e1.dispose();
472+
assert.deepEqual(result, [0]);
473+
474+
e1.fire(0);
475+
assert.deepEqual(result, [0]);
476+
});
477+
478+
test('mutliplexer event dispose works', () => {
479+
const result = [];
480+
const m = new EventMultiplexer<number>();
481+
m.event(r => result.push(r));
482+
483+
const e1 = new Emitter<number>();
484+
const l1 = m.add(e1.event);
485+
486+
assert.deepEqual(result, []);
487+
488+
e1.fire(0);
489+
assert.deepEqual(result, [0]);
490+
491+
l1.dispose();
492+
assert.deepEqual(result, [0]);
493+
494+
e1.fire(0);
495+
assert.deepEqual(result, [0]);
496+
});
497+
498+
test('hot start works', () => {
499+
const result = [];
500+
const m = new EventMultiplexer<number>();
501+
m.event(r => result.push(r));
502+
503+
const e1 = new Emitter<number>();
504+
m.add(e1.event);
505+
const e2 = new Emitter<number>();
506+
m.add(e2.event);
507+
const e3 = new Emitter<number>();
508+
m.add(e3.event);
509+
510+
e1.fire(1);
511+
e2.fire(2);
512+
e3.fire(3);
513+
assert.deepEqual(result, [1, 2, 3]);
514+
});
515+
516+
test('cold start works', () => {
517+
const result = [];
518+
const m = new EventMultiplexer<number>();
519+
520+
const e1 = new Emitter<number>();
521+
m.add(e1.event);
522+
const e2 = new Emitter<number>();
523+
m.add(e2.event);
524+
const e3 = new Emitter<number>();
525+
m.add(e3.event);
526+
527+
m.event(r => result.push(r));
528+
529+
e1.fire(1);
530+
e2.fire(2);
531+
e3.fire(3);
532+
assert.deepEqual(result, [1, 2, 3]);
533+
});
534+
535+
test('late add works', () => {
536+
const result = [];
537+
const m = new EventMultiplexer<number>();
538+
539+
const e1 = new Emitter<number>();
540+
m.add(e1.event);
541+
const e2 = new Emitter<number>();
542+
m.add(e2.event);
543+
544+
m.event(r => result.push(r));
545+
546+
e1.fire(1);
547+
e2.fire(2);
548+
549+
const e3 = new Emitter<number>();
550+
m.add(e3.event);
551+
e3.fire(3);
552+
553+
assert.deepEqual(result, [1, 2, 3]);
554+
});
555+
556+
test('add dispose works', () => {
557+
const result = [];
558+
const m = new EventMultiplexer<number>();
559+
560+
const e1 = new Emitter<number>();
561+
m.add(e1.event);
562+
const e2 = new Emitter<number>();
563+
m.add(e2.event);
564+
565+
m.event(r => result.push(r));
566+
567+
e1.fire(1);
568+
e2.fire(2);
569+
570+
const e3 = new Emitter<number>();
571+
const l3 = m.add(e3.event);
572+
e3.fire(3);
573+
assert.deepEqual(result, [1, 2, 3]);
574+
575+
l3.dispose();
576+
e3.fire(4);
577+
assert.deepEqual(result, [1, 2, 3]);
578+
579+
e2.fire(4);
580+
e1.fire(5);
581+
assert.deepEqual(result, [1, 2, 3, 4, 5]);
582+
});
583+
});
422584
});

src/vs/workbench/parts/debug/browser/breakpointWidget.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55

66
import 'vs/css!../browser/media/breakpointWidget';
77
import * as nls from 'vs/nls';
8-
import * as async from 'vs/base/common/async';
98
import * as errors from 'vs/base/common/errors';
109
import { KeyCode } from 'vs/base/common/keyCodes';
1110
import { isWindows, isMacintosh } from 'vs/base/common/platform';
@@ -18,6 +17,7 @@ import { ZoneWidget } from 'vs/editor/contrib/zoneWidget/browser/zoneWidget';
1817
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
1918
import { IDebugService, IBreakpoint, IRawBreakpoint } from 'vs/workbench/parts/debug/common/debug';
2019
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
20+
import { once } from 'vs/base/common/functional';
2121

2222
const $ = dom.$;
2323
const EXPRESSION_PLACEHOLDER = nls.localize('breakpointWidgetExpressionPlaceholder', "Break when expression evaluates to true. 'Enter' to accept, 'esc' to cancel.");
@@ -96,7 +96,7 @@ export class BreakpointWidget extends ZoneWidget {
9696
setTimeout(() => this.inputBox.focus(), 0);
9797

9898
let disposed = false;
99-
const wrapUp = async.once((success: boolean) => {
99+
const wrapUp = once((success: boolean) => {
100100
if (!disposed) {
101101
disposed = true;
102102
if (success) {

src/vs/workbench/parts/debug/electron-browser/debugViewer.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import { TPromise } from 'vs/base/common/winjs.base';
88
import * as lifecycle from 'vs/base/common/lifecycle';
99
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
1010
import * as paths from 'vs/base/common/paths';
11-
import * as async from 'vs/base/common/async';
1211
import * as errors from 'vs/base/common/errors';
1312
import { equalsIgnoreCase } from 'vs/base/common/strings';
1413
import { isMacintosh } from 'vs/base/common/platform';
@@ -31,6 +30,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti
3130
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
3231
import { Source } from 'vs/workbench/parts/debug/common/debugSource';
3332
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
33+
import { once } from 'vs/base/common/functional';
3434

3535
const $ = dom.$;
3636
const booleanRegex = /^true|false$/i;
@@ -120,7 +120,7 @@ function renderRenameBox(debugService: debug.IDebugService, contextViewService:
120120
let disposed = false;
121121
const toDispose: [lifecycle.IDisposable] = [inputBox];
122122

123-
const wrapUp = async.once((renamed: boolean) => {
123+
const wrapUp = once((renamed: boolean) => {
124124
if (!disposed) {
125125
disposed = true;
126126
if (element instanceof Expression && renamed && inputBox.value) {

src/vs/workbench/parts/files/browser/views/explorerViewer.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import objects = require('vs/base/common/objects');
1111
import DOM = require('vs/base/browser/dom');
1212
import URI from 'vs/base/common/uri';
1313
import { MIME_BINARY } from 'vs/base/common/mime';
14-
import async = require('vs/base/common/async');
14+
import { once } from 'vs/base/common/functional';
1515
import paths = require('vs/base/common/paths');
1616
import errors = require('vs/base/common/errors');
1717
import { isString } from 'vs/base/common/types';
@@ -332,7 +332,7 @@ export class FileRenderer extends ActionsRenderer implements IRenderer {
332332
inputBox.select({ start: 0, end: lastDot > 0 && !stat.isDirectory ? lastDot : value.length });
333333
inputBox.focus();
334334

335-
const done = async.once(commit => {
335+
const done = once(commit => {
336336
tree.clearHighlight();
337337

338338
if (commit && inputBox.value) {

0 commit comments

Comments
 (0)