feat(WebWorker): Add Router Support for WebWorker Apps

Closes #3563.
This commit is contained in:
Jason Teplitz 2016-01-21 09:58:28 -08:00 committed by Rado Kirov
parent 800c8f196f
commit 8bea667a0b
43 changed files with 839 additions and 153 deletions

View File

@ -12,3 +12,5 @@ export 'package:angular2/src/web_workers/shared/service_message_broker.dart'
show ReceivedMessage, ServiceMessageBroker, ServiceMessageBrokerFactory;
export 'package:angular2/src/web_workers/shared/serializer.dart' show PRIMITIVE;
export 'package:angular2/src/web_workers/shared/message_bus.dart';
export 'package:angular2/src/web_workers/worker/router_providers.dart'
show WORKER_APP_ROUTER;

View File

@ -8,12 +8,13 @@ export {
ClientMessageBrokerFactory,
FnArg,
UiArguments
} from '../src/web_workers/shared/client_message_broker';
} from 'angular2/src/web_workers/shared/client_message_broker';
export {
ReceivedMessage,
ServiceMessageBroker,
ServiceMessageBrokerFactory
} from '../src/web_workers/shared/service_message_broker';
export {PRIMITIVE} from '../src/web_workers/shared/serializer';
export * from '../src/web_workers/shared/message_bus';
} from 'angular2/src/web_workers/shared/service_message_broker';
export {PRIMITIVE} from 'angular2/src/web_workers/shared/serializer';
export * from 'angular2/src/web_workers/shared/message_bus';
export {AngularEntrypoint} from 'angular2/src/core/angular_entrypoint';
export {WORKER_APP_ROUTER} from 'angular2/src/web_workers/worker/router_providers';

View File

@ -18,7 +18,9 @@ export '../src/web_workers/shared/service_message_broker.dart'
export '../src/web_workers/shared/serializer.dart' show PRIMITIVE;
export '../src/web_workers/shared/message_bus.dart';
export '../src/web_workers/ui/router_providers.dart' show WORKER_RENDER_ROUTER;
import 'package:angular2/src/platform/worker_render_common.dart';
const WORKER_RENDER_APP = WORKER_RENDER_APPLICATION_COMMON;

View File

@ -24,3 +24,4 @@ import {WORKER_RENDER_APPLICATION} from 'angular2/src/platform/worker_render';
* @deprecated Use WORKER_RENDER_APPLICATION
*/
export const WORKER_RENDER_APP = WORKER_RENDER_APPLICATION;
export {WORKER_RENDER_ROUTER} from 'angular2/src/web_workers/ui/router_providers';

View File

@ -20,18 +20,12 @@ export {OnActivate, OnDeactivate, OnReuse, CanDeactivate, CanReuse} from './src/
export {CanActivate} from './src/router/lifecycle_annotations';
export {Instruction, ComponentInstruction} from './src/router/instruction';
export {OpaqueToken} from 'angular2/core';
export {ROUTER_PROVIDERS_COMMON} from 'angular2/src/router/router_providers_common';
export {ROUTER_PROVIDERS, ROUTER_BINDINGS} from 'angular2/src/router/router_providers';
import {PlatformLocation} from './src/router/platform_location';
import {LocationStrategy} from './src/router/location_strategy';
import {PathLocationStrategy} from './src/router/path_location_strategy';
import {Router, RootRouter} from './src/router/router';
import {RouterOutlet} from './src/router/router_outlet';
import {RouterLink} from './src/router/router_link';
import {RouteRegistry, ROUTER_PRIMARY_COMPONENT} from './src/router/route_registry';
import {Location} from './src/router/location';
import {ApplicationRef, provide, OpaqueToken, Provider} from 'angular2/core';
import {CONST_EXPR} from './src/facade/lang';
import {BaseException} from 'angular2/src/facade/exceptions';
/**
* A list of directives. To use the router directives like {@link RouterOutlet} and
@ -56,63 +50,3 @@ import {BaseException} from 'angular2/src/facade/exceptions';
* ```
*/
export const ROUTER_DIRECTIVES: any[] = CONST_EXPR([RouterOutlet, RouterLink]);
/**
* A list of {@link Provider}s. To use the router, you must add this to your application.
*
* ### Example ([live demo](http://plnkr.co/edit/iRUP8B5OUbxCWQ3AcIDm))
*
* ```
* import {Component} from 'angular2/core';
* import {
* ROUTER_DIRECTIVES,
* ROUTER_PROVIDERS,
* RouteConfig
* } from 'angular2/router';
*
* @Component({directives: [ROUTER_DIRECTIVES]})
* @RouteConfig([
* {...},
* ])
* class AppCmp {
* // ...
* }
*
* bootstrap(AppCmp, [ROUTER_PROVIDERS]);
* ```
*/
export const ROUTER_PROVIDERS: any[] = CONST_EXPR([
RouteRegistry,
CONST_EXPR(new Provider(LocationStrategy, {useClass: PathLocationStrategy})),
PlatformLocation,
Location,
CONST_EXPR(new Provider(
Router,
{
useFactory: routerFactory,
deps: CONST_EXPR([RouteRegistry, Location, ROUTER_PRIMARY_COMPONENT, ApplicationRef])
})),
CONST_EXPR(new Provider(
ROUTER_PRIMARY_COMPONENT,
{useFactory: routerPrimaryComponentFactory, deps: CONST_EXPR([ApplicationRef])}))
]);
/**
* Use {@link ROUTER_PROVIDERS} instead.
*
* @deprecated
*/
export const ROUTER_BINDINGS = ROUTER_PROVIDERS;
function routerFactory(registry, location, primaryComponent, appRef) {
var rootRouter = new RootRouter(registry, location, primaryComponent);
appRef.registerDisposeListener(() => rootRouter.dispose());
return rootRouter;
}
function routerPrimaryComponentFactory(app) {
if (app.componentTypes.length == 0) {
throw new BaseException("Bootstrap at least one component before injecting Router.");
}
return app.componentTypes[0];
}

View File

