Skip to content

Commit 6921b17

Browse files
committed
Upload progress accounts for server processing time
Progress bars now reflect the full upload lifecycle: 90% for request transmission, 10% for server processing and response. Prevents progress bars from completing prematurely while the server is still processing buffered uploads, giving the impression that something has failed or stalled. Progress reporting is split into two phases: - 0-90%: Request transmission. Actual upload progress. - 90-100%: Server processing time, estimated based on file size: - 1s for <1MB; 2s for 1-10MB; 3s+ for larger files The 90/10 split reflects that most direct uploads are unbuffered and complete quickly after transmission. Even buffered uploads typically process faster than their upload time. No app changes required for apps using Active Storage or Action Text. Progress events dispatch with the same `{ progress: number }` format. Fixes rails#42228
1 parent c25c1c1 commit 6921b17

File tree

4 files changed

+106
-2
lines changed

4 files changed

+106
-2
lines changed

actiontext/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
* Attachment upload progress accounts for server processing time.
2+
3+
*Jeremy Daer*
4+
15
* The Trix dependency is now satisfied by a gem, `action_text-trix`, rather than vendored
26
files. This allows applications to bump Trix versions independently of Rails
37
releases. Effectively this also upgrades Trix to `>= 2.1.15`.

actiontext/app/javascript/actiontext/attachment_upload.js

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,61 @@ export class AttachmentUpload {
1414

1515
directUploadWillStoreFileWithXHR(xhr) {
1616
xhr.upload.addEventListener("progress", event => {
17-
const progress = event.loaded / event.total * 100
17+
// Scale upload progress to 0-90% range
18+
const progress = (event.loaded / event.total) * 90
1819
this.attachment.setUploadProgress(progress)
1920
if (progress) {
2021
this.dispatch("progress", { progress: progress })
2122
}
2223
})
24+
25+
// Start simulating progress after upload completes
26+
xhr.upload.addEventListener("loadend", () => {
27+
this.simulateResponseProgress(xhr)
28+
})
29+
}
30+
31+
simulateResponseProgress(xhr) {
32+
let progress = 90
33+
const startTime = Date.now()
34+
35+
const updateProgress = () => {
36+
// Simulate progress from 90% to 99% over estimated time
37+
const elapsed = Date.now() - startTime
38+
const estimatedResponseTime = this.estimateResponseTime()
39+
const responseProgress = Math.min(elapsed / estimatedResponseTime, 1)
40+
progress = 90 + (responseProgress * 9) // 90% to 99%
41+
42+
this.attachment.setUploadProgress(progress)
43+
this.dispatch("progress", { progress })
44+
45+
// Continue until response arrives or we hit 99%
46+
if (xhr.readyState !== XMLHttpRequest.DONE && progress < 99) {
47+
requestAnimationFrame(updateProgress)
48+
}
49+
}
50+
51+
// Stop simulation when response arrives
52+
xhr.addEventListener("loadend", () => {
53+
this.attachment.setUploadProgress(100)
54+
this.dispatch("progress", { progress: 100 })
55+
})
56+
57+
requestAnimationFrame(updateProgress)
58+
}
59+
60+
estimateResponseTime() {
61+
// Base estimate: 1 second for small files, scaling up for larger files
62+
const fileSize = this.attachment.file.size
63+
const MB = 1024 * 1024
64+
65+
if (fileSize < MB) {
66+
return 1000 // 1 second for files under 1MB
67+
} else if (fileSize < 10 * MB) {
68+
return 2000 // 2 seconds for files 1-10MB
69+
} else {
70+
return 3000 + (fileSize / MB * 50) // 3+ seconds for larger files
71+
}
2372
}
2473

2574
directUploadDidComplete(error, attributes) {

activestorage/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
* Direct upload progress accounts for server processing time.
2+
3+
*Jeremy Daer*
4+
15
* Delegate `ActiveStorage::Filename#to_str` to `#to_s`
26

37
Supports checking String equality:

activestorage/app/javascript/activestorage/direct_upload_controller.js

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ export class DirectUploadController {
3131
}
3232

3333
uploadRequestDidProgress(event) {
34-
const progress = event.loaded / event.total * 100
34+
// Scale upload progress to 0-90% range
35+
const progress = (event.loaded / event.total) * 90
3536
if (progress) {
3637
this.dispatch("progress", { progress })
3738
}
@@ -63,5 +64,51 @@ export class DirectUploadController {
6364
directUploadWillStoreFileWithXHR(xhr) {
6465
this.dispatch("before-storage-request", { xhr })
6566
xhr.upload.addEventListener("progress", event => this.uploadRequestDidProgress(event))
67+
68+
// Start simulating progress after upload completes
69+
xhr.upload.addEventListener("loadend", () => {
70+
this.simulateResponseProgress(xhr)
71+
})
72+
}
73+
74+
simulateResponseProgress(xhr) {
75+
let progress = 90
76+
const startTime = Date.now()
77+
78+
const updateProgress = () => {
79+
// Simulate progress from 90% to 99% over estimated time
80+
const elapsed = Date.now() - startTime
81+
const estimatedResponseTime = this.estimateResponseTime()
82+
const responseProgress = Math.min(elapsed / estimatedResponseTime, 1)
83+
progress = 90 + (responseProgress * 9) // 90% to 99%
84+
85+
this.dispatch("progress", { progress })
86+
87+
// Continue until response arrives or we hit 99%
88+
if (xhr.readyState !== XMLHttpRequest.DONE && progress < 99) {
89+
requestAnimationFrame(updateProgress)
90+
}
91+
}
92+
93+
// Stop simulation when response arrives
94+
xhr.addEventListener("loadend", () => {
95+
this.dispatch("progress", { progress: 100 })
96+
})
97+
98+
requestAnimationFrame(updateProgress)
99+
}
100+
101+
estimateResponseTime() {
102+
// Base estimate: 1 second for small files, scaling up for larger files
103+
const fileSize = this.file.size
104+
const MB = 1024 * 1024
105+
106+
if (fileSize < MB) {
107+
return 1000 // 1 second for files under 1MB
108+
} else if (fileSize < 10 * MB) {
109+
return 2000 // 2 seconds for files 1-10MB
110+
} else {
111+
return 3000 + (fileSize / MB * 50) // 3+ seconds for larger files
112+
}
66113
}
67114
}

0 commit comments

Comments
 (0)