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) {
this._onWebWorker = onWebWorker;
if (!onWebWorker) {
this._lookupByIndex = new Map<number, RenderViewRef | RenderFragmentRef>();
this._lookupByView = new Map<RenderViewRef | RenderFragmentRef, number>();
}
this._lookupByIndex = new Map<number, RenderViewRef | RenderFragmentRef>();
this._lookupByView = new Map<RenderViewRef | RenderFragmentRef, number>();
}
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<RenderViewRef | RenderFragmentRef>;
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);
}
}

View File

@ -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<any, Map<int, any>>();
var viewTypeMap = new Map<int, any>();
viewTypeMap[0] = ViewType.HOST;
viewTypeMap[1] = ViewType.COMPONENT;
viewTypeMap[2] = ViewType.EMBEDDED;
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 {
@ -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<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> {
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']
});
}

View File

@ -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<Type | Binding | List<any>> {
} 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<Type | Binding | List<any>> {
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<Type | Binding | List<any>> {
DirectiveResolver,
Parser,
Lexer,
ExceptionHandler,
bind(ExceptionHandler).toFactory(() => new ExceptionHandler(DOM), []),
bind(XHR).toValue(new XHRImpl()),
ComponentUrlMapper,
UrlResolver,
@ -126,6 +125,7 @@ function _injectorBindings(): List<Type | Binding | List<any>> {
DynamicComponentLoader,
Testability,
AnchorBasedAppRootUrl,
bind(AppRootUrl).toAlias(AnchorBasedAppRootUrl),
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 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<string, any>) {
this._bus.sink.send({'type': type, 'value': data});
private _sendWorkerMessage(type: string, value: StringMap<string, any>, 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<string, any>) {
var data: ReceivedMessage = new ReceivedMessage(message['data']);
switch (data.type) {
@ -211,8 +221,7 @@ export class WebWorkerMain {
private _wrapWorkerPromise(id: string, promise: Promise<any>, 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<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 {
method: string;
args: List<any>;

View File

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

View File

@ -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<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']));
}
@ -43,17 +48,17 @@ export class MessageBroker {
if (returnType != null) {
var completer: PromiseCompleter<any> = 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<string, any>): 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<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 {
type: string;
value: MessageResult;
value: any;
id: string;
constructor(data: StringMap<string, any>) {
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<string, any>) {
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<string, any>, key: string) {
if (StringMapWrapper.contains(data, key)) {
return StringMapWrapper.get(data, key);
} else {
return null;
}
}
}

View File

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