-
Notifications
You must be signed in to change notification settings - Fork 918
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
Conversation
d1871ae
to
88a967c
Compare
src/directives/async.ts
Outdated
return; | ||
} | ||
promisedValue.value = value; | ||
part.setValue(value); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)
}`
There was a problem hiding this comment.
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.
src/directives/async.ts
Outdated
interface PromisedValue<T = unknown> { | ||
promise: Promise<T>; | ||
resolved: boolean; | ||
value?: T; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
src/directives/async.ts
Outdated
part.setValue(defaultContent); | ||
} | ||
|
||
Promise.resolve(promise).then((value: any) => { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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()
There was a problem hiding this comment.
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
d22863e
to
4369e05
Compare
@sorvell @kevinpschaaf PTAL |
There was a problem hiding this 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.
src/directives/async.ts
Outdated
// The following code only ever runs once per Promise instance | ||
promisedValue = {promise, resolved: false}; | ||
promises.set(part, promisedValue); | ||
if (defaultContent !== noChange) { |
There was a problem hiding this comment.
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);
}
});
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Used your code. Thanks :)
src/directives/async.ts
Outdated
if (currentPromisedValue !== promisedValue) { | ||
return; | ||
} | ||
part.setValue(value); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@kevinpschaaf PTAL
src/directives/async.ts
Outdated
// The following code only ever runs once per Promise instance | ||
promisedValue = {promise, resolved: false}; | ||
promises.set(part, promisedValue); | ||
if (defaultContent !== noChange) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Used your code. Thanks :)
src/directives/async.ts
Outdated
if (currentPromisedValue !== promisedValue) { | ||
return; | ||
} | ||
part.setValue(value); |
There was a problem hiding this comment.
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
@kevinpschaaf the code as currently intended to work should be good now, but I'm not 100% sure on the One API share we've talked about is accepting a varargs list of Promises: html`${async(result1, result2, fallback)}` Where 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 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... |
ac8f1ac
to
2fd1c3e
Compare
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 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. |
2fd1c3e
to
80cedcf
Compare
@kevinpschaaf PTAL I got the generalized version working so that |
There was a problem hiding this 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.
f5858f7
to
d8bbf9e
Compare
d8bbf9e
to
86e4768
Compare
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.
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