Skip to content

Commit bec503b

Browse files
committed
feat: change the RFC to better reflect contracts based on the implementation
1 parent 6d2fd94 commit bec503b

1 file changed

Lines changed: 109 additions & 48 deletions

File tree

text/1169-route-manager-api.md

Lines changed: 109 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -50,16 +50,20 @@ This RFC is **not** intended to describe APIs that Ember app developers would ge
5050
A Route Manager always has `capabilities`, `createRoute` and a `getDestroyable` method.
5151

5252
```typescript
53-
interface RouteManager {
54-
capabilities: Capabilities;
55-
56-
// Responsible for the creation of a RouteStateBucket. Returns a RouteStateBucket, defined by the manager implementation.
57-
createRoute: (factory: object, args: CreateRouteArgs) => RouteStateBucket;
58-
59-
// Returns the destroyable (if any) for the RouteStateBucket
60-
getDestroyable: (bucket: RouteStateBucket) => Destroyable | null;
61-
62-
// ... see below
53+
interface RouteManager<Bucket extends RouteStateBucket = RouteStateBucket> {
54+
capabilities: RouteCapabilities;
55+
createRoute(factory: object, args: CreateRouteArgs): Bucket;
56+
getDestroyable(bucket: Bucket): object | null;
57+
58+
willEnter(bucket: Bucket, state: WillEnterState): void;
59+
enter(bucket: Bucket, state: EnterState): Promise<unknown>;
60+
didEnter(bucket: Bucket, state: DidEnterState): void;
61+
willExit(bucket: Bucket, state: WillExitState): void;
62+
exit(bucket: Bucket, state?: ExitState): void;
63+
didExit(bucket: Bucket, state: DidExitState): void;
64+
65+
getRouteWrapper(): object;
66+
getInvokable(bucket: Bucket): Promise<object>;
6367
}
6468

6569
interface CreateRouteArgs {
@@ -143,7 +147,7 @@ interface AsyncNavigationState {
143147
// Signal for the current navigation
144148
signal: AbortSignal;
145149

146-
// Retrieve the ancestor promise for an ancestor route, used to await async ancestor behaviour.
150+
// Retrieve the enterPromise for an ancestor route, used to await async ancestor behaviour.
147151
getAncestorPromise(routeInfo: RouteInfo): ReturnType<RouteManager['enter']>;
148152
}
149153
```
@@ -243,7 +247,7 @@ Note: this is the full list of lifecycle events in a single transition between '
243247

244248
This sequence diagram only specifies the order of the hooks that are called as part of the Route Manager API, the dotted lines from the Router to the Browser are there for illustrative purposes only and are not specified as part of this RFC. Individual Route managers might express substates (such as loading states) as part of their own APIs, but they would have to do that within the constraints of the Route Manager API hooks.
245249

246-
In the above diagram the `enter()` is called before the `getInvokable()` for a given route. The promise returned from `enter()` is exposed to `getInvokable()`, so a manager may either await it (to gate rendering on data) or ignore it (to render immediately and coordinate loading inside its wrapper).
250+
In the above diagram the `enter()` is called together with the `getInvokable()` for a given route. Both are executed at the same time and are required to resolve before route info is marked `resolved`.
247251

248252
### Capabilities
249253

@@ -257,23 +261,91 @@ When the `classicInterop` capability is set to `true` the Route Manager will hav
257261

