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

Remove Promise support from core, add async() directive. #555

Merged
merged 12 commits into from
Nov 29, 2018

Conversation

justinfagnani
Copy link
Collaborator

@justinfagnani justinfagnani commented Oct 10, 2018

Removes built-in Promise support. Adds an async() directive as a successor to until() that is required to render Promises. async() handles race conditions, works with attribute, boolean attribute, property and event bindings. It also works as a nested directive value with other directives like repeat().

While this only removes a small amount of current core library code, more core code would have be required to handle attributes, etc., and the race conditions, mainly because current Part factoring has us sharing very little code between Part implementations. It’s possible that we could refactor Parts in the future to share more value-handling code and bring back built-in Promise support in the future. Adding that would not be a breaking change, but we need to remove support before 1.0 since removing later would be a breaking change.

This change keeps directives/until.js to re-export async to ease the transition. It’s deprecated and will be removed before 1.0.

Fixes #550
Closes #272, #544, #451 and #458

src/directives/async.ts Outdated Show resolved Hide resolved
src/directives/async.ts Outdated Show resolved Hide resolved
return;
}
promisedValue.value = value;
part.setValue(value);
Copy link
Contributor

@bgotink bgotink Oct 10, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if the value of the part has changed to a non-async value by now? then callback checks whether a new async(promise) was passed but it doesn't check for non-async(promise) value

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't currently supported. There's really no way to tell that this directive has been removed - it has to be used somewhat statically.

Copy link
Contributor

@bgotink bgotink Oct 11, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I though we could solve it by adding a new property to part, readonly originalValue: any, which would represent the value that gets set from the template regardless of whether it's a directive or something else entirely. That would allow the async directive to check whether it's still the active value in the case of a template like

html`${async(somePromise, 'temp')}`

but it would only work if until is used directly inside of the part, not in cases where it's wrapped in another directive, e.g.

html`${
  when(value instanceof Promise, () => async(value, 'temp'), () => value)
}`

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is something to be designed and solved for directives in general, not just async. Right now there are lots of sharp edges when directives aren't essentially "static" and top-level. Replacing directives can lead to other issues too.

