fix(app): stop aborting shared uploads when cancelling one attachment (#1967)

This commit is contained in:
Hampus
2026-08-24 12:54:50 +02:00
committed by GitHub
parent de97bf908d
commit 51e2eee15a
2 changed files with 49 additions and 29 deletions
@@ -22,6 +22,7 @@ import {
resolveMessageQueueRequestOutcomeDecision,
resolveMessageQueueSendExecutionDecision,
} from '@app/features/messaging/state/MessageQueueStateMachine';
import {planTextareaAttachmentCancellation} from '@app/features/messaging/state/TextareaAttachmentUploadCancellation';
import {
type ChunkedUploadPart,
type ChunkedUploadPlan,
@@ -50,7 +51,7 @@ import * as ModalCommands from '@app/features/ui/commands/ModalCommands';
import {modal} from '@app/features/ui/commands/ModalCommands';
import {formatUserSettingsPath} from '@app/features/user/components/settings_utils/SettingsConstants';
import PrivacyPreferences from '@app/features/user/state/PrivacyPreferences';
import {Queue, type QueueEntry} from '@app/lib/list/ListQueue';
import {Queue} from '@app/lib/list/ListQueue';
import {APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes';
import type {
AllowedMentions,
@@ -254,10 +255,6 @@ const isNetworkRequestError = (error: unknown): boolean => {
return error instanceof Error && error.message === 'Network error during request';
};
function isSendPayload(payload: MessageQueuePayload): payload is SendMessagePayload {
return payload.type === 'send';
}
function isRateLimitError(error: HttpError): boolean {
if (error.status !== 429) return false;
return !isSlowmodeError(error);
@@ -516,23 +513,6 @@ export class MessageQueue extends Queue<MessageQueuePayload, RestResponse<Messag
this.abortControllers.delete(nonce);
}
cancelPendingSendRequests(channelId: string): Array<SendMessagePayload> {
const cancelled: Array<SendMessagePayload> = [];
const remaining: Array<QueueEntry<MessageQueuePayload, RestResponse<Message> | undefined>> = [];
while (this.queue.length > 0) {
const entry = this.queue.shift()!;
if (isSendPayload(entry.message) && entry.message.channelId === channelId) {
cancelled.push(entry.message);
this.cancelRequest(entry.message.nonce);
} else {
remaining.push(entry);
}
}
this.queue.push(...remaining);
logger.info('Cancel pending send requests', cancelled.length);
return cancelled;
}
async sendImmediately(payload: SendMessagePayload): Promise<RestResponse<Message> | undefined> {
while (true) {
const completion = await this.drainSendImmediately(payload);
@@ -713,16 +693,15 @@ export class MessageQueue extends Queue<MessageQueuePayload, RestResponse<Messag
}
private cancelTextareaAttachmentUploads(attachmentIds: ReadonlyArray<number>): void {
const channelIds = new Set<string>();
for (const attachmentId of attachmentIds) {
const upload = this.textareaAttachmentUploads.get(attachmentId);
if (!upload) continue;
channelIds.add(upload.channelId);
upload.requestAbortController.abort();
const plan = planTextareaAttachmentCancellation(this.textareaAttachmentUploads, attachmentIds);
for (const {attachmentId, upload} of plan.cancelled) {
upload.abortController.abort();
this.textareaAttachmentUploads.delete(attachmentId);
}
for (const channelId of channelIds) {
for (const controller of plan.requestControllersToAbort) {
controller.abort();
}
for (const channelId of plan.channelIds) {
this.disposeTextareaAttachmentUploadPrunerIfIdle(channelId);
}
}
@@ -0,0 +1,41 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
export interface CancellableTextareaAttachmentUpload {
channelId: string;
requestAbortController: AbortController;
abortController: AbortController;
}
export interface TextareaAttachmentCancellationPlan<TUpload> {
cancelled: Array<{attachmentId: number; upload: TUpload}>;
requestControllersToAbort: Array<AbortController>;
channelIds: Array<string>;
}
export function planTextareaAttachmentCancellation<TUpload extends CancellableTextareaAttachmentUpload>(
uploads: ReadonlyMap<number, TUpload>,
attachmentIds: ReadonlyArray<number>,
): TextareaAttachmentCancellationPlan<TUpload> {
const cancelled: Array<{attachmentId: number; upload: TUpload}> = [];
const cancelledIds = new Set<number>();
const channelIds = new Set<string>();
const releasedRequestControllers = new Set<AbortController>();
for (const attachmentId of attachmentIds) {
if (cancelledIds.has(attachmentId)) continue;
const upload = uploads.get(attachmentId);
if (!upload) continue;
cancelledIds.add(attachmentId);
cancelled.push({attachmentId, upload});
channelIds.add(upload.channelId);
releasedRequestControllers.add(upload.requestAbortController);
}
for (const [attachmentId, upload] of uploads) {
if (cancelledIds.has(attachmentId)) continue;
releasedRequestControllers.delete(upload.requestAbortController);
}
return {
cancelled,
requestControllersToAbort: Array.from(releasedRequestControllers),
channelIds: Array.from(channelIds),
};
}