Skip to content

Conversation

@bausmeier
Copy link

Event handlers registered after a once handler were being silently skipped, causing unpredictable behaviour in tour interactions. This occurred because modifying an array during iteration is unsafe.

Making a copy of the bindings array before iteration prevents these handers from being skipped.

Event handlers registered after a `once` handler were being silently
skipped, causing unpredictable behaviour in tour interactions. This
occurred because modifying an array during iteration is unsafe.

Making a copy of the bindings array before iteration prevents these
handers from being skipped.
@vercel
Copy link

vercel bot commented May 27, 2025

@bausmeier is attempting to deploy a commit to the shipshapecode Team on Vercel.

A member of the Team first needs to authorize it.

@vercel
Copy link

vercel bot commented May 27, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
shepherd-docs ✅ Ready (Inspect) Visit Preview 💬 Add feedback May 27, 2025 6:01pm

trigger(event: string, ...args: any[]) {
if (!isUndefined(this.bindings) && this.bindings[event]) {
this.bindings[event]?.forEach((binding, index) => {
this.bindings[event]?.slice().forEach((binding, index) => {
Copy link
Author

Choose a reason for hiding this comment

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

Another approach I considered was to iterate the bindings in reverse order with a for loop. This would allow us to avoid making a copy of the array, but could have an observable impact for existing users of the package. I thought it would be best to avoid what could be considered a breaking change.

Copy link
Member

Choose a reason for hiding this comment

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

@bausmeier what if instead of creating a copy, we made a new array like const listenersToRemove = [] and then pushed the ones to remove into that, then after the forEach is done remove them all?

@RobbieTheWagner
Copy link
Member

@bausmeier sorry for the delay in reviewing this! Could you please explain the exact problem a bit more?

@bausmeier
Copy link
Author

bausmeier commented Jul 2, 2025

@RobbieTheWagner Sure thing. Sorry I didn't explain more clearly in the first place.

In our project, we need to listen for the same event in multiple places (analytics and business logic), and in the particular case where this problem arises we're listening for the complete event using once in one place and on in another.

As an illustrative example, we call .once('complete', recordCompletion) before .on('complete', sendAnalytics) on the same tour and expect that both of the listeners will be called when the tour completes, but what we're seeing is that recordCompletion is called and sendAnalytics isn't. If we use on in both places or call .on('complete', sendAnalytics) before .once('complete', recordCompletion) then both listeners are called as expected, but neither of those workarounds is feasible for us.

Hope that clarifies things. If you need an example to reproduce this I can try and put something together, but I was able to write a failing test so didn't think that was necessary.

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.

2 participants