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.

Merge request reports

Loading
Loading