Fix ComponentSystemRegistry.remove leaving the system in the list - #9280
Conversation
`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>
Build size reportThis PR changes the size of the minified bundles.
|
There was a problem hiding this comment.
🟡 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 oflistbefore deleting the registry entry. - Adds unit tests covering
add(),remove(), anddestroy()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.
| const id = system.id; | ||
| if (!this[id]) { | ||
| throw new Error(`No ComponentSystem named '${id}' registered`); | ||
| } |
There was a problem hiding this comment.
Good catch, fixed in d6f19d7 — remove() 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
There was a problem hiding this comment.
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>
Description
ComponentSystemRegistry.remove()never removed the system fromlist.The array lookup was evaluated after the id had already been deleted:
this[id]isundefinedby the timeindexOfruns, so the splice neverexecuted and the removed system stayed in the array. Since
destroy()iterateslist, 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 thatunregisters a custom component system.
Adds
test/framework/components/registry.test.mjscoveringadd(),remove()and
destroy(). Three of its eight cases fail without the fix, including oneasserting 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()andremove()were typed{object}, with the real type only conveyedby an
{@link ComponentSystem}in the description, because the base class wasmissing from the file's
@importblock — only the concrete subclasses werelisted. They now emit
add(system: ComponentSystem)rather thanadd(system: object), and the redundant{@link}is dropped since the typeannotation links.
listemitted asany[]; it is now annotatedComponentSystem[].destroy()carried no JSDoc at all, unlikeadd()andremove().listanddestroy()are tagged@ignore, so the documented API surface isunchanged — verified by building the docs and confirming neither appears on the
ComponentSystemRegistrypage. This deliberately does not touch the question ofwhether
listshould become public API; that belongs with #8086.Checklist