Revert "refactor(router): improve recognition and generation pipeline"

This reverts commit cf7292fcb1.

This commit triggered an existing race condition in Google code. More work is needed on the Router to fix this condition before this refactor can land.
This commit is contained in:
Alex Rickabaugh
2015-11-23 16:26:47 -08:00
parent ba64b5ea5a
commit c5294c77d9
41 changed files with 1117 additions and 2970 deletions

View File

@ -1,3 +1,6 @@
import {PathMatch} from './path_recognizer';
import {RouteRecognizer} from './route_recognizer';
import {Instruction, ComponentInstruction, PrimaryInstruction} from './instruction';
import {ListWrapper, Map, MapWrapper, StringMapWrapper} from 'angular2/src/facade/collection';
import {Promise, PromiseWrapper} from 'angular2/src/facade/async';
import {
@ -13,7 +16,6 @@ import {
getTypeNameForDebugging
} from 'angular2/src/facade/lang';
import {BaseException, WrappedException} from 'angular2/src/facade/exceptions';
import {reflector} from 'angular2/src/core/reflection/reflection';
import {
RouteConfig,
AsyncRoute,
@ -22,16 +24,7 @@ import {
Redirect,
RouteDefinition
} from './route_config_impl';
import {PathMatch, RedirectMatch, RouteMatch} from './route_recognizer';
import {ComponentRecognizer} from './component_recognizer';
import {
Instruction,
ResolvedInstruction,
RedirectInstruction,
UnresolvedInstruction,
DefaultInstruction
} from './instruction';
import {reflector} from 'angular2/src/core/reflection/reflection';
import {Injectable} from 'angular2/angular2';
import {normalizeRouteConfig, assertComponentExists} from './route_config_nomalizer';
import {parser, Url, pathSegmentsToUrl} from './url_parser';
@ -45,13 +38,13 @@ var _resolveToNull = PromiseWrapper.resolve(null);
*/
@Injectable()
export class RouteRegistry {
private _rules = new Map<any, ComponentRecognizer>();
private _rules = new Map<any, RouteRecognizer>();
/**
* Given a component and a configuration object, add the route to this registry
*/
config(parentComponent: any, config: RouteDefinition): void {
config = normalizeRouteConfig(config, this);
config = normalizeRouteConfig(config);
// this is here because Dart type guard reasons
if (config instanceof Route) {
@ -60,10 +53,10 @@ export class RouteRegistry {
assertComponentExists(config.component, config.path);
}
var recognizer: ComponentRecognizer = this._rules.get(parentComponent);
var recognizer: RouteRecognizer = this._rules.get(parentComponent);
if (isBlank(recognizer)) {
recognizer = new ComponentRecognizer();
recognizer = new RouteRecognizer();
this._rules.set(parentComponent, recognizer);
}
@ -109,162 +102,102 @@ export class RouteRegistry {
* Given a URL and a parent component, return the most specific instruction for navigating
* the application into the state specified by the url
*/
recognize(url: string, ancestorComponents: any[]): Promise<Instruction> {
recognize(url: string, parentComponent: any): Promise<Instruction> {
var parsedUrl = parser.parse(url);
return this._recognize(parsedUrl, ancestorComponents);
return this._recognize(parsedUrl, parentComponent);
}
private _recognize(parsedUrl: Url, parentComponent): Promise<Instruction> {
return this._recognizePrimaryRoute(parsedUrl, parentComponent)
.then((instruction: PrimaryInstruction) =>
this._completeAuxiliaryRouteMatches(instruction, parentComponent));
}
/**
* Recognizes all parent-child routes, but creates unresolved auxiliary routes
*/
private _recognize(parsedUrl: Url, ancestorComponents: any[],
_aux = false): Promise<Instruction> {
var parentComponent = ancestorComponents[ancestorComponents.length - 1];
private _recognizePrimaryRoute(parsedUrl: Url, parentComponent): Promise<PrimaryInstruction> {
var componentRecognizer = this._rules.get(parentComponent);
if (isBlank(componentRecognizer)) {
return _resolveToNull;
}
// Matches some beginning part of the given URL
var possibleMatches: Promise<RouteMatch>[] =
_aux ? componentRecognizer.recognizeAuxiliary(parsedUrl) :
componentRecognizer.recognize(parsedUrl);
var possibleMatches = componentRecognizer.recognize(parsedUrl);
var matchPromises: Promise<Instruction>[] = possibleMatches.map(
(candidate: Promise<RouteMatch>) => candidate.then((candidate: RouteMatch) => {
if (candidate instanceof PathMatch) {
if (candidate.instruction.terminal) {
var unresolvedAux =
this._auxRoutesToUnresolved(candidate.remainingAux, parentComponent);
return new ResolvedInstruction(candidate.instruction, null, unresolvedAux);
}
var newAncestorComponents =
ancestorComponents.concat([candidate.instruction.componentType]);
return this._recognize(candidate.remaining, newAncestorComponents)
.then((childInstruction) => {
if (isBlank(childInstruction)) {
return null;
}
// redirect instructions are already absolute
if (childInstruction instanceof RedirectInstruction) {
return childInstruction;
}
var unresolvedAux =
this._auxRoutesToUnresolved(candidate.remainingAux, parentComponent);
return new ResolvedInstruction(candidate.instruction, childInstruction,
unresolvedAux);
});
}
if (candidate instanceof RedirectMatch) {
var instruction = this.generate(candidate.redirectTo, ancestorComponents);
return new RedirectInstruction(instruction.component, instruction.child,
instruction.auxInstruction);
}
}));
if ((isBlank(parsedUrl) || parsedUrl.path == '') && possibleMatches.length == 0) {
return PromiseWrapper.resolve(this.generateDefault(parentComponent));
}
var matchPromises =
possibleMatches.map(candidate => this._completePrimaryRouteMatch(candidate));
return PromiseWrapper.all(matchPromises).then(mostSpecific);
}
private _auxRoutesToUnresolved(auxRoutes: Url[], parentComponent): {[key: string]: Instruction} {
var unresolvedAuxInstructions: {[key: string]: Instruction} = {};
private _completePrimaryRouteMatch(partialMatch: PathMatch): Promise<PrimaryInstruction> {
var instruction = partialMatch.instruction;
return instruction.resolveComponentType().then((componentType) => {
this.configFromComponent(componentType);
auxRoutes.forEach((auxUrl: Url) => {
unresolvedAuxInstructions[auxUrl.path] = new UnresolvedInstruction(
() => { return this._recognize(auxUrl, [parentComponent], true); });
if (instruction.terminal) {
return new PrimaryInstruction(instruction, null, partialMatch.remainingAux);
}
return this._recognizePrimaryRoute(partialMatch.remaining, componentType)
.then((childInstruction) => {
if (isBlank(childInstruction)) {
return null;
} else {
return new PrimaryInstruction(instruction, childInstruction,
partialMatch.remainingAux);
}
});
});
return unresolvedAuxInstructions;
}
private _completeAuxiliaryRouteMatches(instruction: PrimaryInstruction,
parentComponent: any): Promise<Instruction> {
if (isBlank(instruction)) {
return _resolveToNull;
}
var componentRecognizer = this._rules.get(parentComponent);
var auxInstructions: {[key: string]: Instruction} = {};
var promises = instruction.auxUrls.map((auxSegment: Url) => {
var match = componentRecognizer.recognizeAuxiliary(auxSegment);
if (isBlank(match)) {
return _resolveToNull;
}
return this._completePrimaryRouteMatch(match).then((auxInstruction: PrimaryInstruction) => {
if (isPresent(auxInstruction)) {
return this._completeAuxiliaryRouteMatches(auxInstruction, parentComponent)
.then((finishedAuxRoute: Instruction) => {
auxInstructions[auxSegment.path] = finishedAuxRoute;
});
}
});
});
return PromiseWrapper.all(promises).then((_) => {
if (isBlank(instruction.child)) {
return new Instruction(instruction.component, null, auxInstructions);
}
return this._completeAuxiliaryRouteMatches(instruction.child,
instruction.component.componentType)
.then((completeChild) => {
return new Instruction(instruction.component, completeChild, auxInstructions);
});
});
}
/**
* Given a normalized list with component names and params like: `['user', {id: 3 }]`
* generates a url with a leading slash relative to the provided `parentComponent`.
*
* If the optional param `_aux` is `true`, then we generate starting at an auxiliary
* route boundary.
*/
generate(linkParams: any[], ancestorComponents: any[], _aux = false): Instruction {
let parentComponent = ancestorComponents[ancestorComponents.length - 1];
let grandparentComponent =
ancestorComponents.length > 1 ? ancestorComponents[ancestorComponents.length - 2] : null;
let normalizedLinkParams = splitAndFlattenLinkParams(linkParams);
var first = ListWrapper.first(normalizedLinkParams);
var rest = ListWrapper.slice(normalizedLinkParams, 1);
// The first segment should be either '.' (generate from parent) or '' (generate from root).
// When we normalize above, we strip all the slashes, './' becomes '.' and '/' becomes ''.
if (first == '') {
var firstComponent = ancestorComponents[0];
ListWrapper.clear(ancestorComponents);
ancestorComponents.push(firstComponent);
} else if (first == '..') {
// we already captured the first instance of "..", so we need to pop off an ancestor
ancestorComponents.pop();
while (ListWrapper.first(rest) == '..') {
rest = ListWrapper.slice(rest, 1);
ancestorComponents.pop();
if (ancestorComponents.length <= 0) {
throw new BaseException(
`Link "${ListWrapper.toJSON(linkParams)}" has too many "../" segments.`);
}
}
} else if (first != '.') {
// For a link with no leading `./`, `/`, or `../`, we look for a sibling and child.
// If both exist, we throw. Otherwise, we prefer whichever exists.
var childRouteExists = this.hasRoute(first, parentComponent);
var parentRouteExists =
isPresent(grandparentComponent) && this.hasRoute(first, grandparentComponent);
if (parentRouteExists && childRouteExists) {
let msg =
`Link "${ListWrapper.toJSON(linkParams)}" is ambiguous, use "./" or "../" to disambiguate.`;
throw new BaseException(msg);
}
if (parentRouteExists) {
ancestorComponents.pop();
}
rest = linkParams;
}
if (rest[rest.length - 1] == '') {
rest.pop();
}
if (rest.length < 1) {
let msg = `Link "${ListWrapper.toJSON(linkParams)}" must include a route name.`;
throw new BaseException(msg);
}
return this._generate(rest, ancestorComponents, _aux);
}
/*
* Internal helper that does not make any assertions about the beginning of the link DSL
*/
private _generate(linkParams: any[], ancestorComponents: any[], _aux = false): Instruction {
let parentComponent = ancestorComponents[ancestorComponents.length - 1];
if (linkParams.length == 0) {
return this.generateDefault(parentComponent);
}
generate(linkParams: any[], parentComponent: any, _aux = false): Instruction {
let linkIndex = 0;
let routeName = linkParams[linkIndex];
// TODO: this is kind of odd but it makes existing assertions pass
if (isBlank(parentComponent)) {
throw new BaseException(`Could not find route named "${routeName}".`);
}
if (!isString(routeName)) {
throw new BaseException(`Unexpected segment "${routeName}" in link DSL. Expected a string.`);
} else if (routeName == '' || routeName == '.' || routeName == '..') {
@ -283,10 +216,7 @@ export class RouteRegistry {
let auxInstructions: {[key: string]: Instruction} = {};
var nextSegment;
while (linkIndex + 1 < linkParams.length && isArray(nextSegment = linkParams[linkIndex + 1])) {
let auxInstruction = this._generate(nextSegment, [parentComponent], true);
// TODO: this will not work for aux routes with parameters or multiple segments
auxInstructions[auxInstruction.component.urlPath] = auxInstruction;
auxInstructions[nextSegment[0]] = this.generate(nextSegment, parentComponent, true);
linkIndex += 1;
}
@ -296,105 +226,74 @@ export class RouteRegistry {
`Component "${getTypeNameForDebugging(parentComponent)}" has no route config.`);
}
var routeRecognizer =
(_aux ? componentRecognizer.auxNames : componentRecognizer.names).get(routeName);
var componentInstruction = _aux ? componentRecognizer.generateAuxiliary(routeName, params) :
componentRecognizer.generate(routeName, params);
if (!isPresent(routeRecognizer)) {
if (isBlank(componentInstruction)) {
throw new BaseException(
`Component "${getTypeNameForDebugging(parentComponent)}" has no route named "${routeName}".`);
}
if (!isPresent(routeRecognizer.handler.componentType)) {
var compInstruction = routeRecognizer.generateComponentPathValues(params);
return new UnresolvedInstruction(() => {
return routeRecognizer.handler.resolveComponentType().then(
(_) => { return this._generate(linkParams, ancestorComponents, _aux); });
}, compInstruction['urlPath'], compInstruction['urlParams']);
var childInstruction = null;
if (linkIndex + 1 < linkParams.length) {
var remaining = linkParams.slice(linkIndex + 1);
childInstruction = this.generate(remaining, componentInstruction.componentType);
} else if (!componentInstruction.terminal) {
throw new BaseException(
`Link "${ListWrapper.toJSON(linkParams)}" does not resolve to a terminal or async instruction.`);
}
var componentInstruction = _aux ? componentRecognizer.generateAuxiliary(routeName, params) :
componentRecognizer.generate(routeName, params);
var childInstruction: Instruction = null;
var remaining = linkParams.slice(linkIndex + 1);
// the component is sync
if (isPresent(componentInstruction.componentType)) {
if (linkIndex + 1 < linkParams.length) {
let childAncestorComponents =
ancestorComponents.concat([componentInstruction.componentType]);
childInstruction = this._generate(remaining, childAncestorComponents);
} else if (!componentInstruction.terminal) {
// ... look for defaults
childInstruction = this.generateDefault(componentInstruction.componentType);
if (isBlank(childInstruction)) {
throw new BaseException(
`Link "${ListWrapper.toJSON(linkParams)}" does not resolve to a terminal instruction.`);
}
}
}
return new ResolvedInstruction(componentInstruction, childInstruction, auxInstructions);
return new Instruction(componentInstruction, childInstruction, auxInstructions);
}
public hasRoute(name: string, parentComponent: any): boolean {
var componentRecognizer: ComponentRecognizer = this._rules.get(parentComponent);
var componentRecognizer: RouteRecognizer = this._rules.get(parentComponent);
if (isBlank(componentRecognizer)) {
return false;
}
return componentRecognizer.hasRoute(name);
}
public generateDefault(componentCursor: Type): Instruction {
// if the child includes a redirect like : "/" -> "/something",
// we want to honor that redirection when creating the link
private _generateRedirects(componentCursor: Type): Instruction {
if (isBlank(componentCursor)) {
return null;
}
var componentRecognizer = this._rules.get(componentCursor);
if (isBlank(componentRecognizer) || isBlank(componentRecognizer.defaultRoute)) {
if (isBlank(componentRecognizer)) {
return null;
}
for (let i = 0; i < componentRecognizer.redirects.length; i += 1) {
let redirect = componentRecognizer.redirects[i];
var defaultChild = null;
if (isPresent(componentRecognizer.defaultRoute.handler.componentType)) {
var componentInstruction = componentRecognizer.defaultRoute.generate({});
if (!componentRecognizer.defaultRoute.terminal) {
defaultChild = this.generateDefault(componentRecognizer.defaultRoute.handler.componentType);
// we only handle redirecting from an empty segment
if (redirect.segments.length == 1 && redirect.segments[0] == '') {
var toSegments = pathSegmentsToUrl(redirect.toSegments);
var matches = componentRecognizer.recognize(toSegments);
var primaryInstruction =
ListWrapper.maximum(matches, (match: PathMatch) => match.instruction.specificity);
if (isPresent(primaryInstruction)) {
var child = this._generateRedirects(primaryInstruction.instruction.componentType);
return new Instruction(primaryInstruction.instruction, child, {});
}
return null;
}
return new DefaultInstruction(componentInstruction, defaultChild);
}
return new UnresolvedInstruction(() => {
return componentRecognizer.defaultRoute.handler.resolveComponentType().then(
() => this.generateDefault(componentCursor));
});
return null;
}
}
/*
* Given: ['/a/b', {c: 2}]
* Returns: ['', 'a', 'b', {c: 2}]
*/
function splitAndFlattenLinkParams(linkParams: any[]): any[] {
return linkParams.reduce((accumulation: any[], item) => {
if (isString(item)) {
let strItem: string = item;
return accumulation.concat(strItem.split('/'));
}
accumulation.push(item);
return accumulation;
}, []);
}
/*
* Given a list of instructions, returns the most specific instruction
*/
function mostSpecific(instructions: Instruction[]): Instruction {
return ListWrapper.maximum(instructions, (instruction: Instruction) => instruction.specificity);
function mostSpecific(instructions: PrimaryInstruction[]): PrimaryInstruction {
return ListWrapper.maximum(
instructions, (instruction: PrimaryInstruction) => instruction.component.specificity);
}
function assertTerminalComponent(component, path) {