A lapsed hold deadline is read as an empty basket, so a booker holds places they cannot see or release
A booker whose hold deadline had passed, on a site whose cron had not yet run, was stuck: the map drew none of their places, so there was nothing to click to give one back, and every new pick was refused by a limit still counting places they could not see. The basket page listed all of them.
Seen on a real basket: eight tickets, the deadline two hours past, cron last run before the deadline. The endpoint serving the booker's own half answered with all six seats and the pooled units; the map drew none of them.
The deadline was being read as an answer
The deadline travels in the cookie because a hold lapsing is the one change no response can announce. Both clients read a passed deadline as proof that nothing was held. It says when a hold is DUE to end, not whether it HAS ended, and the sweep that really ends one runs in cron, which an operator may leave hours between. Until it runs the places are still held, still counted against their booker's own limit, and still withheld from everybody else.
venue-map.jsno longer reads it, andcalendar.jsno longer hides an overlay because of it. It stays in the payload for a surface that wants to show the time remaining, which is the honest use for it.- Trust is the digest alone, with no lifetime on top. A basket changes only when its own booker changes it, and every one of those changes travels through a request that restates the digest, so the next book or release is what corrects the view. A lifetime would pay a request on a schedule for data that moves only when this visitor moves it.
VenueMineControllerrestates the stamp on its way out. It has just read the basket, so this costs nothing, and a basket that has gone clears the stamp instead of leaving it behind until the visitor happens to mutate something.
And toggle was the deeper fault
The place endpoint took one toggle operation and decided for itself whether that meant book or release, from whatever it found held at the time. So a booker clicking a place their map still showed as theirs, meaning to give it back, was handed a fresh booking instead: the exact opposite of what they asked, at the one moment their view was known to be out of date.
The caller always knew which it meant. It now says so: book and release replace toggle, and an unknown operation is a 400.
releasewith nothing held books nothing. It answersstate: 'gone', and the map drops every place and pooled unit it was crediting to a basket that no longer exists, then says "That place was no longer in your basket." Cause-neutral on purpose: swept by cron, emptied from the basket page or the slot page, or dropped by another tab are indistinguishable from the answer, and the message only has to explain why more disappeared than was clicked.bookon a place already held answers with its state rather than treating it as an error.
Pre-1.0, so toggle is gone rather than kept alongside.
Tests
Two added, both seen to fail first. The lapsed-deadline one ages both hold clocks over an unswept basket; it passed against unfixed code three times before it was right (twice it aged a different, empty basket, once the browser kept a cookie carrying the old deadline), so it now carries two guards: one asserting the aged write landed, one asserting the deadline the page actually carries is the past one. The gone-basket one sweeps a basket exactly as cron would and asserts the next click books nothing, says so, and leaves the panel empty.
VenueMapPickerTest runs 20 tests. phpcs clean over the whole module under the CI ruleset, eslint 0 errors.
Deliberately not here
Answering the own-half endpoint with a conditional request, so a client can ask cheaply (an ETag over the transaction's id, changed and state, which unlike the basket digest does move when the sweep empties it); checking on a slow cadence once the deadline has passed, the only window where a check can learn anything; and warning a booker before their basket expires rather than after. Each wants its own measurement.