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 gthread worker class with a generous threads count (see minimost.gunicorn_conf). Capacity is workers * threads concurrent tabs.

  • No new dependencies. Pure Flask streaming response; the browser’s native EventSource handles 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 /messages needs a cursor, so it goes through minimost.chat.messages_since().

  • Reconcile floor. The message collector is write-driven, but it also re-queries at least every _MESSAGE_RECONCILE_SECONDS even with no observed write. That is the safety net for a dropped counter bump (bump_event_signal swallows 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_SECONDS plus up to _MAX_STREAM_JITTER_SECONDS of jitter; EventSource reconnects 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 the Last-Event-ID header when it auto-reconnects, which lets the message stream resume from exactly where it left off instead of replaying history (see events()).

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 (from jsonify), a bare list/dict, or a (body, status) tuple for errors. Error tuples are reported as None so the caller skips them.

minimost.events._safe_text(collector, *args)[source]

Run a collector and return its JSON text, or None to 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 after cursor query param the way /messages does.

minimost.events._advance_cursor(after, msgs)[source]

Advance the message cursor past every timestamp in msgs.

Mirrors the client’s lastTs bookkeeping so an edit or reaction (which bumps edited_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 None on 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_INTERVAL still 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 finally when 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. channel is the tab’s currently-open channel (the client reconnects with a new value when the user switches channels); after is 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_calls and screenshares — each carrying the same JSON the matching REST endpoint returns.

Returns:

A text/event-stream streaming response.

Return type:

flask.Response

Route Summary

Method

Path

Handler

GET

/events?channel=<ch>&after=<ts>

minimost.events.events()

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.