Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 777b882

Browse files
authored
refactor: centralize daemon transport client (#692)
1 parent eead9e0 commit 777b882

5 files changed

Lines changed: 216 additions & 184 deletions

File tree

src/browser/daemon-client.test.ts

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2+
3+
import {
4+
fetchDaemonStatus,
5+
isDaemonRunning,
6+
isExtensionConnected,
7+
requestDaemonShutdown,
8+
} from './daemon-client.js';
9+
10+
describe('daemon-client', () => {
11+
beforeEach(() => {
12+
vi.stubGlobal('fetch', vi.fn());
13+
});
14+
15+
afterEach(() => {
16+
vi.restoreAllMocks();
17+
});
18+
19+
it('fetchDaemonStatus sends the shared status request and returns parsed data', async () => {
20+
const status = {
21+
ok: true,
22+
pid: 123,
23+
uptime: 10,
24+
extensionConnected: true,
25+
extensionVersion: '1.2.3',
26+
pending: 0,
27+
lastCliRequestTime: Date.now(),
28+
memoryMB: 32,
29+
port: 19825,
30+
};
31+
const fetchMock = vi.mocked(fetch);
32+
fetchMock.mockResolvedValue({
33+
ok: true,
34+
json: () => Promise.resolve(status),
35+
} as Response);
36+
37+
await expect(fetchDaemonStatus()).resolves.toEqual(status);
38+
expect(fetchMock).toHaveBeenCalledWith(
39+
expect.stringMatching(/\/status$/),
40+
expect.objectContaining({
41+
headers: expect.objectContaining({ 'X-OpenCLI': '1' }),
42+
}),
43+
);
44+
});
45+
46+
it('fetchDaemonStatus returns null on network failure', async () => {
47+
vi.mocked(fetch).mockRejectedValue(new Error('ECONNREFUSED'));
48+
49+
await expect(fetchDaemonStatus()).resolves.toBeNull();
50+
});
51+
52+
it('requestDaemonShutdown POSTs to the shared shutdown endpoint', async () => {
53+
const fetchMock = vi.mocked(fetch);
54+
fetchMock.mockResolvedValue({ ok: true } as Response);
55+
56+
await expect(requestDaemonShutdown()).resolves.toBe(true);
57+
expect(fetchMock).toHaveBeenCalledWith(
58+
expect.stringMatching(/\/shutdown$/),
59+
expect.objectContaining({
60+
method: 'POST',
61+
headers: expect.objectContaining({ 'X-OpenCLI': '1' }),
62+
}),
63+
);
64+
});
65+
66+
it('isDaemonRunning reflects shared status availability', async () => {
67+
vi.mocked(fetch).mockResolvedValue({
68+
ok: true,
69+
json: () =>
70+
Promise.resolve({
71+
ok: true,
72+
pid: 123,
73+
uptime: 10,
74+
extensionConnected: false,
75+
pending: 0,
76+
lastCliRequestTime: Date.now(),
77+
memoryMB: 16,
78+
port: 19825,
79+
}),
80+
} as Response);
81+
82+
await expect(isDaemonRunning()).resolves.toBe(true);
83+
});
84+
85+
it('isExtensionConnected reflects shared status payload', async () => {
86+
vi.mocked(fetch).mockResolvedValue({
87+
ok: true,
88+
json: () =>
89+
Promise.resolve({
90+
ok: true,
91+
pid: 123,
92+
uptime: 10,
93+
extensionConnected: false,
94+
pending: 0,
95+
lastCliRequestTime: Date.now(),
96+
memoryMB: 16,
97+
port: 19825,
98+
}),
99+
} as Response);
100+
101+
await expect(isExtensionConnected()).resolves.toBe(false);
102+
});
103+
});

src/browser/daemon-client.ts

Lines changed: 49 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { isTransientBrowserError } from './errors.js';
1111

1212
const DAEMON_PORT = parseInt(process.env.OPENCLI_DAEMON_PORT ?? String(DEFAULT_DAEMON_PORT), 10);
1313
const DAEMON_URL = `http://127.0.0.1:${DAEMON_PORT}`;
14+
const OPENCLI_HEADERS = { 'X-OpenCLI': '1' };
1415

1516
let _idCounter = 0;
1617

@@ -44,42 +45,65 @@ export interface DaemonResult {
4445
error?: string;
4546
}
4647

47-
/**
48-
* Check if daemon is running.
49-
*/
50-
export async function isDaemonRunning(): Promise<boolean> {
48+
export interface DaemonStatus {
49+
ok: boolean;
50+
pid: number;
51+
uptime: number;
52+
extensionConnected: boolean;
53+
extensionVersion?: string;
54+
pending: number;
55+
lastCliRequestTime: number;
56+
memoryMB: number;
57+
port: number;
58+
}
59+
60+
async function requestDaemon(pathname: string, init?: RequestInit & { timeout?: number }): Promise<Response> {
61+
const { timeout = 2000, headers, ...rest } = init ?? {};
62+
const controller = new AbortController();
63+
const timer = setTimeout(() => controller.abort(), timeout);
5164
try {
52-
const controller = new AbortController();
53-
const timer = setTimeout(() => controller.abort(), 2000);
54-
const res = await fetch(`${DAEMON_URL}/status`, {
55-
headers: { 'X-OpenCLI': '1' },
65+
return await fetch(`${DAEMON_URL}${pathname}`, {
66+
...rest,
67+
headers: { ...OPENCLI_HEADERS, ...headers },
5668
signal: controller.signal,
5769
});
70+
} finally {
5871
clearTimeout(timer);
72+
}
73+
}
74+
75+
export async function fetchDaemonStatus(opts?: { timeout?: number }): Promise<DaemonStatus | null> {
76+
try {
77+
const res = await requestDaemon('/status', { timeout: opts?.timeout ?? 2000 });
78+
if (!res.ok) return null;
79+
return await res.json() as DaemonStatus;
80+
} catch {
81+
return null;
82+
}
83+
}
84+
85+
export async function requestDaemonShutdown(opts?: { timeout?: number }): Promise<boolean> {
86+
try {
87+
const res = await requestDaemon('/shutdown', { method: 'POST', timeout: opts?.timeout ?? 5000 });
5988
return res.ok;
6089
} catch {
6190
return false;
6291
}
6392
}
6493

94+
/**
95+
* Check if daemon is running.
96+
*/
97+
export async function isDaemonRunning(): Promise<boolean> {
98+
return (await fetchDaemonStatus()) !== null;
99+
}
100+
65101
/**
66102
* Check if daemon is running AND the extension is connected.
67103
*/
68104
export async function isExtensionConnected(): Promise<boolean> {
69-
try {
70-
const controller = new AbortController();
71-
const timer = setTimeout(() => controller.abort(), 2000);
72-
const res = await fetch(`${DAEMON_URL}/status`, {
73-
headers: { 'X-OpenCLI': '1' },
74-
signal: controller.signal,
75-
});
76-
clearTimeout(timer);
77-
if (!res.ok) return false;
78-
const data = await res.json() as { extensionConnected?: boolean };
79-
return !!data.extensionConnected;
80-
} catch {
81-
return false;
82-
}
105+
const status = await fetchDaemonStatus();
106+
return !!status?.extensionConnected;
83107
}
84108

85109
/**
@@ -98,16 +122,12 @@ export async function sendCommand(
98122
const id = generateId();
99123
const command: DaemonCommand = { id, action, ...params };
100124
try {
101-
const controller = new AbortController();
102-
const timer = setTimeout(() => controller.abort(), 30000);
103-
104-
const res = await fetch(`${DAEMON_URL}/command`, {
125+
const res = await requestDaemon('/command', {
105126
method: 'POST',
106-
headers: { 'Content-Type': 'application/json', 'X-OpenCLI': '1' },
127+
headers: { 'Content-Type': 'application/json' },
107128
body: JSON.stringify(command),
108-
signal: controller.signal,
129+
timeout: 30000,
109130
});
110-
clearTimeout(timer);
111131

112132
const result = (await res.json()) as DaemonResult;
113133

src/browser/discover.ts

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@
22
* Daemon discovery — checks if the daemon is running.
33
*/
44

5-
import { DEFAULT_DAEMON_PORT } from '../constants.js';
6-
import { isDaemonRunning } from './daemon-client.js';
5+
import { fetchDaemonStatus, isDaemonRunning } from './daemon-client.js';
76

87
export { isDaemonRunning };
98

@@ -15,21 +14,13 @@ export async function checkDaemonStatus(opts?: { timeout?: number }): Promise<{
1514
extensionConnected: boolean;
1615
extensionVersion?: string;
1716
}> {
18-
try {
19-
const port = parseInt(process.env.OPENCLI_DAEMON_PORT ?? String(DEFAULT_DAEMON_PORT), 10);
20-
const controller = new AbortController();
21-
const timer = setTimeout(() => controller.abort(), opts?.timeout ?? 2000);
22-
try {
23-
const res = await fetch(`http://127.0.0.1:${port}/status`, {
24-
headers: { 'X-OpenCLI': '1' },
25-
signal: controller.signal,
26-
});
27-
const data = await res.json() as { ok: boolean; extensionConnected: boolean; extensionVersion?: string };
28-
return { running: true, extensionConnected: data.extensionConnected, extensionVersion: data.extensionVersion };
29-
} finally {
30-
clearTimeout(timer);
31-
}
32-
} catch {
17+
const status = await fetchDaemonStatus({ timeout: opts?.timeout ?? 2000 });
18+
if (!status) {
3319
return { running: false, extensionConnected: false };
3420
}
21+
return {
22+
running: true,
23+
extensionConnected: status.extensionConnected,
24+
extensionVersion: status.extensionVersion,
25+
};
3526
}

0 commit comments

Comments
 (0)