Skip to content

Fix hook removal semantics and related hook lifecycle bugs - #1016

Merged
hyanwong merged 12 commits into
OneZoom:mainfrom
ScaleNature:fix/hook-removal-bug
Jul 31, 2026
Merged

Fix hook removal semantics and related hook lifecycle bugs#1016
hyanwong merged 12 commits into
OneZoom:mainfrom
ScaleNature:fix/hook-removal-bug

Conversation

@ScaleNature

Copy link
Copy Markdown
Contributor

This started as a one-line bug fix.

remove_hook() had its logic inverted: asking it to remove one specific hook instead wiped out every hook registered for that event.

Fixing that bug exposed several latent issues that had been masked by the previous behavior.

  1. remove_hook() removed all hooks instead of the requested hook.
  2. Tour.js attempted to remove hooks using a different key than the one used to register them (*_custom suffix mismatch).
  3. Tour.js re-registered interaction callbacks when repeatedly assigned the PLAYING state, creating duplicate registrations.
  4. test_tour_Screensaver.js never cleaned up its Tour instances, leaking hooks into subsequent tests because the hook registry is shared across the test process.

Each issue was fixed as it was uncovered during testing.

Temporary instrumentation was used to verify the hook lifecycle while diagnosing the interaction between these issues. That instrumentation has been removed.

Testing:

  • Added regression coverage for remove_hook().
  • Updated the Screensaver tests to perform proper teardown.
  • Ran the full test suite twice after all fixes.

Result: 241/241 tests passing on consecutive full-suite runs.

Although this PR grew beyond the original one-line fix, each additional change was required to restore correct hook lifecycle behavior and keep the test suite passing once remove_hook() behaved as intended.

The remove_hook function had inverted conditional logic on line 33.

BEFORE:
  if (id) {
    hook[key] = {};  // Clears ALL hooks when id is provided
  } else {
    delete hook[key][id];  // Never reached; would cause error
  }

AFTER:
  if (!id) {
    hook[key] = {};  // Clears ALL hooks only when no id provided
  } else {
    delete hook[key][id];  // Removes the specific hook by id
  }

This bug prevented individual hooks from being removed. Callers like Tour.js
that attempted to remove specific hooks would instead clear all hooks for that
event, breaking subsequent event handlers.

Fixes: Hooks cannot be individually removed, causing memory leaks and
unexpected behavior when modules try to clean up their event listeners.
Add guard clause to prevent TypeError when attempting to remove a hook from a key
that does not exist in the hooks object. This handles edge cases where callers may
accidentally attempt to remove hooks from nonexistent keys.
When removing interaction callbacks, strip the _custom suffix from the stored key
to get the real event name that was used when registering the hook. This prevents
orphaned hooks from persisting across multiple tours, which was causing state
machine corruption and test hangs.

Lines changed: 314-319 only (remove_canvas_interaction_callbacks method)
Only call add_canvas_interaction_callbacks/remove_canvas_interaction_callbacks on
actual state transitions, not every time the state setter is called. This prevents
registering duplicate hooks when state is set to PLAYING while already PLAYING,
which was causing orphaned hooks to accumulate and leak across multiple tours.

Lines changed: 104-138 (state setter only) - now checks was_playing before calling add/remove
Both screensaver tests were leaving their Tour instances in PLAYING state,
leaking interaction hooks into the module-level singleton hook registry.
This was invisible before due to the old buggy remove_hook behavior which
incidentally swept up orphaned hooks, but now surfaces as cross-test
contamination.

Added t.tour.clear() call in both tests' .then() block before test.end()
to explicitly return the tour to INACTIVE state, triggering proper hook
cleanup via remove_canvas_interaction_callbacks().
@lentinj

lentinj commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Good catch! This all looks good to me.

@hyanwong

Copy link
Copy Markdown
Member

Great. Good catch, and thanks for the fixes. I think we can merge and see how it goes on beta.onezoom.org

@hyanwong
hyanwong merged commit e27d285 into OneZoom:main Jul 31, 2026
1 check passed
@hyanwong

Copy link
Copy Markdown
Member

P.s. looks like the tests are being helpful here. Do we need to add any more?

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.

3 participants