something
This commit is contained in:
@@ -21,6 +21,12 @@ function getCacheKey(endpoint, options = {}) {
|
||||
return `${endpoint}${query ? '?' + query : ''}`;
|
||||
}
|
||||
|
||||
// Utility function to normalize array responses (handles both old and new formats)
|
||||
export function normalizeArrayResponse(response) {
|
||||
const data = response?.data || response;
|
||||
return Array.isArray(data) ? data : [];
|
||||
}
|
||||
|
||||
// Sentinel value to indicate a request was superseded (instead of rejecting)
|
||||
// Export it so components can check for it
|
||||
export const REQUEST_SUPERSEDED = Symbol('REQUEST_SUPERSEDED');
|
||||
@@ -36,6 +42,9 @@ function debounceRequest(key, requestFn, delay = DEBOUNCE_DELAY) {
|
||||
if (pending.timestamp && (now - pending.timestamp) < DEDUPE_WINDOW) {
|
||||
pending.promise.then(resolve).catch(reject);
|
||||
return;
|
||||
} else {
|
||||
// Request is older than dedupe window - remove it and create new one
|
||||
pendingRequests.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,8 +83,16 @@ export const setAuthErrorHandler = (handler) => {
|
||||
onAuthError = handler;
|
||||
};
|
||||
|
||||
const handleAuthError = (response) => {
|
||||
// Whitelist of endpoints that should NOT trigger auth error handling
|
||||
// These are endpoints that can legitimately return 401/403 without meaning the user is logged out
|
||||
const AUTH_CHECK_ENDPOINTS = ['/auth/me', '/auth/logout'];
|
||||
|
||||
const handleAuthError = (response, endpoint) => {
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
// Don't trigger auth error handler for endpoints that check auth status
|
||||
if (AUTH_CHECK_ENDPOINTS.includes(endpoint)) {
|
||||
return;
|
||||
}
|
||||
// Trigger auth error handler if set (this will clear user state)
|
||||
if (onAuthError) {
|
||||
onAuthError();
|
||||
@@ -89,60 +106,79 @@ const handleAuthError = (response) => {
|
||||
}
|
||||
};
|
||||
|
||||
// Extract error message from response - centralized to avoid duplication
|
||||
async function extractErrorMessage(response) {
|
||||
try {
|
||||
const errorData = await response.json();
|
||||
return errorData?.error || response.statusText;
|
||||
} catch {
|
||||
return response.statusText;
|
||||
}
|
||||
}
|
||||
|
||||
export const api = {
|
||||
async get(endpoint) {
|
||||
async get(endpoint, options = {}) {
|
||||
const abortController = options.signal || new AbortController();
|
||||
const response = await fetch(`${API_BASE}${endpoint}`, {
|
||||
credentials: 'include', // Include cookies for session
|
||||
signal: abortController.signal,
|
||||
});
|
||||
if (!response.ok) {
|
||||
// Handle auth errors before parsing response
|
||||
// Don't redirect on /auth/me - that's the auth check itself
|
||||
if ((response.status === 401 || response.status === 403) && !endpoint.startsWith('/auth/')) {
|
||||
handleAuthError(response);
|
||||
// Don't redirect - let React handle UI change through state
|
||||
}
|
||||
const errorData = await response.json().catch(() => null);
|
||||
const errorMessage = errorData?.error || response.statusText;
|
||||
handleAuthError(response, endpoint);
|
||||
const errorMessage = await extractErrorMessage(response);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
return response.json();
|
||||
},
|
||||
|
||||
async post(endpoint, data) {
|
||||
async post(endpoint, data, options = {}) {
|
||||
const abortController = options.signal || new AbortController();
|
||||
const response = await fetch(`${API_BASE}${endpoint}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data),
|
||||
credentials: 'include', // Include cookies for session
|
||||
signal: abortController.signal,
|
||||
});
|
||||
if (!response.ok) {
|
||||
// Handle auth errors before parsing response
|
||||
// Don't redirect on /auth/* endpoints - those are login/logout
|
||||
if ((response.status === 401 || response.status === 403) && !endpoint.startsWith('/auth/')) {
|
||||
handleAuthError(response);
|
||||
// Don't redirect - let React handle UI change through state
|
||||
}
|
||||
const errorData = await response.json().catch(() => null);
|
||||
const errorMessage = errorData?.error || response.statusText;
|
||||
handleAuthError(response, endpoint);
|
||||
const errorMessage = await extractErrorMessage(response);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
return response.json();
|
||||
},
|
||||
|
||||
async delete(endpoint) {
|
||||
async patch(endpoint, data, options = {}) {
|
||||
const abortController = options.signal || new AbortController();
|
||||
const response = await fetch(`${API_BASE}${endpoint}`, {
|
||||
method: 'DELETE',
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: data ? JSON.stringify(data) : undefined,
|
||||
credentials: 'include', // Include cookies for session
|
||||
signal: abortController.signal,
|
||||
});
|
||||
if (!response.ok) {
|
||||
// Handle auth errors before parsing response
|
||||
// Don't redirect on /auth/* endpoints
|
||||
if ((response.status === 401 || response.status === 403) && !endpoint.startsWith('/auth/')) {
|
||||
handleAuthError(response);
|
||||
// Don't redirect - let React handle UI change through state
|
||||
}
|
||||
const errorData = await response.json().catch(() => null);
|
||||
const errorMessage = errorData?.error || response.statusText;
|
||||
handleAuthError(response, endpoint);
|
||||
const errorMessage = await extractErrorMessage(response);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
return response.json();
|
||||
},
|
||||
|
||||
async delete(endpoint, options = {}) {
|
||||
const abortController = options.signal || new AbortController();
|
||||
const response = await fetch(`${API_BASE}${endpoint}`, {
|
||||
method: 'DELETE',
|
||||
credentials: 'include', // Include cookies for session
|
||||
signal: abortController.signal,
|
||||
});
|
||||
if (!response.ok) {
|
||||
// Handle auth errors before parsing response
|
||||
handleAuthError(response, endpoint);
|
||||
const errorMessage = await extractErrorMessage(response);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
return response.json();
|
||||
@@ -179,8 +215,7 @@ export const api = {
|
||||
} else {
|
||||
// Handle auth errors
|
||||
if (xhr.status === 401 || xhr.status === 403) {
|
||||
handleAuthError({ status: xhr.status });
|
||||
// Don't redirect - let React handle UI change through state
|
||||
handleAuthError({ status: xhr.status }, endpoint);
|
||||
}
|
||||
try {
|
||||
const errorData = JSON.parse(xhr.responseText);
|
||||
@@ -263,7 +298,7 @@ export const jobs = {
|
||||
if (options.status) params.append('status', options.status);
|
||||
if (options.sort) params.append('sort', options.sort);
|
||||
const query = params.toString();
|
||||
return api.get(`/jobs/summary${query ? '?' + query : ''}`);
|
||||
return api.get(`/jobs/summary${query ? '?' + query : ''}`, options);
|
||||
});
|
||||
},
|
||||
|
||||
@@ -286,7 +321,7 @@ export const jobs = {
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
return api.get(`/jobs/${id}`);
|
||||
return api.get(`/jobs/${id}`, options);
|
||||
});
|
||||
},
|
||||
|
||||
@@ -319,7 +354,7 @@ export const jobs = {
|
||||
if (options.file_type) params.append('file_type', options.file_type);
|
||||
if (options.extension) params.append('extension', options.extension);
|
||||
const query = params.toString();
|
||||
return api.get(`/jobs/${jobId}/files${query ? '?' + query : ''}`);
|
||||
return api.get(`/jobs/${jobId}/files${query ? '?' + query : ''}`, options);
|
||||
});
|
||||
},
|
||||
|
||||
@@ -333,8 +368,8 @@ export const jobs = {
|
||||
});
|
||||
},
|
||||
|
||||
async getContextArchive(jobId) {
|
||||
return api.get(`/jobs/${jobId}/context`);
|
||||
async getContextArchive(jobId, options = {}) {
|
||||
return api.get(`/jobs/${jobId}/context`, options);
|
||||
},
|
||||
|
||||
downloadFile(jobId, fileId) {
|
||||
@@ -354,7 +389,7 @@ export const jobs = {
|
||||
if (options.limit) params.append('limit', options.limit.toString());
|
||||
if (options.sinceId) params.append('since_id', options.sinceId.toString());
|
||||
const query = params.toString();
|
||||
const result = await api.get(`/jobs/${jobId}/tasks/${taskId}/logs${query ? '?' + query : ''}`);
|
||||
const result = await api.get(`/jobs/${jobId}/tasks/${taskId}/logs${query ? '?' + query : ''}`, options);
|
||||
// Handle both old format (array) and new format (object with logs, last_id, limit)
|
||||
if (Array.isArray(result)) {
|
||||
return { logs: result, last_id: result.length > 0 ? result[result.length - 1].id : 0, limit: options.limit || 100 };
|
||||
@@ -363,10 +398,21 @@ export const jobs = {
|
||||
});
|
||||
},
|
||||
|
||||
async getTaskSteps(jobId, taskId) {
|
||||
return api.get(`/jobs/${jobId}/tasks/${taskId}/steps`);
|
||||
async getTaskSteps(jobId, taskId, options = {}) {
|
||||
return api.get(`/jobs/${jobId}/tasks/${taskId}/steps`, options);
|
||||
},
|
||||
|
||||
// New unified client WebSocket - DEPRECATED: Use wsManager from websocket.js instead
|
||||
// This is kept for backwards compatibility but should not be used
|
||||
streamClientWebSocket() {
|
||||
console.warn('streamClientWebSocket() is deprecated - use wsManager from websocket.js instead');
|
||||
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const wsHost = window.location.host;
|
||||
const url = `${wsProtocol}//${wsHost}${API_BASE}/ws`;
|
||||
return new WebSocket(url);
|
||||
},
|
||||
|
||||
// Old WebSocket methods (to be removed after migration)
|
||||
streamTaskLogsWebSocket(jobId, taskId, lastId = 0) {
|
||||
// Convert HTTP to WebSocket URL
|
||||
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
@@ -378,7 +424,7 @@ export const jobs = {
|
||||
streamJobsWebSocket() {
|
||||
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const wsHost = window.location.host;
|
||||
const url = `${wsProtocol}//${wsHost}${API_BASE}/jobs/ws`;
|
||||
const url = `${wsProtocol}//${wsHost}${API_BASE}/jobs/ws-old`;
|
||||
return new WebSocket(url);
|
||||
},
|
||||
|
||||
@@ -408,7 +454,7 @@ export const jobs = {
|
||||
if (options.frameEnd) params.append('frame_end', options.frameEnd.toString());
|
||||
if (options.sort) params.append('sort', options.sort);
|
||||
const query = params.toString();
|
||||
return api.get(`/jobs/${jobId}/tasks${query ? '?' + query : ''}`);
|
||||
return api.get(`/jobs/${jobId}/tasks${query ? '?' + query : ''}`, options);
|
||||
});
|
||||
},
|
||||
|
||||
@@ -421,7 +467,7 @@ export const jobs = {
|
||||
if (options.status) params.append('status', options.status);
|
||||
if (options.sort) params.append('sort', options.sort);
|
||||
const query = params.toString();
|
||||
return api.get(`/jobs/${jobId}/tasks/summary${query ? '?' + query : ''}`);
|
||||
return api.get(`/jobs/${jobId}/tasks/summary${query ? '?' + query : ''}`, options);
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user