Skip to content

Use Server-sent events to add live updates for files, players, and markers#819

Merged
TBlueF merged 12 commits into
BlueMap-Minecraft:masterfrom
pR0Ps:feature/server-sent-events
Jul 20, 2026
Merged

Use Server-sent events to add live updates for files, players, and markers#819
TBlueF merged 12 commits into
BlueMap-Minecraft:masterfrom
pR0Ps:feature/server-sent-events

Conversation

@pR0Ps

@pR0Ps pR0Ps commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

SSE Implementation:

  • A SseConnectionManager is created per-map in the MapRequestHandler.
  • Whenever a client connects to the live/sse endpoint, a new SseConnection is created for it and is added to the SseConnectionManager.
  • The SseConnectionManager receives requests to push updates and sends them to all known SseConnections. If sending fails, the connection is cleaned up and removed.
  • Sending happens on a background thread so normal server operations are not blocked on clients picking up events.

Backend events:

  • Tile updates are event-based: when a new tile is rendered an event is emitted which causes the tile's X+Y and lod to be broadcasted to all connected clients.
  • Players and other markers are still polling-based, but the polling is done server-side (and only once per endpoint) and connected clients are notified only when the data changes.
  • Polling was implemented by replacing the CachedRateLimitDataSupplier with a LiveDataSupplierBroadcaster. This Supplier wrapper can be used in the same way where requests to get() it's data are rate-limited and cached. However, it also runs a background thread that (when listeners are added) will automatically poll the supplier and send events when the data changes.

Frontend changes:

  • On page load, an EventSource connects to the <map>/live/sse endpoint and watches for events.
    • "tile" updates cause the specified tiles to be force-reloaded
    • "player" updates are fed directly into the existing playerMarkerManager
    • "marker" updates are fed directly into the existing markerFileManager
  • Since this takes the place of the marker managers polling, the marker managers were updated to be able to be paused, and are started in a paused state.
  • In order to ensure that markers are still updated if the SSE endpoint can't be contacted, the existing polling marker managers will be unpaused if the SSE connection fails to connect or disconnects. If the SSE connection returns, the marker managers will be paused again.
  • Tile force-reloading is handled by removing the specified tile from the revalidatedUrls cache, the calling load on it. This forces the normal tile loading mechanism to update the tile, reloading it.

pR0Ps and others added 2 commits July 17, 2026 01:56
…rkers

SSE Implementation:
 - A `SseConnectionManager` is created per-map in the `MapRequestHandler`.
 - Whenever a client connects to the `live/sse` endpoint, a new `SseConnection`
   is created for it and is added to the `SseConnectionManager`.
 - The `SseConnectionManager` receives requests to push updates and sends them
   to all known `SseConnections`. If sending fails, the connection is cleaned
   up and removed.
 - Sending happens on a background thread so normal server operations
   are not blocked on clients picking up events.

Backend events:
 - Tile updates are event-based: when a new tile is rendered an event is
   emitted which causes the tile's X+Y and lod to be broadcasted to all
   connected clients.
 - Players and other markers are still polling-based, but the polling is
   done server-side (and only once per endpoint) and connected clients are
   notified only when the data changes.
 - Polling was implemented by replacing the
   `CachedRateLimitDataSupplier` with a `LiveDataSupplierBroadcaster`.
   This `Supplier` wrapper can be used in the same way where requests to
   `get()` it's data are rate-limited and cached. However, it also runs
   a background thread that (when listeners are added) will automatically
   poll the supplier and send events when the data changes.

Frontend changes:
 - On page load, an `EventSource` connects to the `<map>/live/sse` endpoint and
   watches for events.
   - "tile" updates cause the specified tiles to be force-reloaded
   - "player" updates are fed directly into the existing `playerMarkerManager`
   - "marker" updates are fed directly into the existing `markerFileManager`
 - Since this takes the place of the marker managers polling, the marker
   managers were updated to be able to be paused, and are started in a
   paused state.
 - In order to ensure that markers are still updated if the SSE endpoint
   can't be contacted, the existing polling marker managers will be
   unpaused if the SSE connection fails to connect or disconnects. If
   the SSE connection returns, the marker managers will be paused again.
 - Tile force-reloading is handled by removing the specified tile from
   the `revalidatedUrls` cache, the calling `load` on it. This forces
   the normal tile loading mechanism to update the tile, reloading it.
