-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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
Introduce withSyncEvent
action wrapper utility and proxy event
object whenever it is not used
#68097
base: trunk
Are you sure you want to change the base?
Conversation
Flaky tests detected in f0fa92a. 🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/13245243863
|
I don't know the codebase, but you are on track as much as I understand the changes and their implications. Thank you for continuing to work on it. I also wanted to share that it might not be easy to get more feedback in the next 2-3 weeks as many folks, myself included, take longer holiday breaks 🎉 |
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 gave it an initial review and so far it's looking good. The approach of splitting evaluate
into 2 functions seems sound to me. I see that you haven't implemented the proxying yet, so I'm happy to take another look once that's closer to being ready.
PS. The Interactivity API is almost entirely tested using the e2e suite in test/e2e/specs/interactivity/
. For this PR, you'll probably need to modify some of the existing "test" blocks like packages/e2e-tests/plugins/interactive-blocks/directive-on/view.js
or create a new "test" block in that folder.
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.
To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
I have implemented the event proxying logic. For now, I'm warning about accessing the following without
As discussed on the issue, there may be more properties and methods to warn about, but we could always expand later. I think these 4 are a good starting point and should cover what's most common. Keep in mind that for now there is only a warning, but no functional behavior change. We can only yield before all callbacks that aren't using Review much appreciated! |
Any chance I can get 1-2 reviews for the PR this week? That would be super helpful so we could ideally land this in Gutenberg soon. Thank you! 🙌 |
…y to still be recognized as generator functions.
Co-authored-by: Weston Ruter <[email protected]>
…nberg into add/64944-with-sync-event
…tained when proxying functions via withScope.
I have done some extensive testing on the code changes and added testing instructions to the PR description. I identified one crucial bug, which was fixed (see #68097 (comment)). But everything is working well now, and you can work through the testing instructions to validate. |
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, but I defer to deeper review from the Interactivity API team.
Any chance you can take another look at this @michalczaplinski? |
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.
Looks good to me! 🙂
I only have one request: instead of creating two separate functions, we can pass an options object to evaluate
. It's a pattern we're already using in other parts of the Interactivity API. This way, we not only simplify the code, but we also open the door to adding more options in the future in a simple way.
As evaluate
is not publicly exposed yet, we can introduce breaking changes without any issues.
Perhaps we could start with these two options:
const result = evaluate( entry, {
resolveFunctions: false,
arguments: [ ... ], // Arguments passed to the resolved function.
} );
Or we could even not add the option of arguments yet and, if someone needs to pass arguments, use the option resolveFunctions: false
:
const cb = evaluate( entry, { resolveFunctions: false } );
cb( event );
EDIT: I guess e2e tests can be added once we move from deprecation to error, right? What do you think?
packages/interactivity/src/utils.ts
Outdated
export function withSyncEvent( callback: Function ): SyncAwareFunction { | ||
let wrapped: SyncAwareFunction; | ||
|
||
if ( callback?.constructor?.name === 'GeneratorFunction' ) { | ||
wrapped = function* ( this: any, ...args: any[] ) { | ||
yield* callback.apply( this, args ); | ||
}; | ||
} else { | ||
wrapped = function ( this: any, ...args: any[] ) { | ||
return callback.apply( this, args ); | ||
}; | ||
} | ||
|
||
wrapped.sync = true; | ||
return wrapped; | ||
} |
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 don't you simply add the sync
property to the original function? Do you want to prevent modifying it?
export function withSyncEvent( callback: Function ): SyncAwareFunction { | |
let wrapped: SyncAwareFunction; | |
if ( callback?.constructor?.name === 'GeneratorFunction' ) { | |
wrapped = function* ( this: any, ...args: any[] ) { | |
yield* callback.apply( this, args ); | |
}; | |
} else { | |
wrapped = function ( this: any, ...args: any[] ) { | |
return callback.apply( this, args ); | |
}; | |
} | |
wrapped.sync = true; | |
return wrapped; | |
} | |
export function withSyncEvent( callback: Function ): SyncAwareFunction { | |
( callback as SyncAwareFunction ).sync = true; | |
return callback as SyncAwareFunction; | |
} |
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.
Great idea! I thought initially that wrapping it was safer than directly altering it, but since the callback
function would be usually just declared anonymously and immediately passed to withSyncEvent
, there's probably no side effects of directly altering it.
Most importantly, I agree with your suggestion because it makes the logic much simpler and more error-proof, maintaining whichever type the original function had before passing it to withSyncEvent
, instead of trying to recreate/guess it.
I've amended this in f0fa92a, alongside minor wording adjustments based on the change.
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.
Great idea! I thought initially that wrapping it was safer than directly altering it, but since the
callback
function would be usually just declared anonymously and immediately passed towithSyncEvent
, there's probably no side effects of directly altering it.
The key word here is usually, right? You could define a named function in the module and then reference it here in withSyncEvent()
. What if you then use that same function for an async event? Then it would eventually result in that code not being able run asynchronously, since the function has the sync
flag set on it.
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 I agree it wouldn't be 100% guaranteed. But given the wrapping requires other quirks that also don't make it 100% reliable (not every function is a plain function or has constructor GeneratorFunction
), I think avoiding the wrapping is safer for the vast majority of use-cases.
Specifically to your example, I have a hard time seeing that being common: First, because if the function is the same and needs synchronous event
access, why would it elsewhere not access a synchronous event
property? Second, all the Interactivity API examples and Core code define the actions right on the object, so it's not encouraged to do it differently anywhere.
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 also think that the likelihood of the same function being executed synchronously and asynchronously at the same time must be really low, because it would have to include conditional code and depend on the place of the DOM where it is executed. It is not impossible and I'm sure there is some case, but in that case, maybe it would even be safer to always run it synchronously.
That said, I don't oppose leaving that part of the code as it is. Up to you 🙂
@luisherranz Thanks for the review!
Hmm, I'm not sure about that idea, given that the
For what kind of interactions would you like to see e2e tests? Not arguing against it at all, just wondering since I'm personally not very familiar with writing e2e tests. |
For me, it's fine if we increase the complexity in the runtime of the Interactivity API to simplify the external APIs that the extenders will use. Keep in mind that, although it is still far off in the roadmap, the idea is to let the extenders create their own directives someday. So for them, in my opinion, it will be more complex to use multiple functions than to pass options to a single function, especially if we keep adding more options in the future. |
Basically, a test that verifies that if one of the synchronous functions is executed without the wrapper, the deprecation is shown correctly. We can leave it for the next phase, anyway. |
I think at this point this is mostly a theoretical concern. At the moment, none of these functions is externally available, so introducing a boolean flag would only increase complexity. So for that reason I don't think it makes sense to change it now, and we could revisit this again if and once we make those functions externally available. The way it currently works in this PR could easily be adjusted later if we decide to.
👍 Makes sense, thanks! |
I don't like the idea of leaving things in a state that will require changes later. The fewer adjustments we have to make once this is public, the better. It's also important that we, the WordPress Core contributors working on the directives, be the first to use these functions in what we believe is their final form, so we can see how well they fit and how easy they are to use. Before these functions go public, we might discover that relying on a single function with an options object isn’t the best approach after all—but we’ll only know by trying it out ourselves. |
Fair point, let's try to figure it out now. You mentioned before:
Going back to my previous point: Using boolean flags increases the complexity of the code. This also impacts those that use the function(s). Today, if you see the But let's take a step back first, I have a follow up questions to your idea from #68097 (review): How would |
What do you mean? This is trivial to implement on top of the original function: 3dc8989 |
Your code clarifies what I was asking before: Before you were using the name Regarding 3dc8989, I think this could work in principle, but I see two problems with it that the current PR implementation and
To be clear, I'm not fundamentally opposed to your suggestion, but these two drawbacks would need to be addressed to make it beneficial over the current implementation. |
Sorry! My bad. I'm glad it's clear now 🙂
See this comment. But... I've been thinking, and I think we should make things right and stop invoking functions in A bit of context: I should have made // Old Store API
store({
myPlugin: {
state: {
someNumber: 1,
},
selectors: {
sumNumbers: ({ state, context }) => {
return state.someNumber + context.someNumber;
},
},
actions: {
someAction: (store) => {
const { selectors } = store;
selectors.sumNumbers(store);
},
},
},
});
// New Store API
const { state } = store("myPlugin", {
state: {
someNumber: 1,
get sumNumbers(): number {
const { someNumber } = getContext();
return state.someNumber + someNumber;
},
},
actions: {
someAction: () => {
state.sumNumbers;
},
}); // With old Store API
<span data-wp-text="myPlugin.selectors.sumNumbers"></span>
// With new Store API
<span data-wp-text="state.sumNumbers"></span> With the new Store API, there's no need for all the directives that need a value, like const cb = evaluate( entry );
cb( ...args ); // Pass whatever args they need. The problem is that, from time to time, people who still don't know how to use the Interactivity API well yet, use functions instead of getters, and as they work fine, they keep using them (for example). So, to make this 100% right, I'm thinking now that we should follow the same strategy used here for async actions and actually deprecate the fact that directives that are meant to receive a value invoke functions at all. Of course, that is out of scope for this PR, but maybe we can take the first steps here? It should be something as simple as this: f0525ed (not tested) Let me know what you think 🙂 |
That makes a lot of sense now. Thanks for explaining in more detail!
This looks good to me for the most part. The only thing I find a bit confusing for I think the most straightforward solution for this PR is to continue invoking functions in In other words there would be three possible scenarios for an entry for
By doing that, we don't (immediately) break the pattern of Then we can have a separate PR for a later Gutenberg release for WordPress 6.9 that removes |
…ion prior to evaluating it." This reverts commit dba93ec.
…for BC) and move responsibility to the caller.
@luisherranz I reverted the addition of the two new lower-level functions and instead implemented in 0dbd15f the change of no longer invoking functions in This is similar to the example code you provided in f0525ed, just with a few additions to prevent errors, e.g. we have to check for whether the Having these checks everywhere is a bit annoying in terms of our codebase, so ideally we can improve this in the future. One of the reasons for those checks is that The overall behavior remains the same as before, it's just a different implementation under the hood. Update: It looks like something in this change is causing all sorts of e2e tests for the Interactivity API to fail. Maybe you find something in my implementation that's off and can easily be fixed. But this makes me think that it may be better to not change what |
This implements the 1st step from #64944. Please see the issue for additional context on the rationale and overarching approach for this.
Relevant technical choices
evaluate
function resolves the entry into the relevant data, but then also immediately evaluates it.evaluate
function will not only "look up" the callback, but also call it.event
object based on whether the callback is wrapped inwithSyncEvent
prior to calling the callback, theevaluate
function is not sufficient.splitTask()
before running the callback function (see 2nd step from IntroducewithSyncEvent
and require Interactivity API actions that use theevent
object to use it #64944).resolveEntry
andevaluateResolved
. If they are called right after each other, it's effectively the same asevaluate
. In fact, this PR updatesevaluate
to rely on these two lower-level functions, to avoid duplicate code.evaluate
as is, which is more ergonomic. However theon
(but noton-async
) directives require the more granular access of the callback first, to only execute it later.Note to reviewers
Because several files are touched, but only some specific changes are non-trivial, I would advise to review the commits individually at first. That should help convey the bigger picture than looking at the whole PR diff at once.
Testing instructions
Tab
key and ensure no warnings are present in the browser console.handleKeydown
action of thecore/image
block's store passes its callback throughwithSyncEvent
.handleKeydown
action was doing it wrong: Editpackages/block-library/src/image/view.js
and remove thewithSyncEvent
usage from thehandleKeydown
callback, so that it is no longer wrapped. Don't forget to rerun the JS build.withSyncEvent
needs to be used.openSearchInput
action of thecore/search
block's store passes its callback throughwithSyncEvent
.packages/block-library/src/search/view.js
and remove thewithSyncEvent
usage from theopenSearchInput
callback, so that it is no longer wrapped. Don't forget to rerun the JS build.withSyncEvent
needs to be used.