Skip to content

feat(cli): sweep the adapter markers a deleted script leaves behind - #30

Merged
mschmicking merged 2 commits into
mainfrom
feat/scriptenabled-cleanup
Aug 18, 2026
Merged

feat(cli): sweep the adapter markers a deleted script leaves behind#30
mschmicking merged 2 commits into
mainfrom
feat/scriptenabled-cleanup

Conversation

@mschmicking

@mschmicking mschmicking commented Aug 17, 2026

Copy link
Copy Markdown
Owner

What

remove, rename and move now delete the scriptEnabled and scriptProblem states the javascript adapter leaves behind for a script that no longer exists. doctor gains a read-only markers check that finds pre-existing ones.

Why

The adapter keeps two bookkeeping states beside every script, on every javascript instance rather than only the one that runs it:

javascript.<n>.scriptEnabled.<id>
javascript.<n>.scriptProblem.<id>

Both are created by load(), which calls createActiveObject/createProblemObject before prepareScript checks common.engine. Every instance runs load() for every non-global script at startup and again on every source change, so all of them hold a pair for every script.

Deletion, though, is gated on the engine: only the instance that owned the script at the moment it was deleted removes its own pair. Every other instance keeps one for a script that no longer exists, js-controller warns about it for the life of the system, and nothing in ioBroker ever collects it.

This is independent of who deleted the script — the Admin UI leaves the identical residue. Verified against ioBroker.javascript v8.9.2.

Safety

  • The sweep runs only after the object is gone and after the trash backup, and is best-effort: it warns rather than turning a completed delete into a failure.
  • Order is enforced in ObjectsApi.deleteScriptMarker — value first, object second, never the reverse, since an object deleted out from under a surviving value is exactly the orphan being cleaned up. (The adapter's own cleanup gets this backwards, a likely source of "state has no object" warnings.)
  • The marker parser refuses anything without a bare numeric instance segment, so its failure mode is "skip", never "delete something else".
  • Every delete path stays behind --yes and respects --dry-run.
  • AGENTS.md invariant 4 records the one thing swept without a backup, and why there is nothing in it to lose.

remove also stops refusing an id whose script is already gone when markers remain — otherwise pre-existing ones cannot be cleared by anything. It sweeps them, touches nothing else, and keeps the local file.

Testing

  • 303 tests against the in-process fake server, including regressions pinning that no state of either kind survives a delete, and that doctor still counts an orphaned scriptProblem whose scriptEnabled twin is already gone.
  • Exercised end-to-end against a live three-instance ioBroker (Admin 7.6.17, javascript v8.9.2): remove with the script present and with it already gone, plus rename and move. Each result was verified with a raw states-database probe that shares no code with the tool. Ten orphaned states were cleared in the process — six that predated the work, four created by the tests themselves — leaving that instance at zero.

Both marker kinds are driven by MARKER_KINDS in types.ts rather than a pattern repeated per call site — an earlier draft covered only scriptEnabled and left eight orphaned scriptProblem states on that live instance while doctor reported it clean.

Note on the CodeQL comments above

The two resolved github-advanced-security comments on test/fake-server.ts come from the first push of this branch, which was cut from a stale local main and therefore lacked the deepMerge prototype guard added in #25. The branch has since been rebased and merged up; both alerts are recorded as fixed, and the guard is present in the current file.

🤖 Generated with Claude Code

Comment thread test/fake-server.ts Fixed
Comment thread test/fake-server.ts Fixed
The javascript adapter keeps two bookkeeping states beside every script, on every
javascript instance rather than only the one that runs it:

    javascript.<n>.scriptEnabled.<id>
    javascript.<n>.scriptProblem.<id>

Both are created by the adapter's load(), which calls createActiveObject and
createProblemObject before prepareScript checks common.engine to decide whether
this instance should run the script at all. Every instance runs load() for every
non-global script at startup and again on every source change, so all of them hold
a pair for every script.

Deletion, though, is gated on the engine: only the instance that owned the script
at the moment it was deleted removes its own pair. Every other instance keeps one
for a script that no longer exists, js-controller warns about it for the life of
the system, and nothing in ioBroker ever collects it. This is independent of who
deleted the script — the Admin UI leaves the identical residue.

remove, rename and move now sweep both markers of the id they just deleted. The
sweep runs after the object is gone and after the trash backup, and is best-effort:
it warns rather than turning a completed delete into a failure. Order is enforced
in ObjectsApi.deleteScriptMarker — value first, object second, never the reverse,
because an object deleted out from under a surviving value is exactly the orphan
being cleaned up. (The adapter's own cleanup gets this backwards, which is a likely
source of the "state has no object" warnings.)

remove also stops refusing an id whose script is already gone when markers remain,
since otherwise pre-existing ones cannot be cleared by anything. It sweeps them,
touches nothing else, and keeps the local file.

doctor gains a read-only `markers` check that counts both kinds, names the orphans,
and warns rather than fails — nothing here is broken.

Both kinds are handled through MARKER_KINDS in types.ts rather than a pattern
repeated per call site: an earlier draft covered only scriptEnabled and left eight
orphaned scriptProblem states on a live instance while doctor reported it clean.

Verified against ioBroker.javascript v8.9.2 on a live three-instance system, and
covered by tests against the in-process fake server.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@mschmicking
mschmicking force-pushed the feat/scriptenabled-cleanup branch from 1d24564 to de9d34e Compare August 17, 2026 21:42
@mschmicking
mschmicking merged commit da1692c into main Aug 18, 2026
8 checks passed
@mschmicking
mschmicking deleted the feat/scriptenabled-cleanup branch August 18, 2026 07:32
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