Skip to content

Commit 60698da

Browse files
authored
@tus/server: introduce POST_RECEIVE_V2 (#605)
1 parent 86b8b9f commit 60698da

File tree

12 files changed

+152
-25
lines changed

12 files changed

+152
-25
lines changed

.changeset/four-owls-juggle.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@tus/server': minor
3+
'@tus/utils': minor
4+
---
5+
6+
Introduce POST_RECEIVE_V2 event, which correctly fires during the stream write rather than
7+
after it is finished

.eslintrc.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
// eslint-disable-next-line unicorn/prefer-module
21
module.exports = {
32
root: true,
43
// This tells ESLint to load the config from the package `eslint-config-custom`

package-lock.json

Lines changed: 29 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/server/README.md

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@ Max file size (in bytes) allowed when uploading (`number` |
6969
(`(req, id: string | null) => Promise<number> | number`)). When providing a function
7070
during the OPTIONS request the id will be `null`.
7171

72+
#### `options.postReceiveInterval`
73+
74+
Interval in milliseconds for sending progress of an upload over
75+
[`POST_RECEIVE_V2`](#eventspost_receive_v2) (`number`).
76+
7277
#### `options.relativeLocation`
7378

7479
Return a relative URL as the `Location` header to the client (`boolean`).
@@ -228,14 +233,36 @@ server.on(EVENTS.POST_CREATE, (req, res, upload => {})
228233
229234
#### `POST_RECEIVE`
230235
231-
Called every time a `PATCH` request is handled.
236+
**Deprecated**.
237+
238+
Called every time an upload finished writing to the store. This event is emitted whenever
239+
the request handling is completed (which is the same as `onUploadFinish`, almost the same
240+
as `POST_FINISH`), whereas the `POST_RECEIVE_V2` event is emitted _while_ the request is
241+
being handled.
232242
233243
```js
234244
const {EVENTS} = require('@tus/server')
235245
// ...
236246
server.on(EVENTS.POST_RECEIVE, (req, res, upload => {})
237247
```
238248
249+
#### `POST_RECEIVE_V2`
250+
251+
Called every [`postReceiveInterval`](#optionspostreceiveinterval) milliseconds for every
252+
upload while it‘s being written to the store.
253+
254+
This means you are not guaranteed to get (all) events for an upload. For instance if
255+
`postReceiveInterval` is set to 1000ms and an PATCH request takes 500ms, no event is emitted.
256+
If the PATCH request takes 2500ms, you would get the offset at 2000ms, but not at 2500ms.
257+
258+
Use `POST_FINISH` if you need to know when an upload is done.
259+
260+
```js
261+
const {EVENTS} = require('@tus/server')
262+
// ...
263+
server.on(EVENTS.POST_RECEIVE_V2, (req, upload => {})
264+
```
265+
239266
#### `POST_FINISH`
240267
241268
Called an upload has completed and after a response has been sent to the client.

packages/server/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,12 @@
2222
},
2323
"dependencies": {
2424
"@tus/utils": "^0.1.0",
25-
"debug": "^4.3.4"
25+
"debug": "^4.3.4",
26+
"lodash.throttle": "^4.1.1"
2627
},
2728
"devDependencies": {
2829
"@types/debug": "^4.1.12",
30+
"@types/lodash.throttle": "^4.1.9",
2931
"@types/mocha": "^10.0.6",
3032
"@types/node": "^20.11.5",
3133
"@types/sinon": "^17.0.3",

packages/server/src/handlers/BaseHandler.ts

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import EventEmitter from 'node:events'
2+
import stream from 'node:stream/promises'
3+
import {addAbortSignal, PassThrough} from 'node:stream'
4+
import type http from 'node:http'
25

36
import type {ServerOptions} from '../types'
47
import type {DataStore, CancellationContext} from '@tus/utils'
5-
import type http from 'node:http'
6-
import {ERRORS, Upload, StreamLimiter} from '@tus/utils'
7-
import stream from 'node:stream/promises'
8-
import {addAbortSignal, PassThrough} from 'stream'
8+
import {ERRORS, Upload, StreamLimiter, EVENTS} from '@tus/utils'
9+
import throttle from 'lodash.throttle'
910

1011
const reExtractFileID = /([^/]+)\/?$/
1112
const reForwardedHost = /host="?([^";]+)/
@@ -127,8 +128,7 @@ export class BaseHandler extends EventEmitter {
127128

128129
protected writeToStore(
129130
req: http.IncomingMessage,
130-
id: string,
131-
offset: number,
131+
upload: Upload,
132132
maxFileSize: number,
133133
context: CancellationContext
134134
) {
@@ -149,6 +149,20 @@ export class BaseHandler extends EventEmitter {
149149
reject(err.name === 'AbortError' ? ERRORS.ABORTED : err)
150150
})
151151

152+
const postReceive = throttle(
153+
(offset: number) => {
154+
this.emit(EVENTS.POST_RECEIVE_V2, req, {...upload, offset})
155+
},
156+
this.options.postReceiveInterval,
157+
{leading: false}
158+
)
159+
160+
let tempOffset = upload.offset
161+
proxy.on('data', (chunk: Buffer) => {
162+
tempOffset += chunk.byteLength
163+
postReceive(tempOffset)
164+
})
165+
152166
req.on('error', () => {
153167
if (!proxy.closed) {
154168
// we end the stream gracefully here so that we can upload the remaining bytes to the store
@@ -162,7 +176,7 @@ export class BaseHandler extends EventEmitter {
162176
// which would result in a socket hangup error for the client.
163177
stream
164178
.pipeline(req.pipe(proxy), new StreamLimiter(maxFileSize), async (stream) => {
165-
return this.store.write(stream as StreamLimiter, id, offset)
179+
return this.store.write(stream as StreamLimiter, upload.id, upload.offset)
166180
})
167181
.then(resolve)
168182
.catch(reject)

packages/server/src/handlers/PatchHandler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ export class PatchHandler extends BaseHandler {
100100
}
101101

102102
const maxBodySize = await this.calculateMaxBodySize(req, upload, maxFileSize)
103-
newOffset = await this.writeToStore(req, id, offset, maxBodySize, context)
103+
newOffset = await this.writeToStore(req, upload, maxBodySize, context)
104104
} finally {
105105
await lock.unlock()
106106
}

packages/server/src/handlers/PostHandler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ export class PostHandler extends BaseHandler {
144144
// The request MIGHT include a Content-Type header when using creation-with-upload extension
145145
if (validateHeader('content-type', req.headers['content-type'])) {
146146
const bodyMaxSize = await this.calculateMaxBodySize(req, upload, maxFileSize)
147-
const newOffset = await this.writeToStore(req, id, 0, bodyMaxSize, context)
147+
const newOffset = await this.writeToStore(req, upload, bodyMaxSize, context)
148148

149149
headers['Upload-Offset'] = newOffset.toString()
150150
isFinal = newOffset === Number.parseInt(upload_length as string, 10)

packages/server/src/server.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,13 @@ interface TusEvents {
3434
upload: Upload,
3535
url: string
3636
) => void
37+
/** @deprecated this is almost the same as POST_FINISH, use POST_RECEIVE_V2 instead */
3738
[EVENTS.POST_RECEIVE]: (
3839
req: http.IncomingMessage,
3940
res: http.ServerResponse,
4041
upload: Upload
4142
) => void
43+
[EVENTS.POST_RECEIVE_V2]: (req: http.IncomingMessage, upload: Upload) => void
4244
[EVENTS.POST_FINISH]: (
4345
req: http.IncomingMessage,
4446
res: http.ServerResponse,
@@ -96,6 +98,10 @@ export class Server extends EventEmitter {
9698
options.lockDrainTimeout = 3000
9799
}
98100

101+
if (!options.postReceiveInterval) {
102+
options.postReceiveInterval = 1000
103+
}
104+
99105
const {datastore, ...rest} = options
100106
this.options = rest as ServerOptions
101107
this.datastore = datastore

packages/server/src/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ export type ServerOptions = {
3434
*/
3535
allowedHeaders?: string[]
3636

37+
/**
38+
* Interval in milliseconds for sending progress of an upload over `EVENTS.POST_RECEIVE_V2`
39+
*/
40+
postReceiveInterval?: number
41+
3742
/**
3843
* Control how the upload URL is generated.
3944
* @param req - The incoming HTTP request.

0 commit comments

Comments
 (0)