feat(WebWorkers) Add DOM event support

closes #3046
This commit is contained in:
Jason Teplitz 2015-07-10 16:09:18 -07:00
parent 8e960d4052
commit 7b834e02ec
27 changed files with 556 additions and 165 deletions

View File

@ -12,20 +12,22 @@ export class RenderViewWithFragmentsStore {
constructor(@Inject(ON_WEBWORKER) onWebWorker) { constructor(@Inject(ON_WEBWORKER) onWebWorker) {
this._onWebWorker = onWebWorker; this._onWebWorker = onWebWorker;
if (!onWebWorker) { this._lookupByIndex = new Map<number, RenderViewRef | RenderFragmentRef>();
this._lookupByIndex = new Map<number, RenderViewRef | RenderFragmentRef>(); this._lookupByView = new Map<RenderViewRef | RenderFragmentRef, number>();
this._lookupByView = new Map<RenderViewRef | RenderFragmentRef, number>();
}
} }
allocate(fragmentCount: number): RenderViewWithFragments { allocate(fragmentCount: number): RenderViewWithFragments {
var initialIndex = this._nextIndex;
var viewRef = new WorkerRenderViewRef(this._nextIndex++); var viewRef = new WorkerRenderViewRef(this._nextIndex++);
var fragmentRefs = ListWrapper.createGrowableSize(fragmentCount); var fragmentRefs = ListWrapper.createGrowableSize(fragmentCount);
for (var i = 0; i < fragmentCount; i++) { for (var i = 0; i < fragmentCount; i++) {
fragmentRefs[i] = new WorkerRenderFragmentRef(this._nextIndex++); 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) { store(view: RenderViewWithFragments, startIndex: number) {
@ -60,11 +62,7 @@ export class RenderViewWithFragmentsStore {
return null; return null;
} }
if (this._onWebWorker) { return this.retreive(ref);
return WorkerRenderViewRef.deserialize(ref);
} else {
return this.retreive(ref);
}
} }
deserializeRenderFragmentRef(ref: number): RenderFragmentRef { deserializeRenderFragmentRef(ref: number): RenderFragmentRef {
@ -72,11 +70,7 @@ export class RenderViewWithFragmentsStore {
return null; return null;
} }
if (this._onWebWorker) { return this.retreive(ref);
return WorkerRenderFragmentRef.deserialize(ref);
} else {
return this.retreive(ref);
}
} }
private _serializeRenderFragmentOrViewRef(ref: RenderViewRef | RenderFragmentRef): number { private _serializeRenderFragmentOrViewRef(ref: RenderViewRef | RenderFragmentRef): number {
@ -114,20 +108,11 @@ export class RenderViewWithFragmentsStore {
return null; return null;
} }
var viewRef: RenderViewRef | RenderFragmentRef; var viewRef = this.deserializeRenderViewRef(obj['viewRef']);
var fragments: List<RenderViewRef | RenderFragmentRef>; var fragments =
if (this._onWebWorker) { ListWrapper.map(obj['fragmentRefs'], (val) => this.deserializeRenderFragmentRef(val));
viewRef = WorkerRenderViewRef.deserialize(obj['viewRef']);
fragments =
ListWrapper.map(obj['fragmentRefs'], (val) => WorkerRenderFragmentRef.deserialize(val));
return new RenderViewWithFragments(viewRef, fragments); 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);
}
} }
} }

View File

