Skip to main content

Module gotify

Module gotify 

Source
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ยง

GotifyApp ๐Ÿ”’
One entry of Gotifyโ€™s GET /application response. Fields beyond id + name (token, description, image) are ignored.
GotifyMessages ๐Ÿ”’
Gotifyโ€™s GET /message paged response. Only the newest page of messages is consumed; the paging cursor is ignored - a catch-up read wants the newest N, not the full history.
GotifyRecent
One catch-up notification from GET /message, resolved for a gotify__recent reply: 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.
GotifyStream ๐Ÿ”’
An open Gotify receive stream.

Enumsยง

GotifyEvent ๐Ÿ”’
Connection-lifecycle + message events from the stream task to the subsystem pump.
PumpOutcome ๐Ÿ”’
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_INTERVAL and 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 /message window pulled before client-side app + priority filtering. Gotifyโ€™s /message filters 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ยง

GotifyHost
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 application name filter to the id to match against.
app_names
The serverโ€™s application NAMEs (from GET /application), in the order the server returns them. Backs gotify__apps so 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__apps name list, and gotify__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 mirrors route: an unresolved appid never matches a non-empty name filter (its numeric display string canโ€™t sneak past), though its display app still falls back to that id string.
idle_past_deadline ๐Ÿ”’
Whether the stream has gone silent past IDLE_DEADLINE as of now - no inbound frame refreshed last_activity in time, so the path is treated as dead. Any frame resets last_activity, which resets this.
matching_subscriptions
The subscriptions whose min_priority and applications filters priority and resolved_app satisfy, in set order. resolved_app is 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 at BACKOFF_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 at limit. Fetches the newest /message window plus /application (for appid-> name), then narrows locally - Gotifyโ€™s /message filters by neither.
refresh_app_index
Fetch the Gotify /application list 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 msg against 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 as GotifyEvent::Message, and on drop/error emit GotifyEvent::Disconnected then retry with exponential backoff. The backoff resets to the floor only after a healthy session (one that stayed up past MIN_HEALTHY_UPTIME); a fast drop or failed dial keeps it escalating. Exits when shutdown fires 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 shutdown fires or the stream task ends, dropping its own handle to the stream task so that exits too.
stream_url ๐Ÿ”’