mirror of
https://github.com/block/buzz.git
synced 2026-11-07 01:41:06 +00:00
mesh-llm: heartbeat republish — keep offers fresh while the panel is mounted
Without heartbeat, the expires_at TTL added in the previous commit makes
offers vanish from consumer UIs 15 min after the user toggles on, even
if the publisher is still running fine. The original commit message
even noted this gap but didn't implement it. Fixing now.
desktop/src/features/settings/hooks/useMeshOfferHeartbeat.ts (new):
- HEARTBEAT_MS = 5 minutes (~OFFER_TTL_SECS / 3 — so one missed beat
still leaves the offer visible; only two consecutive misses drop us).
- While { enabled, irohRelayUrl } are both truthy, setInterval calls
mesh_publish_offer, which re-stamps expires_at = now + 15 min as a
NIP-33 replace under the same (pubkey, d_tag) address.
- Errors are logged and ignored — the user isn't waiting in front of the
panel; the next tick or an explicit prefs change will surface a fresh
error if the relay is durably down.
desktop/src/features/settings/ui/MeshComputeSettingsCard.tsx:
- New irohRelayUrl state, populated on mount and after every persist().
- useMeshOfferHeartbeat({ enabled, irohRelayUrl }) wired in.
- Mount-time relay probe so a user opening the panel with sharing
already enabled from a previous session starts heartbeating
immediately; failures are non-fatal (console.warn, prefs still
editable).
pnpm typecheck + pnpm check clean.
Signed-off-by: Tyler Longwell <109685178+tlongwell-block@users.noreply.github.com>
Co-authored-by: Dawn (sprout agent) <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
56
desktop/src/features/settings/hooks/useMeshOfferHeartbeat.ts
Normal file
56
desktop/src/features/settings/hooks/useMeshOfferHeartbeat.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { useEffect } from "react";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
/**
|
||||
* Heartbeat interval (ms) for the mesh-LLM offer publisher.
|
||||
*
|
||||
* The Rust side stamps `expires_at = now + OFFER_TTL_SECS` (15 min) on
|
||||
* each publish. We re-publish at ~1/3 of that so even a single missed
|
||||
* heartbeat leaves the offer visible to consumers; only a crash that
|
||||
* misses two consecutive heartbeats actually drops us out of the UI.
|
||||
*
|
||||
* Kept here rather than computed from a Rust constant because the value
|
||||
* is fundamentally a *frontend timer* (Tauri-side has no concept of "the
|
||||
* UI mounted"); we just need it to stay strictly less than the
|
||||
* Rust-side TTL so the invariant holds.
|
||||
*/
|
||||
const HEARTBEAT_MS = 5 * 60 * 1000;
|
||||
|
||||
/**
|
||||
* While `enabled` is true, periodically re-invoke `mesh_publish_offer` so
|
||||
* the kind:31990 event's `expires_at` stays fresh. Republishes are NIP-33
|
||||
* replaces under the same `(pubkey, d_tag)` address — consumers do not see
|
||||
* a flicker; they just see the deadline advance.
|
||||
*
|
||||
* Errors are logged to the console and dropped on the floor — the user
|
||||
* is not in front of the settings panel waiting for them, and the next
|
||||
* heartbeat (or an explicit prefs change) will surface a fresh error if
|
||||
* the relay is durably down.
|
||||
*/
|
||||
export function useMeshOfferHeartbeat(params: {
|
||||
enabled: boolean;
|
||||
irohRelayUrl: string | null;
|
||||
}): void {
|
||||
const { enabled, irohRelayUrl } = params;
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled || irohRelayUrl == null || irohRelayUrl === "") {
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
const tick = async () => {
|
||||
if (cancelled) return;
|
||||
try {
|
||||
await invoke("mesh_publish_offer", { irohRelayUrl });
|
||||
} catch (e) {
|
||||
// Heartbeat failure is non-fatal — log and let the next tick try.
|
||||
console.warn("mesh-llm heartbeat failed:", e);
|
||||
}
|
||||
};
|
||||
const id = setInterval(tick, HEARTBEAT_MS);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
clearInterval(id);
|
||||
};
|
||||
}, [enabled, irohRelayUrl]);
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { invoke } from "@tauri-apps/api/core";
|
||||
import { Cpu, Users } from "lucide-react";
|
||||
|
||||
import { useMeshLlmOffers } from "@/features/settings/hooks/useMeshLlmOffers";
|
||||
import { useMeshOfferHeartbeat } from "@/features/settings/hooks/useMeshOfferHeartbeat";
|
||||
import { Switch } from "@/shared/ui/switch";
|
||||
import { Input } from "@/shared/ui/input";
|
||||
|
||||
@@ -57,11 +58,24 @@ function formatCap(value: number | null): string {
|
||||
export function MeshComputeSettingsCard() {
|
||||
const [prefs, setPrefs] = React.useState<ComputeSharingPrefs | null>(null);
|
||||
const [endpoint, setEndpoint] = React.useState<MeshEndpointInfo | null>(null);
|
||||
const [irohRelayUrl, setIrohRelayUrl] = React.useState<string | null>(null);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
const [saving, setSaving] = React.useState(false);
|
||||
const { offers, error: offersError } = useMeshLlmOffers();
|
||||
|
||||
// Load the persisted prefs + the iroh endpoint identity on mount.
|
||||
// While the card is mounted AND the user has compute-sharing enabled,
|
||||
// re-publish the offer every ~5 min so its `expires_at` doesn't lapse
|
||||
// (see useMeshOfferHeartbeat for the rationale). The heartbeat hook
|
||||
// no-ops when `enabled` is false or `irohRelayUrl` is missing.
|
||||
useMeshOfferHeartbeat({
|
||||
enabled: prefs?.enabled === true,
|
||||
irohRelayUrl,
|
||||
});
|
||||
|
||||
// Load the persisted prefs + the iroh endpoint identity + the relay's
|
||||
// iroh_relay_url on mount. The latter is what the heartbeat republishes
|
||||
// against, so we have to know it before the heartbeat fires its first
|
||||
// tick.
|
||||
React.useEffect(() => {
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
@@ -70,9 +84,20 @@ export function MeshComputeSettingsCard() {
|
||||
invoke<ComputeSharingPrefs>("mesh_get_sharing_prefs"),
|
||||
invoke<MeshEndpointInfo>("mesh_get_endpoint_id"),
|
||||
]);
|
||||
if (!cancelled) {
|
||||
setPrefs(p);
|
||||
setEndpoint(e);
|
||||
if (cancelled) return;
|
||||
setPrefs(p);
|
||||
setEndpoint(e);
|
||||
// Probe the relay's iroh_relay_url so the heartbeat hook has
|
||||
// something to publish against. Failures here are non-fatal —
|
||||
// the user can still see/edit prefs; we just won't heartbeat.
|
||||
try {
|
||||
const relayWsUrl = await invoke<string>("get_relay_ws_url");
|
||||
const irohUrl = await invoke<string | null>("mesh_relay_iroh_url", {
|
||||
relayWsUrl,
|
||||
});
|
||||
if (!cancelled) setIrohRelayUrl(irohUrl ?? null);
|
||||
} catch (probeErr) {
|
||||
console.warn("mesh-llm iroh url probe failed:", probeErr);
|
||||
}
|
||||
} catch (e) {
|
||||
if (!cancelled) setError(String(e));
|
||||
@@ -99,6 +124,7 @@ export function MeshComputeSettingsCard() {
|
||||
const irohUrl = await invoke<string | null>("mesh_relay_iroh_url", {
|
||||
relayWsUrl,
|
||||
});
|
||||
setIrohRelayUrl(irohUrl ?? null);
|
||||
if (irohUrl) {
|
||||
await invoke("mesh_publish_offer", { irohRelayUrl: irohUrl });
|
||||
} else if (next.enabled) {
|
||||
|
||||
Reference in New Issue
Block a user