Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 15 additions & 28 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -252,30 +252,21 @@ function parseContactMergeBatchFile(path: string): Array<{
});
}

function getAppStatusMetadata(db: ReturnType<typeof openCuedDatabase>) {
return {
hostOs: resolveHostOS(),
version: getCurrentAppVersion(),
releaseChannel: getCurrentReleaseChannel(),
install: db.getAppMetadata(),
};
}

async function safeEmitHookEvent(
event: (typeof HOOK_EVENT_NAMES)[number],
payload: unknown,
): Promise<void> {
try {
await emitHookEvent(event, payload as Record<string, unknown>);
} catch (error) {
await emitHookEventSafe(event, payload as Record<string, unknown>, (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,
Expand All @@ -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 }),
Expand Down Expand Up @@ -321,16 +312,12 @@ async function buildLocalStatusFallback(error: DaemonRequestTimeoutError) {
}
}

function readCachedStatusSnapshot(): Record<string, unknown> | 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(),
Expand Down Expand Up @@ -571,7 +558,7 @@ async function main(): Promise<void> {
const overview = db.getMenuBarOverview();
const phoneCalls = db.countObservedPhoneCalls();
printJson({
app: getAppStatusMetadata(db),
app: buildAppStatusMetadata(db),
daemon: db.getDaemonState(),
overview,
relationshipSummary: {
Expand Down Expand Up @@ -602,14 +589,14 @@ async function main(): Promise<void> {
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({
Expand Down
67 changes: 15 additions & 52 deletions src/runtime/daemon/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -16,7 +16,6 @@ import {
import {
type AdapterPlatform,
getDefaultAccountKeyForPlatform,
type HostOS,
isPlatform,
type Platform,
type ProviderRawEventInput,
Expand Down Expand Up @@ -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";
Expand All @@ -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";
Expand Down Expand Up @@ -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;
}
Expand All @@ -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,
Expand All @@ -269,12 +240,6 @@ function writeMenuBarStatusSnapshot(
writeMenuBarStatusCache(snapshot);
}

function readCachedDaemonStatusSnapshot(options?: {
maxAgeMs?: number;
}): Record<string, unknown> | null {
return readMenuBarStatusCache(options)?.snapshot ?? null;
}

function now(): number {
return Date.now();
}
Expand Down Expand Up @@ -910,7 +875,7 @@ function buildDaemonBusyStatusSnapshot(options: {
error: unknown;
}) {
return {
app: getFallbackAppStatusMetadata(),
app: buildFallbackAppStatusMetadata(),
bootstrap: options.bootstrap,
daemon: {
pid: process.pid,
Expand Down Expand Up @@ -1005,11 +970,9 @@ async function safeEmitHookEvent(
| "message.received",
payload: Record<string, unknown>,
): Promise<void> {
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(
Expand Down Expand Up @@ -5076,7 +5039,7 @@ async function dispatchRequest(
};
case "status":
{
const cached = readCachedDaemonStatusSnapshot({
const cached = readMenuBarStatusSnapshot({
maxAgeMs: DAEMON_STATUS_CACHE_MAX_AGE_MS,
});
if (cached) {
Expand All @@ -5091,7 +5054,7 @@ async function dispatchRequest(
}
}
if (isBackgroundWorkActive()) {
const cached = readCachedDaemonStatusSnapshot();
const cached = readMenuBarStatusSnapshot();
if (cached) {
return {
id: request.id,
Expand Down Expand Up @@ -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,
Expand All @@ -5140,7 +5103,7 @@ async function dispatchRequest(
if (!isSqliteBusyError(error)) {
throw error;
}
const cached = readCachedDaemonStatusSnapshot();
const cached = readMenuBarStatusSnapshot();
if (cached) {
return {
id: request.id,
Expand Down Expand Up @@ -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,
Expand Down
25 changes: 25 additions & 0 deletions src/runtime/hooks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
},
]);
});
});
12 changes: 12 additions & 0 deletions src/runtime/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,18 @@ export async function emitHookEvent(
return Promise.all(executions);
}

export async function emitHookEventSafe(
event: HookEventName,
payload: Record<string, unknown>,
onError?: (event: HookEventName, error: unknown) => void,
): Promise<void> {
try {
await emitHookEvent(event, payload);
} catch (error) {
onError?.(event, error);
}
}

export async function testHookEvent(event: HookEventName): Promise<{
event: HookEventName;
executions: HookExecutionResult[];
Expand Down
5 changes: 5 additions & 0 deletions src/runtime/menu-bar-status-cache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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", () => {
Expand Down
6 changes: 6 additions & 0 deletions src/runtime/menu-bar-status-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ export function readMenuBarStatusCache(
}
}

export function readMenuBarStatusSnapshot(
options: ReadMenuBarStatusCacheOptions = {},
): Record<string, unknown> | null {
return readMenuBarStatusCache(options)?.snapshot ?? null;
}

export function writeMenuBarStatusCache(
snapshot: unknown,
options: WriteMenuBarStatusCacheOptions = {},
Expand Down
Loading