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

@ -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,
@ -106,4 +130,4 @@ export class Serializer {
}
export class RenderStoreObject {}
export class RenderStoreObject {}

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