upg-100's first click: onLegendSelect was bailing on an unready layer, and the request was thrown away

rega's re-shoot on main 27eed341 answered it in one run, because the apply now
reports its own verdict. Timeline, ms from navigation:
    t=2958  click 'Show Northwest India on the map'
    t=3015  select-zone = "asia-india-rajasthan" written on the map pane
    t=4887  data-select-zone-state = "applied-but-no-selection"
    t=4900  the same pair AGAIN, 13 ms later, still no selection
    t=5308  the doxa:select-zone listener first appears
So delivery was fine, the id was right, the node WAS found, onLegendSelect WAS
called — and the selection did not hold, twice.

THE CAUSE is onLegendSelect's own first line:
    if (!map || !map.getLayer('tapestry-fill')) return
It returns silently when the map's tapestry layer is not up yet, so selectedKey is
never set. At t=4.9s that layer did not exist. Both applies came from
watch([legendNodes, mapRef]), which is why they landed before the listener did.

So the readiness signal was never legendNodes and never mapRef. It is THE LAYER,
and I had guessed at the other two in turn. This is the THIRD layer of one defect —
apply once against unready state and discard — and each layer hid the next because
every failure was silent.

Two changes, both from the measurement rather than a theory:
1. applySelectZone RETAINS the request when the effect did not take. It used to
   clear _pendingZoneId right after calling onLegendSelect, treating "called" as
   "done"; now a state of applied-but-no-selection keeps the id outstanding.
2. ensureLayers() drains it. That function adds the tapestry source and both
   layers, so it is the exact moment onLegendSelect can hold a selection.

Coverage is complete either way round: if ensureLayers runs first the layer already
exists and the ordinary apply succeeds; if it runs later, the retained request
drains. A click after everything is up still goes through the attribute observer.

Branch was RESET onto main 27eed341 and carries this one commit only — the handoff
lesson from last time: a merged branch's tree holds my stale copies of everyone
else's files, so replaying it (the sane thing for a recipient) reverts their work.
Source only; my-upg-100-list needs a rebuild.
This commit is contained in:
Doxa Maps 2026-09-26 05:39:01 +00:00
parent af436bff6f
commit 3345779bdb

View file

@ -626,6 +626,16 @@ function ensureLayers() {
// F2-D2: brighter, thicker outline gives contrast on dark without washing out light.
paint: { 'line-color': matchColorExpr, 'line-width': 1.2, 'line-opacity': 0.9 },
})
// ★ THE REAL READINESS SIGNAL. onLegendSelect() returns early unless map.getLayer('tapestry-fill')
// exists, so a "Show on map" request that arrives before this function runs cannot hold a
// selection however correctly it was delivered. This is the third layer of the same defect --
// apply once against unready state, then discard -- and this is the point where the state is
// finally ready. Drains any request retained by applySelectZone().
if (_pendingZoneId) {
const id = _pendingZoneId
nextTick(() => { if (_pendingZoneId === id) applySelectZone(id) })
}
}
// ── 09b shift-select tool: selection highlight layers (D-2) ──────────────────
@ -1606,14 +1616,21 @@ function applySelectZone(zoneId) {
const node = findLegendNode(legendNodes.value, zoneId);
if (!node) { _pendingZoneId = zoneId; _markZoneState('unresolved', zoneId); return false; } // not ready -- REMEMBER it
onLegendSelect(node);
_pendingZoneId = undefined;
// selectedKey (:163, "currently isolated zoneKey (or null)") is this component's own record of the
// isolation, so 'applied-but-no-selection' means the path was REACHED and its EFFECT lost -- a
// different bug from delivery, and the one rega's timeline points at.
// ★ I first wrote selectedZoneKey here, which does not exist; the try/catch would have swallowed
// the ReferenceError and reported 'applied' every time. A diagnostic that cannot report failure is
// worse than none. main's check:no-free-identifiers (6b05f513) now catches this class.
_markZoneState(!selectedKey.value ? 'applied-but-no-selection' : 'applied', zoneId);
// ★ IF THE EFFECT DID NOT TAKE, THE REQUEST IS STILL OUTSTANDING. rega's re-shoot on 27eed341
// read exactly this state twice, 13 ms apart, with the right id -- so the node WAS found and
// onLegendSelect WAS called, and the selection still did not hold. The cause is onLegendSelect's
// own first line: `if (!map || !map.getLayer('tapestry-fill')) return`. It bails silently when the
// map's tapestry layer is not up, which at t=4.9s it was not. So the readiness signal was never
// legendNodes or mapRef -- it is THE LAYER, and the old code cleared _pendingZoneId here as though
// the work were done. Keeping it means ensureLayers() can drain it the moment the layer exists.
if (!selectedKey.value) { _pendingZoneId = zoneId; _markZoneState('applied-but-no-selection', zoneId); return false; }
_markZoneState('applied', zoneId);
return true;
}