Enhance upload handling: add db-backed upload rate limiting and lease management, enforce stricter validation for deployment configurations, optimize test suite and retired functionality, and improve input handling and error recovery.
CI / check (22) (push) Failing after 4m52s
CI / check (24) (push) Successful in 9m43s

This commit is contained in:
2026-10-13 14:16:32 +02:00
parent dd23a19ed2
commit 5489e03138
10 changed files with 85 additions and 27 deletions
+10 -1
View File
@@ -9,7 +9,16 @@ export class ApiError extends Error {
toJSON() { return {...this.details, error: this.message, status: this.status, code: this.code, requestId: this.requestId}; }
}
function requestScope({signal, timeoutMs = 30000} = {}) { const controller = new AbortController(); const abort = () => controller.abort(new ApiError("Request cancelled.", {code: "ABORTED"})); if (signal?.aborted) abort(); else signal?.addEventListener("abort", abort, {once: true}); const timer = timeoutMs > 0 ? setTimeout(() => controller.abort(new ApiError("Request timed out. Please try again.", {code: "TIMEOUT"})), timeoutMs) : null; return {signal: controller.signal, close() { clearTimeout(timer); signal?.removeEventListener("abort", abort); }}; }
function abortable(operation, signal) { if (signal.aborted) return Promise.reject(signal.reason); return new Promise((resolve, reject) => { const abort = () => { signal.removeEventListener("abort", abort); reject(signal.reason); }; signal.addEventListener("abort", abort, {once: true}); Promise.resolve().then(operation).then(resolve, reject).finally(() => signal.removeEventListener("abort", abort)); }); }
function abortable(operation, signal) {
if (signal.aborted) return Promise.reject(signal.reason);
return new Promise((resolve, reject) => {
const abort = () => { signal.removeEventListener("abort", abort); reject(signal.reason); };
signal.addEventListener("abort", abort, {once: true});
let result;
try { result = operation(); } catch (error) { reject(error); return; }
Promise.resolve(result).then(resolve, reject).finally(() => signal.removeEventListener("abort", abort));
});
}
function responseError(response, payload = {}, fallback = "", code = "") { const details = payload && typeof payload === "object" ? payload : {}; const error = details.error; return new ApiError(typeof error === "string" ? error : error?.message || fallback || `HTTP ${response.status} ${response.statusText}`.trim(), {...details, status: response.ok && Number.isInteger(details.status) && details.status >= 400 ? details.status : response.status, code: details.code || error?.code || code || `HTTP_${response.status}`, requestId: details.requestId || response.headers.get("X-Request-ID") || null}); }
async function readResponseText(response, signal) {
let textPromise = responseTextCache.get(response);
+14 -1
View File
@@ -179,7 +179,10 @@ export function createComposerController({
else state.chatDrafts.delete(chatId);
const currentAttachments = attachmentComposer.attachments;
if (currentAttachments.length === attachments.length && currentAttachments.every((item, index) => item.id === attachments[index].id)) attachmentComposer.clear();
} else clearSentDraft();
} else {
state.chatDrafts.delete(chatId);
clearSentDraft();
}
if (!followOutput && visible()) requestAnimationFrame(() => {
if (visible()) messagesDiv.scrollTop = previousScrollTop;
});
@@ -194,8 +197,18 @@ export function createComposerController({
const messageText = signal.aborted ? "Generation stopped." : error?.message || "Unable to send message.";
if (visible() && completedReply === null) announceResponseStatus(messageText);
if (completedReply !== null) {
if (visible() && !msgInput.value.trim()) {
msgInput.value = message;
state.chatDrafts.set(chatId ?? "new", message);
resizeComposerInput();
}
setNotice(`Reply sent, but the conversation could not be refreshed: ${messageText}`, "error");
} else {
if (signal.aborted && chatId == null && !msgInput.value.trim()) {
msgInput.value = message;
state.chatDrafts.set("new", message);
resizeComposerInput();
}
if (visible()) {
if (!msgInput.value.trim()) {
msgInput.value = message;
+3 -3
View File
@@ -56,14 +56,14 @@ export function createConversationController({
}
try {
const historyUrl = `/chats/${id}/messages${matchMessageId ? `?around=${matchMessageId}` : ""}`;
let res = await get(historyUrl);
let res = await get(historyUrl, {retries: 0});
if (openRequest !== state.chatOpenRequest) return;
if (res.error) return failed(res);
if (matchMessageId && !res.messages?.some(message => message.id === matchMessageId)) {
const activated = await post(`/chats/${id}/branches/${matchMessageId}/activate`, {includeDescendants: true});
if (openRequest !== state.chatOpenRequest) return;
if (activated.error) return failed(activated);
res = await get(historyUrl);
res = await get(historyUrl, {retries: 0});
if (openRequest !== state.chatOpenRequest) return;
if (res.error) return failed(res);
}
@@ -123,7 +123,7 @@ export function createConversationController({
state.historyLoading = true;
updateHistoryControls();
try {
const result = await get(`/chats/${chatId}/messages?before=${page.before}&leaf=${page.leafId}`);
const result = await get(`/chats/${chatId}/messages?before=${page.before}&leaf=${page.leafId}`, {retries: 0});
if (chatId !== state.activeChatId || requestId !== state.chatOpenRequest) return;
if (result.error) { setNotice(result.error, "error"); return result; }
const oldHeight = messagesDiv.scrollHeight, oldTop = messagesDiv.scrollTop;