Skip to content

Commit 93158d8

Browse files
committed
fixup! Add stall detection to recover from frozen uploads
1 parent 12bf9fa commit 93158d8

File tree

3 files changed

+218
-0
lines changed

3 files changed

+218
-0
lines changed

test/spec/browser-index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ import './test-terminate.js'
1111
import './test-web-stream.js'
1212
import './test-binary-data.js'
1313
import './test-end-to-end.js'
14+
import './test-stall-detection-config.js'

test/spec/node-index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ import './test-terminate.js'
55
import './test-web-stream.js'
66
import './test-binary-data.js'
77
import './test-end-to-end.js'
8+
import './test-stall-detection-config.js'
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
import { Upload } from "tus-js-client"
2+
import { TestHttpStack, getBlob, waitableFunction } from "./helpers/utils.js"
3+
4+
describe("tus-stall-detection", () => {
5+
describe("configuration options", () => {
6+
it("should apply default stall detection options when enabled", () => {
7+
const testStack = new TestHttpStack()
8+
const file = getBlob("hello world")
9+
10+
const options = {
11+
httpStack: testStack,
12+
endpoint: "https://tus.io/uploads",
13+
stallDetection: {
14+
enabled: true,
15+
},
16+
}
17+
18+
const upload = new Upload(file, options)
19+
20+
// Verify stall detection is enabled with default values
21+
expect(upload.options.stallDetection.enabled).toBe(true)
22+
expect(upload.options.stallDetection.stallTimeout).toBe(30000)
23+
expect(upload.options.stallDetection.checkInterval).toBe(5000)
24+
expect(upload.options.stallDetection.minimumBytesPerSecond).toBe(1)
25+
26+
// Abort to clean up
27+
upload.abort()
28+
})
29+
30+
it("should apply custom stall detection options", () => {
31+
const testStack = new TestHttpStack()
32+
const file = getBlob("hello world")
33+
34+
const customOptions = {
35+
enabled: true,
36+
stallTimeout: 15000,
37+
checkInterval: 2500,
38+
minimumBytesPerSecond: 10,
39+
}
40+
41+
const options = {
42+
httpStack: testStack,
43+
endpoint: "https://tus.io/uploads",
44+
stallDetection: customOptions,
45+
}
46+
47+
const upload = new Upload(file, options)
48+
49+
// Verify the custom options were applied
50+
expect(upload.options.stallDetection.enabled).toBe(true)
51+
expect(upload.options.stallDetection.stallTimeout).toBe(15000)
52+
expect(upload.options.stallDetection.checkInterval).toBe(2500)
53+
expect(upload.options.stallDetection.minimumBytesPerSecond).toBe(10)
54+
55+
// Abort to clean up
56+
upload.abort()
57+
})
58+
59+
it("should disable stall detection when configured", () => {
60+
const testStack = new TestHttpStack()
61+
const file = getBlob("hello world")
62+
63+
const options = {
64+
httpStack: testStack,
65+
endpoint: "https://tus.io/uploads",
66+
stallDetection: {
67+
enabled: false,
68+
},
69+
}
70+
71+
const upload = new Upload(file, options)
72+
73+
// Verify stall detection is disabled
74+
expect(upload.options.stallDetection.enabled).toBe(false)
75+
76+
// Abort to clean up
77+
upload.abort()
78+
})
79+
})
80+
81+
describe("integration tests", () => {
82+
it("should upload a file with stall detection enabled", async () => {
83+
const testStack = new TestHttpStack()
84+
const file = getBlob("hello world")
85+
86+
const options = {
87+
httpStack: testStack,
88+
endpoint: "https://tus.io/uploads",
89+
stallDetection: {
90+
enabled: true,
91+
checkInterval: 1000,
92+
stallTimeout: 2000,
93+
minimumBytesPerSecond: 1,
94+
},
95+
onSuccess: waitableFunction("onSuccess"),
96+
onError: waitableFunction("onError"),
97+
}
98+
99+
const upload = new Upload(file, options)
100+
upload.start()
101+
102+
// Handle the POST request to create the upload
103+
let req = await testStack.nextRequest()
104+
expect(req.url).toBe("https://tus.io/uploads")
105+
expect(req.method).toBe("POST")
106+
107+
req.respondWith({
108+
status: 201,
109+
responseHeaders: {
110+
Location: "https://tus.io/uploads/12345",
111+
},
112+
})
113+
114+
// Handle the PATCH request to upload the file
115+
req = await testStack.nextRequest()
116+
expect(req.url).toBe("https://tus.io/uploads/12345")
117+
expect(req.method).toBe("PATCH")
118+
119+
// Complete the upload quickly (before stall detection triggers)
120+
req.respondWith({
121+
status: 204,
122+
responseHeaders: {
123+
"Upload-Offset": "11",
124+
},
125+
})
126+
127+
// Wait for the upload to complete successfully
128+
await options.onSuccess.toBeCalled()
129+
130+
// Make sure the error callback was not called
131+
expect(options.onError.calls.count()).toBe(0)
132+
})
133+
134+
it("should call _handleStall with the correct error message", () => {
135+
const testStack = new TestHttpStack()
136+
const file = getBlob("hello world")
137+
138+
// Mock error handler to capture the error
139+
const errorSpy = jasmine.createSpy("errorSpy")
140+
141+
const options = {
142+
httpStack: testStack,
143+
endpoint: "https://tus.io/uploads",
144+
stallDetection: {
145+
enabled: true,
146+
},
147+
// Disable retries so we get the error directly
148+
retryDelays: null,
149+
onError: (error) => {
150+
errorSpy(error.message)
151+
},
152+
}
153+
154+
const upload = new Upload(file, options)
155+
156+
// Directly call _handleStall to simulate stall detection
157+
upload._handleStall("test reason")
158+
159+
// Verify the error message format
160+
expect(errorSpy).toHaveBeenCalledWith("Upload stalled: test reason")
161+
})
162+
163+
it("should handle errors from stall detection", async () => {
164+
const testStack = new TestHttpStack()
165+
const file = getBlob("hello world")
166+
167+
// Create spies to track callbacks
168+
const errorSpy = jasmine.createSpy("errorSpy")
169+
170+
const options = {
171+
httpStack: testStack,
172+
endpoint: "https://tus.io/uploads",
173+
stallDetection: {
174+
enabled: true,
175+
},
176+
// No retries to get immediate error
177+
retryDelays: null,
178+
onError: (error) => {
179+
errorSpy(error.message)
180+
},
181+
}
182+
183+
const upload = new Upload(file, options)
184+
upload.start()
185+
186+
// Handle the POST request to create the upload
187+
let req = await testStack.nextRequest()
188+
189+
req.respondWith({
190+
status: 201,
191+
responseHeaders: {
192+
Location: "https://tus.io/uploads/12345",
193+
},
194+
})
195+
196+
// Handle first PATCH request
197+
req = await testStack.nextRequest()
198+
199+
req.respondWith({
200+
status: 204,
201+
responseHeaders: {
202+
"Upload-Offset": "5", // Partial progress
203+
},
204+
})
205+
206+
// Simulate stall detection
207+
upload._handleStall("test stall")
208+
209+
// Verify error was emitted with correct message
210+
expect(errorSpy).toHaveBeenCalled()
211+
expect(errorSpy.calls.mostRecent().args[0]).toContain(
212+
"Upload stalled: test stall"
213+
)
214+
})
215+
})
216+
})

0 commit comments

Comments
 (0)