Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Resolve notes #2

Open
christopherthielen opened this issue Aug 26, 2014 · 10 comments
Open

Resolve notes #2

christopherthielen opened this issue Aug 26, 2014 · 10 comments

Comments

@christopherthielen
Copy link
Collaborator

$transition$ injection

Options:

  • 1 Manually manage Injection as a local - how $stateParams is currently injected
  • 2 Inject as root.resolve { $transition$: transitionObject }. This would require us to re-build the locals for each element in the path during each transition (as opposed to storing them on state object and re-using during the next transition). Before each transition, root.resolve.$transition$ is set, then deleted when the transition is complete.
  • 3 Decorate $injector. I'm using this in UI-Router Extras and it has the added bonus that as long as the $injector is decorated, all functions can receive $transition$, not just the ones ui-router explicitly injected via locals.

Resolve process

$view resolves

  • Decouple state resolves from view resolves.
    • View resolves should be triggered when $view service is triggered, not while the state is being resolved

paths

  • I think the Path constructor is trivial and simply makes references to the state[]s in its own data structure. I think inherited locals are injected later, only when we begin the resolve process.
    • I think Path has a function which takes the inherited locals available to be injected into the first node, then starts executing resolve functions.
  • I think Paths have path elements, one for each state in the constructor. Path elements probably start with just the state (and maybe the state's resolves).
  • When Path.startResolvingStuff is invoked, it begins invoking the first Path Element's resolve functions.
  • I'm assuming the existing resolveState() code can be reused for each Path Element, in a very similar way to how it's being used currently for the entire transition. That is, until we can simplify it, rip out the view locals stuff, and perhaps make it easier to comprehend.

incremental

  • State resolves can be resolved incrementally during transition.
    • Each path element will have their associative array of resolve functions and/or pre-resolved objects. This array can be $q.all()'d by the caller of Path.startResolvingStuff to trigger the view service to start rendering when all promises of one path element are completely resolved. Alternatively, the caller could wait for the entire path to be resolved before triggering the view service by $q.all()'ing the path's overall promise.
  • Wild thought: There is the potential for a Policy that either resolves ALL of a path element before proceeding to the next path element, or one that resolves any resolve in the chain as soon as its dependencies are ready.

We should be able to handle most of this by carefully constructing the resolve chains, then operating on a per-path-element or whole path basis when resolving and triggering the $view service.

That's all I can muster for now; it's been another long-ass day 👎

@nateabele
Copy link
Owner

$transition$ injection

Currently, I'm most partial to (1). I'm a bit skeptical of (3), as it seems like it's exposing essentially transient data to potentially persistent code, but maybe I'm thinking about it wrong. I'll take a look at the extras code.

View resolves should be triggered when $view service is triggered, not while the state is being resolved

Agreed. The only real consideration here is that, at some point in the near future, we're going to want to make the pipeline configurable as to whether the template should be rendered (empty) before the resolves (hence, the controller) are available.

I think the Path constructor is trivial and simply makes references to the state[]s in its own data structure. I think inherited locals are injected later, only when we begin the resolve process.

Yeah, my thought on that was that the logic in Path could use $injector.annotate() to get the dependencies for a function attached to a state, then iteratively walk up the path, constructing a dependency tree using $q.all(). If Path caches each of those steps internally, then building the tree for each function call should be fairly direct and reasonably performant (pretty much what you say further down, now that I've read to that point).

I think Paths have path elements, one for each state in the constructor. Path elements probably start with just the state (and maybe the state's resolves).

Yeah, that's pretty close to what I was thinking. Maybe something like Path.exec(fn, state, locals)?

I'm assuming the existing resolveState() code can be reused for each Path Element

I'd rather rip it all apart now if possible, and do away with locals entirely. I think the only question is around figuring out how to coordinate that with $view inside $state, but once we start solidifying Path's API, those answers will emerge naturally.

Wild thought: There is the potential for a Policy that either resolves ALL of a path element before proceeding to the next path element, or one that resolves any resolve in the chain as soon as its dependencies are ready.

What would be the use case for per-level blocking?

@christopherthielen
Copy link
Collaborator Author

What would be the use case for per-level blocking?

I was thinking along the lines of some level of the chain wanting to reject the transition. Let's say there is a transition from A to A.B.C.D. If the resolves at A.B.C and/or A.B.C.D are expensive, but a listener at A.B will potentially reject the transition, a policy that waits for each level could eliminate the expensive resolves.

Upon further thought, this same behavior can be attained by simply injecting a rejected resolve from A.B into A.B.C, so scratch that use case.

@christopherthielen
Copy link
Collaborator Author

except that you'd have to inject the A.B resolve into every expensive resolve in A.B.C and below; hmmm maybe there's still some value to the thought.

@christopherthielen
Copy link
Collaborator Author

make the pipeline configurable as to whether the template should be rendered (empty) before the resolves (hence, the controller) are available.

I don't understand this; what's the use case for rendering a template before the resolves?

Maybe something like Path.exec(fn, state, locals)?

I'm not following... Are you thinking Path (capital P, "class"), path (lowercase p, "instance"), or pathelement? What is fn, a resolve fn?

Edit: Ah, I think you're responding to my comment:

I think Path has a function which takes the inherited locals available to be injected into the first node, then starts executing resolve functions.

Yeah I think we're thinking along similar lines. I might have some time this weekend to do some coding.

I'd rather rip it all apart now if possible, and do away with locals entirely.

Cool

@nateabele
Copy link
Owner

except that you'd have to inject the A.B resolve into every expensive resolve in A.B.C and below; hmmm maybe there's still some value to the thought.

If an A.B listener rejects the transition, resolves further down the chain would never be triggered. Iteratively building the dependency tree on-demand means that resolves are always as lazy as possible, and only ever triggered by an injection, so if they're never used, they're never loaded.

I don't understand this; what's the use case for rendering a template before the resolves?

UX & mobile-first guys talk about rendering the structure of a page immediately, then filling in content as it becomes available. Facebook is actually a pretty good demonstration of this. When you first load it up, you see empty story cards with placeholders for photos and text, which fill in a moment later.

@christopherthielen
Copy link
Collaborator Author

If an A.B listener rejects the transition, resolves further down the chain would never be triggered.

So, if I understand you correctly, you do want all of A.B resolves to load before resolving items on A.B.C and A.B.C.D? This is the behavior I meant by "resolves ALL of a path element before proceeding to the next path element".

The other option would be to eager load any resolve that has all its deps satisfied.

A more concrete example might be:

state: { name: 'patient', resolve: { patient: fn($stateParams){} }, url: '/patient/:patientid' };
state: { name: 'patient.encounter', resolve: { encounter: fn($stateParams){} }, url: '/encounter/:encounterid' };
state: { name: 'patient.encounter.document', resolve: { document: fn($stateParams){} }, url: '/document/:documentid' };

In this example, one of the resolves depend on the other resolves, only on state params. When we transition from root to patient.encounter.document with sample browser url: /patient/26487357/encounter/687354984/document/5873458768.

We can either:

  • resolve all of patient state, then resolve all of encounter state, then all of document state.
  • OR, we can let them all start resolving as soon as their deps (just $stateParams in this example) are available. That way, the 3 resources from different Path Elements could be theoretically fetched in parallel.

rendering the structure of a page immediately, then filling in content as it becomes available

Gotcha.

@nateabele
Copy link
Owner

If an A.B listener rejects the transition, resolves further down the chain would never be triggered.

So, if I understand you correctly, you do want all of A.B resolves to load before resolving items on A.B.C and A.B.C.D? This is the behavior I meant by "resolves ALL of a path element before proceeding to the next path element".

Ah, sorry for not being clear. What I actually meant was, a resolve should never be loaded unless it's depended on by an injectable function. I inferred 'listener' to mean an onEnter function. So, all of A.B's resolves would only be resolved if they were included in A.B's onEnter.

Hope that makes sense.

@christopherthielen
Copy link
Collaborator Author

a resolve should never be loaded unless it's depended on by an injectable function.

That seems to violate "least astonishment" for me. When I add a resolve for a state, I expect it to be resolved when I enter that state, full stop.

I've got some code started (WIP) that could support your use case, however. I'll commit to a branch for you to check out. Let me know if this is along the lines of what you're thinking.

@christopherthielen
Copy link
Collaborator Author

Iteratively building the dependency tree on-demand means that resolves are always as lazy as possible, and only ever triggered by an injection, so if they're never used, they're never loaded.

It's super duper lazy now.

@nateabele
Copy link
Owner

It's super duper lazy now.

https://www.youtube.com/watch?v=IhnUgAaea4M

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants