Skip to content

Commit a6d9b35

Browse files
feat: try to fix double download error (#538)
1 parent 4476b1d commit a6d9b35

File tree

3 files changed

+76
-26
lines changed

3 files changed

+76
-26
lines changed

src/frontend/src/routes/(needs_onboarding)/(navbar_and_footer)/reverse/[room_id]/client.svelte

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,14 @@
9393
: 0
9494
);
9595
96+
const isAnyStreaming = $derived(receiveState.type === 'streaming');
97+
const isAnyProcessing = $derived(receiveState.type === 'processing');
98+
const currentTransferKey = $derived(
99+
receiveState.type === 'streaming' || receiveState.type === 'processing'
100+
? receiveState.key
101+
: null
102+
);
103+
96104
function submitKey() {
97105
const k = keyInput.trim();
98106
if (!k) {
@@ -120,7 +128,7 @@
120128
121129
const data: RoomOut = await res.json();
122130
room = data;
123-
roomFiles = [...data.files];
131+
roomFiles = structuredClone(data.files);
124132
hostCount = data.host_count ?? 1;
125133
loadStatus = 'loaded';
126134
@@ -166,7 +174,7 @@
166174
case 'snapshot': {
167175
const r = msg.room as RoomOut;
168176
room = r;
169-
roomFiles = [...r.files];
177+
roomFiles = structuredClone(r.files);
170178
hostCount = r.host_count ?? 1;
171179
remoteUploads =
172180
r.active_uploads?.map((u) => ({
@@ -232,6 +240,10 @@
232240
case 'file_start':
233241
if (receiveState.type === 'idle') {
234242
if (downloadPreference === 'eager') {
243+
// Skip if already in memory
244+
if (downloadedFiles.some((d) => d.key === msg.key)) {
245+
return;
246+
}
235247
receiveState = {
236248
type: 'streaming',
237249
key: msg.key,
@@ -250,6 +262,9 @@
250262
case 'file_end':
251263
if (receiveState.type === 'streaming' && receiveState.key === msg.key) {
252264
const { key, filename, size, chunks } = receiveState;
265+
// Guard against concurrent file_end processing (e.g. multiple hosts)
266+
receiveState = { type: 'processing', key, filename, size };
267+
253268
console.debug(
254269
'[reverse/client] file_end for',
255270
key,
@@ -287,7 +302,7 @@
287302
downloadedFiles = [...downloadedFiles, { key, filename, size, objectUrl }];
288303
toast.success(`Received: ${filename}`);
289304
290-
// Trigger download automatically if this was a manual request
305+
// Trigger download automatically
291306
const a = document.createElement('a');
292307
a.href = objectUrl;
293308
a.download = filename;
@@ -635,8 +650,9 @@
635650

636651
{#each roomFiles as f}
637652
{@const downloaded = downloadedFiles.find((d) => d.key === f.key)}
638-
{@const isStreaming =
639-
receiveState.type === 'streaming' && receiveState.key === f.key}
653+
{@const isThisStreaming = receiveState.type === 'streaming' && receiveState.key === f.key}
654+
{@const isThisProcessing = receiveState.type === 'processing' && receiveState.key === f.key}
655+
{@const isAnyActive = isAnyStreaming || isAnyProcessing}
640656
{@const displayName = getDisplayFilename(f.filename)}
641657
<div class="rounded-md border px-3 py-2">
642658
<div class="flex items-center gap-3">
@@ -658,9 +674,9 @@
658674
<p class="text-xs text-muted-foreground">{formatFileSize(f.size)}</p>
659675
</div>
660676

661-
{#if isStreaming}
677+
{#if isThisStreaming || isThisProcessing}
662678
<div class="flex items-center gap-2 text-xs text-muted-foreground">
663-
{#if isDecrypting}
679+
{#if isDecrypting || isThisProcessing}
664680
<span class="animate-pulse">Decrypting…</span>
665681
<span class="font-mono">{decryptionProgress.current.toFixed(0)}%</span>
666682
{:else}
@@ -689,8 +705,7 @@
689705
variant="default"
690706
class="h-7 shrink-0 gap-1 px-2 text-xs"
691707
onclick={() => downloadFile(f)}
692-
disabled={receiveState.type === 'streaming' &&
693-
receiveState.key !== f.key}
708+
disabled={isAnyActive && currentTransferKey !== f.key}
694709
>
695710
<Download class="h-3.5 w-3.5" />
696711
Save
@@ -701,8 +716,7 @@
701716
variant="outline"
702717
class="h-7 shrink-0 gap-1 px-2 text-xs"
703718
onclick={() => downloadFile(f)}
704-
disabled={receiveState.type === 'streaming' &&
705-
receiveState.key !== f.key}
719+
disabled={isAnyActive && currentTransferKey !== f.key}
706720
>
707721
<Download class="h-3.5 w-3.5" />
708722
Download
@@ -718,8 +732,8 @@
718732
</a>
719733
</div>
720734
</div>
721-
{#if isStreaming}
722-
{#if isDecrypting}
735+
{#if isThisStreaming || isThisProcessing}
736+
{#if isDecrypting || isThisProcessing}
723737
<Progress value={decryptionProgress.current} max={100} class="mt-2 h-1" />
724738
{:else}
725739
<Progress value={streamProgress} max={100} class="mt-2 h-1" />

src/frontend/src/routes/(needs_onboarding)/(navbar_and_footer)/reverse/[room_id]/host.svelte

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,14 @@
109109
: 0
110110
);
111111
112+
const isAnyStreaming = $derived(receiveState.type === 'streaming');
113+
const isAnyProcessing = $derived(receiveState.type === 'processing');
114+
const currentTransferKey = $derived(
115+
receiveState.type === 'streaming' || receiveState.type === 'processing'
116+
? receiveState.key
117+
: null
118+
);
119+
112120
async function loadRoom() {
113121
loadStatus = 'loading';
114122
try {
@@ -121,7 +129,7 @@
121129
122130
const data: RoomOut = await res.json();
123131
room = data;
124-
roomFiles = [...data.files];
132+
roomFiles = structuredClone(data.files);
125133
hostCount = data.host_count ?? 1;
126134
loadStatus = 'loaded';
127135
connectWebSocket();
@@ -167,7 +175,7 @@
167175
case 'snapshot': {
168176
const r = msg.room as RoomOut;
169177
room = r;
170-
roomFiles = [...r.files];
178+
roomFiles = structuredClone(r.files);
171179
hostCount = r.host_count ?? 1;
172180
remoteUploads =
173181
r.active_uploads?.map((u) => ({
@@ -232,6 +240,10 @@
232240
}
233241
case 'file_start':
234242
if (receiveState.type === 'idle') {
243+
// Skip if already in memory
244+
if (downloadedFiles.some((d) => d.key === msg.key)) {
245+
return;
246+
}
235247
// server started streaming without an explicit client request
236248
receiveState = {
237249
type: 'streaming',
@@ -250,6 +262,9 @@
250262
case 'file_end':
251263
if (receiveState.type === 'streaming' && receiveState.key === msg.key) {
252264
const { key, filename, size, chunks } = receiveState;
265+
// Guard against concurrent file_end processing (e.g. multiple hosts)
266+
receiveState = { type: 'processing', key, filename, size };
267+
253268
console.debug(
254269
'[reverse/host] file_end for',
255270
key,
@@ -399,6 +414,18 @@
399414
if (!roomFiles.some((f) => f.key === fileEntry.key)) {
400415
roomFiles = [...roomFiles, fileEntry];
401416
}
417+
418+
// Add to downloadedFiles so it shows as "Saved" instead of "Download"
419+
const objectUrl = URL.createObjectURL(entry.file);
420+
downloadedFiles = [
421+
...downloadedFiles,
422+
{
423+
key: fileEntry.key,
424+
filename: entry.file.name,
425+
size: entry.file.size,
426+
objectUrl
427+
}
428+
];
402429
} catch (e: any) {
403430
entry.status = 'error';
404431
toast.error(`Upload failed for ${entry.file.name}: ${e.message || String(e)}`);
@@ -711,13 +738,15 @@
711738
<span class="shrink-0 text-xs text-muted-foreground"
712739
>{formatFileSize(file.size)}</span
713740
>
714-
<button
715-
class="shrink-0 text-muted-foreground hover:text-destructive"
741+
<Button
742+
variant="ghost"
743+
size="icon"
744+
class="h-7 w-7 shrink-0 text-muted-foreground hover:text-destructive"
716745
onclick={() => removePendingFile(i)}
717746
aria-label="Remove"
718747
>
719748
<X class="h-4 w-4" />
720-
</button>
749+
</Button>
721750
</div>
722751
{/each}
723752
</div>
@@ -827,8 +856,9 @@
827856

828857
{#each roomFiles as f}
829858
{@const downloaded = downloadedFiles.find((d) => d.key === f.key)}
830-
{@const isStreaming =
831-
receiveState.type === 'streaming' && receiveState.key === f.key}
859+
{@const isThisStreaming = receiveState.type === 'streaming' && receiveState.key === f.key}
860+
{@const isThisProcessing = receiveState.type === 'processing' && receiveState.key === f.key}
861+
{@const isAnyActive = isAnyStreaming || isAnyProcessing}
832862
{@const displayName = getDisplayFilename(f.filename)}
833863
<div class="rounded-md border px-3 py-2">
834864
<div class="flex items-center gap-3">
@@ -850,9 +880,9 @@
850880
<p class="text-xs text-muted-foreground">{formatFileSize(f.size)}</p>
851881
</div>
852882

853-
{#if isStreaming}
883+
{#if isThisStreaming || isThisProcessing}
854884
<div class="flex items-center gap-2 text-xs text-muted-foreground">
855-
{#if isDecrypting}
885+
{#if isDecrypting || isThisProcessing}
856886
<span class="animate-pulse">Decrypting…</span>
857887
<span class="font-mono">{decryptionProgress.current.toFixed(0)}%</span>
858888
{:else}
@@ -882,7 +912,7 @@
882912
variant="default"
883913
class="h-7 shrink-0 gap-1 px-2 text-xs"
884914
onclick={() => downloadFile(f)}
885-
disabled={receiveState.type === 'streaming' && receiveState.key !== f.key}
915+
disabled={isAnyActive && currentTransferKey !== f.key}
886916
>
887917
<Download class="h-3.5 w-3.5" />
888918
Save
@@ -893,7 +923,7 @@
893923
variant="outline"
894924
class="h-7 shrink-0 gap-1 px-2 text-xs"
895925
onclick={() => downloadFile(f)}
896-
disabled={receiveState.type === 'streaming' && receiveState.key !== f.key}
926+
disabled={isAnyActive && currentTransferKey !== f.key}
897927
>
898928
<Download class="h-3.5 w-3.5" />
899929
Download
@@ -909,8 +939,8 @@
909939
</a>
910940
</div>
911941
</div>
912-
{#if isStreaming}
913-
{#if isDecrypting}
942+
{#if isThisStreaming || isThisProcessing}
943+
{#if isDecrypting || isThisProcessing}
914944
<Progress value={decryptionProgress.current} max={100} class="mt-2 h-1" />
915945
{:else}
916946
<Progress value={streamProgress} max={100} class="mt-2 h-1" />

src/frontend/src/routes/(needs_onboarding)/(navbar_and_footer)/reverse/[room_id]/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ export type ReceiveState =
3333
size: number;
3434
received: number;
3535
chunks: BlobPart[];
36+
}
37+
| {
38+
type: 'processing';
39+
key: string;
40+
filename: string;
41+
size: number;
3642
};
3743

3844
export interface DownloadedFile {

0 commit comments

Comments
 (0)