feat(WebWorker) Add channel support to MessageBus

closes #3661 and #3686
This commit is contained in:
Jason Teplitz
2015-08-17 10:28:47 -07:00
parent 104302a958
commit 0b59e664ec
36 changed files with 1155 additions and 753 deletions

View File

@ -0,0 +1,94 @@
import {
AsyncTestCompleter,
inject,
describe,
it,
expect,
beforeEach,
createTestInjector,
beforeEachBindings,
SpyObject,
proxy
} from 'angular2/test_lib';
import {ObservableWrapper} from 'angular2/src/facade/async';
import {MessageBusInterface} from 'angular2/src/web-workers/shared/message_bus';
import {createConnectedMessageBus} from './message_bus_util';
export function main() {
/**
* Tests the PostMessageBus in TypeScript and the IsolateMessageBus in Dart
*/
describe("MessageBus", () => {
var bus: MessageBusInterface;
beforeEach(() => { bus = createConnectedMessageBus(); });
it("should pass messages in the same channel from sink to source",
inject([AsyncTestCompleter], (async) => {
const CHANNEL = "CHANNEL 1";
const MESSAGE = "Test message";
var fromEmitter = bus.from(CHANNEL);
ObservableWrapper.subscribe(fromEmitter, (message: any) => {
expect(message).toEqual(MESSAGE);
async.done();
});
var toEmitter = bus.to(CHANNEL);
ObservableWrapper.callNext(toEmitter, MESSAGE);
}));
it("should broadcast", inject([AsyncTestCompleter], (async) => {
const CHANNEL = "CHANNEL 1";
const MESSAGE = "TESTING";
const NUM_LISTENERS = 2;
var callCount = 0;
var emitHandler = (message: any) => {
expect(message).toEqual(MESSAGE);
callCount++;
if (callCount == NUM_LISTENERS) {
async.done();
}
};
for (var i = 0; i < NUM_LISTENERS; i++) {
var emitter = bus.from(CHANNEL);
ObservableWrapper.subscribe(emitter, emitHandler);
}
var toEmitter = bus.to(CHANNEL);
ObservableWrapper.callNext(toEmitter, MESSAGE);
}));
it("should keep channels independent", inject([AsyncTestCompleter], (async) => {
const CHANNEL_ONE = "CHANNEL 1";
const CHANNEL_TWO = "CHANNEL 2";
const MESSAGE_ONE = "This is a message on CHANNEL 1";
const MESSAGE_TWO = "This is a message on CHANNEL 2";
var callCount = 0;
var firstFromEmitter = bus.from(CHANNEL_ONE);
ObservableWrapper.subscribe(firstFromEmitter, (message) => {
expect(message).toEqual(MESSAGE_ONE);
callCount++;
if (callCount == 2) {
async.done();
}
});
var secondFromEmitter = bus.from(CHANNEL_TWO);
ObservableWrapper.subscribe(secondFromEmitter, (message) => {
expect(message).toEqual(MESSAGE_TWO);
callCount++;
if (callCount == 2) {
async.done();
}
});
var firstToEmitter = bus.to(CHANNEL_ONE);
ObservableWrapper.callNext(firstToEmitter, MESSAGE_ONE);
var secondToEmitter = bus.to(CHANNEL_TWO);
ObservableWrapper.callNext(secondToEmitter, MESSAGE_TWO);
}));
});
}

View File

@ -0,0 +1,20 @@
library angular2.test.web_workers.shared.message_bus_util;
import 'dart:isolate';
import 'package:angular2/src/web-workers/shared/message_bus.dart'
show MessageBusInterface;
import 'package:angular2/src/web-workers/shared/isolate_message_bus.dart';
/*
* Returns an IsolateMessageBus thats sink is connected to its own source.
* Useful for testing the sink and source.
*/
MessageBusInterface createConnectedMessageBus() {
var receivePort = new ReceivePort();
var sendPort = receivePort.sendPort;
var sink = new IsolateMessageBusSink(sendPort);
var source = new IsolateMessageBusSource(receivePort);
return new IsolateMessageBus(sink, source);
}

