minimost.events
minimost.events
Server-Sent Events (SSE) push stream that replaces the legacy HTTP pollers.
Historically the browser opened roughly a dozen setInterval pollers (new
messages every 500 ms, presence/typing/unread badges every 1-5 s, incoming
calls, screen shares, …). Each was a fresh authenticated HTTP request, so an
idle tab fired ~11 requests per second forever.
This module collapses all of that into one long-lived GET /events
connection per tab. The handler holds the request open and, on a short
internal tick, re-reads the same shared SQLite state the old endpoints read and
flushes an SSE event only when something actually changed. Because every
Gunicorn worker sees the same on-disk databases (messages.db /
presence.db), an event written by one worker is visible to a stream held
open in another — no extra cross-process bus is needed.
Design notes
Worker model. A held-open stream occupies one worker thread for its whole lifetime, so the server must run Gunicorn’s
gthreadworker class with a generousthreadscount (seeminimost.gunicorn_conf). Capacity isworkers * threadsconcurrent tabs.No new dependencies. Pure Flask streaming response; the browser’s native
EventSourcehandles reconnection.Single source of truth. The per-event collectors call the very same view functions the REST endpoints expose, so the pushed payloads are byte-for-byte what a poll would have returned. Only
/messagesneeds a cursor, so it goes throughminimost.chat.messages_since().Reconcile floor. The message collector is write-driven, but it also re-queries at least every
_MESSAGE_RECONCILE_SECONDSeven with no observed write. That is the safety net for a dropped counter bump (bump_event_signalswallows its own errors): a lost wake delays delivery by at most one reconcile instead of stranding the message.Self-recycle. Each stream returns after
_MAX_STREAM_SECONDSplus up to_MAX_STREAM_JITTER_SECONDSof jitter;EventSourcereconnects automatically. This bounds the lifetime of any half-closed connection that might otherwise pin a thread, and the jitter keeps tabs that connected together (e.g. just after a restart) from recycling in lockstep.
- minimost.events._sse(event, data, event_id=None)[source]
Format one SSE event frame. data must already be a JSON string.
When event_id is given it is emitted as the frame’s
id:. The browser echoes the most recent id back in theLast-Event-IDheader when it auto-reconnects, which lets the message stream resume from exactly where it left off instead of replaying history (seeevents()).
- minimost.events._json_text(view_return)[source]
Normalise a Flask view return value to a compact JSON string.
Views in this project return either a
flask.Response(fromjsonify), a barelist/dict, or a(body, status)tuple for errors. Error tuples are reported asNoneso the caller skips them.
- minimost.events._safe_text(collector, *args)[source]
Run a collector and return its JSON text, or
Noneto skip emitting.A collector that raises (e.g. a transient SQLite lock) must never tear down the whole stream — the next tick simply tries again.
- minimost.events._safe_messages(channel, user, after)[source]
Run the message collector, returning
[]on a transient failure.The cursor-based message query is the one collector whose result the loop consumes directly (to advance the cursor and build the event id), so it can’t go through
_safe_text(). It needs the same guarantee, though: a single bad tick (e.g. a transient SQLite lock) must not propagate out of the generator and tear the whole stream down — the next tick simply tries again.
- minimost.events._parse_after(raw)[source]
Parse the
aftercursor query param the way/messagesdoes.
- minimost.events._advance_cursor(after, msgs)[source]
Advance the message cursor past every timestamp in msgs.
Mirrors the client’s
lastTsbookkeeping so an edit or reaction (which bumpsedited_ts/reactions_ts) is delivered exactly once.
- minimost.events._open_signal_conn()[source]
Open the long-lived connection used for the per-tick counter read.
One persistent connection keeps that hot path off the connect/close cost the view-function collectors pay. Returns
Noneon failure so the caller falls back to per-read connections.
- minimost.events._collect_messages(channel, user, after, now, wrote, next_due)[source]
Return
(after, sse_event_or_None)for new messages this tick.Messages are cursor-based and write-driven, with a slow time-driven floor (
_MESSAGE_RECONCILE_SECONDS) as a safety net against a lost counter bump;_MESSAGE_MIN_INTERVALstill caps how often a write storm can re-query. The (possibly advanced) cursor is returned so the caller can carry it into the next tick.
- minimost.events._emit_collector_updates(collectors, now, wrote, next_due, last_sent, *args)[source]
Yield SSE frames for any collectors whose JSON changed this tick.
A collector runs when the change counter moved (wrote) or, for a time-driven one, once its interval elapses; its output is emitted only when it differs from the last frame sent for that event.
- minimost.events._event_stream(user, channel, channel_ok, after, cap_streams)[source]
Generate the SSE frames for one held-open
events()connection.Kept at module scope (rather than nested in the view) so its tick loop does not inherit the view’s nesting depth. Releases the per-user stream slot in its
finallywhen the stream ends.
- minimost.events.events()[source]
Hold a Server-Sent Events stream open, pushing change-based updates.
Route:
GET /events?channel=<channel>&after=<ts>Replaces every interval poller with one connection.
channelis the tab’s currently-open channel (the client reconnects with a new value when the user switches channels);afteris the client’s last-seen message timestamp so the stream only sends newer rows.Emits named SSE events —
messages,typing,read_receipts,online_users,dms,channel_unreads,private_channels,mentions,unread_count,incoming_callsandscreenshares— each carrying the same JSON the matching REST endpoint returns.- Returns:
A
text/event-streamstreaming response.- Return type:
Route Summary
Method |
Path |
Handler |
|---|---|---|
GET |
|
The single text/event-stream response carries these named events, each the
push-mode equivalent of a former polling endpoint: messages, typing,
read_receipts, online_users, dms, channel_unreads,
private_channels, mentions, unread_count, incoming_calls and
screenshares. See Architecture for the delivery model and the
gthread worker requirement.