@ -1,5 +1,19 @@
import {Type, isArray, isPresent, serializeEnum, deserializeEnum} from "angular2/src/facade/lang"; import {
import {List, ListWrapper, Map, StringMapWrapper, MapWrapper} from "angular2/src/facade/collection"; Type,
isArray,
isPresent,
serializeEnum,
deserializeEnum,
BaseException
} from "angular2/src/facade/lang";
import {
List,
ListWrapper,
Map,
StringMap,
StringMapWrapper,
MapWrapper
} from "angular2/src/facade/collection";
import { import {
ProtoViewDto, ProtoViewDto,
DirectiveMetadata, DirectiveMetadata,
@ -13,7 +27,8 @@ import {
RenderViewRef, RenderViewRef,
RenderFragmentRef, RenderFragmentRef,
RenderElementRef, RenderElementRef,
ViewType ViewType,
ViewEncapsulation
} from "angular2/src/render/api"; } from "angular2/src/render/api";
import {WorkerElementRef} from 'angular2/src/web-workers/shared/api'; import {WorkerElementRef} from 'angular2/src/web-workers/shared/api';
import {AST, ASTWithSource} from 'angular2/src/change_detection/change_detection'; import {AST, ASTWithSource} from 'angular2/src/change_detection/change_detection';
@ -30,11 +45,18 @@ export class Serializer {
constructor(private _parser: Parser, private _protoViewStore: RenderProtoViewRefStore, constructor(private _parser: Parser, private _protoViewStore: RenderProtoViewRefStore,
private _renderViewStore: RenderViewWithFragmentsStore) { private _renderViewStore: RenderViewWithFragmentsStore) {
this._enumRegistry = new Map<any, Map<int, any>>(); this._enumRegistry = new Map<any, Map<int, any>>();
var viewTypeMap = new Map<int, any>(); var viewTypeMap = new Map<int, any>();
viewTypeMap[0] = ViewType.HOST; viewTypeMap[0] = ViewType.HOST;
viewTypeMap[1] = ViewType.COMPONENT; viewTypeMap[1] = ViewType.COMPONENT;
viewTypeMap[2] = ViewType.EMBEDDED; viewTypeMap[2] = ViewType.EMBEDDED;
this._enumRegistry.set(ViewType, viewTypeMap); this._enumRegistry.set(ViewType, viewTypeMap);
var viewEncapsulationMap = new Map<int, any>();
viewEncapsulationMap[0] = ViewEncapsulation.EMULATED;
viewEncapsulationMap[1] = ViewEncapsulation.NATIVE;
viewEncapsulationMap[2] = ViewEncapsulation.NONE;
this._enumRegistry.set(ViewEncapsulation, viewEncapsulationMap);
} }
serialize(obj: any, type: Type): Object { serialize(obj: any, type: Type): Object {
@ -71,8 +93,10 @@ export class Serializer {
return this._renderViewStore.serializeRenderFragmentRef(obj); return this._renderViewStore.serializeRenderFragmentRef(obj);
} else if (type == WorkerElementRef) { } else if (type == WorkerElementRef) {
return this._serializeWorkerElementRef(obj); return this._serializeWorkerElementRef(obj);
} else if (type == EventBinding) {
return this._serializeEventBinding(obj);
} else { } 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); return this._renderViewStore.deserializeRenderFragmentRef(map);
} else if (type == WorkerElementRef) { } else if (type == WorkerElementRef) {
return this._deserializeWorkerElementRef(map); return this._deserializeWorkerElementRef(map);
} else if (type == EventBinding) {
return this._deserializeEventBinding(map);
} else { } 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); } allocateRenderViews(fragmentCount: number) { this._renderViewStore.allocate(fragmentCount); }
private _serializeEventBinding(binding: EventBinding): StringMap<string, any> {
return {'fullName': binding.fullName, 'source': this.serialize(binding.source, ASTWithSource)};
}
private _deserializeEventBinding(map: StringMap<string, any>): EventBinding {
return new EventBinding(map['fullName'],
this.deserialize(map['source'], ASTWithSource, "binding"));
}
private _serializeWorkerElementRef(elementRef: RenderElementRef): StringMap<string, any> { private _serializeWorkerElementRef(elementRef: RenderElementRef): StringMap<string, any> {
return { return {
'renderView': this.serialize(elementRef.renderView, RenderViewRef), 'renderView': this.serialize(elementRef.renderView, RenderViewRef),
@ -214,7 +249,7 @@ export class Serializer {
'directives': this.serialize(view.directives, DirectiveMetadata), 'directives': this.serialize(view.directives, DirectiveMetadata),
'styleAbsUrls': view.styleAbsUrls, 'styleAbsUrls': view.styleAbsUrls,
'styles': view.styles, 'styles': view.styles,
'encapsulation': view.encapsulation 'encapsulation': serializeEnum(view.encapsulation)
}; };
} }
@ -225,7 +260,8 @@ export class Serializer {
directives: this.deserialize(obj['directives'], DirectiveMetadata), directives: this.deserialize(obj['directives'], DirectiveMetadata),
styleAbsUrls: obj['styleAbsUrls'], styleAbsUrls: obj['styleAbsUrls'],
styles: obj['styles'], 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']), variableBindings: this.objectToMap(obj['variableBindings']),
textBindings: this.deserialize(obj['textBindings'], ASTWithSource, "interpolation"), textBindings: this.deserialize(obj['textBindings'], ASTWithSource, "interpolation"),
type: deserializeEnum(obj['type'], this._enumRegistry.get(ViewType)), type: deserializeEnum(obj['type'], this._enumRegistry.get(ViewType)),
transitiveNgContentCount: obj['transitivengContentCount'] transitiveNgContentCount: obj['transitiveNgContentCount']
}); });
} }

View File

@ -1,5 +1,5 @@
// TODO: This whole file is nearly identical to core/application.ts. // 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 // There should be a way to refactor application so that this file is unnecessary. See #3277
import {Injector, bind, Binding} from "angular2/di"; import {Injector, bind, Binding} from "angular2/di";
import {Type, isBlank, isPresent} from "angular2/src/facade/lang"; import {Type, isBlank, isPresent} from "angular2/src/facade/lang";
import {Reflector, reflector} from 'angular2/src/reflection/reflection'; 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 {HammerGesturesPlugin} from 'angular2/src/render/dom/events/hammer_gestures';
import {AppViewPool, APP_VIEW_POOL_CAPACITY} from 'angular2/src/core/compiler/view_pool'; import {AppViewPool, APP_VIEW_POOL_CAPACITY} from 'angular2/src/core/compiler/view_pool';
import {Renderer, RenderCompiler} from 'angular2/src/render/api'; import {Renderer, RenderCompiler} from 'angular2/src/render/api';
import {AppRootUrl} from 'angular2/src/services/app_root_url';
import { import {
DomRenderer, DomRenderer,
DOCUMENT_TOKEN, DOCUMENT_TOKEN,
@ -28,6 +29,8 @@ import {
DefaultDomCompiler, DefaultDomCompiler,
APP_ID_RANDOM_BINDING APP_ID_RANDOM_BINDING
} from 'angular2/src/render/render'; } 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 { import {
SharedStylesHost, SharedStylesHost,
DomSharedStylesHost DomSharedStylesHost
@ -73,11 +76,6 @@ function _injectorBindings(): List<Type | Binding | List<any>> {
} else if (JitChangeDetection.isSupported()) { } else if (JitChangeDetection.isSupported()) {
bestChangeDetection = JitChangeDetection; 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 [ return [
bind(DOCUMENT_TOKEN) bind(DOCUMENT_TOKEN)
@ -100,6 +98,7 @@ function _injectorBindings(): List<Type | Binding | List<any>> {
bind(SharedStylesHost).toAlias(DomSharedStylesHost), bind(SharedStylesHost).toAlias(DomSharedStylesHost),
Serializer, Serializer,
bind(ON_WEBWORKER).toValue(false), bind(ON_WEBWORKER).toValue(false),
bind(ElementSchemaRegistry).toValue(new DomElementSchemaRegistry()),
RenderViewWithFragmentsStore, RenderViewWithFragmentsStore,
RenderProtoViewRefStore, RenderProtoViewRefStore,
ProtoViewFactory, ProtoViewFactory,
@ -117,7 +116,7 @@ function _injectorBindings(): List<Type | Binding | List<any>> {
DirectiveResolver, DirectiveResolver,
Parser, Parser,
Lexer, Lexer,
ExceptionHandler, bind(ExceptionHandler).toFactory(() => new ExceptionHandler(DOM), []),
bind(XHR).toValue(new XHRImpl()), bind(XHR).toValue(new XHRImpl()),
ComponentUrlMapper, ComponentUrlMapper,
UrlResolver, UrlResolver,
@ -126,6 +125,7 @@ function _injectorBindings(): List<Type | Binding | List<any>> {
DynamicComponentLoader, DynamicComponentLoader,
Testability, Testability,
AnchorBasedAppRootUrl, AnchorBasedAppRootUrl,
bind(AppRootUrl).toAlias(AnchorBasedAppRootUrl),
WebWorkerMain WebWorkerMain
]; ];
} }

View File

@ -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<String, dynamic> serializeMouseEvent(dynamic e) {
return serializeEvent(e, MOUSE_EVENT_PROPERTIES);
}
Map<String, dynamic> serializeKeyboardEvent(dynamic e) {
return serializeEvent(e, KEYBOARD_EVENT_PROPERTIES);
}
Map<String, dynamic> serializeEvent(dynamic e, Map<Symbol, Type> 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;
}

View File

@ -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<string, any> {
return serializeEvent(e, MOUSE_EVENT_PROPERTIES);
}
export function serializeKeyboardEvent(e: KeyboardEvent): StringMap<string, any> {
return serializeEvent(e, KEYBOARD_EVENT_PROPERTIES);
}
function serializeEvent(e: any, properties: List<string>): StringMap<string, any> {
var serialized = {};
for (var i = 0; i < properties.length; i++) {
var prop = properties[i];
serialized[prop] = e[prop];
}
return serialized;
}

View File

@ -3,7 +3,6 @@
* It takes care of spawning the worker and sending it the initial init message * 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 * It also acts and the messenger between the worker thread and the renderer running on the UI
* thread * thread
* TODO: This class might need to be refactored to match application.ts...
*/ */
import {createInjector} from "./di_bindings"; import {createInjector} from "./di_bindings";
@ -16,12 +15,14 @@ import {
RenderProtoViewRef, RenderProtoViewRef,
RenderProtoViewMergeMapping, RenderProtoViewMergeMapping,
RenderViewRef, RenderViewRef,
RenderEventDispatcher,
RenderFragmentRef RenderFragmentRef
} from "angular2/src/render/api"; } 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 {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 {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 { import {
RenderViewWithFragmentsStore RenderViewWithFragmentsStore
} from 'angular2/src/web-workers/shared/render_view_with_fragments_store'; } 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 {Injectable} from 'angular2/di';
import {BrowserDomAdapter} from 'angular2/src/dom/browser_adapter'; import {BrowserDomAdapter} from 'angular2/src/dom/browser_adapter';
import {DOM} from 'angular2/src/dom/dom_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 * 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 * Sends an error back to the worker thread in response to an opeartion on the UI thread
*/ */
private _sendWorkerError(id: string, error: any) { 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<string, any>) { private _sendWorkerMessage(type: string, value: StringMap<string, any>, id?: string) {
this._bus.sink.send({'type': type, 'value': data}); this._bus.sink.send({'type': type, 'id': id, 'value': value});
} }
// TODO: Transfer the types with the serialized data so this can be automated? // TODO: Transfer the types with the serialized data so this can be automated?
@ -190,12 +195,17 @@ export class WebWorkerMain {
var methodArgs = args[2]; var methodArgs = args[2];
this._renderer.invokeElementMethod(elementRef, methodName, methodArgs); this._renderer.invokeElementMethod(elementRef, methodName, methodArgs);
break; 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: default:
throw new BaseException("Not Implemented"); throw new BaseException("Not Implemented");
} }
} }
// TODO: Create message type // TODO(jteplitz602): Create message type enum #3044
private _handleWorkerMessage(message: StringMap<string, any>) { private _handleWorkerMessage(message: StringMap<string, any>) {
var data: ReceivedMessage = new ReceivedMessage(message['data']); var data: ReceivedMessage = new ReceivedMessage(message['data']);
switch (data.type) { switch (data.type) {
@ -211,8 +221,7 @@ export class WebWorkerMain {
private _wrapWorkerPromise(id: string, promise: Promise<any>, type: Type): void { private _wrapWorkerPromise(id: string, promise: Promise<any>, type: Type): void {
PromiseWrapper.then(promise, (result: any) => { PromiseWrapper.then(promise, (result: any) => {
try { try {
this._sendWorkerMessage("result", this._sendWorkerMessage("result", this._serializer.serialize(result, type), id);
{"id": id, "value": this._serializer.serialize(result, type)});
} catch (e) { } catch (e) {
print(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<string, any>) {
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 { class ReceivedMessage {
method: string; method: string;
args: List<any>; args: List<any>;

View File

@ -101,7 +101,7 @@ function _injectorBindings(appComponentType, bus: WorkerMessageBus,
Serializer, Serializer,
bind(WorkerMessageBus).toValue(bus), bind(WorkerMessageBus).toValue(bus),
bind(MessageBroker) bind(MessageBroker)
.toFactory((a, b) => new MessageBroker(a, b), [WorkerMessageBus, Serializer]), .toFactory((a, b, c) => new MessageBroker(a, b, c), [WorkerMessageBus, Serializer, NgZone]),
WorkerRenderer, WorkerRenderer,
bind(Renderer).toAlias(WorkerRenderer), bind(Renderer).toAlias(WorkerRenderer),
WorkerCompiler, WorkerCompiler,
@ -172,11 +172,6 @@ export function bootstrapWebworkerCommon(
PromiseWrapper.then(compRefToken, tick, PromiseWrapper.then(compRefToken, tick,
(err, stackTrace) => { bootstrapProcess.reject(err, stackTrace); }); (err, stackTrace) => { bootstrapProcess.reject(err, stackTrace); });
PromiseWrapper.catchError(compRefToken, (err) => {
print(err);
bootstrapProcess.reject(err, err.stack);
});
bus.source.removeListener(listenerId); bus.source.removeListener(listenerId);
}); });

View File

@ -6,12 +6,17 @@ import {ListWrapper, StringMapWrapper, MapWrapper} from "../../facade/collection
import {Serializer} from "angular2/src/web-workers/shared/serializer"; import {Serializer} from "angular2/src/web-workers/shared/serializer";
import {Injectable} from "angular2/di"; import {Injectable} from "angular2/di";
import {Type} from "angular2/src/facade/lang"; 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() @Injectable()
export class MessageBroker { export class MessageBroker {
private _pending: Map<string, Function> = new Map<string, Function>(); private _pending: Map<string, PromiseCompleter<any>> = new Map<string, PromiseCompleter<any>>();
private _eventDispatchRegistry: Map<RenderViewRef, RenderEventDispatcher> =
new Map<RenderViewRef, RenderEventDispatcher>();
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'])); this._messageBus.source.addListener((data) => this._handleMessage(data['data']));
} }
@ -43,17 +48,17 @@ export class MessageBroker {
if (returnType != null) { if (returnType != null) {
var completer: PromiseCompleter<any> = PromiseWrapper.completer(); var completer: PromiseCompleter<any> = PromiseWrapper.completer();
id = this._generateMessageId(args.type + args.method); id = this._generateMessageId(args.type + args.method);
this._pending.set(id, completer.resolve); this._pending.set(id, completer);
PromiseWrapper.catchError(completer.promise, (err, stack?) => { PromiseWrapper.catchError(completer.promise, (err, stack?) => {
print(err); print(err);
completer.reject(err, stack); completer.reject(err, stack);
}); });
promise = PromiseWrapper.then(completer.promise, (data: MessageResult) => { promise = PromiseWrapper.then(completer.promise, (value: any) => {
if (this._serializer == null) { if (this._serializer == null) {
return data.value; return value;
} else { } else {
return this._serializer.deserialize(data.value, returnType); return this._serializer.deserialize(value, returnType);
} }
}); });
} else { } else {
@ -73,35 +78,67 @@ export class MessageBroker {
private _handleMessage(message: StringMap<string, any>): void { private _handleMessage(message: StringMap<string, any>): void {
var data = new MessageData(message); var data = new MessageData(message);
// TODO(jteplitz602): replace these strings with messaging constants // TODO(jteplitz602): replace these strings with messaging constants
var id = data.value.id; if (data.type === "event") {
if (this._pending.has(id)) { this._dispatchEvent(new RenderEventData(data.value, this._serializer));
this._pending.get(id)(data.value); } else if (data.type === "result" || data.type === "error") {
this._pending.delete(id); 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<string, any>;
constructor(message: StringMap<string, any>, 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 { class MessageData {
type: string; type: string;
value: MessageResult; value: any;
id: string;
constructor(data: StringMap<string, any>) { constructor(data: StringMap<string, any>) {
this.type = StringMapWrapper.get(data, "type"); this.type = StringMapWrapper.get(data, "type");
if (StringMapWrapper.contains(data, "value")) { this.id = this._getValueIfPresent(data, "id");
this.value = new MessageResult(StringMapWrapper.get(data, "value")); this.value = this._getValueIfPresent(data, "value");
} else {
this.value = null;
}
} }
}
class MessageResult { /**
id: string; * Returns the value from the StringMap if present. Otherwise returns null
value: any; */
_getValueIfPresent(data: StringMap<string, any>, key: string) {
constructor(result: StringMap<string, any>) { if (StringMapWrapper.contains(data, key)) {
this.id = StringMapWrapper.get(result, "id"); return StringMapWrapper.get(data, key);
this.value = StringMapWrapper.get(result, "value"); } else {
return null;
}
} }
} }

View File

@ -255,6 +255,9 @@ export class WorkerRenderer implements Renderer {
* Sets the dispatcher for all events of the given view * Sets the dispatcher for all events of the given view
*/ */
setEventDispatcher(viewRef: RenderViewRef, dispatcher: RenderEventDispatcher) { 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);
} }
} }

View File

@ -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<string, any>) {
this.wasDispatched = true;
this.elementIndex = elementIndex;
this.eventName = eventName;
}
}

View File

@ -26,12 +26,6 @@ import {
RenderViewRef, RenderViewRef,
RenderFragmentRef RenderFragmentRef
} from "angular2/src/render/api"; } from "angular2/src/render/api";
import {
MessageBus,
MessageBusSource,
MessageBusSink,
SourceListener
} from "angular2/src/web-workers/shared/message_bus";
import { import {
RenderProtoViewRefStore, RenderProtoViewRefStore,
WebworkerRenderProtoViewRef 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 {someComponent} from '../../render/dom/dom_renderer_integration_spec';
import {WebWorkerMain} from 'angular2/src/web-workers/ui/impl'; import {WebWorkerMain} from 'angular2/src/web-workers/ui/impl';
import {AnchorBasedAppRootUrl} from 'angular2/src/services/anchor_based_app_root_url'; import {AnchorBasedAppRootUrl} from 'angular2/src/services/anchor_based_app_root_url';
import {MockMessageBus, MockMessageBusSink, MockMessageBusSource} from './worker_test_util';
export function main() { export function main() {
function createBroker(workerSerializer: Serializer, uiSerializer: Serializer, tb: DomTestbed, function createBroker(workerSerializer: Serializer, uiSerializer: Serializer, tb: DomTestbed,
@ -56,7 +51,7 @@ export function main() {
workerMessageBus.attachToBus(uiMessageBus); workerMessageBus.attachToBus(uiMessageBus);
// set up the worker side // set up the worker side
var broker = new MessageBroker(workerMessageBus, workerSerializer); var broker = new MessageBroker(workerMessageBus, workerSerializer, null);
// set up the ui side // set up the ui side
var webWorkerMain = new WebWorkerMain(tb.compiler, tb.renderer, uiRenderViewStore, uiSerializer, var webWorkerMain = new WebWorkerMain(tb.compiler, tb.renderer, uiRenderViewStore, uiSerializer,
@ -118,7 +113,6 @@ export function main() {
}); });
describe("Web Worker Renderer", () => { describe("Web Worker Renderer", () => {
beforeEachBindings(() => [DomTestbed]);
var renderer: WorkerRenderer; var renderer: WorkerRenderer;
var workerSerializer: Serializer; var workerSerializer: Serializer;
var workerRenderViewStore: RenderViewWithFragmentsStore; var workerRenderViewStore: RenderViewWithFragmentsStore;
@ -145,7 +139,6 @@ export function main() {
workerRenderViewStore); workerRenderViewStore);
}); });
it('should create and destroy root host views while using the given elements in place', it('should create and destroy root host views while using the given elements in place',
inject([AsyncTestCompleter], (async) => { inject([AsyncTestCompleter], (async) => {
tb.compiler.compileHost(someComponent) tb.compiler.compileHost(someComponent)
@ -313,32 +306,3 @@ function createSerializer(protoViewRefStore: RenderProtoViewRefStore,
]); ]);
return injector.get(Serializer); return injector.get(Serializer);
} }
class MockMessageBusSource implements MessageBusSource {
private _listenerStore: Map<int, SourceListener> = new Map<int, SourceListener>();
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); }
}

View File

@ -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<int, SourceListener> = new Map<int, SourceListener>();
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); }
}

View File

@ -1,4 +1,5 @@
import {verifyNoBrowserErrors} from 'angular2/src/test_lib/e2e_util'; import {verifyNoBrowserErrors} from 'angular2/src/test_lib/e2e_util';
import {Promise} from 'angular2/src/facade/async';
function waitForElement(selector) { function waitForElement(selector) {
var EC = (<any>protractor).ExpectedConditions; var EC = (<any>protractor).ExpectedConditions;
@ -6,6 +7,12 @@ function waitForElement(selector) {
browser.wait(EC.presenceOf($(selector)), 10000); 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() { describe('routing inbox-app', function() {
afterEach(verifyNoBrowserErrors); afterEach(verifyNoBrowserErrors);
@ -62,6 +69,7 @@ describe('routing inbox-app', function() {
waitForElement('#item-10'); waitForElement('#item-10');
element(by.css('#item-10')).click(); element(by.css('#item-10')).click();
waitForElement('#record-id'); waitForElement('#record-id');
browser.wait(wait(500), 600);
expect(element(by.css('#record-id')).getText()).toEqual('ID: 10'); expect(element(by.css('#record-id')).getText()).toEqual('ID: 10');
}); });

View File

@ -0,0 +1,5 @@
library examples.e2e_test.web_workers.web_workers_spec;
main() {
}

View File

@ -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");
});
});

View File

@ -4,6 +4,7 @@ import "package:angular2/src/web-workers/worker/application.dart"
show WorkerMessageBus, WorkerMessageBusSource, WorkerMessageBusSink; show WorkerMessageBus, WorkerMessageBusSource, WorkerMessageBusSink;
import "package:angular2/src/web-workers/worker/broker.dart" import "package:angular2/src/web-workers/worker/broker.dart"
show MessageBroker, UiArguments; show MessageBroker, UiArguments;
import "package:angular2/src/web-workers/shared/serializer.dart" show Serializer;
import "dart:isolate"; import "dart:isolate";
@ -17,7 +18,7 @@ main(List<String> 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"); var args = new UiArguments("test", "tester");
broker.runOnUiThread(args, String).then((data) { broker.runOnUiThread(args, String).then((data) {
bus.sink.send({"type": "result", "value": data}); bus.sink.send({"type": "result", "value": data});

View File

@ -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"); var args = new UiArguments("test", "tester");
broker.runOnUiThread(args, String) broker.runOnUiThread(args, String)
.then((data: string) => { bus.sink.send({type: "result", value: data}); }); .then((data: string) => { bus.sink.send({type: "result", value: data}); });

View File

@ -19,7 +19,7 @@ main() {
.appendHtml("<span class='response'>${data['value']}</span>"); .appendHtml("<span class='response'>${data['value']}</span>");
} else if (identical(data['type'], "test")) { } else if (identical(data['type'], "test")) {
bus.sink.send( bus.sink.send(
{'type': "result", 'value': {'id': data['id'], 'value': VALUE}}); {'type': "result", 'id': data['id'], 'value': VALUE});
} else if (identical(data['type'], "result")) { } else if (identical(data['type'], "result")) {
querySelector("#ui_result") querySelector("#ui_result")
.appendHtml("<span class='result'>${data['value']}</span>"); .appendHtml("<span class='result'>${data['value']}</span>");

View File

@ -19,7 +19,7 @@ bus.source.addListener((message) => {
document.getElementById("echo_result").innerHTML = document.getElementById("echo_result").innerHTML =
`<span class='response'>${message.data.value}</span>`; `<span class='response'>${message.data.value}</span>`;
} else if (message.data.type === "test") { } 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") { } else if (message.data.type == "result") {
document.getElementById("ui_result").innerHTML = document.getElementById("ui_result").innerHTML =
`<span class='result'>${message.data.value}</span>`; `<span class='result'>${message.data.value}</span>`;

View File

@ -9,5 +9,5 @@ import "package:angular2/src/reflection/reflection.dart";
main(List<String> args, SendPort replyTo) { main(List<String> args, SendPort replyTo) {
reflector.reflectionCapabilities = new ReflectionCapabilities(); reflector.reflectionCapabilities = new ReflectionCapabilities();
bootstrapWebworker(replyTo, HelloCmp); bootstrapWebworker(replyTo, HelloCmp).catchError((error) => throw error);
} }

View File

@ -1,11 +0,0 @@
<!doctype html>
<html>
<title>Hello Angular 2.0</title>
<body>
<hello-app>
Loading...
</hello-app>
$SCRIPTS$
</body>
</html>

View File

@ -0,0 +1,26 @@
<!doctype html>
<html>
<title>Hello Angular 2.0</title>
<style>
.sample-area {
text-align: center;
margin: 5px;
height: 50px;
line-height: 50px;
border-radius: 5px;
border: 1px solid #d0d0d0;
}
.sample-area:focus {
border: 1px solid blue;
color: blue;
font-weight: bold;
}
</style>
<body>
<hello-app>
Loading...
</hello-app>
$SCRIPTS$
</body>
</html>

View File

@ -1,4 +1,5 @@
import {ElementRef, Component, Directive, View, Injectable, Renderer} from 'angular2/angular2'; 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. // A service available to the Injector, used by the HelloCmp component.
@Injectable() @Injectable()
@ -37,7 +38,8 @@ class RedDec {
// Expressions in the template (like {{greeting}}) are evaluated in the // Expressions in the template (like {{greeting}}) are evaluated in the
// context of the HelloCmp class below. // context of the HelloCmp class below.
template: `<div class="greeting">{{greeting}} <span red>world</span>!</div> template: `<div class="greeting">{{greeting}} <span red>world</span>!</div>
<button class="changeButton">change greeting</button>`, <button class="changeButton" (click)="changeGreeting()">change greeting</button>
<div (keydown)="onKeyDown($event)" class="sample-area" tabindex="0">{{lastKey}}</div><br>`,
// All directives used in the template need to be specified. This allows for // All directives used in the template need to be specified. This allows for
// modularity (RedDec can only be used in this template) // modularity (RedDec can only be used in this template)
// and better tooling (the template can be invalidated if the attribute is // and better tooling (the template can be invalidated if the attribute is
@ -46,8 +48,11 @@ class RedDec {
}) })
export class HelloCmp { export class HelloCmp {
greeting: string; greeting: string;
lastKey: string = '(none)';
constructor(service: GreetingService) { this.greeting = service.greeting; } constructor(service: GreetingService) { this.greeting = service.greeting; }
changeGreeting(): void { this.greeting = 'howdy'; } changeGreeting(): void { this.greeting = 'howdy'; }
onKeyDown(event): void { this.lastKey = StringWrapper.fromCharCode(event['keyCode']); }
} }

View File

@ -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) System.import("examples/src/web_workers/background_index")
window = { .then(
setTimeout: setTimeout, function(m) {
Map: Map, console.log("running main");
Set: Set, try {
Array: Array, m.main();
Reflect: Reflect, } catch (e) {
RegExp: RegExp, console.error(e);
Promise: Promise, }
Date: Date },
}; function(error) { console.error("error loading background", error); });
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); });