From 419dd6814595517f4fcc4197b9fef77cb9d83dfb Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 5 Sep 2026 23:13:56 +0200 Subject: [PATCH] Servarr events: don't crash the runtime when unconfigured MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The radarr/sonarr/lidarr/bookshelf events entrypoints built their client with XClient.fromEnv() at import time, which throws when no API key/URL is available yet — crash-looping the runtime container. The tools already guard this; the events entrypoint did not. Mirror the tools' guard: build the client in a try/catch, and only start the poll loop when it succeeds. When it fails, log one line and stay idle until a key is available. Behaviour when configured is unchanged. Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF --- modules/bookshelf/index.ts | 22 ++++++++++++++++++---- modules/lidarr/index.ts | 22 ++++++++++++++++++---- modules/radarr/index.ts | 22 ++++++++++++++++++---- modules/sonarr/index.ts | 22 ++++++++++++++++++---- 4 files changed, 72 insertions(+), 16 deletions(-) diff --git a/modules/bookshelf/index.ts b/modules/bookshelf/index.ts index 3a7bfc5..6b870d6 100644 --- a/modules/bookshelf/index.ts +++ b/modules/bookshelf/index.ts @@ -19,7 +19,17 @@ import { emit } from "@novox/mesh-sdk/events"; import { BookshelfClient, type BookshelfQueueItem } from "./client.js"; -const bookshelf = BookshelfClient.fromEnv(); +// Building the client throws when Bookshelf has no URL/key yet. Like the tools (see tools/index.ts), +// the events entrypoint must not crash the runtime for that — it stays idle until configured. +function buildClient(): BookshelfClient | null { + try { + return BookshelfClient.fromEnv(); + } catch { + return null; + } +} + +const bookshelf = buildClient(); // Bookshelf removes an item from the queue once it has been imported; a "warning"/"failed" status is // how a stuck or broken grab shows itself, so we do not call those a completion when they vanish. @@ -28,7 +38,7 @@ const FAILED_STATUSES = new Set(["failed", "warning"]); const inQueue = new Map(); let primed = false; -async function pollQueue(): Promise { +async function pollQueue(bookshelf: BookshelfClient): Promise { const { items } = await bookshelf.getQueue(); const now = new Map(items.map((i) => [i.id, i])); @@ -55,6 +65,10 @@ const tick = (fn: () => Promise, everyMs: number): void => { setInterval(run, everyMs); run(); }; -tick(pollQueue, 30_000); -console.log("[bookshelf] watching the download queue, emitting grabs and completions"); +if (bookshelf) { + tick(() => pollQueue(bookshelf), 30_000); + console.log("[bookshelf] watching the download queue, emitting grabs and completions"); +} else { + console.log("[bookshelf] not configured — events idle until an API key is available"); +} diff --git a/modules/lidarr/index.ts b/modules/lidarr/index.ts index 10e4a81..f286e2c 100644 --- a/modules/lidarr/index.ts +++ b/modules/lidarr/index.ts @@ -14,7 +14,17 @@ import { emit } from "@novox/mesh-sdk/events"; import { LidarrClient, type LidarrQueueItem } from "./client.js"; -const lidarr = LidarrClient.fromEnv(); +// Building the client throws when Lidarr has no URL/key yet. Like the tools (see tools/index.ts), +// the events entrypoint must not crash the runtime for that — it stays idle until configured. +function buildClient(): LidarrClient | null { + try { + return LidarrClient.fromEnv(); + } catch { + return null; + } +} + +const lidarr = buildClient(); // Lidarr removes an item from the queue once it has been imported; a "warning"/"failed" status is // how a stuck or broken grab shows itself, so we do not call those a completion when they vanish. @@ -23,7 +33,7 @@ const FAILED_STATUSES = new Set(["failed", "warning"]); const inQueue = new Map(); let primed = false; -async function pollQueue(): Promise { +async function pollQueue(lidarr: LidarrClient): Promise { const { items } = await lidarr.getQueue(); const now = new Map(items.map((i) => [i.id, i])); @@ -50,6 +60,10 @@ const tick = (fn: () => Promise, everyMs: number): void => { setInterval(run, everyMs); run(); }; -tick(pollQueue, 30_000); -console.log("[lidarr] watching the download queue, emitting grabs and completions"); +if (lidarr) { + tick(() => pollQueue(lidarr), 30_000); + console.log("[lidarr] watching the download queue, emitting grabs and completions"); +} else { + console.log("[lidarr] not configured — events idle until an API key is available"); +} diff --git a/modules/radarr/index.ts b/modules/radarr/index.ts index 28cd367..03c8fc2 100644 --- a/modules/radarr/index.ts +++ b/modules/radarr/index.ts @@ -14,7 +14,17 @@ import { emit } from "@novox/mesh-sdk/events"; import { RadarrClient, type RadarrQueueItem } from "./client.js"; -const radarr = RadarrClient.fromEnv(); +// Building the client throws when Radarr has no URL/key yet. Like the tools (see tools/index.ts), +// the events entrypoint must not crash the runtime for that — it stays idle until configured. +function buildClient(): RadarrClient | null { + try { + return RadarrClient.fromEnv(); + } catch { + return null; + } +} + +const radarr = buildClient(); // Radarr removes an item from the queue once it has been imported; a "warning"/"failed" status is // how a stuck or broken grab shows itself, so we do not call those a completion when they vanish. @@ -23,7 +33,7 @@ const FAILED_STATUSES = new Set(["failed", "warning"]); const inQueue = new Map(); let primed = false; -async function pollQueue(): Promise { +async function pollQueue(radarr: RadarrClient): Promise { const { items } = await radarr.getQueue(); const now = new Map(items.map((i) => [i.id, i])); @@ -50,6 +60,10 @@ const tick = (fn: () => Promise, everyMs: number): void => { setInterval(run, everyMs); run(); }; -tick(pollQueue, 30_000); -console.log("[radarr] watching the download queue, emitting grabs and completions"); +if (radarr) { + tick(() => pollQueue(radarr), 30_000); + console.log("[radarr] watching the download queue, emitting grabs and completions"); +} else { + console.log("[radarr] not configured — events idle until an API key is available"); +} diff --git a/modules/sonarr/index.ts b/modules/sonarr/index.ts index d2f1b05..67c7b6d 100644 --- a/modules/sonarr/index.ts +++ b/modules/sonarr/index.ts @@ -14,7 +14,17 @@ import { emit } from "@novox/mesh-sdk/events"; import { SonarrClient, type SonarrQueueItem } from "./client.js"; -const sonarr = SonarrClient.fromEnv(); +// Building the client throws when Sonarr has no URL/key yet. Like the tools (see tools/index.ts), +// the events entrypoint must not crash the runtime for that — it stays idle until configured. +function buildClient(): SonarrClient | null { + try { + return SonarrClient.fromEnv(); + } catch { + return null; + } +} + +const sonarr = buildClient(); // Sonarr removes an item from the queue once it has been imported; a "warning"/"failed" status is // how a stuck or broken grab shows itself, so we do not call those a completion when they vanish. @@ -23,7 +33,7 @@ const FAILED_STATUSES = new Set(["failed", "warning"]); const inQueue = new Map(); let primed = false; -async function pollQueue(): Promise { +async function pollQueue(sonarr: SonarrClient): Promise { const { items } = await sonarr.getQueue(); const now = new Map(items.map((i) => [i.id, i])); @@ -50,6 +60,10 @@ const tick = (fn: () => Promise, everyMs: number): void => { setInterval(run, everyMs); run(); }; -tick(pollQueue, 30_000); -console.log("[sonarr] watching the download queue, emitting grabs and completions"); +if (sonarr) { + tick(() => pollQueue(sonarr), 30_000); + console.log("[sonarr] watching the download queue, emitting grabs and completions"); +} else { + console.log("[sonarr] not configured — events idle until an API key is available"); +} -- 2.54.0