View File

@ -0,0 +1,30 @@
import {
PostMessageBusSource,
PostMessageBusSink,
PostMessageBus
} from 'angular2/src/web-workers/shared/post_message_bus';
import {MessageBusInterface} from 'angular2/src/web-workers/shared/message_bus';
/*
* Returns a PostMessageBus thats sink is connected to its own source.
* Useful for testing the sink and source.
*/
export function createConnectedMessageBus(): MessageBusInterface {
var mockPostMessage = new MockPostMessage();
var source = new PostMessageBusSource(<any>mockPostMessage);
var sink = new PostMessageBusSink(mockPostMessage);
return new PostMessageBus(sink, source);
}
class MockPostMessage {
private _listener: EventListener;
addEventListener(type: string, listener: EventListener, useCapture?: boolean): void {
if (type === "message") {
this._listener = listener;
}
}
postMessage(data: any, transfer?:[ArrayBuffer]): void { this._listener(<any>{data: data}); }
}

View File

@ -13,8 +13,6 @@ import {
import {IMPLEMENTS} from 'angular2/src/facade/lang';
import {Serializer} from 'angular2/src/web-workers/shared/serializer';
import {NgZone} from 'angular2/src/core/zone/ng_zone';
import {MessageBroker} from 'angular2/src/web-workers/worker/broker';
import {MockMessageBus, MockMessageBusSink, MockMessageBusSource} from './worker_test_util';
import {ON_WEB_WORKER} from 'angular2/src/web-workers/shared/api';
import {bind} from 'angular2/di';
import {RenderProtoViewRefStore} from 'angular2/src/web-workers/shared/render_proto_view_ref_store';
@ -23,9 +21,13 @@ import {
WebWorkerRenderViewRef
} from 'angular2/src/web-workers/shared/render_view_with_fragments_store';
import {RenderEventDispatcher, RenderViewRef} from 'angular2/src/render/api';
import {createPairedMessageBuses} from './worker_test_util';
import {WebWorkerEventDispatcher} from 'angular2/src/web-workers/worker/event_dispatcher';
import {ObservableWrapper} from 'angular2/src/facade/async';
import {EVENT_CHANNEL} from 'angular2/src/web-workers/shared/messaging_api';
export function main() {
describe("MessageBroker", () => {
describe("EventDispatcher", () => {
beforeEachBindings(() => [
bind(ON_WEB_WORKER)
.toValue(true),
@ -33,47 +35,41 @@ export function main() {
RenderViewWithFragmentsStore
]);
it("should dispatch events", inject([Serializer, NgZone], (serializer, zone) => {
var bus = new MockMessageBus(new MockMessageBusSink(), new MockMessageBusSource());
var broker = new MessageBroker(bus, serializer, zone);
var eventDispatcher = new SpyEventDispatcher();
var viewRef = new WebWorkerRenderViewRef(0);
serializer.allocateRenderViews(0); // serialize the ref so it's in the store
viewRef =
serializer.deserialize(serializer.serialize(viewRef, RenderViewRef), RenderViewRef);
broker.registerEventDispatcher(viewRef, eventDispatcher);
it("should dispatch events",
inject([Serializer, NgZone, AsyncTestCompleter], (serializer, zone, async) => {
var messageBuses = createPairedMessageBuses();
var webWorkerEventDispatcher =
new WebWorkerEventDispatcher(messageBuses.worker, serializer, zone);
var elementIndex = 15;
var eventName = 'click';
bus.source.receive({
'data': {
'type': 'event',
'value': {
'viewRef': viewRef.serialize(),
'elementIndex': elementIndex,
'eventName': eventName,
'locals': {'$event': {'target': {value: null}}}
}
}
var eventDispatcher = new SpyEventDispatcher((elementIndex, eventName, locals) => {
expect(elementIndex).toEqual(elementIndex);
expect(eventName).toEqual(eventName);
async.done();
});
expect(eventDispatcher.wasDispatched).toBeTruthy();
expect(eventDispatcher.elementIndex).toEqual(elementIndex);
expect(eventDispatcher.eventName).toEqual(eventName);
var viewRef = new WebWorkerRenderViewRef(0);
serializer.allocateRenderViews(0); // serialize the ref so it's in the store
viewRef =
serializer.deserialize(serializer.serialize(viewRef, RenderViewRef), RenderViewRef);
webWorkerEventDispatcher.registerEventDispatcher(viewRef, eventDispatcher);
ObservableWrapper.callNext(messageBuses.ui.to(EVENT_CHANNEL), {
'viewRef': viewRef.serialize(),
'elementIndex': elementIndex,
'eventName': eventName,
'locals': {'$event': {'target': {value: null}}}
});
}));
});
}
class SpyEventDispatcher implements RenderEventDispatcher {
wasDispatched: boolean = false;
elementIndex: number;
eventName: string;
constructor(private _callback: Function) {}
dispatchRenderEvent(elementIndex: number, eventName: string, locals: Map<string, any>) {
this.wasDispatched = true;
this.elementIndex = elementIndex;
this.eventName = eventName;
this._callback(elementIndex, eventName, locals);
}
}

View File

@ -0,0 +1,21 @@
library angular2.test.web_workers.worker.mock_event_emitter;
import 'dart:core';
import 'dart:async';
import "package:angular2/src/facade/async.dart";
class MockEventEmitter extends EventEmitter {
List<Function> _nextFns = new List();
@override
StreamSubscription listen(void onData(dynamic line),
{void onError(Error error), void onDone(), bool cancelOnError}) {
_nextFns.add(onData);
return null;
}
@override
void add(value) {
_nextFns.forEach((fn) => fn(value));
}
}

View File

@ -0,0 +1,18 @@
import {EventEmitter} from 'angular2/src/facade/async';
import * as Rx from 'rx';
import {ListWrapper} from 'angular2/src/facade/collection';
export class MockEventEmitter extends EventEmitter {
private _nextFns: List<Function> = [];
constructor() { super(); }
observer(generator: any): Rx.IDisposable {
this._nextFns.push(generator.next);
return null;
}
next(value: any) {
ListWrapper.forEach(this._nextFns, (fn) => { fn(value); });
}
}

View File

@ -12,7 +12,7 @@ import {DOM} from 'angular2/src/dom/dom_adapter';
import {DomTestbed, TestRootView, elRef} from '../../render/dom/dom_testbed';
import {bind} from 'angular2/di';
import {WebWorkerCompiler, WebWorkerRenderer} from "angular2/src/web-workers/worker/renderer";
import {MessageBroker, UiArguments, FnArg} from "angular2/src/web-workers/worker/broker";
import {MessageBrokerFactory, UiArguments, FnArg} from "angular2/src/web-workers/worker/broker";
import {Serializer} from "angular2/src/web-workers/shared/serializer";
import {isPresent, isBlank, BaseException, Type} from "angular2/src/facade/lang";
import {MapWrapper, ListWrapper} from "angular2/src/facade/collection";
@ -37,42 +37,47 @@ import {
import {resolveInternalDomProtoView, DomProtoView} from 'angular2/src/render/dom/view/proto_view';
import {someComponent} from '../../render/dom/dom_renderer_integration_spec';
import {WebWorkerMain} from 'angular2/src/web-workers/ui/impl';
import {AnchorBasedAppRootUrl} from 'angular2/src/services/anchor_based_app_root_url';
import {MockMessageBus, MockMessageBusSink, MockMessageBusSource} from './worker_test_util';
import {MessageBasedRenderCompiler} from 'angular2/src/web-workers/ui/render_compiler';
import {MessageBasedRenderer} from 'angular2/src/web-workers/ui/renderer';
import {
createPairedMessageBuses
} from './worker_test_util'
export function main() {
function createBroker(workerSerializer: Serializer, uiSerializer: Serializer, tb: DomTestbed,
uiRenderViewStore: RenderViewWithFragmentsStore,
workerRenderViewStore: RenderViewWithFragmentsStore): MessageBroker {
// set up the two message buses to pass messages to each other
var uiMessageBus = new MockMessageBus(new MockMessageBusSink(), new MockMessageBusSource());
var workerMessageBus = new MockMessageBus(new MockMessageBusSink(), new MockMessageBusSource());
uiMessageBus.attachToBus(workerMessageBus);
workerMessageBus.attachToBus(uiMessageBus);
export function
main() {
function createBrokerFactory(workerSerializer: Serializer, uiSerializer: Serializer,
tb: DomTestbed, uiRenderViewStore: RenderViewWithFragmentsStore,
workerRenderViewStore: RenderViewWithFragmentsStore):
MessageBrokerFactory {
var messageBuses = createPairedMessageBuses();
var uiMessageBus = messageBuses.ui;
var workerMessageBus = messageBuses.worker;
// set up the worker side
var broker = new MessageBroker(workerMessageBus, workerSerializer, null);
var brokerFactory = new MessageBrokerFactory(workerMessageBus, workerSerializer);
// set up the ui side
var webWorkerMain = new WebWorkerMain(tb.compiler, tb.renderer, uiRenderViewStore, uiSerializer,
new AnchorBasedAppRootUrl(), null);
webWorkerMain.attachToWebWorker(uiMessageBus);
return broker;
var renderCompiler = new MessageBasedRenderCompiler(uiMessageBus, uiSerializer, tb.compiler);
var renderer =
new MessageBasedRenderer(uiMessageBus, uiSerializer, uiRenderViewStore, tb.renderer);
new WebWorkerMain(renderCompiler, renderer, null, null);
return brokerFactory;
}
function createWorkerRenderer(workerSerializer: Serializer, uiSerializer: Serializer,
tb: DomTestbed, uiRenderViewStore: RenderViewWithFragmentsStore,
workerRenderViewStore: RenderViewWithFragmentsStore):
WebWorkerRenderer {
var broker =
createBroker(workerSerializer, uiSerializer, tb, uiRenderViewStore, workerRenderViewStore);
return new WebWorkerRenderer(broker, workerRenderViewStore);
var brokerFactory = createBrokerFactory(workerSerializer, uiSerializer, tb, uiRenderViewStore,
workerRenderViewStore);
return new WebWorkerRenderer(brokerFactory, workerRenderViewStore, null);
}
function createWorkerCompiler(workerSerializer: Serializer, uiSerializer: Serializer,
tb: DomTestbed): WebWorkerCompiler {
var broker = createBroker(workerSerializer, uiSerializer, tb, null, null);
return new WebWorkerCompiler(broker);
var brokerFactory = createBrokerFactory(workerSerializer, uiSerializer, tb, null, null);
return new WebWorkerCompiler(brokerFactory);
}
describe("Web Worker Compiler", function() {

View File

@ -1,36 +1,60 @@
import {StringMap, StringMapWrapper, ListWrapper} from 'angular2/src/facade/collection';
import {
MessageBus,
MessageBusSource,
MessageBusInterface,
MessageBusSink,
SourceListener
} from "angular2/src/web-workers/shared/message_bus";
import {MapWrapper} from "angular2/src/facade/collection";
MessageBusSource,
MessageBus
} from 'angular2/src/web-workers/shared/message_bus';
import {MockEventEmitter} from './mock_event_emitter';
/**
* Returns two MessageBus instances that are attached to each other.
* Such that whatever goes into one's sink comes out the others source.
*/
export function createPairedMessageBuses(): PairedMessageBuses {
var firstChannels: StringMap<string, MockEventEmitter> = {};
var workerMessageBusSink = new MockMessageBusSink(firstChannels);
var uiMessageBusSource = new MockMessageBusSource(firstChannels);
var secondChannels: StringMap<string, MockEventEmitter> = {};
var uiMessageBusSink = new MockMessageBusSink(secondChannels);
var workerMessageBusSource = new MockMessageBusSource(secondChannels);
return new PairedMessageBuses(new MockMessageBus(uiMessageBusSink, uiMessageBusSource),
new MockMessageBus(workerMessageBusSink, workerMessageBusSource));
}
export class PairedMessageBuses {
constructor(public ui: MessageBusInterface, public worker: MessageBusInterface) {}
}
export class MockMessageBusSource implements MessageBusSource {
private _listenerStore: Map<int, SourceListener> = new Map<int, SourceListener>();
private _numListeners: number = 0;
constructor(private _channels: StringMap<string, MockEventEmitter>) {}
addListener(fn: SourceListener): int {
this._listenerStore.set(++this._numListeners, fn);
return this._numListeners;
}
removeListener(index: int): void { MapWrapper.delete(this._listenerStore, index); }
receive(message: Object): void {
MapWrapper.forEach(this._listenerStore, (fn: SourceListener, key: int) => { fn(message); });
from(channel: string): MockEventEmitter {
if (!StringMapWrapper.contains(this._channels, channel)) {
this._channels[channel] = new MockEventEmitter();
}
return this._channels[channel];
}
}
export class MockMessageBusSink implements MessageBusSink {
private _sendTo: MockMessageBusSource;
constructor(private _channels: StringMap<string, MockEventEmitter>) {}
send(message: Object): void { this._sendTo.receive({'data': message}); }
attachToSource(source: MockMessageBusSource) { this._sendTo = source; }
to(channel: string): MockEventEmitter {
if (!StringMapWrapper.contains(this._channels, channel)) {
this._channels[channel] = new MockEventEmitter();
}
return this._channels[channel];
}
}
export class MockMessageBus implements MessageBus {
constructor(public sink: MockMessageBusSink, public source: MockMessageBusSource) {}
attachToBus(bus: MockMessageBus) { this.sink.attachToSource(bus.source); }
export class MockMessageBus extends MessageBus {
constructor(public sink: MockMessageBusSink, public source: MockMessageBusSource) { super(); }
to(channel: string): MockEventEmitter { return this.sink.to(channel); }
from(channel: string): MockEventEmitter { return this.source.from(channel); }
}

View File

@ -11,7 +11,11 @@ import {
proxy
} from 'angular2/test_lib';
import {IMPLEMENTS, Type} from 'angular2/src/facade/lang';
import {MessageBroker, UiArguments} from 'angular2/src/web-workers/worker/broker';
import {
MessageBroker,
UiArguments,
MessageBrokerFactory
} from 'angular2/src/web-workers/worker/broker';
import {WebWorkerXHRImpl} from "angular2/src/web-workers/worker/xhr_impl";
import {PromiseWrapper} from "angular2/src/facade/async";
@ -25,14 +29,13 @@ export function main() {
var messageBroker: any = new SpyMessageBroker();
messageBroker.spy("runOnUiThread")
.andCallFake((args: UiArguments, returnType: Type) => {
expect(args.type).toEqual("xhr");
expect(args.method).toEqual("get");
expect(args.args.length).toEqual(1);
expect(args.args[0].value).toEqual(URL);
return PromiseWrapper.wrap(() => { return RESPONSE; });
});
var xhrImpl = new WebWorkerXHRImpl(messageBroker);
var xhrImpl = new WebWorkerXHRImpl(new MockMessageBrokerFactory(messageBroker));
xhrImpl.get(URL).then((response) => {
expect(response).toEqual(RESPONSE);
async.done();
@ -47,3 +50,8 @@ class SpyMessageBroker extends SpyObject {
constructor() { super(MessageBroker); }
noSuchMethod(m) { return super.noSuchMethod(m); }
}
class MockMessageBrokerFactory extends MessageBrokerFactory {
constructor(private _messageBroker: MessageBroker) { super(null, null); }
createMessageBroker(channel: string) { return this._messageBroker; }
}