258262
```typescript
259263
// Classic Router interoperability
260-
interface RouteManagerWithClassicInterop = RouteManager & {
261-
getRouteName(bucket: RouteStateBucket) => string;
262-
getFullRouteName(bucket: RouteStateBucket) => string;
264+
interface RouteManagerWithClassicInterop<
265+
Bucket extends RouteStateBucket = RouteStateBucket,
266+
> extends RouteManager<Bucket> {
267+
getRoute(bucket: Bucket): unknown;
268+
269+
willEnter(bucket: Bucket, state: ClassicWillEnterState): void;
270+
enter(bucket: Bucket, state: ClassicEnterState): Promise<unknown>;
271+
didEnter(bucket: Bucket, state: ClassicDidEnterState): void;
272+
willExit(bucket: Bucket, state: ClassicWillExitState): void;
273+
exit(bucket: Bucket, state?: ClassicExitState): void;
274+
didExit(bucket: Bucket, state: ClassicDidExitState): void;
263275

264276
// Query Parameter handling
265-
stashNames(bucket: RouteStateBucket, routeInfo: ExtendedInternalRouteInfo<Route>, dynamicParent: ExtendedInternalRouteInfo<Route>) => void;
266-
qp(bucket: RouteStateBucket): it's complicated
267-
268-
serializeQueryParam(bucket: RouteStateBucket, value: unknown, urlKey: string, defaultValueType: string);
269-
deserializeQueryParam(bucket: RouteStateBucket, value: unknown, urlKey: string, defaultValueType: string);
270-
277+
stashNames(
278+
bucket: Bucket,
279+
routeInfo: InternalRouteInfo<BaseRoute>,
280+
dynamicParent: InternalRouteInfo<BaseRoute>,
281+
): void;
282+
serializeQueryParam(
283+
bucket: Bucket,
284+
value: unknown,
285+
urlKey: string,
286+
defaultValueType: string,
287+
): unknown;
288+
deserializeQueryParam(
289+
bucket: Bucket,
290+
value: unknown,
291+
urlKey: string,
292+
defaultValueType: string,
293+
): unknown;
294+
qp(bucket: Bucket): unknown;
271295
// this allows for the implementation of Route.serialize()
272-
serializeContext(bucket: RouteStateBucket, routeInfo: RouteInfo<Route>, value: unknown) => Record<string, unknown>;
296+
serializeContext(
297+
bucket: Bucket,
298+
routeInfo: InternalRouteInfo<BaseRoute>,
299+
value: unknown,
300+
): Record<string, unknown> | undefined;
273301

274302
// Actions/event handlers
275-
queryParamsDidChange(bucket: RouteStateBucket, changed: {}, totalPresent: unknown, removed: {}) => boolean | void;
276-
finalizeQueryParamChange(bucket: RouteStateBucket, params: Record<string, string | null | undefined>, finalParams: {}[], transition: Transition) => boolean | void;
303+
queryParamsDidChange(
304+
bucket: Bucket,
305+
changed: {},
306+
totalPresent: unknown,
307+
removed: {},
308+
): boolean | void;
309+
finalizeQueryParamChange(
310+
bucket: Bucket,
311+
params: Record<string, string | null | undefined>,
312+
finalParams: {}[],
313+
transition: Transition,
314+
): boolean | void;
315+
316+
getContext(
317+
bucket: Bucket,
318+
params: Record<string, unknown>,
319+
transition: Transition,
320+
): unknown;
321+
redirect(
322+
bucket: Bucket,
323+
routeInfo: RouteInfo,
324+
context: unknown,
325+
transition: Transition,
326+
): void;
327+
328+
// Route's actions: { `error`, `loading` } triggers and handlers
329+
triggerLoadingEvent(bucket: Bucket, transition: Transition): void;
330+
triggerErrorEvent(
331+
bucket: Bucket,
332+
transition: Transition,
333+
error: Error,
334+
route: unknown,
335+
): void;
336+
handleLoadingEvent(
337+
bucket: Bucket,
338+
transition: Transition,
339+
originRoute: unknown,
340+
): void;
341+
handleErrorEvent(
342+
bucket: Bucket,
343+
transition: Transition,
344+
error: Error,
345+
originRoute: unknown,
346+
): boolean;
347+
348+
getRouteInfoMetadata(bucket: Bucket): unknown;
277349
}
278350
```
279351

@@ -292,19 +364,24 @@ interface RouteManager<T extends ComponentLike<unknown>> = {
292364
Component: T;
293365
context: ReturnType<RouteManager['enter']>;
294366
bucket: RouteStateBucket;
367+
outlet: object;
295368
}
296369
}>;
297370