@ -36,6 +36,7 @@ import {BrowserDomAdapter} from './browser/browser_adapter';
import {wtfInit} from 'angular2/src/core/profile/wtf_init';
import {MessageBasedRenderer} from 'angular2/src/web_workers/ui/renderer';
import {MessageBasedXHRImpl} from 'angular2/src/web_workers/ui/xhr_impl';
import {BrowserPlatformLocation} from 'angular2/src/router/browser_platform_location';
import {
ServiceMessageBrokerFactory,
ServiceMessageBrokerFactory_
@ -59,6 +60,13 @@ export const WORKER_RENDER_PLATFORM: Array<any /*Type | Provider | any[]*/> = CO
new Provider(PLATFORM_INITIALIZER, {useValue: initWebWorkerRenderPlatform, multi: true})
]);
/**
* A list of {@link Provider}s. To use the router in a Worker enabled application you must
* include these providers when setting up the render thread.
*/
export const WORKER_RENDER_ROUTER: Array<any /*Type | Provider | any[]*/> =
CONST_EXPR([BrowserPlatformLocation]);
export const WORKER_RENDER_APPLICATION_COMMON: Array<any /*Type | Provider | any[]*/> = CONST_EXPR([
APPLICATION_COMMON_PROVIDERS,
WORKER_RENDER_MESSAGING_PROVIDERS,

View File

@ -0,0 +1,58 @@
import {Injectable} from 'angular2/core';
import {History, Location} from 'angular2/src/facade/browser';
import {UrlChangeListener} from './platform_location';
import {PlatformLocation} from './platform_location';
import {DOM} from 'angular2/src/platform/dom/dom_adapter';
/**
* `PlatformLocation` encapsulates all of the direct calls to platform APIs.
* This class should not be used directly by an application developer. Instead, use
* {@link Location}.
*/
@Injectable()
export class BrowserPlatformLocation extends PlatformLocation {
private _location: Location;
private _history: History;
constructor() {
super();
this._init();
}
// This is moved to its own method so that `MockPlatformLocationStrategy` can overwrite it
/** @internal */
_init() {
this._location = DOM.getLocation();
this._history = DOM.getHistory();
}
/** @internal */
get location(): Location { return this._location; }
getBaseHrefFromDOM(): string { return DOM.getBaseHref(); }
onPopState(fn: UrlChangeListener): void {
DOM.getGlobalEventTarget('window').addEventListener('popstate', fn, false);
}
onHashChange(fn: UrlChangeListener): void {
DOM.getGlobalEventTarget('window').addEventListener('hashchange', fn, false);
}
get pathname(): string { return this._location.pathname; }
get search(): string { return this._location.search; }
get hash(): string { return this._location.hash; }
set pathname(newPath: string) { this._location.pathname = newPath; }
pushState(state: any, title: string, url: string): void {
this._history.pushState(state, title, url);
}
replaceState(state: any, title: string, url: string): void {
this._history.replaceState(state, title, url);
}
forward(): void { this._history.forward(); }
back(): void { this._history.back(); }
}

View File

@ -5,7 +5,7 @@ import {
APP_BASE_HREF,
normalizeQueryParams
} from './location_strategy';
import {EventListener} from 'angular2/src/facade/browser';
import {UrlChangeListener} from './platform_location';
import {isPresent} from 'angular2/src/facade/lang';
import {PlatformLocation} from './platform_location';
@ -58,7 +58,7 @@ export class HashLocationStrategy extends LocationStrategy {
}
}
onPopState(fn: EventListener): void {
onPopState(fn: UrlChangeListener): void {
this._platformLocation.onPopState(fn);
this._platformLocation.onHashChange(fn);
}

View File

@ -1,5 +1,6 @@
import {CONST_EXPR} from 'angular2/src/facade/lang';
import {OpaqueToken} from 'angular2/core';
import {UrlChangeListener} from './platform_location';
/**
* `LocationStrategy` is responsible for representing and reading route state
@ -24,7 +25,7 @@ export abstract class LocationStrategy {
abstract replaceState(state: any, title: string, url: string, queryParams: string): void;
abstract forward(): void;
abstract back(): void;
abstract onPopState(fn: (_: any) => any): void;
abstract onPopState(fn: UrlChangeListener): void;
abstract getBaseHref(): string;
}

View File

@ -1,5 +1,4 @@
import {Injectable, Inject, Optional} from 'angular2/core';
import {EventListener, History, Location} from 'angular2/src/facade/browser';
import {isBlank} from 'angular2/src/facade/lang';
import {BaseException} from 'angular2/src/facade/exceptions';
import {
@ -8,7 +7,7 @@ import {
normalizeQueryParams,
joinWithSlash
} from './location_strategy';
import {PlatformLocation} from './platform_location';
import {PlatformLocation, UrlChangeListener} from './platform_location';
/**
* `PathLocationStrategy` is a {@link LocationStrategy} used to configure the
@ -75,7 +74,7 @@ export class PathLocationStrategy extends LocationStrategy {
this._baseHref = href;
}
onPopState(fn: EventListener): void {
onPopState(fn: UrlChangeListener): void {
this._platformLocation.onPopState(fn);
this._platformLocation.onHashChange(fn);
}

View File

@ -1,50 +1,48 @@
import {DOM} from 'angular2/src/platform/dom/dom_adapter';
import {Injectable} from 'angular2/core';
import {EventListener, History, Location} from 'angular2/src/facade/browser';
/**
* `PlatformLocation` encapsulates all of the direct calls to platform APIs.
* This class should not be used directly by an application developer. Instead, use
* {@link Location}.
*
* `PlatformLocation` encapsulates all calls to DOM apis, which allows the Router to be platform
* agnostic.
* This means that we can have different implementation of `PlatformLocation` for the different
* platforms
* that angular supports. For example, the default `PlatformLocation` is {@link
* BrowserPlatformLocation},
* however when you run your app in a WebWorker you use {@link WebWorkerPlatformLocation}.
*
* The `PlatformLocation` class is used directly by all implementations of {@link LocationStrategy}
* when
* they need to interact with the DOM apis like pushState, popState, etc...
*
* {@link LocationStrategy} in turn is used by the {@link Location} service which is used directly
* by
* the {@link Router} in order to navigate between routes. Since all interactions between {@link
* Router} /
* {@link Location} / {@link LocationStrategy} and DOM apis flow through the `PlatformLocation`
* class
* they are all platform independent.
*/
@Injectable()
export class PlatformLocation {
private _location: Location;
private _history: History;
export abstract class PlatformLocation {
abstract getBaseHrefFromDOM(): string;
abstract onPopState(fn: UrlChangeListener): void;
abstract onHashChange(fn: UrlChangeListener): void;
constructor() { this._init(); }
pathname: string;
search: string;
hash: string;
// This is moved to its own method so that `MockPlatformLocationStrategy` can overwrite it
/** @internal */
_init() {
this._location = DOM.getLocation();
this._history = DOM.getHistory();
}
abstract replaceState(state: any, title: string, url: string): void;
getBaseHrefFromDOM(): string { return DOM.getBaseHref(); }
abstract pushState(state: any, title: string, url: string): void;
onPopState(fn: EventListener): void {
DOM.getGlobalEventTarget('window').addEventListener('popstate', fn, false);
}
abstract forward(): void;
onHashChange(fn: EventListener): void {
DOM.getGlobalEventTarget('window').addEventListener('hashchange', fn, false);
}
get pathname(): string { return this._location.pathname; }
get search(): string { return this._location.search; }
get hash(): string { return this._location.hash; }
set pathname(newPath: string) { this._location.pathname = newPath; }
pushState(state: any, title: string, url: string): void {
this._history.pushState(state, title, url);
}
replaceState(state: any, title: string, url: string): void {
this._history.replaceState(state, title, url);
}
forward(): void { this._history.forward(); }
back(): void { this._history.back(); }
abstract back(): void;
}
/**
* A serializable version of the event from onPopState or onHashChange
*/
export interface UrlChangeEvent { type: string; }
export interface UrlChangeListener { (e: UrlChangeEvent): any; }

View File

@ -0,0 +1,42 @@
// import {ROUTER_PROVIDERS_COMMON} from './router_providers_common';
import {ROUTER_PROVIDERS_COMMON} from 'angular2/router';
import {Provider} from 'angular2/core';
import {CONST_EXPR} from 'angular2/src/facade/lang';
import {BrowserPlatformLocation} from './browser_platform_location';
import {PlatformLocation} from './platform_location';
/**
* A list of {@link Provider}s. To use the router, you must add this to your application.
*
* ### Example ([live demo](http://plnkr.co/edit/iRUP8B5OUbxCWQ3AcIDm))
*
* ```
* import {Component} from 'angular2/core';
* import {
* ROUTER_DIRECTIVES,
* ROUTER_PROVIDERS,
* RouteConfig
* } from 'angular2/router';
*
* @Component({directives: [ROUTER_DIRECTIVES]})
* @RouteConfig([
* {...},
* ])
* class AppCmp {
* // ...
* }
*
* bootstrap(AppCmp, [ROUTER_PROVIDERS]);
* ```
*/
export const ROUTER_PROVIDERS: any[] = CONST_EXPR([
ROUTER_PROVIDERS_COMMON,
CONST_EXPR(new Provider(PlatformLocation, {useClass: BrowserPlatformLocation})),
]);
/**
* Use {@link ROUTER_PROVIDERS} instead.
*
* @deprecated
*/
export const ROUTER_BINDINGS = ROUTER_PROVIDERS;

View File

@ -0,0 +1,40 @@
import {LocationStrategy} from 'angular2/src/router/location_strategy';
import {PathLocationStrategy} from 'angular2/src/router/path_location_strategy';
import {Router, RootRouter} from 'angular2/src/router/router';
import {RouteRegistry, ROUTER_PRIMARY_COMPONENT} from 'angular2/src/router/route_registry';
import {Location} from 'angular2/src/router/location';
import {CONST_EXPR, Type} from 'angular2/src/facade/lang';
import {ApplicationRef, OpaqueToken, Provider} from 'angular2/core';
import {BaseException} from 'angular2/src/facade/exceptions';
/**
* The Platform agnostic ROUTER PROVIDERS
*/
export const ROUTER_PROVIDERS_COMMON: any[] = CONST_EXPR([
RouteRegistry,
CONST_EXPR(new Provider(LocationStrategy, {useClass: PathLocationStrategy})),
Location,
CONST_EXPR(new Provider(
Router,
{
useFactory: routerFactory,
deps: CONST_EXPR([RouteRegistry, Location, ROUTER_PRIMARY_COMPONENT, ApplicationRef])
})),
CONST_EXPR(new Provider(
ROUTER_PRIMARY_COMPONENT,
{useFactory: routerPrimaryComponentFactory, deps: CONST_EXPR([ApplicationRef])}))
]);
function routerFactory(registry: RouteRegistry, location: Location, primaryComponent: Type,
appRef: ApplicationRef): RootRouter {
var rootRouter = new RootRouter(registry, location, primaryComponent);
appRef.registerDisposeListener(() => rootRouter.dispose());
return rootRouter;
}
function routerPrimaryComponentFactory(app: ApplicationRef): Type {
if (app.componentTypes.length == 0) {
throw new BaseException("Bootstrap at least one component before injecting Router.");
}
return app.componentTypes[0];
}

View File

@ -4,4 +4,5 @@
*/
export const RENDERER_CHANNEL = "ng-Renderer";
export const XHR_CHANNEL = "ng-XHR";
export const EVENT_CHANNEL = "ng-events";
export const EVENT_CHANNEL = "ng-Events";
export const ROUTER_CHANNEL = "ng-Router";

View File

@ -0,0 +1,7 @@
// This file contains interface versions of browser types that can be serialized to Plain Old
// JavaScript Objects
export class LocationType {
constructor(public href: string, public protocol: string, public host: string,
public hostname: string, public port: string, public pathname: string,
public search: string, public hash: string, public origin: string) {}
}

View File

@ -6,6 +6,7 @@ import {RenderComponentType} from "angular2/src/core/render/api";
import {Injectable} from "angular2/src/core/di";
import {RenderStore} from 'angular2/src/web_workers/shared/render_store';
import {ViewEncapsulation, VIEW_ENCAPSULATION_VALUES} from 'angular2/src/core/metadata/view';
import {LocationType} from './serialized_types';
// PRIMITIVE is any type that does not need to be serialized (string, number, boolean)
// We set it to String so that it is considered a Type.
@ -31,6 +32,8 @@ export class Serializer {
return this._serializeRenderComponentType(obj);
} else if (type === ViewEncapsulation) {
return serializeEnum(obj);
} else if (type === LocationType) {
return this._serializeLocation(obj);
} else {
throw new BaseException("No serializer for " + type.toString());
}
@ -55,6 +58,8 @@ export class Serializer {
return this._deserializeRenderComponentType(map);
} else if (type === ViewEncapsulation) {
return VIEW_ENCAPSULATION_VALUES[map];
} else if (type === LocationType) {
return this._deserializeLocation(map);
} else {
throw new BaseException("No deserializer for " + type.toString());
}
@ -90,6 +95,25 @@ export class Serializer {
}
}
private _serializeLocation(loc: LocationType): Object {
return {
'href': loc.href,
'protocol': loc.protocol,
'host': loc.host,
'hostname': loc.hostname,
'port': loc.port,
'pathname': loc.pathname,
'search': loc.search,
'hash': loc.hash,
'origin': loc.origin
};
}
private _deserializeLocation(loc: {[key: string]: any}): LocationType {
return new LocationType(loc['href'], loc['protocol'], loc['host'], loc['hostname'], loc['port'],
loc['pathname'], loc['search'], loc['hash'], loc['origin']);
}
private _serializeRenderComponentType(obj: RenderComponentType): Object {
return {
'id': obj.id,

View File

@ -50,11 +50,13 @@ export class ServiceMessageBroker_ extends ServiceMessageBroker {
ObservableWrapper.subscribe(source, (message) => this._handleMessage(message));
}
registerMethod(methodName: string, signature: Type[], method: Function, returnType?: Type): void {
registerMethod(methodName: string, signature: Type[], method: (..._: any[]) => Promise<any>| void,
returnType?: Type): void {
this._methods.set(methodName, (message: ReceivedMessage) => {
var serializedArgs = message.args;
var deserializedArgs: any[] = ListWrapper.createFixedSize(signature.length);
for (var i = 0; i < signature.length; i++) {
let numArgs = signature === null ? 0 : signature.length;
var deserializedArgs: any[] = ListWrapper.createFixedSize(numArgs);
for (var i = 0; i < numArgs; i++) {
var serializedArg = serializedArgs[i];
deserializedArgs[i] = this._serializer.deserialize(serializedArg, signature[i]);
}

View File

@ -0,0 +1,54 @@
import {BrowserPlatformLocation} from 'angular2/src/router/browser_platform_location';
import {Injectable} from 'angular2/src/core/di';
import {ROUTER_CHANNEL} from 'angular2/src/web_workers/shared/messaging_api';
import {
ServiceMessageBrokerFactory,
ServiceMessageBroker
} from 'angular2/src/web_workers/shared/service_message_broker';
import {PRIMITIVE, Serializer} from 'angular2/src/web_workers/shared/serializer';
import {bind} from './bind';
import {LocationType} from 'angular2/src/web_workers/shared/serialized_types';
import {MessageBus} from 'angular2/src/web_workers/shared/message_bus';
import {Promise, EventEmitter, ObservableWrapper, PromiseWrapper} from 'angular2/src/facade/async';
import {UrlChangeListener} from 'angular2/src/router/platform_location';
@Injectable()
export class MessageBasedPlatformLocation {
private _channelSink: EventEmitter<Object>;
private _broker: ServiceMessageBroker;
constructor(private _brokerFactory: ServiceMessageBrokerFactory,
private _platformLocation: BrowserPlatformLocation, bus: MessageBus,
private _serializer: Serializer) {
this._platformLocation.onPopState(<UrlChangeListener>bind(this._sendUrlChangeEvent, this));
this._platformLocation.onHashChange(<UrlChangeListener>bind(this._sendUrlChangeEvent, this));
this._broker = this._brokerFactory.createMessageBroker(ROUTER_CHANNEL);
this._channelSink = bus.to(ROUTER_CHANNEL);
}
start(): void {
this._broker.registerMethod("getLocation", null, bind(this._getLocation, this), LocationType);
this._broker.registerMethod("setPathname", [PRIMITIVE], bind(this._setPathname, this));
this._broker.registerMethod("pushState", [PRIMITIVE, PRIMITIVE, PRIMITIVE],
bind(this._platformLocation.pushState, this._platformLocation));
this._broker.registerMethod("replaceState", [PRIMITIVE, PRIMITIVE, PRIMITIVE],
bind(this._platformLocation.replaceState, this._platformLocation));
this._broker.registerMethod("forward", null,
bind(this._platformLocation.forward, this._platformLocation));
this._broker.registerMethod("back", null,
bind(this._platformLocation.back, this._platformLocation));
}
private _getLocation(): Promise<Location> {
return PromiseWrapper.resolve(this._platformLocation.location);
}
private _sendUrlChangeEvent(e: Event): void {
let loc = this._serializer.serialize(this._platformLocation.location, LocationType);
let serializedEvent = {'type': e.type};
ObservableWrapper.callEmit(this._channelSink, {'event': serializedEvent, 'location': loc});
}
private _setPathname(pathname: string): void { this._platformLocation.pathname = pathname; }
}

View File

@ -0,0 +1,20 @@
import {MessageBasedPlatformLocation} from './platform_location';
import {CONST_EXPR} from 'angular2/src/facade/lang';
import {BrowserPlatformLocation} from 'angular2/src/router/browser_platform_location';
import {APP_INITIALIZER, Provider, Injector, NgZone} from 'angular2/core';
export const WORKER_RENDER_ROUTER = CONST_EXPR([
MessageBasedPlatformLocation,
BrowserPlatformLocation,
CONST_EXPR(
new Provider(APP_INITIALIZER,
{useFactory: initRouterListeners, multi: true, deps: CONST_EXPR([Injector])}))
]);
function initRouterListeners(injector: Injector): () => void {
return () => {
let zone = injector.get(NgZone);
zone.run(() => injector.get(MessageBasedPlatformLocation).start());
};
}

View File

@ -0,0 +1,136 @@
import {Injectable} from 'angular2/src/core/di';
import {
PlatformLocation,
UrlChangeEvent,
UrlChangeListener
} from 'angular2/src/router/platform_location';
import {
FnArg,
UiArguments,
ClientMessageBroker,
ClientMessageBrokerFactory
} from 'angular2/src/web_workers/shared/client_message_broker';
import {ROUTER_CHANNEL} from 'angular2/src/web_workers/shared/messaging_api';
import {LocationType} from 'angular2/src/web_workers/shared/serialized_types';
import {Promise, PromiseWrapper, EventEmitter, ObservableWrapper} from 'angular2/src/facade/async';
import {BaseException} from 'angular2/src/facade/exceptions';
import {PRIMITIVE, Serializer} from 'angular2/src/web_workers/shared/serializer';
import {MessageBus} from 'angular2/src/web_workers/shared/message_bus';
import {StringMapWrapper} from 'angular2/src/facade/collection';
import {StringWrapper} from 'angular2/src/facade/lang';
import {deserializeGenericEvent} from './event_deserializer';
@Injectable()
export class WebWorkerPlatformLocation extends PlatformLocation {
private _broker: ClientMessageBroker;
private _popStateListeners: Array<Function> = [];
private _hashChangeListeners: Array<Function> = [];
private _location: LocationType = null;
private _channelSource: EventEmitter<Object>;
constructor(brokerFactory: ClientMessageBrokerFactory, bus: MessageBus,
private _serializer: Serializer) {
super();
this._broker = brokerFactory.createMessageBroker(ROUTER_CHANNEL);
this._channelSource = bus.from(ROUTER_CHANNEL);
ObservableWrapper.subscribe(this._channelSource, (msg: {[key: string]: any}) => {
var listeners: Array<Function> = null;
if (StringMapWrapper.contains(msg, 'event')) {
let type: string = msg['event']['type'];
if (StringWrapper.equals(type, "popstate")) {
listeners = this._popStateListeners;
} else if (StringWrapper.equals(type, "hashchange")) {
listeners = this._hashChangeListeners;
}
if (listeners !== null) {
let e = deserializeGenericEvent(msg['event']);
// There was a popState or hashChange event, so the location object thas been updated
this._location = this._serializer.deserialize(msg['location'], LocationType);
listeners.forEach((fn: Function) => fn(e));
}
}
});
}
/** @internal **/
init(): Promise<boolean> {
var args: UiArguments = new UiArguments("getLocation");
var locationPromise: Promise<LocationType> = this._broker.runOnService(args, LocationType);
return PromiseWrapper.then(locationPromise, (val: LocationType): boolean => {
this._location = val;
return true;
}, (err): boolean => { throw new BaseException(err); });
}
getBaseHrefFromDOM(): string {
throw new BaseException(
"Attempt to get base href from DOM from WebWorker. You must either provide a value for the APP_BASE_HREF token through DI or use the hash location strategy.");
}
onPopState(fn: UrlChangeListener): void { this._popStateListeners.push(fn); }
onHashChange(fn: UrlChangeListener): void { this._hashChangeListeners.push(fn); }
get pathname(): string {
if (this._location === null) {
return null;
}
return this._location.pathname;
}
get search(): string {
if (this._location === null) {
return null;
}
return this._location.search;
}
get hash(): string {
if (this._location === null) {
return null;
}
return this._location.hash;
}
set pathname(newPath: string) {
if (this._location === null) {
throw new BaseException("Attempt to set pathname before value is obtained from UI");
}
this._location.pathname = newPath;
var fnArgs = [new FnArg(newPath, PRIMITIVE)];
var args = new UiArguments("setPathname", fnArgs);
this._broker.runOnService(args, null);
}
pushState(state: any, title: string, url: string): void {
var fnArgs =
[new FnArg(state, PRIMITIVE), new FnArg(title, PRIMITIVE), new FnArg(url, PRIMITIVE)];
var args = new UiArguments("pushState", fnArgs);
this._broker.runOnService(args, null);
}
replaceState(state: any, title: string, url: string): void {
var fnArgs =
[new FnArg(state, PRIMITIVE), new FnArg(title, PRIMITIVE), new FnArg(url, PRIMITIVE)];
var args = new UiArguments("replaceState", fnArgs);
this._broker.runOnService(args, null);
}
forward(): void {
var args = new UiArguments("forward");
this._broker.runOnService(args, null);
}
back(): void {
var args = new UiArguments("back");
this._broker.runOnService(args, null);
}
}

View File

@ -0,0 +1,21 @@
import {ApplicationRef, Provider, NgZone, APP_INITIALIZER} from 'angular2/core';
import {PlatformLocation} from 'angular2/src/router/platform_location';
import {WebWorkerPlatformLocation} from './platform_location';
import {ROUTER_PROVIDERS_COMMON} from 'angular2/src/router/router_providers_common';
import {Promise} from 'angular2/src/facade/async';
export var WORKER_APP_ROUTER = [
ROUTER_PROVIDERS_COMMON,
new Provider(PlatformLocation, {useClass: WebWorkerPlatformLocation}),
new Provider(APP_INITIALIZER,
{
useFactory: (platformLocation: WebWorkerPlatformLocation, zone: NgZone) => () =>
initRouter(platformLocation, zone),
multi: true,
deps: [PlatformLocation, NgZone]
})
];
function initRouter(platformLocation: WebWorkerPlatformLocation, zone: NgZone): Promise<boolean> {
return zone.run(() => { return platformLocation.init(); });
}

View File

@ -4,7 +4,8 @@ import 'package:angular2/src/platform/server/html_adapter.dart';
import "package:angular2/testing_internal.dart";
import "package:angular2/src/core/reflection/reflection_capabilities.dart";
import "package:angular2/src/core/reflection/reflection.dart";
import "package:angular2/src/platform/worker_app_common.dart" show WORKER_APP_APPLICATION_COMMON;
import "package:angular2/src/platform/worker_app_common.dart"
show WORKER_APP_APPLICATION_COMMON;
import "package:angular2/platform/worker_app.dart" show WORKER_APP_PLATFORM;
import "package:angular2/core.dart";
import "../shared/web_worker_test_util.dart";

View File

@ -217,7 +217,9 @@ SpySocketWrapper createSocket({Function messageHandler}) {
var socket = new SpyWebSocket();
if (messageHandler != null) {
socket.spy("add").andCallFake(messageHandler);
socket.spy("addStream").andCallFake((Stream stream) => stream.listen(messageHandler));
socket
.spy("addStream")
.andCallFake((Stream stream) => stream.listen(messageHandler));
}
var controller = new StreamController<String>.broadcast();

View File

@ -1,9 +1,18 @@
import {StringMapWrapper, ListWrapper} from 'angular2/src/facade/collection';
import {PromiseWrapper} from 'angular2/src/facade/async';
import {UiArguments} from 'angular2/src/web_workers/shared/client_message_broker';
import {Type, isPresent} from 'angular2/src/facade/lang';
import {SpyMessageBroker} from '../worker/spies';
import {expect} from 'angular2/testing_internal';
import {
MessageBusSink,
MessageBusSource,
MessageBus
} from 'angular2/src/web_workers/shared/message_bus';
import {
ClientMessageBroker,
ClientMessageBrokerFactory_
} from 'angular2/src/web_workers/shared/client_message_broker';
import {MockEventEmitter} from './mock_event_emitter';
import {BaseException, WrappedException} from 'angular2/src/facade/exceptions';
import {NgZone} from 'angular2/src/core/zone/ng_zone';
@ -25,6 +34,37 @@ export function createPairedMessageBuses(): PairedMessageBuses {
new MockMessageBus(workerMessageBusSink, workerMessageBusSource));
}
/**
* Spies on the given {@link SpyMessageBroker} and expects a call with the given methodName
* andvalues.
* If a handler is provided it will be called to handle the request.
* Only intended to be called on a given broker instance once.
*/
export function expectBrokerCall(broker: SpyMessageBroker, methodName: string, vals?: Array<any>,
handler?: (..._: any[]) => Promise<any>| void): void {
broker.spy("runOnService")
.andCallFake((args: UiArguments, returnType: Type) => {
expect(args.method).toEqual(methodName);
if (isPresent(vals)) {
expect(args.args.length).toEqual(vals.length);
ListWrapper.forEachWithIndex(vals, (v, i) => {expect(v).toEqual(args.args[i].value)});
}
var promise = null;
if (isPresent(handler)) {
let givenValues = args.args.map((arg) => {arg.value});
if (givenValues.length > 0) {
promise = handler(givenValues);
} else {
promise = handler();
}
}
if (promise == null) {
promise = PromiseWrapper.wrap(() => {});
}
return promise;
});
}
export class PairedMessageBuses {
constructor(public ui: MessageBus, public worker: MessageBus) {}
}
@ -85,3 +125,8 @@ export class MockMessageBus extends MessageBus {
attachToZone(zone: NgZone) {}
}
export class MockMessageBrokerFactory extends ClientMessageBrokerFactory_ {
constructor(private _messageBroker: ClientMessageBroker) { super(null, null); }
createMessageBroker(channel: string, runInZone = true) { return this._messageBroker; }
}

View File

@ -0,0 +1,97 @@
import {
AsyncTestCompleter,
inject,
describe,
it,
expect,
beforeEach,
beforeEachProviders
} from 'angular2/testing_internal';
import {SpyMessageBroker} from './spies';
import {WebWorkerPlatformLocation} from 'angular2/src/web_workers/worker/platform_location';
import {LocationType} from 'angular2/src/web_workers/shared/serialized_types';
import {MessageBus} from 'angular2/src/web_workers/shared/message_bus';
import {
createPairedMessageBuses,
MockMessageBrokerFactory,
expectBrokerCall
} from '../shared/web_worker_test_util';
import {UiArguments} from 'angular2/src/web_workers/shared/client_message_broker';
import {Type} from 'angular2/src/facade/lang';
import {PromiseWrapper} from "angular2/src/facade/async";
import {CONST_EXPR} from 'angular2/src/facade/lang';
export function main() {
describe("WebWorkerPlatformLocation", () => {
var uiBus: MessageBus = null;
var workerBus: MessageBus = null;
var broker: any = null;
var TEST_LOCATION =
new LocationType("http://www.example.com", "http", "example.com", "example.com", "80", "/",
"", "", "http://www.example.com");
function createWebWorkerPlatformLocation(loc: LocationType): WebWorkerPlatformLocation {
broker.spy("runOnService")
.andCallFake((args: UiArguments, returnType: Type) => {
if (args.method === 'getLocation') {
return PromiseWrapper.resolve(loc);
}
});
var factory = new MockMessageBrokerFactory(broker);
return new WebWorkerPlatformLocation(factory, workerBus, null);
}
function testPushOrReplaceState(pushState: boolean) {
let platformLocation = createWebWorkerPlatformLocation(null);
const TITLE = "foo";
const URL = "http://www.example.com/foo";
expectBrokerCall(broker, pushState ? "pushState" : "replaceState", [null, TITLE, URL]);
if (pushState) {
platformLocation.pushState(null, TITLE, URL);
} else {
platformLocation.replaceState(null, TITLE, URL);
}
}
beforeEach(() => {
var buses = createPairedMessageBuses();
uiBus = buses.ui;
workerBus = buses.worker;
workerBus.initChannel("ng-Router");
uiBus.initChannel("ng-Router");
broker = new SpyMessageBroker();
});
it("should throw if getBaseHrefFromDOM is called", () => {
let platformLocation = createWebWorkerPlatformLocation(null);
expect(() => platformLocation.getBaseHrefFromDOM()).toThrowError();
});
it("should get location on init", () => {
let platformLocation = createWebWorkerPlatformLocation(null);
expectBrokerCall(broker, "getLocation");
platformLocation.init();
});
it("should throw if set pathname is called before init finishes", () => {
let platformLocation = createWebWorkerPlatformLocation(null);
platformLocation.init();
expect(() => platformLocation.pathname = "TEST").toThrowError();
});
it("should send pathname to render thread", inject([AsyncTestCompleter], (async) => {
let platformLocation = createWebWorkerPlatformLocation(TEST_LOCATION);
platformLocation.init().then((_) => {
let PATHNAME = "/test";
expectBrokerCall(broker, "setPathname", [PATHNAME]);
platformLocation.pathname = PATHNAME;
async.done();
});
}));
it("should send pushState to render thread", () => { testPushOrReplaceState(true); });
it("should send replaceState to render thread", () => { testPushOrReplaceState(false); });
});
}

View File

@ -8,15 +8,9 @@ import {
beforeEachProviders
} from 'angular2/testing_internal';
import {SpyMessageBroker} from './spies';
import {Type} from 'angular2/src/facade/lang';
import {
ClientMessageBroker,
UiArguments,
ClientMessageBrokerFactory,
ClientMessageBrokerFactory_
} from 'angular2/src/web_workers/shared/client_message_broker';
import {WebWorkerXHRImpl} from "angular2/src/web_workers/worker/xhr_impl";
import {PromiseWrapper} from "angular2/src/facade/async";
import {MockMessageBrokerFactory, expectBrokerCall} from "../shared/web_worker_test_util";
export function main() {
describe("WebWorkerXHRImpl", () => {
@ -25,16 +19,10 @@ export function main() {
const URL = "http://www.example.com/test";
const RESPONSE = "Example response text";
var messageBroker: any = new SpyMessageBroker();
messageBroker.spy("runOnService")
.andCallFake((args: UiArguments, returnType: Type) => {
expect(args.method).toEqual("get");
expect(args.args.length).toEqual(1);
expect(args.args[0].value).toEqual(URL);
return PromiseWrapper.wrap(() => { return RESPONSE; });
});
var xhrImpl = new WebWorkerXHRImpl(new MockMessageBrokerFactory(messageBroker));
var messageBroker = new SpyMessageBroker();
expectBrokerCall(messageBroker, "get", [URL],
(_) => { return PromiseWrapper.wrap(() => { return RESPONSE; }); });
var xhrImpl = new WebWorkerXHRImpl(new MockMessageBrokerFactory(<any>messageBroker));
xhrImpl.get(URL).then((response) => {
expect(response).toEqual(RESPONSE);
async.done();
@ -42,8 +30,3 @@ export function main() {
}));
});
}
class MockMessageBrokerFactory extends ClientMessageBrokerFactory_ {
constructor(private _messageBroker: ClientMessageBroker) { super(null, null); }
createMessageBroker(channel: string, runInZone = true) { return this._messageBroker; }
}

View File

@ -0,0 +1,3 @@
library playground.e2e_test.web_workers.router.router_spec;
main() {}

View File

@ -0,0 +1,72 @@
import {verifyNoBrowserErrors} from 'angular2/src/testing/e2e_util';
describe("WebWorker Router", () => {
afterEach(() => {
verifyNoBrowserErrors();
browser.ignoreSynchronization = false;
});
let contentSelector = "app main h1";
let navSelector = "app nav ul";
var baseUrl = "playground/src/web_workers/router/index.html";
it("should route on click", () => {
// This test can't wait for Angular 2 as Testability is not available when using WebWorker
browser.ignoreSynchronization = true;
browser.get(baseUrl);
waitForElement(contentSelector);
var content = element(by.css(contentSelector));
expect(content.getText()).toEqual("Start");
let aboutBtn = element(by.css(navSelector + " .about"));
aboutBtn.click();
waitForUrl(/\/about/);
waitForElement(contentSelector);
content = element(by.css(contentSelector));
waitForElementText(content, "About");
expect(content.getText()).toEqual("About");
expect(browser.getCurrentUrl()).toMatch(/\/about/);
let contactBtn = element(by.css(navSelector + " .contact"));
contactBtn.click();
waitForUrl(/\/contact/);
waitForElement(contentSelector);
content = element(by.css(contentSelector));
waitForElementText(content, "Contact");
expect(content.getText()).toEqual("Contact");
expect(browser.getCurrentUrl()).toMatch(/\/contact/);
});
it("should load the correct route from the URL", () => {
// This test can't wait for Angular 2 as Testability is not available when using WebWorker
browser.ignoreSynchronization = true;
browser.get(baseUrl + "#/about");
waitForElement(contentSelector);
let content = element(by.css(contentSelector));
waitForElementText(content, "About");
expect(content.getText()).toEqual("About");
});
function waitForElement(selector: string): void {
browser.wait(protractor.until.elementLocated(by.css(selector)), 15000);
}
function waitForElementText(elem: protractor.ElementFinder, expected: string): void {
browser.wait(() => {
let deferred = protractor.promise.defer();
elem.getText().then((text) => { return deferred.fulfill(text === expected); });
return deferred.promise;
}, 5000);
}
function waitForUrl(regex): void {
browser.wait(() => {
let deferred = protractor.promise.defer();
browser.getCurrentUrl().then(
(url) => { return deferred.fulfill(url.match(regex) !== null); });
return deferred.promise;
}, 5000);
}
});

View File

@ -50,6 +50,8 @@ transformers:
- web/src/web_workers/todo/background_index.dart
- web/src/web_workers/todo/index.dart
- web/src/web_workers/todo/server_index.dart
- web/src/web_workers/router/index.dart
- web/src/web_workers/router/background_index.dart
- web/src/zippy_component/index.dart
- $dart2js:

View File

@ -9,7 +9,8 @@ import "package:angular2/src/core/reflection/reflection.dart";
main(List<String> args, SendPort replyTo) {
reflector.reflectionCapabilities = new ReflectionCapabilities();
platform([WORKER_APP_PLATFORM, new Provider(RENDER_SEND_PORT, useValue: replyTo)])
.application([WORKER_APP_APPLICATION])
.bootstrap(ImageDemo);
platform([
WORKER_APP_PLATFORM,
new Provider(RENDER_SEND_PORT, useValue: replyTo)
]).application([WORKER_APP_APPLICATION]).bootstrap(ImageDemo);
}

View File

@ -0,0 +1,11 @@
<nav>
<ul>
<li class="start" [routerLink]="['/Start']">Start</li>
<li class="about" [routerLink]="['/About']">About</li>
<li class="contact" [routerLink]="['/Contact']">Contact</li>
</ul>
</nav>
<main>
<router-outlet></router-outlet>
</main>

View File

@ -0,0 +1,20 @@
library playground.src.web_workers.router.background_index;
import "index_common.dart" show App;
import "dart:isolate";
import "package:angular2/platform/worker_app.dart";
import "package:angular2/core.dart";
import "package:angular2/src/web_workers/worker/router_providers.dart";
import "package:angular2/router.dart";
@AngularEntrypoint()
main(List<String> args, SendPort replyTo) {
platform([
WORKER_APP_PLATFORM,
new Provider(RENDER_SEND_PORT, useValue: replyTo)
]).asyncApplication(null, [
WORKER_APP_APPLICATION,
WORKER_APP_ROUTER,
new Provider(LocationStrategy, useClass: HashLocationStrategy)
]).then((ref) => ref.bootstrap(App));
}

View File

@ -0,0 +1,18 @@
import {platform, Provider, NgZone} from "angular2/core";
import {
WORKER_APP_PLATFORM,
WORKER_APP_APPLICATION,
WORKER_APP_ROUTER
} from "angular2/platform/worker_app";
import {App} from "./index_common";
import {HashLocationStrategy, LocationStrategy} from "angular2/router";
export function main() {
let refPromise = platform([WORKER_APP_PLATFORM])
.asyncApplication(null, [
WORKER_APP_APPLICATION,
WORKER_APP_ROUTER,
new Provider(LocationStrategy, {useClass: HashLocationStrategy})
]);
refPromise.then((ref) => ref.bootstrap(App));
}

View File

@ -0,0 +1,4 @@
import {Component} from 'angular2/core';
@Component({selector: 'about', template: '<h1>About</h1>'})
export class About {
}

View File

@ -0,0 +1,4 @@
import {Component} from 'angular2/core';
@Component({selector: 'contact', template: '<h1>Contact</h1>'})
export class Contact {
}

View File

@ -0,0 +1,4 @@
import {Component} from 'angular2/core';
@Component({selector: 'start', template: '<h1>Start</h1>'})
export class Start {
}

View File

@ -0,0 +1,13 @@
library angular2.examples.web_workers.router.index;
import "package:angular2/platform/worker_render.dart";
import "package:angular2/core.dart";
import "package:angular2/src/core/reflection/reflection_capabilities.dart";
import "package:angular2/src/core/reflection/reflection.dart";
@AngularEntrypoint()
main() {
reflector.reflectionCapabilities = new ReflectionCapabilities();
platform([WORKER_RENDER_PLATFORM]).asyncApplication(
initIsolate("background_index.dart"), [WORKER_RENDER_ROUTER]);
}

View File

@ -0,0 +1,8 @@
<!doctype html>
<html>
<title>Web Worker Router Example</title>
<body>
<app></app>
$SCRIPTS$
</body>
</html>

View File

@ -0,0 +1,16 @@
import {platform, Provider} from 'angular2/core';
import {
WORKER_RENDER_APP,
WORKER_RENDER_PLATFORM,
WORKER_SCRIPT,
WORKER_RENDER_ROUTER
} from 'angular2/platform/worker_render';
import {BrowserPlatformLocation} from "angular2/src/router/browser_platform_location";
import {MessageBasedPlatformLocation} from "angular2/src/web_workers/ui/platform_location";
let ref = platform([WORKER_RENDER_PLATFORM])
.application([
WORKER_RENDER_APP,
new Provider(WORKER_SCRIPT, {useValue: "loader.js"}),
WORKER_RENDER_ROUTER
]);

View File

@ -0,0 +1,14 @@
import {Component, View} from 'angular2/core';
import {Start} from './components/start';
import {About} from './components/about';
import {Contact} from './components/contact';
import {ROUTER_DIRECTIVES, RouteConfig, Route} from 'angular2/router';
@Component({selector: 'app', directives: [ROUTER_DIRECTIVES], templateUrl: 'app.html'})
@RouteConfig([
new Route({path: '/', component: Start, name: "Start"}),
new Route({path: '/contact', component: Contact, name: "Contact"}),
new Route({path: '/about', component: About, name: "About"})
])
export class App {
}

View File

@ -0,0 +1,17 @@
$SCRIPTS$
System.config({
baseURL: '/',
defaultJSExtensions: true
});
System.import("playground/src/web_workers/router/background_index")
.then(
function(m) {
try {
m.main();
} catch (e) {
console.error(e);
}
},
function(error) { console.error("error loading background", error); });

View File

@ -10,7 +10,6 @@ main() {
var webSocket = new WebSocket("ws://127.0.0.1:1337/ws");
webSocket.onOpen.listen((e) {
var bus = new WebSocketMessageBus.fromWebSocket(webSocket);
platform([WORKER_RENDER_PLATFORM])
.application([WORKER_RENDER_APPLICATION_COMMON, new Provider(MessageBus, useValue: bus),
new Provider(APP_INITIALIZER,

View File

@ -68,7 +68,8 @@ const kServedPaths = [
'playground/src/web_workers/kitchen_sink',
'playground/src/web_workers/todo',
'playground/src/web_workers/images',
'playground/src/web_workers/message_broker'
'playground/src/web_workers/message_broker',
'playground/src/web_workers/router'
];