Issue #3615824: Read a booker's own basket over the shared availability list, so seats taken or given back show on the map at once
The seat map's shared half (which seats are gone) is held for availability_max_age seconds, so a session that has just written to a slot is the one reader it is wrong for. It was wrong in both directions.
Taking. mergePayload() in venue-map.js promoted a seat to the booker's own only when the shared list already called it gone. A seat just taken is in the live mine map and not yet in taken, so it fell through to available until the lifetime lapsed. The resync path a few hundred lines up had always consulted mine first; the two readers of mine disagreed.
Releasing. Releasing leaves nothing behind to read, so a seat given back stayed in the shared list, came back drawn as somebody else's, and was then unclickable (toggle() acts only on available and mine) while a hold would have been granted under the slot lock.
The fix
One rule: the shared answer is corrected by what this session itself has just done, and by nothing else.
- Taking needs no record; the basket is the record, and
mergePayload()now prefers it. ReleasedPlaceRecordnotes released place ids in the private tempstore;VenueMapBuilder::state()subtracts the notes younger than the lifetime. The initial render and the resync feed both readstate().ReleasedPlaceSubscriberkeeps the record in step fromBookingEvents::HELD/RELEASED, so a seat given back from the cart or the operator UI corrects the map too.
Notes prune on every read and write. Nothing shared is invalidated: forgetMovingState() on every release is exactly the per-booking invalidation the lifetime exists to avoid.
Tests
Two browser tests in VenueMapPickerTest. The fold of state onto seats lives twice, in PHP (withState(), via build()) and in the JS the browser runs; build() has no caller outside the test suite, so the kernel test pinning this rule was asserting on the copy no request executes. Both new tests widen the lifetime first so a slow run cannot rebuild the shared list and pass for the wrong reason.
Docs updated for both directions. No new translatable strings.
Two follow-ups noted on the issue: pooled areas have the same defect but need a signed count delta, and build()/withState() should go.