diff --git a/modules/angular2/src/facade/async.dart b/modules/angular2/src/facade/async.dart index a15fc66edd..317002d070 100644 --- a/modules/angular2/src/facade/async.dart +++ b/modules/angular2/src/facade/async.dart @@ -22,7 +22,7 @@ class PromiseWrapper { static Future wrap(Function fn) { return new Future(fn); } - + // Note: We can't rename this method to `catch`, as this is not a valid // method name in Dart. static Future catchError(Future promise, Function onError) { diff --git a/modules/angular2/src/web-workers/shared/message_bus.ts b/modules/angular2/src/web-workers/shared/message_bus.ts new file mode 100644 index 0000000000..2648a803ff --- /dev/null +++ b/modules/angular2/src/web-workers/shared/message_bus.ts @@ -0,0 +1,18 @@ +// TODO(jteplitz602) to be idiomatic these should be releated to Observable's or Streams +/** + * Message Bus is a low level API used to communicate between the UI and the worker. + * It smooths out the differences between Javascript's postMessage and Dart's Isolate + * allowing you to work with one consistent API. + */ +export interface MessageBus { + sink: MessageBusSink; + source: MessageBusSource; +} + +export interface SourceListener { + (data: any): void; // TODO: Replace this any type with the type of a real messaging protocol +} + +export interface MessageBusSource { listen(fn: SourceListener): void; } + +export interface MessageBusSink { send(message: Object): void; } diff --git a/modules/angular2/src/web-workers/shared/serializer.ts b/modules/angular2/src/web-workers/shared/serializer.ts new file mode 100644 index 0000000000..9aca3d5708 --- /dev/null +++ b/modules/angular2/src/web-workers/shared/serializer.ts @@ -0,0 +1,271 @@ +import {Type, isArray, isPresent} from "angular2/src/facade/lang"; +import {List, ListWrapper, Map, StringMapWrapper, MapWrapper} from "angular2/src/facade/collection"; +import { + ProtoViewDto, + DirectiveMetadata, + ElementBinder, + DirectiveBinder, + ElementPropertyBinding, + EventBinding, + ViewDefinition +} from "angular2/src/render/api"; +import {AST, ASTWithSource} from "angular2/change_detection"; +import {Parser} from "angular2/src/change_detection/parser/parser"; + +export class Serializer { + static parser: Parser = null; + + static serialize(obj: any, type: Type): Object { + if (!isPresent(obj)) { + return null; + } + if (isArray(obj)) { + var serializedObj = []; + ListWrapper.forEach(obj, (val) => { serializedObj.push(Serializer.serialize(val, type)); }); + return serializedObj; + } + if (type == ViewDefinition) { + return ViewDefinitionSerializer.serialize(obj); + } else if (type == DirectiveBinder) { + return DirectiveBinderSerializer.serialize(obj); + } else if (type == ProtoViewDto) { + return ProtoViewDtoSerializer.serialize(obj); + } else if (type == ElementBinder) { + return ElementBinderSerializer.serialize(obj); + } else if (type == DirectiveMetadata) { + return DirectiveMetadataSerializer.serialize(obj); + } else if (type == ASTWithSource) { + return ASTWithSourceSerializer.serialize(obj); + } else { + throw "No serializer for " + type.toString(); + } + } + + // TODO: template this to return the type that is passed if possible + static deserialize(map, type: Type, data?): any { + if (!isPresent(map)) { + return null; + } + if (isArray(map)) { + var obj: List = new List(); + ListWrapper.forEach(map, (val) => { obj.push(Serializer.deserialize(val, type, data)); }); + return obj; + } + + if (type == ViewDefinition) { + return ViewDefinitionSerializer.deserialize(map); + } else if (type == DirectiveBinder) { + return DirectiveBinderSerializer.deserialize(map); + } else if (type == ProtoViewDto) { + return ProtoViewDtoSerializer.deserialize(map); + } else if (type == DirectiveMetadata) { + return DirectiveMetadataSerializer.deserialize(map); + } else if (type == ElementBinder) { + return ElementBinderSerializer.deserialize(map); + } else if (type == ASTWithSource) { + return ASTWithSourceSerializer.deserialize(map, data); + } else { + throw "No deserializer for " + type.toString(); + } + } + + static mapToObject(map, type?: Type): Object { + var object = {}; + var serialize = isPresent(type); + + MapWrapper.forEach(map, (value, key) => { + if (serialize) { + object[key] = Serializer.serialize(value, type); + } else { + object[key] = value; + } + }); + return object; + } + + /* + * Transforms a Javascript object into a Map + * If the values need to be deserialized pass in their type + * and they will be deserialized before being placed in the map + */ + static objectToMap(obj, type?: Type, data?): Map { + if (isPresent(type)) { + var map: Map = new Map(); + StringMapWrapper.forEach( + obj, (key, val) => { map.set(key, Serializer.deserialize(val, type, data)); }); + return map; + } else { + return MapWrapper.createFromStringMap(obj); + } + } +} + +class ASTWithSourceSerializer { + static serialize(tree: ASTWithSource): Object { + return { 'input': tree.source, 'location': tree.location } + } + + static deserialize(obj: any, data: string): AST { + // TODO: make ASTs serializable + var ast: AST; + switch (data) { + case "interpolation": + ast = Serializer.parser.parseInterpolation(obj.input, obj.location); + break; + case "binding": + ast = Serializer.parser.parseBinding(obj.input, obj.location); + break; + case "simpleBinding": + ast = Serializer.parser.parseSimpleBinding(obj.input, obj.location); + break; + /*case "templateBindings": + ast = Serializer.parser.parseTemplateBindings(obj.input, obj.location); + break;*/ + case "interpolation": + ast = Serializer.parser.parseInterpolation(obj.input, obj.location); + break; + default: + throw "No AST deserializer for " + data; + } + return ast; + } +} + +class ViewDefinitionSerializer { + static serialize(view: ViewDefinition): Object { + return { + 'componentId': view.componentId, + 'templateAbsUrl': view.templateAbsUrl, + 'template': view.template, + 'directives': Serializer.serialize(view.directives, DirectiveMetadata), + 'styleAbsUrls': view.styleAbsUrls, + 'styles': view.styles + }; + } + static deserialize(obj): ViewDefinition { + return new ViewDefinition({ + componentId: obj.componentId, + templateAbsUrl: obj.templateAbsUrl, template: obj.template, + directives: Serializer.deserialize(obj.directives, DirectiveMetadata), + styleAbsUrls: obj.styleAbsUrls, + styles: obj.styles + }); + } +} + +class DirectiveBinderSerializer { + static serialize(binder: DirectiveBinder): Object { + return { + 'directiveIndex': binder.directiveIndex, + 'propertyBindings': Serializer.mapToObject(binder.propertyBindings, ASTWithSource), + 'eventBindings': Serializer.serialize(binder.eventBindings, EventBinding), + 'hostPropertyBindings': + Serializer.serialize(binder.hostPropertyBindings, ElementPropertyBinding) + }; + } + + static deserialize(obj): DirectiveBinder { + return new DirectiveBinder({ + directiveIndex: obj.directiveIndex, + propertyBindings: Serializer.objectToMap(obj.propertyBindings, ASTWithSource, "binding"), + eventBindings: Serializer.deserialize(obj.eventBindings, EventBinding), + hostPropertyBindings: Serializer.deserialize(obj.hostPropertyBindings, ElementPropertyBinding) + }); + } +} + +class ElementBinderSerializer { + static serialize(binder: ElementBinder): Object { + return { + 'index': binder.index, 'parentIndex': binder.parentIndex, + 'distanceToParent': binder.distanceToParent, + 'directives': Serializer.serialize(binder.directives, DirectiveBinder), + 'nestedProtoView': Serializer.serialize(binder.nestedProtoView, ProtoViewDto), + 'propertyBindings': Serializer.serialize(binder.propertyBindings, ElementPropertyBinding), + 'variableBindings': Serializer.mapToObject(binder.variableBindings), + 'eventBindings': Serializer.serialize(binder.eventBindings, EventBinding), + 'textBindings': Serializer.serialize(binder.textBindings, ASTWithSource), + 'readAttributes': Serializer.mapToObject(binder.readAttributes) + } + } + + static deserialize(obj): ElementBinder { + return new ElementBinder({ + index: obj.index, + parentIndex: obj.parentIndex, + distanceToParent: obj.distanceToParent, + directives: Serializer.deserialize(obj.directives, DirectiveBinder), + nestedProtoView: Serializer.deserialize(obj.nestedProtoView, ProtoViewDto), + propertyBindings: Serializer.deserialize(obj.propertyBindings, ElementPropertyBinding), + variableBindings: Serializer.objectToMap(obj.variableBindings), + eventBindings: Serializer.deserialize(obj.eventBindings, EventBinding), + textBindings: Serializer.deserialize(obj.textBindings, ASTWithSource, "interpolation"), + readAttributes: Serializer.objectToMap(obj.readAttributes) + }); + } +} + +class ProtoViewDtoSerializer { + static serialize(view: ProtoViewDto): Object { + // TODO: fix render refs and write a serializer for them + return { + 'render': null, 'elementBinders': Serializer.serialize(view.elementBinders, ElementBinder), + 'variableBindings': Serializer.mapToObject(view.variableBindings), 'type': view.type + } + } + + static deserialize(obj): ProtoViewDto { + return new ProtoViewDto({ + render: null, // TODO: fix render refs and write a serializer for them + elementBinders: Serializer.deserialize(obj.elementBinders, ElementBinder), + variableBindings: Serializer.objectToMap(obj.variableBindings), + type: obj.type + }); + } +} + +class DirectiveMetadataSerializer { + static serialize(meta: DirectiveMetadata): Object { + var obj = { + 'id': meta.id, + 'selector': meta.selector, + 'compileChildren': meta.compileChildren, + 'hostProperties': Serializer.mapToObject(meta.hostProperties), + 'hostListeners': Serializer.mapToObject(meta.hostListeners), + 'hostActions': Serializer.mapToObject(meta.hostActions), + 'hostAttributes': Serializer.mapToObject(meta.hostAttributes), + 'properties': meta.properties, + 'readAttributes': meta.readAttributes, + 'type': meta.type, + 'exportAs': meta.exportAs, + 'callOnDestroy': meta.callOnDestroy, + 'callOnCheck': meta.callOnCheck, + 'callOnInit': meta.callOnInit, + 'callOnAllChangesDone': meta.callOnAllChangesDone, + 'changeDetection': meta.changeDetection, + 'events': meta.events + }; + return obj; + } + static deserialize(obj): DirectiveMetadata { + return new DirectiveMetadata({ + id: obj.id, + selector: obj.selector, + compileChildren: obj.compileChildren, + hostProperties: Serializer.objectToMap(obj.hostProperties), + hostListeners: Serializer.objectToMap(obj.hostListeners), + hostActions: Serializer.objectToMap(obj.hostActions), + hostAttributes: Serializer.objectToMap(obj.hostAttributes), + properties: obj.properties, + readAttributes: obj.readAttributes, + type: obj.type, + exportAs: obj.exportAs, + callOnDestroy: obj.callOnDestroy, + callOnCheck: obj.callOnCheck, + callOnInit: obj.callOnInit, + callOnAllChangesDone: obj.callOnAllChangesDone, + changeDetection: obj.changeDetection, + events: obj.events + }); + } +} diff --git a/modules/angular2/src/web-workers/shared/serializer.ts.good b/modules/angular2/src/web-workers/shared/serializer.ts.good new file mode 100644 index 0000000000..c04de2d8b4 --- /dev/null +++ b/modules/angular2/src/web-workers/shared/serializer.ts.good @@ -0,0 +1,275 @@ +import {Type, isArray, isPresent} from "angular2/src/facade/lang"; +import {List, ListWrapper, Map, StringMapWrapper, MapWrapper} from "angular2/src/facade/collection"; +import { + ProtoViewDto, + DirectiveMetadata, + ElementBinder, + DirectiveBinder, + ElementPropertyBinding, + EventBinding, + ViewDefinition +} from "angular2/src/render/api"; +import {AST, ASTWithSource} from "angular2/change_detection"; +import {Parser} from "angular2/src/change_detection/parser/parser"; + +export class Serializer { + static parser: Parser = null; + + static serialize(obj: any, type: Type): Object { + if (!isPresent(obj)) { + return null; + } + if (isArray(obj)) { + var serializedObj = []; + ListWrapper.forEach(obj, (val) => { serializedObj.push(Serializer.serialize(val, type)); }); + return serializedObj; + } + if (type == ViewDefinition) { + return ViewDefinitionSerializer.serialize(obj); + } else if (type == DirectiveBinder) { + return DirectiveBinderSerializer.serialize(obj); + } else if (type == ProtoViewDto) { + return ProtoViewDtoSerializer.serialize(obj); + } else if (type == ElementBinder) { + return ElementBinderSerializer.serialize(obj); + } else if (type == DirectiveMetadata) { + return DirectiveMetadataSerializer.serialize(obj); + } else if (type == ASTWithSource) { + return ASTWithSourceSerializer.serialize(obj); + } else { + throw "No serializer for " + type.toString(); + } + } + + // TODO: template this to return the type that is passed if possible + static deserialize(map, type: Type, data?): any { + if (!isPresent(map)) { + return null; + } + if (isArray(map)) { + var obj: List = new List(); + ListWrapper.forEach(map, (val) => { obj.push(Serializer.deserialize(val, type, data)); }); + return obj; + } + + if (type == ViewDefinition) { + return ViewDefinitionSerializer.deserialize(map); + } else if (type == DirectiveBinder) { + return DirectiveBinderSerializer.deserialize(map); + } else if (type == ProtoViewDto) { + return ProtoViewDtoSerializer.deserialize(map); + } else if (type == DirectiveMetadata) { + return DirectiveMetadataSerializer.deserialize(map); + } else if (type == ElementBinder) { + return ElementBinderSerializer.deserialize(map); + } else if (type == ASTWithSource) { + return ASTWithSourceSerializer.deserialize(map, data); + } else { + throw "No deserializer for " + type.toString(); + } + } + + static mapToObject(map, type?: Type): Object { + var object = {}; + var serialize = isPresent(type); + + MapWrapper.forEach(map, (value, key) => { + if (serialize) { + object[key] = Serializer.serialize(value, type); + } else { + object[key] = value; + } + }); + return object; + } + + /* + * Transforms a Javascript object into a Map + * If the values need to be deserialized pass in their type + * and they will be deserialized before being placed in the map + */ + static objectToMap(obj, type?: Type, data?): Map { + if (isPresent(type)) { + var map: Map = new Map(); + StringMapWrapper.forEach( + obj, (key, val) => { map.set(key, Serializer.deserialize(val, type, data)); }); + return map; + } else { + return MapWrapper.createFromStringMap(obj); + } + } +} + +class ASTWithSourceSerializer { + static serialize(tree: ASTWithSource): Object { + return { 'input': tree.source, 'location': tree.location } + } + + static deserialize(obj: any, data: string): AST { + // TODO: make ASTs serializable + var ast: AST; + switch (data) { + case "interpolation": + ast = Serializer.parser.parseInterpolation(obj.input, obj.location); + break; + case "binding": + ast = Serializer.parser.parseBinding(obj.input, obj.location); + break; + case "simpleBinding": + ast = Serializer.parser.parseSimpleBinding(obj.input, obj.location); + break; + /*case "templateBindings": + ast = Serializer.parser.parseTemplateBindings(obj.input, obj.location); + break;*/ + case "interpolation": + ast = Serializer.parser.parseInterpolation(obj.input, obj.location); + break; + default: + throw "No AST deserializer for " + data; + } + return ast; + } +} + +class ViewDefinitionSerializer { + static serialize(view: ViewDefinition): Object{ + return { + 'componentId': view.componentId, + 'templateAbsUrl': view.templateAbsUrl, + 'template': view.template, + 'directives': Serializer.serialize(view.directives, DirectiveMetadata), + 'styleAbsUrls': view.styleAbsUrls, + 'styles': view.styles + }; + } + static deserialize(obj): ViewDefinition { + return new ViewDefinition({ + 'componentId': obj.componentId, + 'templateAbsUrl': obj.templateAbsUrl, + 'template': obj.template, + 'directives': Serializer.deserialize(obj.directives, DirectiveMetadata), + 'styleAbsUrls': obj.styleAbsUrls, + 'styles': obj.styles + }); + } +} + +class DirectiveBinderSerializer { + static serialize(binder: DirectiveBinder): Object { + return { + 'directiveIndex': binder.directiveIndex, + 'propertyBindings': Serializer.mapToObject(binder.propertyBindings, ASTWithSource), + 'eventBindings': Serializer.serialize(binder.eventBindings, EventBinding), + 'hostPropertyBindings': + Serializer.serialize(binder.hostPropertyBindings, ElementPropertyBinding) + }; + } + + static deserialize(obj): DirectiveBinder { + return new DirectiveBinder({ + 'directiveIndex': obj.directiveIndex, + 'propertyBindings': Serializer.objectToMap(obj.propertyBindings, ASTWithSource, "binding"), + 'eventBindings': Serializer.deserialize(obj.eventBindings, EventBinding), + 'hostPropertyBindings': + Serializer.deserialize(obj.hostPropertyBindings, ElementPropertyBinding) + }); + } +} + +class ElementBinderSerializer { + static serialize(binder: ElementBinder): Object { + return { + 'index': binder.index, + 'parentIndex': binder.parentIndex, + 'distanceToParent': binder.distanceToParent, + 'directives': Serializer.serialize(binder.directives, DirectiveBinder), + 'nestedProtoView': Serializer.serialize(binder.nestedProtoView, ProtoViewDto), + 'propertyBindings': Serializer.serialize(binder.propertyBindings, ElementPropertyBinding), + 'variableBindings': Serializer.mapToObject(binder.variableBindings), + 'eventBindings': Serializer.serialize(binder.eventBindings, EventBinding), + 'textBindings': Serializer.serialize(binder.textBindings, ASTWithSource), + 'readAttributes': Serializer.mapToObject(binder.readAttributes) + } + } + + static deserialize(obj): ElementBinder { + return new ElementBinder({ + 'index': obj.index, + 'parentIndex': obj.parentIndex, + 'distanceToParent': obj.distanceToParent, + 'directives': Serializer.deserialize(obj.directives, DirectiveBinder), + 'nestedProtoView': Serializer.deserialize(obj.nestedProtoView, ProtoViewDto), + 'propertyBindings': Serializer.deserialize(obj.propertyBindings, ElementPropertyBinding), + 'variableBindings': Serializer.objectToMap(obj.variableBindings), + 'eventBindings': Serializer.deserialize(obj.eventBindings, EventBinding), + 'textBindings': Serializer.deserialize(obj.textBindings, ASTWithSource, "interpolation"), + 'readAttributes': Serializer.objectToMap(obj.readAttributes) + }); + } +} + +class ProtoViewDtoSerializer { + static serialize(view: ProtoViewDto): Object { + return { + 'render': null, // TODO: fix render refs and write a serializer for them + 'elementBinders': Serializer.serialize(view.elementBinders, ElementBinder), + 'variableBindings': Serializer.mapToObject(view.variableBindings), + 'type': view.type + } + } + + static deserialize(obj): ProtoViewDto { + return new ProtoViewDto({ + 'render': null, // TODO: fix render refs and write a serializer for them + 'elementBinders': Serializer.deserialize(obj.elementBinders, ElementBinder), + 'variableBindings': Serializer.objectToMap(obj.variableBindings), + 'type': obj.type + }); + } +} + +class DirectiveMetadataSerializer { + static serialize(meta: DirectiveMetadata): Object { + var obj = { + 'id': meta.id, + 'selector': meta.selector, + 'compileChildren': meta.compileChildren, + 'hostProperties': Serializer.mapToObject(meta.hostProperties), + 'hostListeners': Serializer.mapToObject(meta.hostListeners), + 'hostActions': Serializer.mapToObject(meta.hostActions), + 'hostAttributes': Serializer.mapToObject(meta.hostAttributes), + 'properties': meta.properties, + 'readAttributes': meta.readAttributes, + 'type': meta.type, + 'exportAs': meta.exportAs, + 'callOnDestroy': meta.callOnDestroy, + 'callOnCheck': meta.callOnCheck, + 'callOnInit': meta.callOnInit, + 'callOnAllChangesDone': meta.callOnAllChangesDone, + 'changeDetection': meta.changeDetection, + 'events': meta.events + }; + return obj; + } + static deserialize(obj): DirectiveMetadata { + return new DirectiveMetadata({ + 'id': obj.id, + 'selector': obj.selector, + 'compileChildren': obj.compileChildren, + 'hostProperties': Serializer.objectToMap(obj.hostProperties), + 'hostListeners': Serializer.objectToMap(obj.hostListeners), + 'hostActions': Serializer.objectToMap(obj.hostActions), + 'hostAttributes': Serializer.objectToMap(obj.hostAttributes), + 'properties': obj.properties, + 'readAttributes': obj.readAttributes, + 'type': obj.type, + 'exportAs': obj.exportAs, + 'callOnDestroy': obj.callOnDestroy, + 'callOnCheck': obj.callOnCheck, + 'callOnInit': obj.callOnInit, + 'callOnAllChangesDone': obj.callOnAllChangesDone, + 'changeDetection': obj.changeDetection, + 'events': obj.events + }); + } +} diff --git a/modules/angular2/src/web-workers/ui/application.dart b/modules/angular2/src/web-workers/ui/application.dart new file mode 100644 index 0000000000..03baeb8600 --- /dev/null +++ b/modules/angular2/src/web-workers/ui/application.dart @@ -0,0 +1,69 @@ +library angular2.src.web_workers.ui; + +import 'dart:isolate'; +import 'dart:async'; +import "package:angular2/src/web-workers/shared/message_bus.dart" + show MessageBus, MessageBusSink, MessageBusSource; + +/** + * Bootstrapping a WebWorker + * + * You instantiate a WebWorker application by calling bootstrap with the URI of your worker's index script + * Note: The WebWorker script must call bootstrapWebworker once it is set up to complete the bootstrapping process + */ +void bootstrap(String uri) { + throw "Not Implemented"; +} + +/** + * To be called from the main thread to spawn and communicate with the worker thread + */ +Future spawnWorker(Uri uri) { + var receivePort = new ReceivePort(); + var isolateEndSendPort = receivePort.sendPort; + return Isolate.spawnUri(uri, const [], isolateEndSendPort).then((_) { + var source = new UIMessageBusSource(receivePort); + return source.sink.then((sendPort) { + var sink = new UIMessageBusSink(sendPort); + return new UIMessageBus(sink, source); + }); + }); +} + +class UIMessageBus extends MessageBus { + final UIMessageBusSink sink; + final UIMessageBusSource source; + + UIMessageBus(UIMessageBusSink sink, UIMessageBusSource source) + : sink = sink, + source = source; +} + +class UIMessageBusSink extends MessageBusSink { + final SendPort _port; + + UIMessageBusSink(SendPort port) : _port = port; + + void send(message) { + _port.send(message); + } +} + +class UIMessageBusSource extends MessageBusSource { + final ReceivePort _port; + final Stream rawDataStream; + + UIMessageBusSource(ReceivePort port) + : _port = port, + rawDataStream = port.asBroadcastStream(); + + Future get sink => rawDataStream.firstWhere((message) { + return message is SendPort; + }); + + void listen(Function fn) { + rawDataStream.listen((message) { + fn({"data": message}); + }); + } +} diff --git a/modules/angular2/src/web-workers/ui/application.ts b/modules/angular2/src/web-workers/ui/application.ts new file mode 100644 index 0000000000..31b629db41 --- /dev/null +++ b/modules/angular2/src/web-workers/ui/application.ts @@ -0,0 +1,40 @@ +import { + MessageBus, + MessageBusSource, + MessageBusSink, + SourceListener +} from "angular2/src/web-workers/shared/message_bus"; +import {BaseException} from "angular2/src/facade/lang"; + +/** + * Bootstrapping a WebWorker + * + * You instantiate a WebWorker application by calling bootstrap with the URI of your worker's index + * script + * Note: The WebWorker script must call bootstrapWebworker once it is set up to complete the + * bootstrapping process + */ +export function bootstrap(uri: string): void { + throw new BaseException("Not Implemented"); +} + +export function spawnWorker(uri: string): MessageBus { + var worker: Worker = new Worker(uri); + return new UIMessageBus(new UIMessageBusSink(worker), new UIMessageBusSource(worker)); +} + +export class UIMessageBus implements MessageBus { + constructor(public sink: UIMessageBusSink, public source: UIMessageBusSource) {} +} + +export class UIMessageBusSink implements MessageBusSink { + constructor(private _worker: Worker) {} + + send(message: Object): void { this._worker.postMessage(message); } +} + +export class UIMessageBusSource implements MessageBusSource { + constructor(private _worker: Worker) {} + + listen(fn: SourceListener): void { this._worker.addEventListener("message", fn); } +} diff --git a/modules/angular2/src/web-workers/worker/application.dart b/modules/angular2/src/web-workers/worker/application.dart new file mode 100644 index 0000000000..5fdcd14a95 --- /dev/null +++ b/modules/angular2/src/web-workers/worker/application.dart @@ -0,0 +1,65 @@ +library angular2.src.web_workers.worker; + +import "package:angular2/src/web-workers/shared/message_bus.dart" + show MessageBus, MessageBusSource, MessageBusSink; +import "package:angular2/src/facade/async.dart" show Future; +import "package:angular2/src/core/application.dart" show ApplicationRef; +import "package:angular2/src/facade/lang.dart" show Type, BaseException; +import "dart:isolate"; +import "dart:async"; + +/** + * Bootstrapping a Webworker Application + * + * You instantiate the application side by calling bootstrapWebworker from your webworker index + * script. + * You must supply a SendPort for communicating with the UI side in order to instantiate + * the application. + * Other than the SendPort you can call bootstrapWebworker() exactly as you would call + * bootstrap() in a regular Angular application + * See the bootstrap() docs for more details. + */ +Future bootstrapWebworker( + SendPort replyTo, Type appComponentType, + [List componentInjectableBindings = null, + Function errorReporter = null]) { + throw new BaseException("Not implemented"); +} + +class WorkerMessageBus extends MessageBus { + final WorkerMessageBusSink sink; + final WorkerMessageBusSource source; + + WorkerMessageBus(this.sink, this.source); + + WorkerMessageBus.fromPorts(SendPort sPort, ReceivePort rPort) + : sink = new WorkerMessageBusSink(sPort, rPort), + source = new WorkerMessageBusSource(rPort); +} + +class WorkerMessageBusSink extends MessageBusSink { + final SendPort _port; + + WorkerMessageBusSink(SendPort sPort, ReceivePort rPort) : _port = sPort { + this.send(rPort.sendPort); + } + + void send(dynamic message) { + this._port.send(message); + } +} + +class WorkerMessageBusSource extends MessageBusSource { + final ReceivePort _port; + final Stream rawDataStream; + + WorkerMessageBusSource(ReceivePort rPort) + : _port = rPort, + rawDataStream = rPort.asBroadcastStream(); + + void listen(Function fn) { + rawDataStream.listen((message) { + fn({"data": message}); + }); + } +} diff --git a/modules/angular2/src/web-workers/worker/application.ts b/modules/angular2/src/web-workers/worker/application.ts new file mode 100644 index 0000000000..60b46d5e17 --- /dev/null +++ b/modules/angular2/src/web-workers/worker/application.ts @@ -0,0 +1,43 @@ +import { + MessageBus, + MessageBusSource, + MessageBusSink, + SourceListener +} from "angular2/src/web-workers/shared/message_bus"; +import {Type, BaseException} from "angular2/src/facade/lang"; +import {Binding} from "angular2/di"; + +import {ApplicationRef} from "angular2/src/core/application"; + +/** + * Bootstrapping a Webworker Application + * + * You instantiate the application side by calling bootstrapWebworker from your webworker index + * script. + * You can call bootstrapWebworker() exactly as you would call bootstrap() in a regular Angular + * application + * See the bootstrap() docs for more details. + */ +export function bootstrapWebworker( + appComponentType: Type, componentInjectableBindings: List> = null, + errorReporter: Function = null): Promise { + throw new BaseException("Not Implemented"); +} + +export class WorkerMessageBus implements MessageBus { + sink: WorkerMessageBusSink; + source: WorkerMessageBusSource; + + constructor(sink: WorkerMessageBusSink, source: WorkerMessageBusSource) { + this.sink = sink; + this.source = source; + } +} + +export class WorkerMessageBusSink implements MessageBusSink { + public send(message: Object) { postMessage(message, null); } +} + +export class WorkerMessageBusSource implements MessageBusSource { + public listen(fn: SourceListener) { addEventListener("message", fn); } +} diff --git a/modules/angular2/src/web-workers/worker/broker.ts b/modules/angular2/src/web-workers/worker/broker.ts new file mode 100644 index 0000000000..dc1f37375e --- /dev/null +++ b/modules/angular2/src/web-workers/worker/broker.ts @@ -0,0 +1,89 @@ +/// +import {MessageBus} from "angular2/src/web-workers/shared/message_bus"; +import {print, isPresent, DateWrapper, stringify} from "../../facade/lang"; +import {Promise, PromiseCompleter, PromiseWrapper} from "angular2/src/facade/async"; +import {ListWrapper, StringMapWrapper, MapWrapper} from "../../facade/collection"; +import {Serializer} from "angular2/src/web-workers/shared/serializer"; + +export class MessageBroker { + private _pending: Map = new Map(); + + constructor(private _messageBus: MessageBus) { + this._messageBus.source.listen((data) => this._handleMessage(data['data'])); + } + + private _generateMessageId(name: string): string { + var time: string = stringify(DateWrapper.toMillis(DateWrapper.now())); + var iteration: number = 0; + var id: string = name + time + stringify(iteration); + while (isPresent(this._pending[id])) { + id = `${name}${time}${iteration}`; + iteration++; + } + return id; + } + + runOnUiThread(args: UiArguments): Promise { + var completer = PromiseWrapper.completer(); + var id: string = this._generateMessageId(args.type + args.method); + this._pending.set(id, completer.resolve); + PromiseWrapper.catchError(completer.promise, (err, stack?) => { + print(err); + completer.reject(err, stack); + }); + + var fnArgs = []; + if (isPresent(args.args)) { + ListWrapper.forEach(args.args, (argument) => { + fnArgs.push(Serializer.serialize(argument.value, argument.type)); + }); + } + + // TODO(jteplitz602): Create a class for these messages so we don't keep using StringMap + var message = {'type': args.type, 'method': args.method, 'args': fnArgs, 'id': id}; + this._messageBus.sink.send(message); + return completer.promise; + } + + 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); + } + } +} + +class MessageData { + type: string; + value: MessageResult; + + 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; + } + } +} + +class MessageResult { + id: string; + value: any; + + constructor(result: StringMap) { + this.id = StringMapWrapper.get(result, "id"); + this.value = StringMapWrapper.get(result, "value"); + } +} + +export class FnArg { + constructor(public value, public type) {} +} + +export class UiArguments { + constructor(public type: string, public method: string, public args?: List) {} +} diff --git a/modules/examples/e2e_test/message_broker/message_broker_spec.dart b/modules/examples/e2e_test/message_broker/message_broker_spec.dart new file mode 100644 index 0000000000..5ccc3fa49e --- /dev/null +++ b/modules/examples/e2e_test/message_broker/message_broker_spec.dart @@ -0,0 +1,5 @@ +library examples.e2e_test.message_bus; + +main() { + +} diff --git a/modules/examples/e2e_test/message_broker/message_broker_spec.ts b/modules/examples/e2e_test/message_broker/message_broker_spec.ts new file mode 100644 index 0000000000..8506c303c2 --- /dev/null +++ b/modules/examples/e2e_test/message_broker/message_broker_spec.ts @@ -0,0 +1,44 @@ +import {verifyNoBrowserErrors} from 'angular2/src/test_lib/e2e_util'; +import {PromiseWrapper} from "angular2/src/facade/async"; + +var URL = 'examples/src/message_broker/index.html'; + +describe('message bus', function() { + + afterEach(verifyNoBrowserErrors); + + it('should receive a response from the worker', function() { + browser.get(URL); + + var VALUE = "hi there"; + var input = element.all(by.css("#echo_input")).first(); + input.sendKeys(VALUE); + clickComponentButton("body", "#send_echo"); + browser.wait(protractor.until.elementLocated(protractor.By.css("#echo_result .response")), + 5000); + expect(getComponentText("#echo_result", ".response")).toEqual(VALUE); + }); +}); + +describe('message broker', function() { + afterEach(verifyNoBrowserErrors); + + + it('should be able to run tasks on the UI thread after init', () => { + var VALUE = '5'; + + browser.get(URL); + browser.wait(protractor.until.elementLocated(protractor.By.css("#ui_result .result")), 5000); + expect(getComponentText("#ui_result", ".result")).toEqual(VALUE); + }); +}); + +function getComponentText(selector, innerSelector) { + return browser.executeScript('return document.querySelector("' + selector + '").querySelector("' + + innerSelector + '").textContent'); +} + +function clickComponentButton(selector, innerSelector) { + return browser.executeScript('return document.querySelector("' + selector + '").querySelector("' + + innerSelector + '").click()'); +} diff --git a/modules/examples/src/message_broker/.index.dart.swp b/modules/examples/src/message_broker/.index.dart.swp new file mode 100644 index 0000000000..040e4271a9 Binary files /dev/null and b/modules/examples/src/message_broker/.index.dart.swp differ diff --git a/modules/examples/src/message_broker/background_index.dart b/modules/examples/src/message_broker/background_index.dart new file mode 100644 index 0000000000..ad165d5f50 --- /dev/null +++ b/modules/examples/src/message_broker/background_index.dart @@ -0,0 +1,25 @@ +library angular2.examples.message_broker.background_index; + +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 "dart:isolate"; + +main(List args, SendPort replyTo) { + ReceivePort rPort = new ReceivePort(); + WorkerMessageBus bus = new WorkerMessageBus.fromPorts(replyTo, rPort); + bus.source.listen((message) { + if (identical(message['data']['type'], "echo")) { + bus.sink + .send({"type": "echo_response", "value": message['data']['value']}); + } + }); + + MessageBroker broker = new MessageBroker(bus); + var args = new UiArguments("test", "tester"); + broker.runOnUiThread(args).then((data) { + bus.sink.send({"type": "result", "value": data.value}); + }); +} diff --git a/modules/examples/src/message_broker/background_index.ts b/modules/examples/src/message_broker/background_index.ts new file mode 100644 index 0000000000..4b4e6e0f13 --- /dev/null +++ b/modules/examples/src/message_broker/background_index.ts @@ -0,0 +1,20 @@ +import { + WorkerMessageBus, + WorkerMessageBusSource, + WorkerMessageBusSink +} from "angular2/src/web-workers/worker/application"; +import {MessageBroker, UiArguments} from "angular2/src/web-workers/worker/broker"; + +export function main() { + var bus = new WorkerMessageBus(new WorkerMessageBusSink(), new WorkerMessageBusSource()); + bus.source.listen((message) => { + if (message.data.type === "echo") { + bus.sink.send({type: "echo_response", 'value': message.data.value}); + } + }); + + var broker = new MessageBroker(bus); + var args = new UiArguments("test", "tester"); + broker.runOnUiThread(args) + .then((data) => { bus.sink.send({type: "result", value: data.value}); }); +} diff --git a/modules/examples/src/message_broker/index.dart b/modules/examples/src/message_broker/index.dart new file mode 100644 index 0000000000..12d7a661b0 --- /dev/null +++ b/modules/examples/src/message_broker/index.dart @@ -0,0 +1,31 @@ +library angular2.examples.message_broker.index; + +import "package:angular2/src/web-workers/ui/application.dart" + show spawnWorker, UIMessageBus, UIMessageBusSink, UIMessageBusSource; + +import "dart:html"; + +main() { + var VALUE = 5; + spawnWorker(Uri.parse("background_index.dart")).then((bus) { + querySelector("#send_echo").addEventListener("click", (e) { + var val = (querySelector("#echo_input") as InputElement).value; + bus.sink.send({'type': 'echo', 'value': val}); + }); + bus.source.listen((message) { + var data = message['data']; + if (identical(data['type'], "echo_response")) { + querySelector("#echo_result") + .appendHtml("${data['value']}"); + } else if (identical(data['type'], "test")) { + bus.sink.send( + {'type': "result", 'value': {'id': data['id'], 'value': VALUE}}); + } else if (identical(data['type'], "result")) { + querySelector("#ui_result") + .appendHtml("${data['value']}"); + } else if (identical(data['type'], "ready")) { + bus.sink.send({'type': "init"}); + } + }); + }); +} diff --git a/modules/examples/src/message_broker/index.html b/modules/examples/src/message_broker/index.html new file mode 100644 index 0000000000..aea5bc922f --- /dev/null +++ b/modules/examples/src/message_broker/index.html @@ -0,0 +1,11 @@ + + + Message Broker Example + + + +

+

+ $SCRIPTS$ + + diff --git a/modules/examples/src/message_broker/index.ts b/modules/examples/src/message_broker/index.ts new file mode 100644 index 0000000000..ddd336056e --- /dev/null +++ b/modules/examples/src/message_broker/index.ts @@ -0,0 +1,28 @@ +import { + UIMessageBus, + UIMessageBusSink, + UIMessageBusSource +} from "angular2/src/web-workers/ui/application"; + +var worker = new Worker("loader.js"); +var bus = new UIMessageBus(new UIMessageBusSink(worker), new UIMessageBusSource(worker)); +var VALUE = 5; + +document.getElementById("send_echo") + .addEventListener("click", (e) => { + var val = (document.getElementById("echo_input")).value; + bus.sink.send({type: "echo", value: val}); + }); +bus.source.listen((message) => { + if (message.data.type === "echo_response") { + 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}}); + } else if (message.data.type == "result") { + document.getElementById("ui_result").innerHTML = + `${message.data.value}`; + } else if (message.data.type == "ready") { + bus.sink.send({type: "init"}); + } +}); diff --git a/modules/examples/src/message_broker/loader.js b/modules/examples/src/message_broker/loader.js new file mode 100644 index 0000000000..aa449f2b4e --- /dev/null +++ b/modules/examples/src/message_broker/loader.js @@ -0,0 +1,29 @@ +$SCRIPTS$ +//importScripts("math_worker.js").execute(); +//System.import("examples/src/web_workers/math_worker").then(function(m){console.log("got", m)}); +//importScripts("rx.js"); + +// TODO: do this correctly with lang facade +window = { + setTimeout: setTimeout, + Map: Map, + Set: Set, + Array: Array, + Reflect: Reflect, + RegExp: RegExp, + Promise: Promise, + Date: Date +} +assert = function(){} + + +System.import("examples/src/message_broker/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); +}); diff --git a/tools/broccoli/js-replace/.SCRIPTS.js.swp b/tools/broccoli/js-replace/.SCRIPTS.js.swp new file mode 100644 index 0000000000..e31b22634a Binary files /dev/null and b/tools/broccoli/js-replace/.SCRIPTS.js.swp differ diff --git a/tools/broccoli/js-replace/SCRIPTS.js b/tools/broccoli/js-replace/SCRIPTS.js new file mode 100644 index 0000000000..71fe642e4d --- /dev/null +++ b/tools/broccoli/js-replace/SCRIPTS.js @@ -0,0 +1,7 @@ +importScripts("traceur-runtime.js", + "es6-module-loader-sans-promises.src.js", + "system.src.js", + "extension-register.js", + "extension-cjs.js", + "Reflect.js", + "runtime_paths.js"); diff --git a/tools/broccoli/js-replace/index.ts b/tools/broccoli/js-replace/index.ts new file mode 100644 index 0000000000..0b5495eefa --- /dev/null +++ b/tools/broccoli/js-replace/index.ts @@ -0,0 +1,11 @@ +var fs = require('fs'); +var path = require('path'); + +module.exports = readJs; +function readJs(file) { + var content = + fs.readFileSync(path.join('tools/broccoli/js-replace', file + '.js'), {encoding: 'utf-8'}); + // TODO(broccoli): we don't really need this, it's here to make the output match the + // tools/build/html + return content.substring(0, content.lastIndexOf("\n")); +} diff --git a/tools/broccoli/trees/browser_tree.ts b/tools/broccoli/trees/browser_tree.ts index f665fd0deb..298d05f707 100644 --- a/tools/broccoli/trees/browser_tree.ts +++ b/tools/broccoli/trees/browser_tree.ts @@ -2,6 +2,7 @@ var Funnel = require('broccoli-funnel'); var htmlReplace = require('../html-replace'); +var jsReplace = require("../js-replace"); var path = require('path'); var stew = require('broccoli-stew'); @@ -57,7 +58,8 @@ const kServedPaths = [ 'examples/src/material/input', 'examples/src/material/progress-linear', 'examples/src/material/radio', - 'examples/src/material/switcher' + 'examples/src/material/switcher', + 'examples/src/message_broker' ]; @@ -66,27 +68,22 @@ module.exports = function makeBrowserTree(options, destinationPath) { 'modules', {include: ['**/**'], exclude: ['**/*.cjs', 'benchmarks/e2e_test/**'], destDir: '/'}); - // Use Traceur to transpile *.js sources to ES6 - var traceurTree = transpileWithTraceur(modulesTree, { - destExtension: '.js', - destSourceMapExtension: '.map', - traceurOptions: { - sourceMaps: true, - annotations: true, // parse annotations - types: true, // parse types - script: false, // parse as a module - memberVariables: true, // parse class fields - modules: 'instantiate', - // typeAssertionModule: 'rtts_assert/rtts_assert', - // typeAssertions: options.typeAssertions, - outputLanguage: 'es6' + var scriptPathPatternReplacement = { + match: '@@FILENAME_NO_EXT', + replacement: function(replacement, relativePath) { + return relativePath.replace(/\.\w+$/, '').replace(/\\/g, '/'); } + }; + + modulesTree = replace(modulesTree, { + files: ["examples*/**/*.js"], + patterns: [{match: /\$SCRIPTS\$/, replacement: jsReplace('SCRIPTS')}] }); // Use TypeScript to transpile the *.ts files to ES6 // We don't care about errors: we let the TypeScript compilation to ES5 // in node_tree.ts do the type-checking. - var typescriptTree = compileWithTypescript(modulesTree, { + var es6Tree = compileWithTypescript(modulesTree, { allowNonTsExtensions: false, declaration: true, emitDecoratorMetadata: true, @@ -98,8 +95,6 @@ module.exports = function makeBrowserTree(options, destinationPath) { target: 'ES6' }); - var es6Tree = mergeTrees([traceurTree, typescriptTree]); - // Call Traceur again to lower the ES6 build tree to ES5 var es5Tree = transpileWithTraceur(es6Tree, { destExtension: '.js', @@ -113,7 +108,6 @@ module.exports = function makeBrowserTree(options, destinationPath) { es6Tree = mergeTrees([es6Tree, extras]); }); - var vendorScriptsTree = flatten(new Funnel('.', { files: [ 'node_modules/zone.js/dist/zone-microtask.js', @@ -148,22 +142,16 @@ module.exports = function makeBrowserTree(options, destinationPath) { return funnels; } - var scriptPathPatternReplacement = { - match: '@@FILENAME_NO_EXT', - replacement: function(replacement, relativePath) { - return relativePath.replace(/\.\w+$/, '').replace(/\\/g, '/'); - } - }; - var htmlTree = new Funnel(modulesTree, {include: ['*/src/**/*.html'], destDir: '/'}); htmlTree = replace(htmlTree, { - files: ['examples*/**'], + files: ['examples*/**/*.html'], patterns: [ {match: /\$SCRIPTS\$/, replacement: htmlReplace('SCRIPTS')}, scriptPathPatternReplacement ] }); + htmlTree = replace(htmlTree, { files: ['benchmarks/**'], patterns: [