interface PromisedValue<T = unknown> {
promise: Promise<T>;
resolved: boolean;
value?: T;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This property is unused (it's set at line 55 but never read)
Did you mean to add

if (promisedValue.resolved) {
  part.setValue(promisedValue.value);
}

in the if-check at lines 33-41?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, turns out it's just not needed

part.setValue(defaultContent);
}

Promise.resolve(promise).then((value: any) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is an extra Promise.resolve needed? Is that to support non-promise values?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, and mostly this is safer than blindly calling .then()

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In Canary I have queueMicrotask which does the same. Maybe it makes sense to call that when available, as it might have slightly less overhead

https://www.chromestatus.com/features/5111086432911360

@justinfagnani
Copy link
Collaborator Author

@sorvell @kevinpschaaf PTAL

Copy link
Member

@kevinpschaaf kevinpschaaf left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor suggestion for code reduction/readability, otherwise LGTM.

// The following code only ever runs once per Promise instance
promisedValue = {promise, resolved: false};
promises.set(part, promisedValue);
if (defaultContent !== noChange) {
Copy link
Member

@kevinpschaaf kevinpschaaf Oct 19, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This factoring is slightly more readable to me and avoids duplicating the part.setValue(defaultContent) code (sorry for rewriting, it was easier than explaining the change):

export const async = directive(
    (promise: Promise<any>,
     defaultContent: unknown = noChange) => (part: Part) => {
      let promisedValue = promises.get(part);
      // If the promise changed, record the new promise and await it
      if (promisedValue === undefined || promisedValue.promise !== promise) {
        promisedValue = {promise, resolved: false};
        promises.set(part, promisedValue);
        Promise.resolve(promise).then((value: unknown) => {
          const currentPromisedValue = promises.get(part);
          promisedValue.resolved = true;
          if (currentPromisedValue === promisedValue) {
            part.setValue(value);
            part.commit();
          }
        });
      }
      // If the promise has not yet resolved, set/update the defaultContent
      if (!promisedValue.resolved && defaultContent !== noChange) {
        part.setValue(defaultContent);
      }
    });

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Used your code. Thanks :)

if (currentPromisedValue !== promisedValue) {
return;
}
part.setValue(value);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gah, also looks like we're forgetting to set promisedValue.resolved = true, right? Should add a test that catches this case? Or did I miss something?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added test that fails without setting resolved

Copy link
Collaborator Author

@justinfagnani justinfagnani left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// The following code only ever runs once per Promise instance
promisedValue = {promise, resolved: false};
promises.set(part, promisedValue);
if (defaultContent !== noChange) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Used your code. Thanks :)

if (currentPromisedValue !== promisedValue) {
return;
}
part.setValue(value);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added test that fails without setting resolved

@justinfagnani
Copy link
Collaborator Author

@kevinpschaaf the code as currently intended to work should be good now, but I'm not 100% sure on the until-style API.

One API share we've talked about is accepting a varargs list of Promises:

html`${async(result1, result2, fallback)}`

Where result1 is prefered over result2 which is prefered over fallback. Any value could be sync or async. The defaultContent parameter would no longer be special, just assumed to be sync or a faster-resolving Promise.

Another potential change is that I think we might want to also support passing a function and invoking it. This will make using async functions easier, since they won't have to be immediate invoked:

<div>
  html`${async(async () => {
    const result = await fetchSomething();
    return html`<h1>${result.title}</h1>`;
  })}`
</div>

The problem there is the unfortunate async(async run. This makes me question the name...

It also kind of makes me want to keep text-position Promise support in core, and even expand text-position types to invoke functions, since they don't render to anything useful anyway. Then we support this:

<div>
  html`${async () => {
    const result = await fetchSomething();
    return html`<h1>${result.title}</h1>`;
  }}`
</div>

But I'm still apprehensive about the usability danger of Promises being supported in text-position but not attribute position. That could be added later too...

@justinfagnani
Copy link
Collaborator Author

Coming back around to this. My previous comment had a few problems, mainly that we don't want to support invoking an async function each render. That would produce a new Promise each render and possibly re-render the placeholders while it resolves. The runAsync() directive generator pattern is the safe way to do that. This means the async name is fine.

I do think it's better to accept varargs of Promise or sync values and have them cascade in priority from left to right. I'll see if that's diable in a simple implementation.

I'll update, rebase and ping when done.

@justinfagnani justinfagnani changed the title Move Promise support to directives. Remove Promise support from core, add async() directive. Nov 25, 2018
@justinfagnani
Copy link
Collaborator Author

@kevinpschaaf PTAL

I got the generalized version working so that async() accepts any number of Promises and non-Promises and renders them in priority order. Handling cases where the arguments change over time is not incredibly obvious, but wasn't that bad either, so I added that with hopefully enough comments to explain and test coverage for it.

Copy link
Member

@kevinpschaaf kevinpschaaf left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM after adding test for rendering unresolved promises twice while swapping positions.

Removes built-in Promise support. Adds an async() directive as a successor to until() that is requires to render Promises. async() handles race conditions, works with attribute, boolean attribute, property and event bindings. It also works as a nested directive value with other directives like repeat().

While this only removes a small amount of current core library code, more core code would have be required to handle attributes, etc., and the race conditions, mainly because current Part factoring has us sharing very little code between Part implementations. It’s possible that we could refactor Parts in the future to share more value-handling code and bring back built-in Promise support in the future. Adding that would not be a breaking change, but we need to remove support before 1.0 since removing later would be a breaking change.

This change keeps directives/until.js to re-export async to ease the transition. It’s deprecated and will be removed before 1.0.
@justinfagnani justinfagnani force-pushed the async-directive branch 3 times, most recently from f5858f7 to d8bbf9e Compare November 28, 2018 23:48
@justinfagnani justinfagnani merged commit a219606 into master Nov 29, 2018
@dorivaught dorivaught added this to the 1.0.0 milestone Dec 3, 2018
@justinfagnani justinfagnani deleted the async-directive branch February 9, 2019 23:52
neuronetio pushed a commit to neuronetio/lit-html that referenced this pull request Dec 2, 2019
Removes built-in Promise support. Requires the until() directive to handle Promises. until() is updated to handle race conditions, work with attributes, boolean attributes, properties and event bindings.

While this only removes a small amount of current core library code, more core code would have be required to handle attributes, etc., and the race conditions, mainly because current Part factoring has us sharing very little code between Part implementations. It’s possible that we could refactor Parts in the future to share more value-handling code and bring back built-in Promise support in the future. Adding that would not be a breaking change, but we need to remove support before 1.0 since removing later would be a breaking change.
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

Successfully merging this pull request may close these issues.

7 participants