Skip to content

Commit ca5d5a3

Browse files
committed
improve reliability of chats
1 parent 567d4f2 commit ca5d5a3

File tree

3 files changed

+214
-55
lines changed

3 files changed

+214
-55
lines changed

src/bridge/api.ts

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import type {
1515
SessionResponse,
1616
} from './types';
1717
import type { Protocol } from './utils';
18-
import { localNetworkFetchOptions, resolveBaseUrl } from './utils';
18+
import { fetchWithTimeout, resolveBaseUrl } from './utils';
1919

2020
export class EcaRemoteApi {
2121
private baseUrl: string;
@@ -37,12 +37,17 @@ export class EcaRemoteApi {
3737
return h;
3838
}
3939

40+
/** Default timeout for REST requests (ms). */
41+
private static readonly REQUEST_TIMEOUT_MS = 15_000;
42+
4043
/**
4144
* Generic fetch-check-parse helper.
4245
* - Adds auth headers automatically.
4346
* - Throws an `Error` when the response status is not OK,
4447
* unless `allowStatus` includes that specific code.
4548
* - Returns `undefined` for 204 No Content or void endpoints.
49+
* - Enforces a per-request timeout via AbortController to prevent
50+
* hung connections during page refresh or network instability.
4651
*/
4752
private async request<T = void>(
4853
path: string,
@@ -52,24 +57,33 @@ export class EcaRemoteApi {
5257
auth?: boolean;
5358
/** HTTP status codes that should NOT throw (e.g. 409 for idempotent ops). */
5459
allowStatus?: number[];
60+
/** Override the default request timeout (ms). */
61+
timeoutMs?: number;
5562
} = {},
5663
): Promise<T> {
57-
const { method = 'GET', body, auth = true, allowStatus = [] } = options;
64+
const { method = 'GET', body, auth = true, allowStatus = [], timeoutMs } = options;
5865
const hasBody = body !== undefined;
5966

6067
const url = `${this.baseUrl}${path}`;
61-
const res = await fetch(url, {
62-
...localNetworkFetchOptions(url),
63-
method,
64-
headers: auth ? this.headers(hasBody) : (hasBody ? { 'Content-Type': 'application/json' } : undefined),
65-
...(hasBody ? { body: JSON.stringify(body) } : {}),
66-
});
68+
const res = await fetchWithTimeout(
69+
url,
70+
{
71+
method,
72+
headers: auth ? this.headers(hasBody) : (hasBody ? { 'Content-Type': 'application/json' } : undefined),
73+
...(hasBody ? { body: JSON.stringify(body) } : {}),
74+
},
75+
timeoutMs ?? EcaRemoteApi.REQUEST_TIMEOUT_MS,
76+
);
6777

6878
if (!res.ok && !allowStatus.includes(res.status)) {
6979
// Try to extract a structured error message from the body
7080
const errBody = await res.json().catch(() => null);
7181
const message = errBody?.error?.message || `${method} ${path} failed: ${res.status}`;
72-
throw new Error(message);
82+
const error = new Error(message);
83+
// Attach the HTTP status code so callers can distinguish 404 from
84+
// transient errors (500, timeout) without fragile string matching.
85+
(error as any).status = res.status;
86+
throw error;
7387
}
7488

7589
// Return parsed JSON for responses that have a body

src/bridge/outbound-handler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export interface OutboundContext {
2626
/** Triggers initial state dispatch (called on webview/ready). */
2727
dispatchInitialState: () => Promise<void>;
2828
/** Lazy-load messages for a chat (fetches from server if not yet loaded). */
29-
loadChatMessages: (chatId: string) => Promise<boolean>;
29+
loadChatMessages: (chatId: string) => Promise<boolean | 'not_found' | 'error'>;
3030
}
3131

3232
/**

0 commit comments

Comments
 (0)