pR0Ps and others added 5 commits July 18, 2026 21:31
This is an Nginx-specific header, but may be supported by other reverse
proxies as well. At the very lest, it can't hurt?
The polling of data at a fixed interval is now scheduled by the
BlueMap.SCHEDULER and the work of doing the updates is run in the
BlueMap.THREAD_POOL. This makes the LiveDataSupplierBroadcaster only
responsible for starting/stopping the scheduled task when listeners
connect/disconnect.

This avoids the overhead of having to spin up a polling thread for every
supplier * every map, as well is a much simpler implementation.
Previously a single slow consumer of events would block all other
consumers. This was because the `SseConnectionManager` would handle
sending data to each connection in a single worker thread.

This commit refactors the code so that the `SseConnectionManager` is no
longer responsible for actually sending the data to each connection.
Instead, it simply enqueues events to every managed `SseConnection`
which handles sending them.

Details:

- Each `SseConnection` now has a queue and a virtual thread that pulls
  events off it and sends them to the consumer. This means that a slow
  consumer now only blocks its own connection.
- The per-connection queues now limit how many events can be queued to
  send. After the limit is reached, any new events are simply dropped to
  avoid slow clients forcing the server to buffer an unlimited number of
  old events.
- An `onClose` callback can now be registered on the `SseConnection`
  class. This allows it to own its entire lifecycle, while still
  allowing the `SseConnectionManager` to be notified when it's been
  closed and needs removing from the set of managed connections.
@pR0Ps

pR0Ps commented Jul 19, 2026

Copy link
Copy Markdown
Contributor Author

Doing the latest SSE connection refactor made me realize that the way that the SSE data is written could probably be optimized, though it would take some changing of code more on the HTTP sever side of things.

Currently the SseConnection uses a virtual thread to push an event into a buffer that the HttpResponseOutputStream.write reads out into another buffer, that it then writes to the connected client (via a BufferedOutputStream).

This first event buffer was required before the refactor in order to ensure that we could write an event and move on to the next connection without waiting for the client to consume the data. However, since the SseConnection is now using a blockable virtual thread to do the writing, there's no reason it needs to write to a buffer at all. We could skip all the buffering entirely if the SseConnection could be handed the output stream (after some headers setup) and just block on writing the data directly to that, bypassing everything else.

I think it would save the creation of a virtual thread, 2 buffers, some cross-thread overhead, and would avoid having to make this wider change. However, it would involve some amount of changes more on the HTTP server side which might be a bit out of scope for at least this initial version.

Comment thread common/src/main/java/de/bluecolored/bluemap/common/web/SseConnection.java Outdated
Comment thread common/src/main/java/de/bluecolored/bluemap/common/web/SseConnectionManager.java Outdated
@TBlueF

TBlueF commented Jul 19, 2026

Copy link
Copy Markdown
Member

Doing the latest SSE connection refactor...

@pR0Ps I already had the exact same thought!
So I am actually already looking into if it is feasible to implement what you just suggested :)

Another idea i had was moving the tile-change listeners from the HiresModelManager and the LowresTileManager, over to the Storage implementation. Which would imho be cleaner and a lot more powerful, since Addons then could use Listeners on the GridStorage/ItemStorage as well, and i am not yet 100% happy with having these listeners inside HiresModelManager/LowresTileManager..
Problem with that is that it requires a rather large change in the Storage implementations which would probably also break some addons. So i am still on the fence there..

@TBlueF TBlueF left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright, i think i would be fine with merging it like this for now, and then any improvements can be made in follow-up PR's :)

If you agree with that, and with the latest changes i made, then i would press that merge button 👍

@pR0Ps

pR0Ps commented Jul 20, 2026

Copy link
Copy Markdown
Contributor Author

Agreed, I think this is good to go as-is. All your changes look good, thanks for adding SSE support to the CLI version!

@TBlueF
TBlueF merged commit 3a459bc into BlueMap-Minecraft:master Jul 20, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants