Serve the venue geometry from a cacheable endpoint instead of drupalSettings
What this does
The picker received its whole payload inline in the page, on a form that declares
max-age: 0 and a session context, so nothing in it was ever cached by
anything. Measured on a 1,422-seat hall: 293 KB of JSON per page view.
The payload splits along a natural line. The drawing (the plan, the sprites, the legend, every drawable place with its coordinates) is the same for every production in the hall and every visitor. The state (which seats are gone, which are the visitor's own and at what tariff, the pooled areas, the tariffs, where to post a click) moves with every booking. So the drawing moves behind a cacheable GET and only the state stays in the page.
| before | after | |
|---|---|---|
| picker page | ~322 KB | 29 KB |
the map's drupalSettings |
293 KB | 858 bytes |
| the drawing | inline, uncacheable | 260 KB, fetched once, cacheable |
The endpoint
GET /booking/venue-map/{yoyaku_venue}
GET /booking/venue-map/{yoyaku_venue}/{yoyaku_configuration}Built on the AvailabilityController model. place booking permission, the same
gate as the picker page, so the endpoint adds no surface of its own.
It is keyed by venue and configuration, which is the grain the answer actually has. A configuration decides which areas are sold as places and nothing else in the drawing varies, so every production running the same layout shares one document instead of downloading its own copy. Keying it by resource would have been finer than necessary: a season of thirty productions on one layout would have meant thirty byte-identical documents in every cache. Keying it by venue alone would have meant publishing the seats a reduced-capacity layout has closed and trusting the client to hide them. On a two-resource venue where the resources use different configurations, the documents are 260 KB and 186 KB, and the difference is exactly the closed areas.
The page builds the URL, so the browser never has to resolve which configuration its production runs under.
Cacheability. Contexts languages:language_content (the labels are
translated, the geometry is not). Tags: the three inventory list tags plus
yoyaku_configuration_list and yoyaku_configuration_section_list, because the
configuration filter is baked into this body even though it is not baked into the
builder's own per-venue entry. Getting that layering backwards either rebuilds the
geometry whenever any configuration is touched, or keeps serving a balcony that
has been closed. No max-age of its own: the tags say when a drawing changed, and
core emits no shareable lifetime unless the site sets a page cache lifetime, which
it caps anyway.
Upcasting proves both entities exist but not that they belong together, so a configuration from another venue is a 404 rather than a nonsense answer that mints a cache entry per combination asked for.
The split, and a second seam
VenueMapBuilder gains inventory($venue, $configuration) and state($slot);
build($slot) is now the two put back together, so anything consuming the whole
payload server-side keeps working and the existing tests needed no edits.
hook_yoyaku_placement_map_alter() keeps running per request on the state half,
which is where prices belong: the drawing is shared, so a price cached into it
would be one slot's price on another slot's map. New
hook_yoyaku_placement_map_inventory_alter() is the correct home for anything
venue-stable, documented as: nothing slot-scoped, session-scoped or read from the
request may enter it.
The JS, and the once() hazard
initPlaceMap() and the other ~1,300 lines are untouched; only the bootstrap
changed, because it already took the payload as an argument. It fetches the
drawing, folds taken/mine back onto each place as state/tier, and hands
initPlaceMap() the shape it always saw.
The real risk is that once() claims the element before the drawing arrives,
so a failed fetch would leave it flagged as initialized and permanently empty with
nothing said. The bootstrap now shows a loading line synchronously and a message
on failure, and there is a browser test for exactly that.
Tests
yoyaku's first FunctionalJavascript tests. One asserts the seats are drawn
from the fetched document (they exist nowhere else, so their appearance is the
proof); one points the map at a 404 through a small test module and asserts a
message appears rather than an empty box. Setup copied from orchestra's existing
WebDriverTestBase tests.
Plus a kernel test pinning the boundary (the drawing has exactly five keys, and
state/tier/pools/categories/opUrl/slot are absent from it; the
configuration filters it; drawable() answers per configuration), and a
functional test on the route (200 with the expected keys, 403 without the
permission, 404 for a foreign configuration, the cache tags on the response, and
the page no longer carrying places).
Whole placement suite green: 24 kernel classes, 11 functional and browser tests.
Also in here
The geometry() docblock justified its packed row format by claiming an oversize
entry would breach a 1 MB memcache item ceiling and that the write would fail
silently. That was wrong and I wrote it: MemcacheBackend::set() splits an
oversize item into 470 KB chunks behind a MultipartItem parent. The packing is
still worth keeping (roughly 175 to 105 bytes a seat) but the stated reason has
been corrected to the real one.
The failure message is deliberately on one line: potx does not extract a
Drupal.t() that Prettier has wrapped with a trailing comma (#3164803), and a
string it cannot see is a string nobody can translate. Both new strings are in
fr.po.
Not in here
The picker page itself stays max-age: 0, since it still carries the state half.
And a closed area still renders as an empty region of the plan with no explanation,
which is a separate gap in the picker rather than something this changes.