diff --git a/web/src/core-bridge/worker.ts b/web/src/core-bridge/worker.ts index a75bcb6..0b65d99 100644 --- a/web/src/core-bridge/worker.ts +++ b/web/src/core-bridge/worker.ts @@ -109,9 +109,26 @@ async function dispatch(operation: string, input: unknown): Promise { } } +/** + * 账号和空间的操作按顺序执行(登录、同步、改设置等互相依赖)。但上传、发消息、删除、取消是长任务或互不依赖: + * 如果也排在这条队列里,一个大文件的 sendFiles 会让取消按钮、聊天消息、删除全部等到上传结束才生效。 + */ +function laneOf(operation: string, payload: Record): string | undefined { + const space = String(payload.id ?? ''); + switch (operation) { + case 'cancelUpload': return undefined; + case 'sendFiles': return `upload:${space}`; + case 'sendText': case 'editChannelText': return `chat:${space}`; + case 'removeChannelItem': return `item:${space}`; + default: return 'account-scope'; + } +} + scope.onmessage = (event) => { const request = event.data; - const task = serial('account-scope', () => dispatch(request.operation, request.payload)); + const lane = laneOf(request.operation, request.payload as Record); + const run = () => dispatch(request.operation, request.payload); + const task = lane ? serial(lane, run) : run(); void task.then( (result) => scope.postMessage({ requestId: request.requestId, result }), (error: unknown) => scope.postMessage({ diff --git a/web/src/core-bridge/worker/uploadQueue.ts b/web/src/core-bridge/worker/uploadQueue.ts index 3a370ab..b13df0d 100644 --- a/web/src/core-bridge/worker/uploadQueue.ts +++ b/web/src/core-bridge/worker/uploadQueue.ts @@ -3,6 +3,7 @@ import { dequeueFrame, dequeueItem, queueFrame, queueLocation, saveResource, typ import { errorMessage } from '../errors'; import type { FileItem, FileUploadOptions } from '../types'; import { prepareObject } from './channelDataKeys'; +import { removeChannelItem } from './channelItems'; import { queueFileCatalog } from './channelThreads'; import { deviceIdentity, publishWorkspace } from './presentation'; import { senderSnapshot } from './senderSnapshot'; @@ -160,6 +161,8 @@ async function discard(resource: SavedResource, item: FileItem | undefined): Pro await saveResource(resource); blobs.delete(`${resource.id}:${item.id}`); publishWorkspace(resource.id, { t: 'removed', id: item.id }); + // 服务端可能已收下前几帧:删掉这个没传完的条目,不然它会一直占着空间配额(没传完的条目服务端允许直接删)。第 0 帧都没到服务端时返回 404,忽略 + void removeChannelItem(resource.id, item.id).catch(() => undefined); } /** diff --git a/web/src/features/channel/ChatMessage.tsx b/web/src/features/channel/ChatMessage.tsx index db073e4..84f4ebb 100644 --- a/web/src/features/channel/ChatMessage.tsx +++ b/web/src/features/channel/ChatMessage.tsx @@ -62,7 +62,7 @@ export function ChatMessage({ id, compact = false, onOpenFile, onDelete }: Props : item.state.kind === 'incomplete' ? 发送未完成,等待续传。 : null} diff --git a/web/src/store/workspace.ts b/web/src/store/workspace.ts index 4e250b0..02a032d 100644 --- a/web/src/store/workspace.ts +++ b/web/src/store/workspace.ts @@ -63,7 +63,7 @@ interface Actions { /** 重试发送失败的本地占位项 */ retrySend: (localId: string) => Promise; /** 取消排队中或上传中的文件 */ - cancelUpload: (localId: string) => Promise; + cancelUpload: (localId: string, itemId?: string) => Promise; updateChannelSettings: (patch: ChannelSettingsPatch) => Promise; sendFiles: (files: File[], options?: FileUploadOptions, threadId?: string, folderId?: string) => Promise; createThread: (name: string, groupId?: string) => Promise; @@ -105,10 +105,12 @@ export const useWorkspace = create()(() => ({ return deliver(id); }, retrySend: (id) => deliver(id), - cancelUpload: async (localId) => { + cancelUpload: async (localId, itemId) => { pendingJobs.delete(localId); useWorkspace.setState((s) => withoutItem(s, localId)); - await api.cancelUpload(localId).catch(() => false); + const found = await api.cancelUpload(localId).catch(() => false); + // worker 里没有这个任务(上次会话遗留的半截上传):当普通条目删除 + if (!found && itemId) { useWorkspace.setState((s) => withoutItem(s, itemId)); await api.remove(itemId).catch(() => undefined); } }, updateChannelSettings: (patch) => api.updateChannelSettings(patch), sendFiles: async (files, options, threadId, folderId) => { @@ -139,7 +141,13 @@ export const useWorkspace = create()(() => ({ moveItems: (itemIds, threadId, folderId) => api.moveItems(itemIds, threadId, folderId), editText: (id, t): Promise => api.editText(id, t, useWorkspace.getState().byId[id]?.threadId), resolveConflict: (id, c) => api.resolveConflict(id, c), - remove: (id) => { if (!pendingJobs.has(id)) return api.remove(id); settleLocal(id); return Promise.resolve(); }, + remove: async (id) => { + if (pendingJobs.has(id)) { settleLocal(id); await api.cancelUpload(id).catch(() => false); return; } + const item = useWorkspace.getState().byId[id]; + // 还在上传的文件:删除就是取消(先停掉上传,否则后面的帧会打到已删除的条目上) + if (item?.kind === 'file' && item.state.kind === 'receiving') { await useWorkspace.getState().cancelUpload(item.localId ?? id, id); return; } + await api.remove(id); + }, setSnapshot: (t) => api.setSnapshot(t), getBlob: (id, v) => api.getBlob(id, v), markSeen: (id) => api.markSeen(id),