fix: stabilize mobile uploads and show transfer speed

This commit is contained in:
justme committed 2026-10-03 01:37:43 +08:00
1 parent 09d66092f5
commit c0d155c608
12 files changed
+279 -69

No files matched your search

+62 -23
View File
@@ -1,14 +1,15 @@
import { dequeueItem, removeDraftFile, saveDraftFile, saveResource, type ManifestEntry } from '../database';
import type { FileItem, TextItem } from '../types';
import { makeFileItem } from './presentation';
import { deviceIdentity, makeFileItem } from './presentation';
import { getSession } from './sessions';
import { apiFetch, fail } from './http';
import { flushOutbox, persistPlan } from './transfer';
import { flushOutbox, MAX_FRAME_DATA, MOBILE_UPLOAD_FRAME_DATA_BYTES, persistPlan } from './transfer';
import { publishShare } from './presentation';
import { invalidateManifest } from './shareState';
import { blobs } from './sessions';
import { errorMessage } from '../errors';
function newTextEntry(text: string): { item: TextItem; entry: Extract<ManifestEntry, { t: 'text' }> } {
@@ -36,29 +37,67 @@ export async function addShareFiles(id: string, files: File[]): Promise<void> {
if (resource.kind !== 'share' || !session.can('owner') || resource.state !== 'draft') throw new Error('此分享已封存');
await invalidateManifest(session, resource);
const downloadLimit = resource.maxDownloads ?? 0;
const maxFrameData = deviceIdentity().kind === 'phone' ? MOBILE_UPLOAD_FRAME_DATA_BYTES : MAX_FRAME_DATA;
for (const file of files) {
const ts = Date.now();
const plan = session.encryptFile(file, { name: file.name, mime: file.type || 'application/octet-stream', size: file.size }, {
gated: true, maxDownloads: downloadLimit, ts,
});
const item = makeFileItem(plan.itemId, file.name, file.type || 'application/octet-stream', file.size, ts, { kind: 'receiving', progress: 0 }, downloadLimit || null);
const entry: Extract<ManifestEntry, { t: 'file' }> = { t: 'file', id: plan.itemId, name: file.name, mime: file.type || 'application/octet-stream', size: file.size, ts };
blobs.set(`${id}:${item.id}`, file);
try { await saveDraftFile(id, item.id, file); }
catch { publishShare(id, { t: 'error', message: '本机存储空间不足,刷新后可能无法预览此文件。' }); }
resource.draftItems = [...(resource.draftItems ?? []), item];
resource.manifestEntries = [...(resource.manifestEntries ?? []), entry];
await saveResource(resource);
publishShare(id, { t: 'item', item });
await persistPlan(resource, plan);
const uploaded = await flushOutbox(resource, session);
const complete: FileItem = { ...item, state: uploaded ? { kind: 'complete' } : { kind: 'incomplete' } };
resource.draftItems = resource.draftItems.map((draft) => draft.id === item.id ? complete : draft);
await saveResource(resource);
publishShare(id, { t: 'item', item: complete });
if (!uploaded) {
publishShare(id, { t: 'error', message: '上传暂停了。重新打开草稿并连接网络后,可以继续发送已加密帧。' });
continue;
let item: FileItem | undefined;
try {
const plan = session.encryptFile(file, { name: file.name, mime: file.type || 'application/octet-stream', size: file.size }, {
gated: true, maxDownloads: downloadLimit, ts, maxFrameData,
});
item = makeFileItem(plan.itemId, file.name, file.type || 'application/octet-stream', file.size, ts, { kind: 'receiving', progress: 0, phase: 'encrypting' }, downloadLimit || null);
const entry: Extract<ManifestEntry, { t: 'file' }> = { t: 'file', id: plan.itemId, name: file.name, mime: file.type || 'application/octet-stream', size: file.size, ts };
blobs.set(`${id}:${item.id}`, file);
try { await saveDraftFile(id, item.id, file); }
catch { publishShare(id, { t: 'error', message: '本机存储空间不足,刷新后可能无法预览此文件。' }); }
resource.draftItems = [...(resource.draftItems ?? []), item];
resource.manifestEntries = [...(resource.manifestEntries ?? []), entry];
await saveResource(resource);
publishShare(id, { t: 'item', item });
const updateProgress = (progress: number, phase: 'encrypting' | 'uploading', speedBytesPerSecond?: number) => {
if (!item) return;
item = {
...item,
state: {
kind: 'receiving',
progress: Math.max(0, Math.min(0.99, progress)),
phase,
...(speedBytesPerSecond === undefined ? {} : { speedBytesPerSecond }),
},
};
publishShare(id, { t: 'item', item });
};
const planned = await persistPlan(resource, plan, (_frames) => {
const processed = Math.min(file.size, _frames * MAX_FRAME_DATA);
updateProgress(file.size === 0 ? 0.5 : Math.min(0.5, processed / file.size * 0.5), 'encrypting');
});
const uploadedFrames = new Set<number>();
let uploadedBytes = 0;
const uploadStartedAt = performance.now();
const uploaded = await flushOutbox(resource, session, (frame, loadedBytes, totalBytes, complete) => {
if (frame.itemId !== item?.id) return;
if (complete && !uploadedFrames.has(frame.index)) {
uploadedFrames.add(frame.index);
uploadedBytes += totalBytes;
}
const currentBytes = uploadedBytes + (complete ? 0 : loadedBytes);
const elapsed = Math.max(1, performance.now() - uploadStartedAt);
updateProgress(0.5 + 0.5 * currentBytes / planned.bytes, 'uploading', currentBytes * 1000 / elapsed);
});
const complete: FileItem = { ...item, state: uploaded ? { kind: 'complete' } : { kind: 'incomplete' } };
resource.draftItems = resource.draftItems.map((draft) => draft.id === item?.id ? complete : draft);
await saveResource(resource);
publishShare(id, { t: 'item', item: complete });
if (!uploaded) publishShare(id, { t: 'error', message: '上传暂停了。重新打开草稿并连接网络后,可以继续发送已加密帧。' });
} catch (error) {
if (item) {
item = { ...item, state: { kind: 'incomplete' } };
resource.draftItems = (resource.draftItems ?? []).map((draft) => draft.id === item?.id ? item! : draft);
await saveResource(resource);
publishShare(id, { t: 'item', item });
}
publishShare(id, { t: 'error', message: `文件上传失败:${errorMessage(error)}` });
throw error;
}
}
}