Skip to content

Fix ComponentSystemRegistry.remove leaving the system in the list - #9280

Merged
willeastcott merged 6 commits into
mainfrom
fix-component-system-registry-remove
Sep 2, 2026
Merged

Fix ComponentSystemRegistry.remove leaving the system in the list#9280
willeastcott merged 6 commits into
mainfrom
fix-component-system-registry-remove

Conversation

@willeastcott

@willeastcott willeastcott commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Description

ComponentSystemRegistry.remove() never removed the system from list.

The array lookup was evaluated after the id had already been deleted:

delete this[id];

// Update the component system array
const index = this.list.indexOf(this[id]);  // indexOf(undefined) -> -1
if (index !== -1) {
    this.list.splice(index, 1);
}

this[id] is undefined by the time indexOf runs, so the splice never
executed and the removed system stayed in the array. Since destroy() iterates
list, an already-removed system was still destroyed on app teardown.

This PR performs the splice before deleting the id — the fix is a pure reorder.

There are no in-tree callers of remove(), so this only affects code that
unregisters a custom component system.

Adds test/framework/components/registry.test.mjs covering add(), remove()
and destroy(). Three of its eight cases fail without the fix, including one
asserting that a removed system is not destroyed on teardown.

Type declaration tidy-up

While in the file, three annotations that were producing weak declarations:

  • add() and remove() were typed {object}, with the real type only conveyed
    by an {@link ComponentSystem} in the description, because the base class was
    missing from the file's @import block — only the concrete subclasses were
    listed. They now emit add(system: ComponentSystem) rather than
    add(system: object), and the redundant {@link} is dropped since the type
    annotation links.
  • list emitted as any[]; it is now annotated ComponentSystem[].
  • destroy() carried no JSDoc at all, unlike add() and remove().

list and destroy() are tagged @ignore, so the documented API surface is
unchanged
— verified by building the docs and confirming neither appears on the
ComponentSystemRegistry page. This deliberately does not touch the question of
whether list should become public API; that belongs with #8086.

Checklist

  • I have read the contributing guidelines
  • My code follows the project's coding standards
  • This PR focuses on a single change

`remove()` deleted `this[id]` before looking the system up in `list`, so the
lookup was evaluated as `indexOf(undefined)` and always returned -1. The splice
never ran, leaving the removed system in the array. As `destroy()` iterates
`list`, an already-removed system was still destroyed on teardown.

Perform the splice before deleting the id, and add tests covering `add()`,
`remove()` and `destroy()`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown

Build size report

This PR changes the size of the minified bundles.

Bundle Minified Gzip Brotli
playcanvas.min.js 2391.2 KB — 615.0 KB — 477.4 KB (−0.0 KB, −0.00%)
playcanvas.min.mjs 2388.6 KB — 613.6 KB (+0.0 KB, +0.00%) 476.6 KB (+0.0 KB, +0.01%)

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

remove()’s existence check can be bypassed by inherited properties (e.g. constructor), so it may not throw for unregistered ids and should verify the id is an own property of the registry instance.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Fixes ComponentSystemRegistry.remove() leaving removed component systems in list, which could cause removed systems to be destroyed later during registry teardown.

Changes:

  • Reorders remove() to splice the system out of list before deleting the registry entry.
  • Adds unit tests covering add(), remove(), and destroy() behavior for the registry.
File summaries
File Description
src/framework/components/registry.js Fixes remove() ordering so the removed system is actually removed from list.
test/framework/components/registry.test.mjs Adds coverage to prevent regressions in registry add/remove/destroy behavior.
Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines 264 to 267
const id = system.id;
if (!this[id]) {
throw new Error(`No ComponentSystem named '${id}' registered`);
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch, fixed in d6f19d7remove() now also requires the id to be an own property. Note an own-property check alone would have regressed the common case: the class declares a field per known system id (render, camera, ...) and those are own properties holding undefined, so hasOwn alone stops throwing for unregistered-but-declared ids. Both conditions are needed, and there is now a test for each.

🤖 Addressed by Claude Code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Correction: this has been backed out in fae660f. The finding is valid, but the guard used Object.hasOwn, which is not yet used anywhere else in the engine, so we are keeping this PR to the ordering fix rather than introducing it here. remove() still no-ops rather than throwing for an id that resolves to an inherited member — tracked separately.

🤖 Addressed by Claude Code

`remove()` used `!this[id]` to test whether a system was registered, which is
truthy for inherited members such as `constructor` and `toString`, so it
silently no-opped rather than throwing. `add()` already rejects those ids.

Require the id to be an own property as well. Note that an own-property check
alone is not sufficient, as the class declares a field per known system id and
those are own properties holding undefined.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both were annotated `{object}`, with the real type only conveyed by the
`{@link ComponentSystem}` in the description, because the base class was missing
from the file's `@import` block - only the concrete subclasses were listed.

Add the import and use the type, so the declarations emit
`add(system: ComponentSystem)` rather than `add(system: object)`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The `{ComponentSystem}` type annotation already links, matching how
`Component` and `ComponentSystem` word the same parameter.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…erty"

This reverts commit d6f19d7.

The guard relied on `Object.hasOwn`, which is not used anywhere else in the
codebase yet. Backing it out to keep this PR to the ordering fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`list` emitted as `any[]` in the type declarations - annotate it as
`ComponentSystem[]`. `destroy()` carried no JSDoc at all, unlike `add()` and
`remove()`, so mark it up to match.

Both are tagged `@ignore`, so the documented API surface is unchanged - verified
by building the docs and confirming neither member appears on the
ComponentSystemRegistry page.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@willeastcott
willeastcott merged commit 642808d into main Sep 2, 2026
10 checks passed
@willeastcott
willeastcott deleted the fix-component-system-registry-remove branch September 2, 2026 12:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants