task: #3621930 Let an island decline, and let any island ask for the stream
Three gaps in the island contract, all found while building a real island in another module, all general.
-
A View island cannot decline to render. buildStartButtons() and buildDynamicTabs() build the button and the tab from the island list, never from what build() returned, so an island with nothing to show still gets an affordance that opens an empty drawer. Button islands do not have this problem: buildButtons() wraps build() in a span, so returning [] renders nothing. isApplicable() cannot serve here - its base implementation is Contextual semantics, re-asked per node selection, consulted only inside IslandPluginBase::build(), which every View panel bypasses.
IslandInterface::isAvailable() answers on what the site and current user allow. It defaults to TRUE, and getIslandsEnableSorted() drops an island that declines, so no pane and no button survive it.
-
The event stream was owned by the Collaboration island, which ships disabled, even though buildPanes() already wires sse-swap to every island. IslandInterface::needsSse() defaults to FALSE and the builder root opens the stream when any enabled island asks. Collaboration::alterRenderable() gives way to needsSse(), which also drops its assignment of #attributes over any other island's.
-
The SSE write was locked in a protected controller method, so nothing outside the htmx controllers could record a change for the stream to relay. The display_builder.sse_state service owns the slot now. Its stored sessionId becomes renderedBy: the stream skips a session because that session already received its out-of-band swaps, not because it made the change, and a writer that answered nobody records NULL and reaches everyone.
Two details in SseState that no test would have caught. record() stamps getCurrentTime(), not getRequestTime(): request time is frozen at process start, so a long-running writer would record a change already older than STALE and it would never be delivered, silently. shouldRelay() reads its keys defensively, because the collection is shared and ephemeral and a PHP warning raised inside a streamed response is written into the event stream and breaks its framing.
Co-Authored-By: Claude Opus 5 noreply@anthropic.com Claude-Session: https://claude.ai/code/session_01YMfbY1TyK2cdzRWcSpJ2BE
Closes #3621930