Expand description
The Gotify connector: WebSocket receive stream, /application +
/message REST lookups, the subscription matcher, and the
reconnecting subsystem pump that routes matched messages into the
hostโs sessions.
Everything the connector needs from the workspace arrives through
the GotifyHost port, so this module stays stream + mapping and
is testable offline.
Re-exportsยง
use std::collections::HashMap;๐use std::time::Duration;๐use std::time::Instant;๐use anyhow::Context;๐use forge_primitives::GotifyConfig;๐use forge_primitives::GotifyMessage;๐use forge_primitives::GotifySubscription;๐use futures::SinkExt;๐use futures::StreamExt;๐use serde::Deserialize;๐use serde::Deserialize;๐use tokio::net::TcpStream;๐use tokio::sync::mpsc;๐use tokio::sync::oneshot;๐use tokio_tungstenite::tungstenite::Bytes;๐use tokio_tungstenite::tungstenite::Message;๐use tokio_tungstenite::MaybeTlsStream;๐use tokio_tungstenite::WebSocketStream;๐use tokio_tungstenite::connect_async;๐
Structsยง
- Gotify
App ๐ - One entry of Gotifyโs
GET /applicationresponse. Fields beyondid+name(token, description, image) are ignored. - Gotify
Messages ๐ - Gotifyโs
GET /messagepaged response. Only the newest page ofmessagesis consumed; thepagingcursor is ignored - a catch-up read wants the newest N, not the full history. - Gotify
Recent - One catch-up notification from
GET /message, resolved for agotify__recentreply: the application display name (from the appid, or the id as a string when the app index hasnโt seen it), title, message body, priority, and the serverโs verbatim RFC3339 timestamp. - Gotify
Stream ๐ - An open Gotify receive stream.
Enumsยง
- Gotify
Event ๐ - Connection-lifecycle + message events from the stream task to the subsystem pump.
- Pump
Outcome ๐ - How a pumped stream ended, steering the reconnect loop.
Constantsยง
- BACKOFF_
CAP ๐ - BACKOFF_
START ๐ - Reconnect backoff: starts at 500ms, doubles per failed/short-lived
connect, caps at 30s, and resets to the floor only after a connection
proves healthy (see
MIN_HEALTHY_UPTIME). - CONNECT_
TIMEOUT ๐ - Dial timeout so a stuck
connect_async(no OS-level deadline) canโt hang the loop; the dial is also raced against shutdown so a signal is observed promptly rather than bounded by the OS TCP timeout. - IDLE_
DEADLINE ๐ - Treat an established streamโs path as dead when no frame (message, server
ping, or pong) arrives within this window. Sits comfortably past both 2x
PING_INTERVALand Gotifyโs own ~45s server-ping period, so a healthy but quiet stream never trips it; a half-open socket left by a silent network drop does, which breaks the read loop into a reconnect. - MIN_
HEALTHY_ ๐UPTIME - Minimum time a connection must stay up to count as healthy. A session that drops sooner keeps the backoff escalating, so an accept-then-drop server (post-upgrade auth reject, an idle-conn-dropping proxy) canโt spin at the 500ms floor.
- PING_
INTERVAL ๐ - Send a WebSocket keepalive ping this often on an established stream, so a half-open path surfaces as an unanswered ping (write error or no returning pong) instead of a silently blocked read that never wakes.
- RECENT_
FETCH_ ๐WINDOW - Newest
/messagewindow pulled before client-side app + priority filtering. Gotifyโs/messagefilters by neither, so we fetch the newest page and narrow locally; 200 is its per-request maximum. - REST_
TIMEOUT ๐ - Per-request timeout for the read-only REST lookups (
/application,/message) - keep a slow or unreachable server from stalling subsystem start or a tool call.
Traitsยง
- Gotify
Host - The host port, implemented by forge-workspace. The only workspace-side state, delivery or TLS-trust plumbing a connector may reach, so this crate stays stream + mapping.
Functionsยง
- app_
index - Fetch the serverโs application list and fold it into a name->appid
map. Inbound stream messages carry the numeric appid, so the map
resolves an
applicationname filter to the id to match against. - app_
names - The serverโs application NAMEs (from
GET /application), in the order the server returns them. Backsgotify__appsso a session can self-discover which apps it may subscribe to. - build_
app_ ๐index - build_
app_ ๐names - build_
id_ ๐index - fetch_
apps ๐ - GET the serverโs application list. Shared by the name->id index, the
gotify__appsname list, andgotify__recentโs appid->name resolution. - fetch_
messages ๐ - filter_
recent ๐ - Resolve appid->name, filter by app + priority, sort newest-first (by
monotonic id), and truncate to
limit. App matching mirrorsroute: an unresolved appid never matches a non-empty name filter (its numeric display string canโt sneak past), though its displayappstill falls back to that id string. - idle_
past_ ๐deadline - Whether the stream has gone silent past
IDLE_DEADLINEas ofnow- no inbound frame refreshedlast_activityin time, so the path is treated as dead. Any frame resetslast_activity, which resets this. - matching_
subscriptions - The subscriptions whose
min_priorityandapplicationsfilterspriorityandresolved_appsatisfy, in set order.resolved_appis the messageโs application NAME resolved from the app index; an unresolved appid matches only an empty (match-any) filter set, never a filter that happens to list the id numerically. - next_
backoff ๐ - Next reconnect delay after a session ended: reset to the floor when the
connection proved healthy (stayed up past
MIN_HEALTHY_UPTIME), else escalate (double, capped atBACKOFF_CAP). - normalize ๐
- Parse a raw stream Text frame into a typed
GotifyMessage. - recent_
messages - The most recent notifications, newest first, filtered by application
NAME (empty = any) and
min_priority(None= any), capped atlimit. Fetches the newest/messagewindow plus/application(for appid-> name), then narrows locally - Gotifyโs/messagefilters by neither. - refresh_
app_ index - Fetch the Gotify
/applicationlist and store the name->appid map. Warns (never silently drops) on failure - an unresolved index would otherwise leave every application-name-filtered subscription silently matching nothing while the stream still reports connected. Returns whether the index was stored, so a caller that owes the user an answer about the names can give one. - rest_
client ๐ - A reqwest client for the read-only REST lookups, carrying the shared native-roots trust extension every forge HTTPS site uses.
- route ๐
- Match
msgagainst every active subscription and deliver one message per match. Multiple matches fan out to every subscriber. - run ๐
- Long-lived reconnect loop: connect (emit
GotifyEvent::Connected), forward each message asGotifyEvent::Message, and on drop/error emitGotifyEvent::Disconnectedthen retry with exponential backoff. The backoff resets to the floor only after a healthy session (one that stayed up pastMIN_HEALTHY_UPTIME); a fast drop or failed dial keeps it escalating. Exits whenshutdownfires or the sender is dropped. - run_
subsystem - The Gotify subsystem pump: run the reconnecting stream task and
translate its events into host state + message routing. Holds the
host for its lifetime - the host impl is the weak boundary, so a
workspace-backed host degrades to no-ops once the workspace drops;
exits when
shutdownfires or the stream task ends, dropping its own handle to the stream task so that exits too. - stream_
url ๐