A partially-implemented KrillApp.Trigger.Timer node never started its
countdown. Dropping a Timer under a source and firing the source did nothing —
no countdown, no observers fired — and the node had no editor (the editor when
silently fell through to the empty else).
Two wiring gaps in the half-finished feature:
KrillApp.Trigger.Timer was missing from KrillAppEmit.processor(), so it
hit the else -> null arm. ServerNodeManager.invoke() dispatches via
target.type.processor()?.onInvoke(...); a null processor means the
server-side onInvoke was never reached, so TimerBoss never ran.ServerTimerProcessor.onInvoke routed both EXECUTE and RESET to
timerBoss.stop() — so even if dispatch had worked, EXECUTE would have
stopped (a no-op) instead of starting the countdown.
KrillAppMeta.meta() also handed the node a generic TriggerMetaData
(no delay field) instead of TimerMetaData, and TimerProcessor was
absent from ClientProcessModule’s binds.Added the KrillApp.Trigger.Timer -> TimerProcessor arm to
KrillAppEmit.processor(), the TimerProcessor::class binding to
ClientProcessModule, and pointed KrillAppMeta.meta() at TimerMetaData.
Corrected ServerTimerProcessor.onInvoke to EXECUTE -> start, RESET -> stop.
Rebuilt TimerBoss around a single cancellable delay(meta.delay) (no
wall-clock busy loop), persisting PROCESSING on start, firing
executeSources + clearing state on completion, and force-broadcasting
STATE_CHANGE(RESET) on stop so clients drop the countdown pie. The client
draws the pie purely from timerProgress(now, startTimestamp, delay), and the
generic 1500ms activity-pulse reset is skipped for Timer nodes so a long
countdown isn’t truncated.
Once dispatch worked, a Timer node persisted before the meta() fix still
carried a generic TriggerMetaData. TimerBoss.start’s hard cast
(node.meta as TimerMetaData) then threw an uncaught ClassCastException on
the IO scope — and the editor’s as? TimerMetaData ?: return silently hid the
form. Fixed with an asTimerMetaData() coercion (carries over SourceMetaData
wiring, supplies the default delay) applied at the server choke point
(TimerBoss.start, which persists the repair), in EditTimer (so the form
renders and the next edit writes back the corrected meta), and in the
IconManager countdown overlay (so the pie still animates when a client’s
in-memory copy lags behind a state-only SSE update). Lesson: a node’s persisted
meta subtype can lag a meta() change — never hard-cast it; coerce/heal.
The Sources tab toggles (sources / Invoked-By checkboxes / Execute-Reset radio)
were inert on a Timer. SourcesTab writes through NodeMetaData.withWiring(),
a per-subtype when whose else -> this returns the meta unchanged for any
unlisted type — so every edit was a silent no-op. TimerMetaData was missing
from both withWiring and its lockstep twin withParentSourceDefault. Added the
arm to both. Also hardened EditTimer: it now writes only the delay delta read
from the freshest node (instead of a whole-meta draft), so a concurrent Sources
tab edit is never clobbered, and it stages via edited() (Save → submit())
like every other editor.
The “add a node type” checklist must include the dispatch entry point
(KrillAppEmit.processor()), not just the handler body — a missing arm there is
a silent no-op, not a crash. TimerWiringTest now guards that
KrillApp.Trigger.Timer.meta() returns TimerMetaData, and
ServerTimerProcessorTest pins EXECUTE -> start / RESET -> stop so the
verb-swap regression can’t return. When a refactor changes how a unit is
invoked, verify the dispatch path resolves to a processor, not only that the
processor’s logic is correct.