From 7b834e02ec0d6d9837231f61d44e76d371a1c9d2 Mon Sep 17 00:00:00 2001 From: Jason Teplitz Date: Fri, 10 Jul 2015 16:09:18 -0700 Subject: [PATCH] feat(WebWorkers) Add DOM event support closes #3046 --- .../render_view_with_fragments_store.ts | 41 +++------ .../src/web-workers/shared/serializer.ts | 52 ++++++++++-- .../src/web-workers/ui/di_bindings.ts | 16 ++-- .../src/web-workers/ui/event_serializer.dart | 82 ++++++++++++++++++ .../src/web-workers/ui/event_serializer.ts | 49 +++++++++++ modules/angular2/src/web-workers/ui/impl.ts | 71 ++++++++++++++-- .../web-workers/worker/application_common.ts | 7 +- .../angular2/src/web-workers/worker/broker.ts | 85 +++++++++++++------ .../angular2/src/web-workers/worker/loader.js | 0 .../src/web-workers/worker/renderer.ts | 5 +- .../test/web-workers/worker/broker_spec.ts | 78 +++++++++++++++++ .../test/web-workers/worker/renderer_spec.ts | 40 +-------- .../web-workers/worker/worker_test_util.ts | 36 ++++++++ .../examples/e2e_test/routing/routing_spec.ts | 8 ++ .../web_workers/web_workers_spec.dart | 5 ++ .../e2e_test/web_workers/web_workers_spec.ts | 42 +++++++++ .../src/message_broker/background_index.dart | 3 +- .../src/message_broker/background_index.ts | 2 +- .../examples/src/message_broker/index.dart | 2 +- modules/examples/src/message_broker/index.ts | 2 +- .../src/web_workers/background_index.dart | 2 +- modules/examples/src/web_workers/example.html | 11 --- .../web_workers/{example.dart => index.dart} | 0 modules/examples/src/web_workers/index.html | 26 ++++++ .../src/web_workers/{example.ts => index.ts} | 0 .../examples/src/web_workers/index_common.ts | 7 +- modules/examples/src/web_workers/loader.js | 49 +++++------ 27 files changed, 556 insertions(+), 165 deletions(-) create mode 100644 modules/angular2/src/web-workers/ui/event_serializer.dart create mode 100644 modules/angular2/src/web-workers/ui/event_serializer.ts delete mode 100644 modules/angular2/src/web-workers/worker/loader.js create mode 100644 modules/angular2/test/web-workers/worker/broker_spec.ts create mode 100644 modules/angular2/test/web-workers/worker/worker_test_util.ts create mode 100644 modules/examples/e2e_test/web_workers/web_workers_spec.dart create mode 100644 modules/examples/e2e_test/web_workers/web_workers_spec.ts delete mode 100644 modules/examples/src/web_workers/example.html rename modules/examples/src/web_workers/{example.dart => index.dart} (100%) create mode 100644 modules/examples/src/web_workers/index.html rename modules/examples/src/web_workers/{example.ts => index.ts} (100%) diff --git a/modules/angular2/src/web-workers/shared/render_view_with_fragments_store.ts b/modules/angular2/src/web-workers/shared/render_view_with_fragments_store.ts index 9acba48181..62f829e131 100644 --- a/modules/angular2/src/web-workers/shared/render_view_with_fragments_store.ts +++ b/modules/angular2/src/web-workers/shared/render_view_with_fragments_store.ts @@ -12,20 +12,22 @@ export class RenderViewWithFragmentsStore { constructor(@Inject(ON_WEBWORKER) onWebWorker) { this._onWebWorker = onWebWorker; - if (!onWebWorker) { - this._lookupByIndex = new Map(); - this._lookupByView = new Map(); - } + this._lookupByIndex = new Map(); + this._lookupByView = new Map(); } allocate(fragmentCount: number): RenderViewWithFragments { + var initialIndex = this._nextIndex; + var viewRef = new WorkerRenderViewRef(this._nextIndex++); var fragmentRefs = ListWrapper.createGrowableSize(fragmentCount); for (var i = 0; i < fragmentCount; i++) { fragmentRefs[i] = new WorkerRenderFragmentRef(this._nextIndex++); } - return new RenderViewWithFragments(viewRef, fragmentRefs); + var renderViewWithFragments = new RenderViewWithFragments(viewRef, fragmentRefs); + this.store(renderViewWithFragments, initialIndex); + return renderViewWithFragments; } store(view: RenderViewWithFragments, startIndex: number) { @@ -60,11 +62,7 @@ export class RenderViewWithFragmentsStore { return null; } - if (this._onWebWorker) { - return WorkerRenderViewRef.deserialize(ref); - } else { - return this.retreive(ref); - } + return this.retreive(ref); } deserializeRenderFragmentRef(ref: number): RenderFragmentRef { @@ -72,11 +70,7 @@ export class RenderViewWithFragmentsStore { return null; } - if (this._onWebWorker) { - return WorkerRenderFragmentRef.deserialize(ref); - } else { - return this.retreive(ref); - } + return this.retreive(ref); } private _serializeRenderFragmentOrViewRef(ref: RenderViewRef | RenderFragmentRef): number { @@ -114,20 +108,11 @@ export class RenderViewWithFragmentsStore { return null; } - var viewRef: RenderViewRef | RenderFragmentRef; - var fragments: List; - if (this._onWebWorker) { - viewRef = WorkerRenderViewRef.deserialize(obj['viewRef']); - fragments = - ListWrapper.map(obj['fragmentRefs'], (val) => WorkerRenderFragmentRef.deserialize(val)); + var viewRef = this.deserializeRenderViewRef(obj['viewRef']); + var fragments = + ListWrapper.map(obj['fragmentRefs'], (val) => this.deserializeRenderFragmentRef(val)); - return new RenderViewWithFragments(viewRef, fragments); - } else { - viewRef = this.retreive(obj['viewRef']); - fragments = ListWrapper.map(obj['fragmentRefs'], (val) => this.retreive(val)); - - return new RenderViewWithFragments(viewRef, fragments); - } + return new RenderViewWithFragments(viewRef, fragments); } } diff --git a/modules/angular2/src/web-workers/shared/serializer.ts b/modules/angular2/src/web-workers/shared/serializer.ts index 5b2a03c0bb..62fd98b382 100644 --- a/modules/angular2/src/web-workers/shared/serializer.ts +++ b/modules/angular2/src/web-workers/shared/serializer.ts @@ -1,5 +1,19 @@ -import {Type, isArray, isPresent, serializeEnum, deserializeEnum} from "angular2/src/facade/lang"; -import {List, ListWrapper, Map, StringMapWrapper, MapWrapper} from "angular2/src/facade/collection"; +import { + Type, + isArray, + isPresent, + serializeEnum, + deserializeEnum, + BaseException +} from "angular2/src/facade/lang"; +import { + List, + ListWrapper, + Map, + StringMap, + StringMapWrapper, + MapWrapper +} from "angular2/src/facade/collection"; import { ProtoViewDto, DirectiveMetadata, @@ -13,7 +27,8 @@ import { RenderViewRef, RenderFragmentRef, RenderElementRef, - ViewType + ViewType, + ViewEncapsulation } from "angular2/src/render/api"; import {WorkerElementRef} from 'angular2/src/web-workers/shared/api'; import {AST, ASTWithSource} from 'angular2/src/change_detection/change_detection'; @@ -30,11 +45,18 @@ export class Serializer { constructor(private _parser: Parser, private _protoViewStore: RenderProtoViewRefStore, private _renderViewStore: RenderViewWithFragmentsStore) { this._enumRegistry = new Map>(); + var viewTypeMap = new Map(); viewTypeMap[0] = ViewType.HOST; viewTypeMap[1] = ViewType.COMPONENT; viewTypeMap[2] = ViewType.EMBEDDED; this._enumRegistry.set(ViewType, viewTypeMap); + + var viewEncapsulationMap = new Map(); + viewEncapsulationMap[0] = ViewEncapsulation.EMULATED; + viewEncapsulationMap[1] = ViewEncapsulation.NATIVE; + viewEncapsulationMap[2] = ViewEncapsulation.NONE; + this._enumRegistry.set(ViewEncapsulation, viewEncapsulationMap); } serialize(obj: any, type: Type): Object { @@ -71,8 +93,10 @@ export class Serializer { return this._renderViewStore.serializeRenderFragmentRef(obj); } else if (type == WorkerElementRef) { return this._serializeWorkerElementRef(obj); + } else if (type == EventBinding) { + return this._serializeEventBinding(obj); } else { - throw "No serializer for " + type.toString(); + throw new BaseException("No serializer for " + type.toString()); } } @@ -111,8 +135,10 @@ export class Serializer { return this._renderViewStore.deserializeRenderFragmentRef(map); } else if (type == WorkerElementRef) { return this._deserializeWorkerElementRef(map); + } else if (type == EventBinding) { + return this._deserializeEventBinding(map); } else { - throw "No deserializer for " + type.toString(); + throw new BaseException("No deserializer for " + type.toString()); } } @@ -148,6 +174,15 @@ export class Serializer { allocateRenderViews(fragmentCount: number) { this._renderViewStore.allocate(fragmentCount); } + private _serializeEventBinding(binding: EventBinding): StringMap { + return {'fullName': binding.fullName, 'source': this.serialize(binding.source, ASTWithSource)}; + } + + private _deserializeEventBinding(map: StringMap): EventBinding { + return new EventBinding(map['fullName'], + this.deserialize(map['source'], ASTWithSource, "binding")); + } + private _serializeWorkerElementRef(elementRef: RenderElementRef): StringMap { return { 'renderView': this.serialize(elementRef.renderView, RenderViewRef), @@ -214,7 +249,7 @@ export class Serializer { 'directives': this.serialize(view.directives, DirectiveMetadata), 'styleAbsUrls': view.styleAbsUrls, 'styles': view.styles, - 'encapsulation': view.encapsulation + 'encapsulation': serializeEnum(view.encapsulation) }; } @@ -225,7 +260,8 @@ export class Serializer { directives: this.deserialize(obj['directives'], DirectiveMetadata), styleAbsUrls: obj['styleAbsUrls'], styles: obj['styles'], - encapsulation: obj['encapsulation'] + encapsulation: + deserializeEnum(obj['encapsulation'], this._enumRegistry.get(ViewEncapsulation)) }); } @@ -293,7 +329,7 @@ export class Serializer { variableBindings: this.objectToMap(obj['variableBindings']), textBindings: this.deserialize(obj['textBindings'], ASTWithSource, "interpolation"), type: deserializeEnum(obj['type'], this._enumRegistry.get(ViewType)), - transitiveNgContentCount: obj['transitivengContentCount'] + transitiveNgContentCount: obj['transitiveNgContentCount'] }); } diff --git a/modules/angular2/src/web-workers/ui/di_bindings.ts b/modules/angular2/src/web-workers/ui/di_bindings.ts index fd323829ea..ec7199df33 100644 --- a/modules/angular2/src/web-workers/ui/di_bindings.ts +++ b/modules/angular2/src/web-workers/ui/di_bindings.ts @@ -1,5 +1,5 @@ -// TODO: This whole file is nearly identical to core/application.ts. -// There should be a way to refactor application so that this file is unnecessary +// TODO (jteplitz602): This whole file is nearly identical to core/application.ts. +// There should be a way to refactor application so that this file is unnecessary. See #3277 import {Injector, bind, Binding} from "angular2/di"; import {Type, isBlank, isPresent} from "angular2/src/facade/lang"; import {Reflector, reflector} from 'angular2/src/reflection/reflection'; @@ -21,6 +21,7 @@ import {KeyEventsPlugin} from 'angular2/src/render/dom/events/key_events'; import {HammerGesturesPlugin} from 'angular2/src/render/dom/events/hammer_gestures'; import {AppViewPool, APP_VIEW_POOL_CAPACITY} from 'angular2/src/core/compiler/view_pool'; import {Renderer, RenderCompiler} from 'angular2/src/render/api'; +import {AppRootUrl} from 'angular2/src/services/app_root_url'; import { DomRenderer, DOCUMENT_TOKEN, @@ -28,6 +29,8 @@ import { DefaultDomCompiler, APP_ID_RANDOM_BINDING } from 'angular2/src/render/render'; +import {ElementSchemaRegistry} from 'angular2/src/render/dom/schema/element_schema_registry'; +import {DomElementSchemaRegistry} from 'angular2/src/render/dom/schema/dom_element_schema_registry'; import { SharedStylesHost, DomSharedStylesHost @@ -73,11 +76,6 @@ function _injectorBindings(): List> { } else if (JitChangeDetection.isSupported()) { bestChangeDetection = JitChangeDetection; } - // compute the root url to pass to AppRootUrl - /*var rootUrl: string; - var a = DOM.createElement('a'); - DOM.resolveAndSetHref(a, './', null); - rootUrl = DOM.getHref(a);*/ return [ bind(DOCUMENT_TOKEN) @@ -100,6 +98,7 @@ function _injectorBindings(): List> { bind(SharedStylesHost).toAlias(DomSharedStylesHost), Serializer, bind(ON_WEBWORKER).toValue(false), + bind(ElementSchemaRegistry).toValue(new DomElementSchemaRegistry()), RenderViewWithFragmentsStore, RenderProtoViewRefStore, ProtoViewFactory, @@ -117,7 +116,7 @@ function _injectorBindings(): List> { DirectiveResolver, Parser, Lexer, - ExceptionHandler, + bind(ExceptionHandler).toFactory(() => new ExceptionHandler(DOM), []), bind(XHR).toValue(new XHRImpl()), ComponentUrlMapper, UrlResolver, @@ -126,6 +125,7 @@ function _injectorBindings(): List> { DynamicComponentLoader, Testability, AnchorBasedAppRootUrl, + bind(AppRootUrl).toAlias(AnchorBasedAppRootUrl), WebWorkerMain ]; } diff --git a/modules/angular2/src/web-workers/ui/event_serializer.dart b/modules/angular2/src/web-workers/ui/event_serializer.dart new file mode 100644 index 0000000000..97b0708735 --- /dev/null +++ b/modules/angular2/src/web-workers/ui/event_serializer.dart @@ -0,0 +1,82 @@ +library angular2.src.web_workers.event_serializer; + +import 'package:angular2/src/facade/collection.dart'; +// TODO(jteplitz602): Remove Mirrors from serialization #3348 +@MirrorsUsed( + symbols: "altKey, bubbles, button, cancelable, client, ctrlKey, " + + "defaultPrevented, detail, eventPhase, layer, metaKey, offset, page, region, screen, " + + "shiftKey, timeStamp, type, magnitude, x, y, charCode, keyCode, keyLocation, location, repeat") +import 'dart:mirrors'; +import 'dart:core'; +import 'dart:html'; + +// These Maps can't be const due to a dartj2 bug (see http://github.com/dart-lang/sdk/issues/21825) +// Once that bug is fixed these should be const +final Map MOUSE_EVENT_PROPERTIES = { + #altKey: bool, + #bubbles: bool, + #button: int, + #cancelable: bool, + #client: Point, + #ctrlKey: bool, + #defaultPrevented: bool, + #detail: int, + #eventPhase: int, + #layer: Point, + #metaKey: bool, + #offset: Point, + #page: Point, + #region: String, + #screen: Point, + #shiftKey: bool, + #timeStamp: int, + #type: String +}; + +final Map KEYBOARD_EVENT_PROPERTIES = { + #altKey: bool, + #bubbles: bool, + #cancelable: bool, + #charCode: int, + #ctrlKey: bool, + #defaultPrevented: bool, + #detail: int, + #eventPhase: int, + #keyCode: int, + #keyLocation: int, + #layer: Point, + #location: int, + #repeat: bool, + #shiftKey: bool, + #timeStamp: int, + #type: String +}; + +Map serializeMouseEvent(dynamic e) { + return serializeEvent(e, MOUSE_EVENT_PROPERTIES); +} + +Map serializeKeyboardEvent(dynamic e) { + return serializeEvent(e, KEYBOARD_EVENT_PROPERTIES); +} + +Map serializeEvent(dynamic e, Map PROPERTIES) { + var serialized = StringMapWrapper.create(); + var mirror = reflect(e); + PROPERTIES.forEach((property, type) { + var value = mirror.getField(property).reflectee; + var propertyName = MirrorSystem.getName(property); + if (type == int || type == bool || type == String) { + serialized[propertyName] = value; + } else if (type == Point) { + var point = reflect(value); + serialized[propertyName] = { + 'magnitude': point.getField(#magnitude).reflectee, + 'x': point.getField(#x).reflectee, + 'y': point.getField(#y).reflectee + }; + } + }); + + return serialized; +} diff --git a/modules/angular2/src/web-workers/ui/event_serializer.ts b/modules/angular2/src/web-workers/ui/event_serializer.ts new file mode 100644 index 0000000000..e830706e76 --- /dev/null +++ b/modules/angular2/src/web-workers/ui/event_serializer.ts @@ -0,0 +1,49 @@ +import {StringMap} from 'angular2/src/facade/collection'; + +const MOUSE_EVENT_PROPERTIES = [ + "altKey", + "button", + "clientX", + "clientY", + "metaKey", + "movementX", + "movementY", + "offsetX", + "offsetY", + "region", + "screenX", + "screenY", + "shiftKey" +]; + +const KEYBOARD_EVENT_PROPERTIES = [ + 'altkey', + 'charCode', + 'code', + 'ctrlKey', + 'isComposing', + 'key', + 'keyCode', + 'location', + 'metaKey', + 'repeat', + 'shiftKey', + 'which' +]; + +export function serializeMouseEvent(e: MouseEvent): StringMap { + return serializeEvent(e, MOUSE_EVENT_PROPERTIES); +} + +export function serializeKeyboardEvent(e: KeyboardEvent): StringMap { + return serializeEvent(e, KEYBOARD_EVENT_PROPERTIES); +} + +function serializeEvent(e: any, properties: List): StringMap { + var serialized = {}; + for (var i = 0; i < properties.length; i++) { + var prop = properties[i]; + serialized[prop] = e[prop]; + } + return serialized; +} diff --git a/modules/angular2/src/web-workers/ui/impl.ts b/modules/angular2/src/web-workers/ui/impl.ts index f0ef6c1da3..fe61be2e03 100644 --- a/modules/angular2/src/web-workers/ui/impl.ts +++ b/modules/angular2/src/web-workers/ui/impl.ts @@ -3,7 +3,6 @@ * It takes care of spawning the worker and sending it the initial init message * It also acts and the messenger between the worker thread and the renderer running on the UI * thread - * TODO: This class might need to be refactored to match application.ts... */ import {createInjector} from "./di_bindings"; @@ -16,12 +15,14 @@ import { RenderProtoViewRef, RenderProtoViewMergeMapping, RenderViewRef, + RenderEventDispatcher, RenderFragmentRef } from "angular2/src/render/api"; -import {Type, print, BaseException} from "angular2/src/facade/lang"; +import {Type, print, BaseException, isFunction} from "angular2/src/facade/lang"; import {Promise, PromiseWrapper} from "angular2/src/facade/async"; +import {StringMapWrapper, SetWrapper} from 'angular2/src/facade/collection'; import {Serializer} from "angular2/src/web-workers/shared/serializer"; -import {MessageBus} from "angular2/src/web-workers/shared/message_bus"; +import {MessageBus, MessageBusSink} from "angular2/src/web-workers/shared/message_bus"; import { RenderViewWithFragmentsStore } from 'angular2/src/web-workers/shared/render_view_with_fragments_store'; @@ -32,6 +33,10 @@ import {ExceptionHandler} from 'angular2/src/core/exception_handler'; import {Injectable} from 'angular2/di'; import {BrowserDomAdapter} from 'angular2/src/dom/browser_adapter'; import {DOM} from 'angular2/src/dom/dom_adapter'; +import { + serializeMouseEvent, + serializeKeyboardEvent +} from 'angular2/src/web-workers/ui/event_serializer'; /** * Creates a zone, sets up the DI bindings @@ -74,11 +79,11 @@ export class WebWorkerMain { * Sends an error back to the worker thread in response to an opeartion on the UI thread */ private _sendWorkerError(id: string, error: any) { - this._sendWorkerMessage("error", {"id": id, "error": error}); + this._sendWorkerMessage("error", {"error": error}, id); } - private _sendWorkerMessage(type: string, data: StringMap) { - this._bus.sink.send({'type': type, 'value': data}); + private _sendWorkerMessage(type: string, value: StringMap, id?: string) { + this._bus.sink.send({'type': type, 'id': id, 'value': value}); } // TODO: Transfer the types with the serialized data so this can be automated? @@ -190,12 +195,17 @@ export class WebWorkerMain { var methodArgs = args[2]; this._renderer.invokeElementMethod(elementRef, methodName, methodArgs); break; + case "setEventDispatcher": + var viewRef = this._serializer.deserialize(args[0], RenderViewRef); + var dispatcher = new EventDispatcher(viewRef, this._bus.sink, this._serializer); + this._renderer.setEventDispatcher(viewRef, dispatcher); + break; default: throw new BaseException("Not Implemented"); } } - // TODO: Create message type + // TODO(jteplitz602): Create message type enum #3044 private _handleWorkerMessage(message: StringMap) { var data: ReceivedMessage = new ReceivedMessage(message['data']); switch (data.type) { @@ -211,8 +221,7 @@ export class WebWorkerMain { private _wrapWorkerPromise(id: string, promise: Promise, type: Type): void { PromiseWrapper.then(promise, (result: any) => { try { - this._sendWorkerMessage("result", - {"id": id, "value": this._serializer.serialize(result, type)}); + this._sendWorkerMessage("result", this._serializer.serialize(result, type), id); } catch (e) { print(e); } @@ -220,6 +229,50 @@ export class WebWorkerMain { } } +class EventDispatcher implements RenderEventDispatcher { + constructor(private _viewRef: RenderViewRef, private _sink: MessageBusSink, + private _serializer: Serializer) {} + + dispatchRenderEvent(elementIndex: number, eventName: string, locals: Map) { + var e = locals.get('$event'); + var serializedEvent; + switch (eventName) { + case "click": + case "mouseup": + case "mousedown": + case "dblclick": + case "contextmenu": + case "mouseenter": + case "mouseleave": + case "mousemove": + case "mouseout": + case "mouseover": + case "show": + serializedEvent = serializeMouseEvent(e); + break; + case "keydown": + case "keypress": + case "keyup": + serializedEvent = serializeKeyboardEvent(e); + break; + default: + throw new BaseException(eventName + " not supported on WebWorkers"); + } + var serializedLocals = StringMapWrapper.create(); + StringMapWrapper.set(serializedLocals, '$event', serializedEvent); + + this._sink.send({ + "type": "event", + "value": { + "viewRef": this._serializer.serialize(this._viewRef, RenderViewRef), + "elementIndex": elementIndex, + "eventName": eventName, + "locals": serializedLocals + } + }); + } +} + class ReceivedMessage { method: string; args: List; diff --git a/modules/angular2/src/web-workers/worker/application_common.ts b/modules/angular2/src/web-workers/worker/application_common.ts index 8fcc226f1c..1f74657578 100644 --- a/modules/angular2/src/web-workers/worker/application_common.ts +++ b/modules/angular2/src/web-workers/worker/application_common.ts @@ -101,7 +101,7 @@ function _injectorBindings(appComponentType, bus: WorkerMessageBus, Serializer, bind(WorkerMessageBus).toValue(bus), bind(MessageBroker) - .toFactory((a, b) => new MessageBroker(a, b), [WorkerMessageBus, Serializer]), + .toFactory((a, b, c) => new MessageBroker(a, b, c), [WorkerMessageBus, Serializer, NgZone]), WorkerRenderer, bind(Renderer).toAlias(WorkerRenderer), WorkerCompiler, @@ -172,11 +172,6 @@ export function bootstrapWebworkerCommon( PromiseWrapper.then(compRefToken, tick, (err, stackTrace) => { bootstrapProcess.reject(err, stackTrace); }); - PromiseWrapper.catchError(compRefToken, (err) => { - print(err); - bootstrapProcess.reject(err, err.stack); - }); - bus.source.removeListener(listenerId); }); diff --git a/modules/angular2/src/web-workers/worker/broker.ts b/modules/angular2/src/web-workers/worker/broker.ts index 44c5e2792f..098b60e216 100644 --- a/modules/angular2/src/web-workers/worker/broker.ts +++ b/modules/angular2/src/web-workers/worker/broker.ts @@ -6,12 +6,17 @@ import {ListWrapper, StringMapWrapper, MapWrapper} from "../../facade/collection import {Serializer} from "angular2/src/web-workers/shared/serializer"; import {Injectable} from "angular2/di"; import {Type} from "angular2/src/facade/lang"; +import {RenderViewRef, RenderEventDispatcher} from 'angular2/src/render/api'; +import {NgZone} from 'angular2/src/core/zone/ng_zone'; @Injectable() export class MessageBroker { - private _pending: Map = new Map(); + private _pending: Map> = new Map>(); + private _eventDispatchRegistry: Map = + new Map(); - constructor(private _messageBus: MessageBus, protected _serializer: Serializer) { + constructor(private _messageBus: MessageBus, protected _serializer: Serializer, + private _zone: NgZone) { this._messageBus.source.addListener((data) => this._handleMessage(data['data'])); } @@ -43,17 +48,17 @@ export class MessageBroker { if (returnType != null) { var completer: PromiseCompleter = PromiseWrapper.completer(); id = this._generateMessageId(args.type + args.method); - this._pending.set(id, completer.resolve); + this._pending.set(id, completer); PromiseWrapper.catchError(completer.promise, (err, stack?) => { print(err); completer.reject(err, stack); }); - promise = PromiseWrapper.then(completer.promise, (data: MessageResult) => { + promise = PromiseWrapper.then(completer.promise, (value: any) => { if (this._serializer == null) { - return data.value; + return value; } else { - return this._serializer.deserialize(data.value, returnType); + return this._serializer.deserialize(value, returnType); } }); } else { @@ -73,35 +78,67 @@ export class MessageBroker { private _handleMessage(message: StringMap): void { var data = new MessageData(message); // TODO(jteplitz602): replace these strings with messaging constants - var id = data.value.id; - if (this._pending.has(id)) { - this._pending.get(id)(data.value); - this._pending.delete(id); + if (data.type === "event") { + this._dispatchEvent(new RenderEventData(data.value, this._serializer)); + } else if (data.type === "result" || data.type === "error") { + var id = data.id; + if (this._pending.has(id)) { + if (data.type === "result") { + this._pending.get(id).resolve(data.value); + } else { + this._pending.get(id).reject(data.value, null); + } + this._pending.delete(id); + } } } + + private _dispatchEvent(eventData: RenderEventData): void { + var dispatcher = this._eventDispatchRegistry.get(eventData.viewRef); + this._zone.run(() => { + dispatcher.dispatchRenderEvent(eventData.elementIndex, eventData.eventName, eventData.locals); + }); + } + + registerEventDispatcher(viewRef: RenderViewRef, dispatcher: RenderEventDispatcher): void { + this._eventDispatchRegistry.set(viewRef, dispatcher); + } +} + +class RenderEventData { + viewRef: RenderViewRef; + elementIndex: number; + eventName: string; + locals: Map; + + constructor(message: StringMap, serializer: Serializer) { + this.viewRef = serializer.deserialize(message['viewRef'], RenderViewRef); + this.elementIndex = message['elementIndex']; + this.eventName = message['eventName']; + this.locals = MapWrapper.createFromStringMap(message['locals']); + } } class MessageData { type: string; - value: MessageResult; + value: any; + id: string; constructor(data: StringMap) { this.type = StringMapWrapper.get(data, "type"); - if (StringMapWrapper.contains(data, "value")) { - this.value = new MessageResult(StringMapWrapper.get(data, "value")); - } else { - this.value = null; - } + this.id = this._getValueIfPresent(data, "id"); + this.value = this._getValueIfPresent(data, "value"); } -} -class MessageResult { - id: string; - value: any; - - constructor(result: StringMap) { - this.id = StringMapWrapper.get(result, "id"); - this.value = StringMapWrapper.get(result, "value"); + /** + * Returns the value from the StringMap if present. Otherwise returns null + */ + _getValueIfPresent(data: StringMap, key: string) { + if (StringMapWrapper.contains(data, key)) { + return StringMapWrapper.get(data, key); + } else { + return null; + } } } diff --git a/modules/angular2/src/web-workers/worker/loader.js b/modules/angular2/src/web-workers/worker/loader.js deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/modules/angular2/src/web-workers/worker/renderer.ts b/modules/angular2/src/web-workers/worker/renderer.ts index 387e2dda2c..93898bc2ff 100644 --- a/modules/angular2/src/web-workers/worker/renderer.ts +++ b/modules/angular2/src/web-workers/worker/renderer.ts @@ -255,6 +255,9 @@ export class WorkerRenderer implements Renderer { * Sets the dispatcher for all events of the given view */ setEventDispatcher(viewRef: RenderViewRef, dispatcher: RenderEventDispatcher) { - // TODO(jteplitz602) support dom events in web worker. See #3046 + var fnArgs = [new FnArg(viewRef, RenderViewRef)]; + var args = new UiArguments("renderer", "setEventDispatcher", fnArgs); + this._messageBroker.registerEventDispatcher(viewRef, dispatcher); + this._messageBroker.runOnUiThread(args, null); } } diff --git a/modules/angular2/test/web-workers/worker/broker_spec.ts b/modules/angular2/test/web-workers/worker/broker_spec.ts new file mode 100644 index 0000000000..54610d230e --- /dev/null +++ b/modules/angular2/test/web-workers/worker/broker_spec.ts @@ -0,0 +1,78 @@ +import { + AsyncTestCompleter, + inject, + describe, + it, + expect, + beforeEach, + createTestInjector, + beforeEachBindings, + SpyObject, + proxy +} from 'angular2/test_lib'; +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_WEBWORKER} 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'; +import { + RenderViewWithFragmentsStore, + WorkerRenderViewRef +} from 'angular2/src/web-workers/shared/render_view_with_fragments_store'; +import {RenderEventDispatcher, RenderViewRef} from 'angular2/src/render/api'; + +export function main() { + describe("MessageBroker", () => { + beforeEachBindings(() => [ + bind(ON_WEBWORKER) + .toValue(true), + RenderProtoViewRefStore, + 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 WorkerRenderViewRef(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); + + var elementIndex = 15; + var eventName = 'click'; + + bus.source.receive({ + 'data': { + 'type': 'event', + 'value': { + 'viewRef': viewRef.serialize(), + 'elementIndex': elementIndex, + 'eventName': eventName + } + } + }); + + expect(eventDispatcher.wasDispatched).toBeTruthy(); + expect(eventDispatcher.elementIndex).toEqual(elementIndex); + expect(eventDispatcher.eventName).toEqual(eventName); + })); + }); +} + +class SpyEventDispatcher implements RenderEventDispatcher { + wasDispatched: boolean = false; + elementIndex: number; + eventName: string; + + dispatchRenderEvent(elementIndex: number, eventName: string, locals: Map) { + this.wasDispatched = true; + this.elementIndex = elementIndex; + this.eventName = eventName; + } +} diff --git a/modules/angular2/test/web-workers/worker/renderer_spec.ts b/modules/angular2/test/web-workers/worker/renderer_spec.ts index 601134bee6..6b56d16615 100644 --- a/modules/angular2/test/web-workers/worker/renderer_spec.ts +++ b/modules/angular2/test/web-workers/worker/renderer_spec.ts @@ -26,12 +26,6 @@ import { RenderViewRef, RenderFragmentRef } from "angular2/src/render/api"; -import { - MessageBus, - MessageBusSource, - MessageBusSink, - SourceListener -} from "angular2/src/web-workers/shared/message_bus"; import { RenderProtoViewRefStore, WebworkerRenderProtoViewRef @@ -44,6 +38,7 @@ import {resolveInternalDomProtoView} from 'angular2/src/render/dom/view/proto_vi 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'; export function main() { function createBroker(workerSerializer: Serializer, uiSerializer: Serializer, tb: DomTestbed, @@ -56,7 +51,7 @@ export function main() { workerMessageBus.attachToBus(uiMessageBus); // set up the worker side - var broker = new MessageBroker(workerMessageBus, workerSerializer); + var broker = new MessageBroker(workerMessageBus, workerSerializer, null); // set up the ui side var webWorkerMain = new WebWorkerMain(tb.compiler, tb.renderer, uiRenderViewStore, uiSerializer, @@ -118,7 +113,6 @@ export function main() { }); describe("Web Worker Renderer", () => { - beforeEachBindings(() => [DomTestbed]); var renderer: WorkerRenderer; var workerSerializer: Serializer; var workerRenderViewStore: RenderViewWithFragmentsStore; @@ -145,7 +139,6 @@ export function main() { workerRenderViewStore); }); - it('should create and destroy root host views while using the given elements in place', inject([AsyncTestCompleter], (async) => { tb.compiler.compileHost(someComponent) @@ -313,32 +306,3 @@ function createSerializer(protoViewRefStore: RenderProtoViewRefStore, ]); return injector.get(Serializer); } - -class MockMessageBusSource implements MessageBusSource { - private _listenerStore: Map = new Map(); - private _numListeners: number = 0; - - 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); }); - } -} - -class MockMessageBusSink implements MessageBusSink { - private _sendTo: MockMessageBusSource; - - send(message: Object): void { this._sendTo.receive({'data': message}); } - - attachToSource(source: MockMessageBusSource) { this._sendTo = source; } -} - -class MockMessageBus implements MessageBus { - constructor(public sink: MockMessageBusSink, public source: MockMessageBusSource) {} - attachToBus(bus: MockMessageBus) { this.sink.attachToSource(bus.source); } -} diff --git a/modules/angular2/test/web-workers/worker/worker_test_util.ts b/modules/angular2/test/web-workers/worker/worker_test_util.ts new file mode 100644 index 0000000000..8509bf3dbc --- /dev/null +++ b/modules/angular2/test/web-workers/worker/worker_test_util.ts @@ -0,0 +1,36 @@ +import { + MessageBus, + MessageBusSource, + MessageBusSink, + SourceListener +} from "angular2/src/web-workers/shared/message_bus"; +import {MapWrapper} from "angular2/src/facade/collection"; + +export class MockMessageBusSource implements MessageBusSource { + private _listenerStore: Map = new Map(); + private _numListeners: number = 0; + + 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); }); + } +} + +export class MockMessageBusSink implements MessageBusSink { + private _sendTo: MockMessageBusSource; + + send(message: Object): void { this._sendTo.receive({'data': message}); } + + attachToSource(source: MockMessageBusSource) { this._sendTo = source; } +} + +export class MockMessageBus implements MessageBus { + constructor(public sink: MockMessageBusSink, public source: MockMessageBusSource) {} + attachToBus(bus: MockMessageBus) { this.sink.attachToSource(bus.source); } +} diff --git a/modules/examples/e2e_test/routing/routing_spec.ts b/modules/examples/e2e_test/routing/routing_spec.ts index 34d8116b51..352d263dc3 100644 --- a/modules/examples/e2e_test/routing/routing_spec.ts +++ b/modules/examples/e2e_test/routing/routing_spec.ts @@ -1,4 +1,5 @@ import {verifyNoBrowserErrors} from 'angular2/src/test_lib/e2e_util'; +import {Promise} from 'angular2/src/facade/async'; function waitForElement(selector) { var EC = (protractor).ExpectedConditions; @@ -6,6 +7,12 @@ function waitForElement(selector) { browser.wait(EC.presenceOf($(selector)), 10000); } +// returns a promise that resolves in the given number of milliseconds +function wait(time) { + var promise = new Promise((resolve, reject) => { setTimeout(resolve, time); }); + return promise; +} + describe('routing inbox-app', function() { afterEach(verifyNoBrowserErrors); @@ -62,6 +69,7 @@ describe('routing inbox-app', function() { waitForElement('#item-10'); element(by.css('#item-10')).click(); waitForElement('#record-id'); + browser.wait(wait(500), 600); expect(element(by.css('#record-id')).getText()).toEqual('ID: 10'); }); diff --git a/modules/examples/e2e_test/web_workers/web_workers_spec.dart b/modules/examples/e2e_test/web_workers/web_workers_spec.dart new file mode 100644 index 0000000000..8873540d4e --- /dev/null +++ b/modules/examples/e2e_test/web_workers/web_workers_spec.dart @@ -0,0 +1,5 @@ +library examples.e2e_test.web_workers.web_workers_spec; + +main() { + +} diff --git a/modules/examples/e2e_test/web_workers/web_workers_spec.ts b/modules/examples/e2e_test/web_workers/web_workers_spec.ts new file mode 100644 index 0000000000..c37d1b9d4d --- /dev/null +++ b/modules/examples/e2e_test/web_workers/web_workers_spec.ts @@ -0,0 +1,42 @@ +import {verifyNoBrowserErrors} from 'angular2/src/test_lib/e2e_util'; +import {Promise} from 'angular2/src/facade/async'; + +// returns a promise that resolves in the given number of milliseconds +function wait(time) { + var promise = new Promise((resolve, reject) => { setTimeout(resolve, time); }); + return promise; +} + +describe('WebWorkers', function() { + afterEach(verifyNoBrowserErrors); + var selector = "hello-app .greeting"; + var URL = "examples/src/web_workers/index.html"; + + it('should greet', () => { + browser.get(URL); + + browser.wait(protractor.until.elementLocated(by.css(selector)), 5000); + expect(element.all(by.css(selector)).first().getText()).toEqual("hello world!"); + }); + + it('should change greeting', () => { + browser.get(URL); + + browser.wait(protractor.until.elementLocated(by.css(selector)), 5000); + element.all(by.css(".changeButton")).first().click(); + browser.wait(wait(500), 600); + expect(element.all(by.css(selector)).first().getText()).toEqual("howdy world!"); + }); + + it("should display correct key names", () => { + browser.get(URL); + browser.wait(protractor.until.elementLocated(by.css(".sample-area")), 5000); + + var area = element.all(by.css(".sample-area")).first(); + expect(area.getText()).toEqual('(none)'); + browser.wait(wait(500), 600); + + area.sendKeys('u'); + expect(area.getText()).toEqual("U"); + }); +}); diff --git a/modules/examples/src/message_broker/background_index.dart b/modules/examples/src/message_broker/background_index.dart index a476f48583..79fe32f5fb 100644 --- a/modules/examples/src/message_broker/background_index.dart +++ b/modules/examples/src/message_broker/background_index.dart @@ -4,6 +4,7 @@ import "package:angular2/src/web-workers/worker/application.dart" show WorkerMessageBus, WorkerMessageBusSource, WorkerMessageBusSink; import "package:angular2/src/web-workers/worker/broker.dart" show MessageBroker, UiArguments; +import "package:angular2/src/web-workers/shared/serializer.dart" show Serializer; import "dart:isolate"; @@ -17,7 +18,7 @@ main(List args, SendPort replyTo) { } }); - MessageBroker broker = new MessageBroker(bus, null); + MessageBroker broker = new MessageBroker(bus, new Serializer(null, null, null), null); var args = new UiArguments("test", "tester"); broker.runOnUiThread(args, String).then((data) { bus.sink.send({"type": "result", "value": data}); diff --git a/modules/examples/src/message_broker/background_index.ts b/modules/examples/src/message_broker/background_index.ts index d31c318ca9..35f23ca4bc 100644 --- a/modules/examples/src/message_broker/background_index.ts +++ b/modules/examples/src/message_broker/background_index.ts @@ -14,7 +14,7 @@ export function main() { } }); - var broker = new MessageBroker(bus, new Serializer(null)); + var broker = new MessageBroker(bus, new Serializer(null, null, null), null); var args = new UiArguments("test", "tester"); broker.runOnUiThread(args, String) .then((data: string) => { bus.sink.send({type: "result", value: data}); }); diff --git a/modules/examples/src/message_broker/index.dart b/modules/examples/src/message_broker/index.dart index b180edc114..bee72e3b27 100644 --- a/modules/examples/src/message_broker/index.dart +++ b/modules/examples/src/message_broker/index.dart @@ -19,7 +19,7 @@ main() { .appendHtml("${data['value']}"); } else if (identical(data['type'], "test")) { bus.sink.send( - {'type': "result", 'value': {'id': data['id'], 'value': VALUE}}); + {'type': "result", 'id': data['id'], 'value': VALUE}); } else if (identical(data['type'], "result")) { querySelector("#ui_result") .appendHtml("${data['value']}"); diff --git a/modules/examples/src/message_broker/index.ts b/modules/examples/src/message_broker/index.ts index 62383e3adc..340736670e 100644 --- a/modules/examples/src/message_broker/index.ts +++ b/modules/examples/src/message_broker/index.ts @@ -19,7 +19,7 @@ bus.source.addListener((message) => { document.getElementById("echo_result").innerHTML = `${message.data.value}`; } else if (message.data.type === "test") { - bus.sink.send({type: "result", value: {id: message.data.id, value: VALUE}}); + bus.sink.send({type: "result", id: message.data.id, value: VALUE}); } else if (message.data.type == "result") { document.getElementById("ui_result").innerHTML = `${message.data.value}`; diff --git a/modules/examples/src/web_workers/background_index.dart b/modules/examples/src/web_workers/background_index.dart index 45f97352a5..95d9ad5a49 100644 --- a/modules/examples/src/web_workers/background_index.dart +++ b/modules/examples/src/web_workers/background_index.dart @@ -9,5 +9,5 @@ import "package:angular2/src/reflection/reflection.dart"; main(List args, SendPort replyTo) { reflector.reflectionCapabilities = new ReflectionCapabilities(); - bootstrapWebworker(replyTo, HelloCmp); + bootstrapWebworker(replyTo, HelloCmp).catchError((error) => throw error); } diff --git a/modules/examples/src/web_workers/example.html b/modules/examples/src/web_workers/example.html deleted file mode 100644 index 1e6008f42b..0000000000 --- a/modules/examples/src/web_workers/example.html +++ /dev/null @@ -1,11 +0,0 @@ - - - Hello Angular 2.0 - - - Loading... - - - $SCRIPTS$ - - diff --git a/modules/examples/src/web_workers/example.dart b/modules/examples/src/web_workers/index.dart similarity index 100% rename from modules/examples/src/web_workers/example.dart rename to modules/examples/src/web_workers/index.dart diff --git a/modules/examples/src/web_workers/index.html b/modules/examples/src/web_workers/index.html new file mode 100644 index 0000000000..89898234f1 --- /dev/null +++ b/modules/examples/src/web_workers/index.html @@ -0,0 +1,26 @@ + + + Hello Angular 2.0 + + + + Loading... + + + $SCRIPTS$ + + diff --git a/modules/examples/src/web_workers/example.ts b/modules/examples/src/web_workers/index.ts similarity index 100% rename from modules/examples/src/web_workers/example.ts rename to modules/examples/src/web_workers/index.ts diff --git a/modules/examples/src/web_workers/index_common.ts b/modules/examples/src/web_workers/index_common.ts index 4e948150cc..009f742468 100644 --- a/modules/examples/src/web_workers/index_common.ts +++ b/modules/examples/src/web_workers/index_common.ts @@ -1,4 +1,5 @@ import {ElementRef, Component, Directive, View, Injectable, Renderer} from 'angular2/angular2'; +import {StringWrapper} from 'angular2/src/facade/lang'; // A service available to the Injector, used by the HelloCmp component. @Injectable() @@ -37,7 +38,8 @@ class RedDec { // Expressions in the template (like {{greeting}}) are evaluated in the // context of the HelloCmp class below. template: `
{{greeting}} world!
- `, + +
{{lastKey}}

`, // All directives used in the template need to be specified. This allows for // modularity (RedDec can only be used in this template) // and better tooling (the template can be invalidated if the attribute is @@ -46,8 +48,11 @@ class RedDec { }) export class HelloCmp { greeting: string; + lastKey: string = '(none)'; constructor(service: GreetingService) { this.greeting = service.greeting; } changeGreeting(): void { this.greeting = 'howdy'; } + + onKeyDown(event): void { this.lastKey = StringWrapper.fromCharCode(event['keyCode']); } } diff --git a/modules/examples/src/web_workers/loader.js b/modules/examples/src/web_workers/loader.js index 3af4738512..992289a184 100644 --- a/modules/examples/src/web_workers/loader.js +++ b/modules/examples/src/web_workers/loader.js @@ -1,27 +1,24 @@ -$SCRIPTS$ +$SCRIPTS$ window = { + setTimeout: setTimeout, + Map: Map, + Set: Set, + Array: Array, + Reflect: Reflect, + RegExp: RegExp, + Promise: Promise, + Date: Date, + zone: zone +}; +assert = function() {}; - // TODO (jteplitz) Monkey patch this from within angular (#3207) - window = { - setTimeout: setTimeout, - Map: Map, - Set: Set, - Array: Array, - Reflect: Reflect, - RegExp: RegExp, - Promise: Promise, - Date: Date - }; -assert = function() {} - - - System.import("examples/src/web_workers/background_index") - .then( - function(m) { - console.log("running main"); - try { - m.main(); - } catch (e) { - console.error(e); - } - }, - function(error) { console.error("error loading background", error); }); +System.import("examples/src/web_workers/background_index") + .then( + function(m) { + console.log("running main"); + try { + m.main(); + } catch (e) { + console.error(e); + } + }, + function(error) { console.error("error loading background", error); });