298-
getInvokable(
299-
bucket: RouteStateBucket,
300-
enterPromise: Promise<unknown>,
301-
): Promise<T>;
371+
getInvokable(bucket: RouteStateBucket): Promise<T>;
302372
}
303373
```
304374

305-
`getRouteWrapper` returns a component that calls the route's invokable. The router curries `@Component` (the invokable), the context, and the bucket onto it. The wrapper should be stable across renders so that the rendering layer can use identity to determine when to tear it down.
375+
`getRouteWrapper` returns a component that calls the route's invokable. The router curries `@Component` (the invokable), the context, the outlet, and the bucket onto it. The wrapper should be stable across renders so that the rendering layer can use identity to determine when to tear it down.
306376

307-
`getInvokable` returns the component for the current route. It receives the in-flight `enterPromise` so the manager can choose whether to await data before resolving, or to resolve immediately and defer loading-state handling to the wrapper. The promise is async to allow `await import()` for lazy-loaded route modules, and is never exposed elsewhere on the manager-facing API.
377+
`getInvokable` returns the component for the rendered route. The promise is async to allow `await import()` for lazy-loaded route modules, and is never exposed elsewhere on the manager-facing API.
378+
379+
- `@Component` represents a route for the currently rendered level
380+
The wrapper component can curry new args onto it. The args would typically come from the `@bucket` which is both an identity and a data holder of a given route.
381+
- `@outlet` is effectively a `@Component` of a child route
382+
The wrapper is not allowed to add new args to it. Given it's possible to zebra-stripe route-managers, the contract for `<@outlet />` might be different to what you expect at a current level.
383+
- `@context` is the resolved value of what `manager.enter()` returns.
384+
- `@bucket` is the object returns from `CreateRoute()`
308385

309386
## How we teach this
310387

@@ -328,15 +405,14 @@ This will require the Classic Route Manager to do some more elaborate internal w
328405

329406
A previous version of this RFC had a sync version of the `getInvokable()` function on the Route Manager API. This was changed to give a slightly better developer experience to allow people to absorb asynchronous imports of modules. Note: this is not intended to have any implications on the `enter()` hook and the async data loading is never intended to happen during the `getInvokable()` promise lifecycle.
330407

331-
We do not strictly need to have an async `getInvokable()` because you could always return a sync invokable that managed the async internally, i.e. using a resource-style pattern. As these APIs are quite low-level it doesn't really matter which way we lean on this decision since the complexity will never leak into Ember App Developer ergonomics.
408+
The Framework has an opinion over when `getInvokable()` should load and resolve however. Users are given a way to dynamically load contents of a given route but the Framework owns it.
332409

333410
### Merging enter() and getInvokable() hooks
334411

335412
Comments on this RFC proposed that we could unify the `enter()` and the `getInvokable()` functions. We are explicitly not merging those two functions because the `enter()` hook returns context (usually from data-loading) which is entirely separate from the concerns of `getInvokable()`.
336413

337-
Separate functions also allow for more flexible implementations of the manager lifecycle, for example you could have a manager that always resolves `getInvokable()` immediately and does not gate rendering on the result of `enter()`, or you could have a manager that waits for the result of `enter()` before resolving `getInvokable()`.
414+
It's worth noting that the promise returned by the `getInvokable()` is never exposed to any route via the Route Manager API, and will be an internal concern of the Router itself. The promise returned from `enter()` is exposed to child routes via the `getAncestorPromise()` function so they can await the result to get the context of parent routes.
338415

339-
Also, it's worth noting that the promise returned by the `getInvokable()` is never exposed to any route via the Route Manager API, and will be an internal concern of the Router itself. The promise returned from `enter()` is exposed to child routes via the `getAncestorPromise()` function so they can await the result to get the context of parent routes.
340416

341417
## Unresolved questions
342418

@@ -383,21 +459,6 @@ The model hooks are an RSVP Promise chain handled by router_js. We can put them
383459

384460
---
385461

386-
#### Updating the model for an existing route mapped to manager hooks:
387-
388-
- `willUpdate` (leaf-most)
389-
- `willTransition` event
390-
- `routeWillChange` event, router service
391-
- `update`
392-
- `beforeModel`
393-
- `model`
394-
- `afterModel`
395-
- `didUpdate` (leaf-most)
396-
- `resetController` (conditionally, if model return value changed)
397-
- `setupController` (conditionally, if model return value changed)
398-
- `didTransition` (event, leafmost)
399-
- `routeDidChange` event, router service
400-
401462
#### Mapping of existing events and methods to the new API
402463

403464
```mermaid

0 commit comments

Comments
 (0)