From 32e85951258433e4aca3f94637ec619d087637da Mon Sep 17 00:00:00 2001 From: Soham Bafana Date: Sun, 24 May 2026 13:17:51 -0400 Subject: [PATCH] Deduplicate status and hook helpers --- src/cli.ts | 43 +++++---------- src/runtime/daemon/server.ts | 67 +++++------------------ src/runtime/hooks.test.ts | 25 +++++++++ src/runtime/hooks.ts | 12 ++++ src/runtime/menu-bar-status-cache.test.ts | 5 ++ src/runtime/menu-bar-status-cache.ts | 6 ++ src/runtime/status.ts | 20 +++++++ 7 files changed, 98 insertions(+), 80 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 25a3a6ff..715e6b8f 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -6,9 +6,8 @@ import { dirname, join, resolve } from "node:path"; import process from "node:process"; import { fileURLToPath } from "node:url"; import { DaemonRequestTimeoutError, sendDaemonRequest } from "./client.js"; -import { getCurrentAppVersion, getCurrentReleaseChannel } from "./core/app-metadata.js"; +import { getCurrentAppVersion } from "./core/app-metadata.js"; import { CUED_DB_PATH, CUED_SOCKET_PATH, ensureCuedDirs } from "./core/config.js"; -import { resolveHostOS } from "./core/platform-capabilities.js"; import { openCuedDatabase, openCuedDatabaseReadOnly, @@ -43,7 +42,7 @@ import { runDaemon } from "./runtime/daemon/server.js"; import { buildDoctorReport, buildPermissionStatus } from "./runtime/doctor.js"; import { doctorHooksConfig, - emitHookEvent, + emitHookEventSafe, HOOK_EVENT_NAMES, initHooksConfig, testHookEvent, @@ -54,9 +53,10 @@ import { parseLogsCommandArgs, readRecentLogLines, } from "./runtime/logs.js"; -import { readMenuBarStatusCache } from "./runtime/menu-bar-status-cache.js"; +import { readMenuBarStatusSnapshot } from "./runtime/menu-bar-status-cache.js"; import { buildOnboardingSnapshot } from "./runtime/onboarding.js"; import { runProjectionWorkerFromEnv } from "./runtime/projection/worker.js"; +import { buildAppStatusMetadata } from "./runtime/status.js"; import { checkForUpdates, clearUpdateHelperPendingState, @@ -252,30 +252,21 @@ function parseContactMergeBatchFile(path: string): Array<{ }); } -function getAppStatusMetadata(db: ReturnType) { - return { - hostOs: resolveHostOS(), - version: getCurrentAppVersion(), - releaseChannel: getCurrentReleaseChannel(), - install: db.getAppMetadata(), - }; -} - async function safeEmitHookEvent( event: (typeof HOOK_EVENT_NAMES)[number], payload: unknown, ): Promise { - try { - await emitHookEvent(event, payload as Record); - } catch (error) { + await emitHookEventSafe(event, payload as Record, (failedEvent, error) => { console.warn( - `[cued hooks] ${event} failed: ${error instanceof Error ? error.message : String(error)}`, + `[cued hooks] ${failedEvent} failed: ${ + error instanceof Error ? error.message : String(error) + }`, ); - } + }); } async function buildLocalStatusFallback(error: DaemonRequestTimeoutError) { - const cached = readCachedStatusSnapshot(); + const cached = readMenuBarStatusSnapshot(); if (cached) { return { ...cached, @@ -293,7 +284,7 @@ async function buildLocalStatusFallback(error: DaemonRequestTimeoutError) { const db = openExistingCuedDatabase(undefined, { readonly: true }); try { return { - app: getAppStatusMetadata(db), + app: buildAppStatusMetadata(db), daemon: db.getDaemonState(), overview: db.getOverview(), projection: db.getProjectionBacklog({ initializeProjectionState: false }), @@ -321,16 +312,12 @@ async function buildLocalStatusFallback(error: DaemonRequestTimeoutError) { } } -function readCachedStatusSnapshot(): Record | null { - return readMenuBarStatusCache()?.snapshot ?? null; -} - async function buildLocalDoctorFallback(error: DaemonRequestTimeoutError) { try { const db = openExistingCuedDatabase(undefined, { readonly: true }); try { return { - app: getAppStatusMetadata(db), + app: buildAppStatusMetadata(db), ...(await buildDoctorReport(db)), projection: db.getProjectionBacklog({ initializeProjectionState: false }), hooks: doctorHooksConfig(), @@ -571,7 +558,7 @@ async function main(): Promise { const overview = db.getMenuBarOverview(); const phoneCalls = db.countObservedPhoneCalls(); printJson({ - app: getAppStatusMetadata(db), + app: buildAppStatusMetadata(db), daemon: db.getDaemonState(), overview, relationshipSummary: { @@ -602,14 +589,14 @@ async function main(): Promise { printJson( command === "doctor" ? { - app: getAppStatusMetadata(db), + app: buildAppStatusMetadata(db), ...(await buildDoctorReport(db)), projection: db.getProjectionBacklog(), hooks: doctorHooksConfig(), update: getUpdateStatus(db), } : { - app: getAppStatusMetadata(db), + app: buildAppStatusMetadata(db), daemon: db.getDaemonState(), overview: db.getOverview(), projection: db.getProjectionBacklog({ diff --git a/src/runtime/daemon/server.ts b/src/runtime/daemon/server.ts index b3025f2e..3684c3ae 100644 --- a/src/runtime/daemon/server.ts +++ b/src/runtime/daemon/server.ts @@ -3,7 +3,7 @@ import { existsSync, type FSWatcher, rmSync, watch } from "node:fs"; import { createConnection, createServer, type Socket } from "node:net"; import { basename, dirname } from "node:path"; import process from "node:process"; -import { getCurrentAppVersion, getCurrentReleaseChannel } from "../../core/app-metadata.js"; +import { getCurrentAppVersion } from "../../core/app-metadata.js"; import { CUED_DAEMON_LOCK_PATH, CUED_SOCKET_PATH } from "../../core/config.js"; import { createLogger } from "../../core/logging.js"; import { @@ -16,7 +16,6 @@ import { import { type AdapterPlatform, getDefaultAccountKeyForPlatform, - type HostOS, isPlatform, type Platform, type ProviderRawEventInput, @@ -118,10 +117,10 @@ import { } from "../../telemetry/context.js"; import { fetchAttachment, listAttachments, searchAttachments } from "../attachments.js"; import { buildPermissionStatus } from "../doctor.js"; -import { emitHookEvent } from "../hooks.js"; +import { emitHookEventSafe } from "../hooks.js"; import type { DaemonRequest, DaemonResponse } from "../ipc.js"; import { - readMenuBarStatusCache, + readMenuBarStatusSnapshot, removeMenuBarStatusCache, writeMenuBarStatusCache, } from "../menu-bar-status-cache.js"; @@ -140,8 +139,10 @@ import { import type { ProjectionWorkerMessage, ProjectionWorkerSuccess } from "../projection/worker.js"; import { RunQueueService } from "../run-queue.js"; import { + buildAppStatusMetadata, buildDaemonStatusSnapshot, buildDoctorSnapshot, + buildFallbackAppStatusMetadata, buildMenuBarDaemonStatusSnapshot, type DaemonBootstrapSnapshot, } from "../status.js"; @@ -211,36 +212,6 @@ const slackLogger = createLogger("slack"); const signalLogger = createLogger("signal"); const whatsAppLogger = createLogger("whatsapp"); -function getAppStatusMetadata(db: { getAppMetadata: () => unknown }): { - hostOs: HostOS; - version: string; - releaseChannel: string; - install: unknown; -} { - return { - hostOs: - process.platform === "darwin" ? "macos" : process.platform === "win32" ? "windows" : "linux", - version: DAEMON_VERSION, - releaseChannel: getCurrentReleaseChannel(), - install: db.getAppMetadata(), - }; -} - -function getFallbackAppStatusMetadata(): { - hostOs: HostOS; - version: string; - releaseChannel: string; - install: null; -} { - return { - hostOs: - process.platform === "darwin" ? "macos" : process.platform === "win32" ? "windows" : "linux", - version: DAEMON_VERSION, - releaseChannel: getCurrentReleaseChannel(), - install: null, - }; -} - function isOutboundSendEnabled(): boolean { return false; } @@ -257,7 +228,7 @@ function writeMenuBarStatusSnapshot( }, ): void { const snapshot = buildMenuBarDaemonStatusSnapshot(db, { - app: getAppStatusMetadata(db), + app: buildAppStatusMetadata(db), discordRealtime: options.discordRealtime, slackRealtime: options.slackRealtime, linkedInRealtime: options.linkedInRealtime, @@ -269,12 +240,6 @@ function writeMenuBarStatusSnapshot( writeMenuBarStatusCache(snapshot); } -function readCachedDaemonStatusSnapshot(options?: { - maxAgeMs?: number; -}): Record | null { - return readMenuBarStatusCache(options)?.snapshot ?? null; -} - function now(): number { return Date.now(); } @@ -910,7 +875,7 @@ function buildDaemonBusyStatusSnapshot(options: { error: unknown; }) { return { - app: getFallbackAppStatusMetadata(), + app: buildFallbackAppStatusMetadata(), bootstrap: options.bootstrap, daemon: { pid: process.pid, @@ -1005,11 +970,9 @@ async function safeEmitHookEvent( | "message.received", payload: Record, ): Promise { - try { - await emitHookEvent(event, payload); - } catch (error) { - hooksLogger.warn(`${event} failed`, error); - } + await emitHookEventSafe(event, payload, (failedEvent, error) => { + hooksLogger.warn(`${failedEvent} failed`, error); + }); } async function emitAuthenticatedHook( @@ -5076,7 +5039,7 @@ async function dispatchRequest( }; case "status": { - const cached = readCachedDaemonStatusSnapshot({ + const cached = readMenuBarStatusSnapshot({ maxAgeMs: DAEMON_STATUS_CACHE_MAX_AGE_MS, }); if (cached) { @@ -5091,7 +5054,7 @@ async function dispatchRequest( } } if (isBackgroundWorkActive()) { - const cached = readCachedDaemonStatusSnapshot(); + const cached = readMenuBarStatusSnapshot(); if (cached) { return { id: request.id, @@ -5125,7 +5088,7 @@ async function dispatchRequest( ok: true, result: await db.withBusyTimeout(DAEMON_STATUS_BUSY_TIMEOUT_MS, () => buildDaemonStatusSnapshot(db, { - app: getAppStatusMetadata(db), + app: buildAppStatusMetadata(db), discordRealtime, slackRealtime, linkedInRealtime, @@ -5140,7 +5103,7 @@ async function dispatchRequest( if (!isSqliteBusyError(error)) { throw error; } - const cached = readCachedDaemonStatusSnapshot(); + const cached = readMenuBarStatusSnapshot(); if (cached) { return { id: request.id, @@ -5205,7 +5168,7 @@ async function dispatchRequest( DAEMON_STATUS_BUSY_TIMEOUT_MS, async () => await buildDoctorSnapshot(db, { - app: getAppStatusMetadata(db), + app: buildAppStatusMetadata(db), discordRealtime, slackRealtime, linkedInRealtime, diff --git a/src/runtime/hooks.test.ts b/src/runtime/hooks.test.ts index 38770a66..35e03897 100644 --- a/src/runtime/hooks.test.ts +++ b/src/runtime/hooks.test.ts @@ -78,4 +78,29 @@ args = ["-lc", "cat > ${join(home, "hook-payload.json")}"] expect(payload).toContain('"event": "sync.completed"'); expect(payload).toContain('"runId": "123"'); }); + + it("reports hook load errors without throwing through the safe emitter", async () => { + const home = setTempHome(); + const { emitHookEventSafe } = await loadHooksService(); + const hooksDir = join(home, ".cued"); + mkdirSync(hooksDir, { recursive: true }); + writeFileSync(join(hooksDir, "hooks.toml"), "[[hooks]\n", "utf8"); + + const errors: Array<{ event: string; message: string }> = []; + await expect( + emitHookEventSafe("sync.completed", { ok: true }, (event, error) => { + errors.push({ + event, + message: error instanceof Error ? error.message : String(error), + }); + }), + ).resolves.toBeUndefined(); + + expect(errors).toEqual([ + { + event: "sync.completed", + message: expect.any(String), + }, + ]); + }); }); diff --git a/src/runtime/hooks.ts b/src/runtime/hooks.ts index 7e4a401e..f04c9250 100644 --- a/src/runtime/hooks.ts +++ b/src/runtime/hooks.ts @@ -288,6 +288,18 @@ export async function emitHookEvent( return Promise.all(executions); } +export async function emitHookEventSafe( + event: HookEventName, + payload: Record, + onError?: (event: HookEventName, error: unknown) => void, +): Promise { + try { + await emitHookEvent(event, payload); + } catch (error) { + onError?.(event, error); + } +} + export async function testHookEvent(event: HookEventName): Promise<{ event: HookEventName; executions: HookExecutionResult[]; diff --git a/src/runtime/menu-bar-status-cache.test.ts b/src/runtime/menu-bar-status-cache.test.ts index aa43e573..6fb717d6 100644 --- a/src/runtime/menu-bar-status-cache.test.ts +++ b/src/runtime/menu-bar-status-cache.test.ts @@ -4,6 +4,7 @@ import { join } from "node:path"; import { afterEach, describe, expect, it } from "vitest"; import { readMenuBarStatusCache, + readMenuBarStatusSnapshot, removeMenuBarStatusCache, writeMenuBarStatusCache, } from "./menu-bar-status-cache.js"; @@ -50,6 +51,10 @@ describe("menu bar status cache", () => { mtimeMs: expect.any(Number), }), ); + expect(readMenuBarStatusSnapshot({ statusPath, dbPath: "/tmp/cued-test/local.db" })).toEqual({ + dbPath: "/tmp/cued-test/local.db", + overview: { messages: 12 }, + }); }); it("rejects stale, invalid, and wrong-database cache entries", () => { diff --git a/src/runtime/menu-bar-status-cache.ts b/src/runtime/menu-bar-status-cache.ts index 880cd6ea..df100da4 100644 --- a/src/runtime/menu-bar-status-cache.ts +++ b/src/runtime/menu-bar-status-cache.ts @@ -54,6 +54,12 @@ export function readMenuBarStatusCache( } } +export function readMenuBarStatusSnapshot( + options: ReadMenuBarStatusCacheOptions = {}, +): Record | null { + return readMenuBarStatusCache(options)?.snapshot ?? null; +} + export function writeMenuBarStatusCache( snapshot: unknown, options: WriteMenuBarStatusCacheOptions = {}, diff --git a/src/runtime/status.ts b/src/runtime/status.ts index 41567774..81eb5316 100644 --- a/src/runtime/status.ts +++ b/src/runtime/status.ts @@ -1,3 +1,5 @@ +import { getCurrentAppVersion, getCurrentReleaseChannel } from "../core/app-metadata.js"; +import { resolveHostOS } from "../core/platform-capabilities.js"; import type { CuedDatabase } from "../db/database.js"; import { buildIntegrationStatus, @@ -20,6 +22,24 @@ export interface DaemonBootstrapSnapshot { error: string | null; } +export function buildAppStatusMetadata(db: Pick) { + return { + hostOs: resolveHostOS(), + version: getCurrentAppVersion(), + releaseChannel: getCurrentReleaseChannel(), + install: db.getAppMetadata(), + }; +} + +export function buildFallbackAppStatusMetadata() { + return { + hostOs: resolveHostOS(), + version: getCurrentAppVersion(), + releaseChannel: getCurrentReleaseChannel(), + install: null, + }; +} + export async function buildDoctorSnapshot( db: CuedDatabase, options: {