Skip to content

Fix node_removed event sent before BasicInformation Leave event#439

Closed
markvp wants to merge 1 commit intomatter-js:mainfrom
markvp:worktree-fix-node-removed-event-order-75
Closed

Fix node_removed event sent before BasicInformation Leave event#439
markvp wants to merge 1 commit intomatter-js:mainfrom
markvp:worktree-fix-node-removed-event-order-75

Conversation

@markvp
Copy link
Copy Markdown
Contributor

@markvp markvp commented Mar 24, 2026

Summary

  • Defers nodeDecommissioned emission via queueMicrotask() so that events emitted synchronously in the same call stack (e.g. the BasicInformation Leave event that triggers decommissioning from another controller) are relayed to WebSocket clients before node_removed
  • Cleans up internal state (#nodes, #customClusterPoller) in the decommissioned handler so that nodes removed by an external controller are cleaned up identically to the WebSocket remove_node command path
  • Wraps the deferred microtask body in try/catch to prevent uncaught exceptions from crashing the process
  • Adds unit tests for the decommission event ordering pattern

Fixes #75

Details

When another controller (e.g. Apple Home) removes a node from the fabric, matter.js's PairedNode processes the Leave event in this order within a single call stack:

  1. _triggerEventUpdate() fires the cluster-level Leave event → Peers.#onLeave()decommissioned.emit() (synchronous)
  2. eventTriggered.emit() fires the PairedNode-level event

Since ControllerCommandHandler relayed both synchronously, node_removed reached WebSocket clients before the Leave node_event. By deferring nodeDecommissioned to a microtask, the synchronous eventTriggered completes first.

Internal state cleanup

Previously, the decommissioned event handler only emitted nodeDecommissioned to notify WebSocket clients but did not clean up #nodes or #customClusterPoller. This meant nodes removed by an external controller left stale entries in server state. The handler now mirrors the cleanup performed by decommissionNode().

Offline removal via WebSocket command

When a node is removed via the WebSocket remove_node command while the device is offline, no Leave event is emitted — only node_removed. This is correct behaviour because events.decommissioned is always emitted regardless of the removal path:

  1. External controller: Leave event → Peers.#onLeave()decommissioned.emit()
  2. WebSocket command (online): PairedNode.decommission()decommissioned.emit()
  3. WebSocket command (offline): PairedNode.close(true)decommissioned.emit()

The Leave event is purely informational for clients — the server performs no special processing on it. node_removed is the authoritative signal that cleanup is complete.

Test plan

  • npm run format passes
  • npm run lint passes
  • npm run build passes
  • npm test — ws-controller (3/3 new + existing), ws-client (43/43) pass; integration test failures are pre-existing on main
  • New tests verify:
    • Event ordering: node_event (Leave) arrives before node_removed when both fire synchronously
    • Offline removal: node_removed is emitted even when no Leave event fires
    • Error resilience: listener exceptions are caught and don't crash the process

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings March 24, 2026 09:54
@markvp markvp marked this pull request as draft March 24, 2026 09:55
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Defers nodeDecommissioned emission to a microtask so that synchronous node events (notably BasicInformation Leave) are relayed to WebSocket clients before node_removed, ensuring node_removed is always the final event for a node (fixes #75).

Changes:

  • Wrap nodeDecommissioned emission in queueMicrotask() to reorder event delivery across the same call stack.

Comment thread packages/ws-controller/src/controller/ControllerCommandHandler.ts Outdated
@markvp markvp force-pushed the worktree-fix-node-removed-event-order-75 branch from b10e4b0 to 29808cb Compare March 24, 2026 10:13
When another controller removes a node from the fabric, defer
nodeDecommissioned emission via queueMicrotask so that any events
emitted synchronously in the same call stack (e.g. the BasicInformation
Leave event) are relayed to WebSocket clients before node_removed.

Also clean up internal state (#nodes, #customClusterPoller) in the
decommissioned handler so that nodes removed by an external controller
are cleaned up identically to the WebSocket remove_node command path.

Wrap the deferred microtask body in try/catch to prevent uncaught
exceptions from crashing the process if a listener throws.

Add unit tests for the decommission event ordering pattern.

Fixes matter-js#75

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@markvp markvp force-pushed the worktree-fix-node-removed-event-order-75 branch from 29808cb to 466b1ca Compare March 24, 2026 10:19
@markvp markvp marked this pull request as ready for review March 24, 2026 10:21
Copy link
Copy Markdown
Collaborator

@Apollon77 Apollon77 left a comment

Choose a reason for hiding this comment

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

Honestly ... this PR is difficult because this test does not at all test the reality but just takes assumptions. So when we need to very this then via enhancing the integration test.

And even then I am unsure if I like this solution because this is still hacky and only works in very defined considerations that might not match the reality

@Apollon77 Apollon77 closed this Apr 9, 2026
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.

Change order for "node_removed" and Basic Information Cluster Leave events

3 participants