Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 5e35b13

Browse files
committedApr 28, 2025··
Type results of slicing and HTTP request content
1 parent 01bd299 commit 5e35b13

File tree

6 files changed

+68
-45
lines changed

6 files changed

+68
-45
lines changed
 

‎lib/browser/FetchHttpStack.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import type { HttpProgressHandler, HttpRequest, HttpResponse, HttpStack } from '../options.js'
1+
import { readable as isNodeReadableStream } from 'is-stream'
2+
import type {
3+
HttpProgressHandler,
4+
HttpRequest,
5+
HttpResponse,
6+
HttpStack,
7+
SliceType,
8+
} from '../options.js'
29

310
// TODO: Add tests for this.
411
export class FetchHttpStack implements HttpStack {
@@ -42,7 +49,13 @@ class FetchRequest implements HttpRequest {
4249
// The Fetch API currently does not expose a way to track upload progress.
4350
}
4451

45-
async send(body?: Blob): Promise<FetchResponse> {
52+
async send(body?: SliceType): Promise<FetchResponse> {
53+
if (isNodeReadableStream(body)) {
54+
throw new Error(
55+
'Using a Node.js readable stream as HTTP request body is not supported using the Fetch API HTTP stack.',
56+
)
57+
}
58+
4659
const res = await fetch(this._url, {
4760
method: this._method,
4861
headers: this._headers,

‎lib/node/NodeHttpStack.ts

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,15 @@ import * as http from 'node:http'
44
import * as https from 'node:https'
55
import { Readable, Transform, type Writable } from 'node:stream'
66
import { parse } from 'node:url'
7+
import isStream from 'is-stream'
78
import throttle from 'lodash.throttle'
8-
import type { HttpProgressHandler, HttpRequest, HttpResponse, HttpStack } from '../options.js'
9-
import type { FileSliceTypes } from './index.js'
9+
import type {
10+
HttpProgressHandler,
11+
HttpRequest,
12+
HttpResponse,
13+
HttpStack,
14+
SliceType,
15+
} from '../options.js'
1016

1117
export class NodeHttpStack implements HttpStack {
1218
private _requestOptions: http.RequestOptions
@@ -63,9 +69,25 @@ class Request implements HttpRequest {
6369
this._progressHandler = progressHandler
6470
}
6571

66-
async send(body?: FileSliceTypes): Promise<HttpResponse> {
67-
if (body instanceof Blob) {
68-
body = await body.arrayBuffer()
72+
async send(body?: SliceType): Promise<HttpResponse> {
73+
let nodeBody: Readable | Uint8Array | undefined
74+
if (body != null) {
75+
if (body instanceof Blob) {
76+
nodeBody = new Uint8Array(await body.arrayBuffer())
77+
} else if (body instanceof Uint8Array) {
78+
nodeBody = body
79+
} else if (ArrayBuffer.isView(body)) {
80+
// Any typed array other than Uint8Array or a DataVew
81+
nodeBody = new Uint8Array(body.buffer, body.byteOffset, body.byteLength)
82+
} else if (isStream.readable(body)) {
83+
nodeBody = body
84+
} else {
85+
throw new Error(
86+
// @ts-expect-error According to the types, this case cannot happen. But
87+
// we still want to try logging the constructor if this code is reached by accident.
88+
`Unsupported HTTP request body type in Node.js HTTP stack: ${typeof body} (constructor: ${body?.constructor?.name})`,
89+
)
90+
}
6991
}
7092

7193
return new Promise((resolve, reject) => {
@@ -106,27 +128,19 @@ class Request implements HttpRequest {
106128
reject(err)
107129
})
108130

109-
if (body instanceof ArrayBuffer || body instanceof SharedArrayBuffer) {
110-
body = new Uint8Array(body)
111-
}
112-
113-
if (ArrayBuffer.isView(body) && !(body instanceof Uint8Array)) {
114-
body = new Uint8Array(body.buffer, body.byteOffset, body.byteLength)
115-
}
116-
117-
if (body instanceof Readable) {
131+
if (nodeBody instanceof Readable) {
118132
// Readable stream are piped through a PassThrough instance, which
119133
// counts the number of bytes passed through. This is used, for example,
120134
// when an fs.ReadStream is provided to tus-js-client.
121-
body.pipe(new ProgressEmitter(this._progressHandler)).pipe(req)
122-
} else if (body instanceof Uint8Array) {
135+
nodeBody.pipe(new ProgressEmitter(this._progressHandler)).pipe(req)
136+
} else if (nodeBody instanceof Uint8Array) {
123137
// For Buffers and Uint8Arrays (in Node.js all buffers are instances of Uint8Array),
124138
// we write chunks of the buffer to the stream and use that to track the progress.
125139
// This is used when either a Buffer or a normal readable stream is provided
126140
// to tus-js-client.
127-
writeBufferToStreamWithProgress(req, body, this._progressHandler)
141+
writeBufferToStreamWithProgress(req, nodeBody, this._progressHandler)
128142
} else {
129-
req.end(body)
143+
req.end()
130144
}
131145
})
132146
}

‎lib/node/index.ts

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
1-
import type { ReadStream } from 'node:fs'
2-
import type { Readable } from 'node:stream'
31
import { DetailedError } from '../DetailedError.js'
42
import { NoopUrlStorage } from '../NoopUrlStorage.js'
53
import { enableDebugLog } from '../logger.js'
6-
import type { PathReference, UploadInput, UploadOptions } from '../options.js'
4+
import type { UploadInput, UploadOptions } from '../options.js'
75
import { BaseUpload, defaultOptions as baseDefaultOptions, terminate } from '../upload.js'
86

97
import { canStoreURLs } from './FileUrlStorage.js'
@@ -19,16 +17,6 @@ const defaultOptions = {
1917
fingerprint,
2018
}
2119

22-
export type FileTypes =
23-
| ArrayBuffer
24-
| SharedArrayBuffer
25-
| ArrayBufferView
26-
| Blob
27-
| Readable
28-
| PathReference
29-
30-
export type FileSliceTypes = ArrayBuffer | SharedArrayBuffer | ArrayBufferView | Blob | ReadStream
31-
3220
class Upload extends BaseUpload {
3321
constructor(file: UploadInput, options: Partial<UploadOptions> = {}) {
3422
const allOpts = { ...defaultOptions, ...options }

‎lib/node/sources/PathFileSource.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ export class PathFileSource implements FileSource {
3535
}
3636

3737
slice(start: number, end: number) {
38+
// TODO: Does this create multiple file descriptors? Can we reduce this by
39+
// using a file handle instead?
3840
// The path reference might be configured to not read from the beginning,
3941
// but instead start at a different offset. The start value from the caller
4042
// does not include the offset, so we need to add this offset to our range later.

‎lib/options.ts

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Readable } from 'node:stream'
1+
import type { Readable as NodeReadableStream } from 'node:stream'
22
import type { DetailedError } from './DetailedError.js'
33

44
export const PROTOCOL_TUS_V1 = 'tus-v1'
@@ -36,17 +36,17 @@ export interface PathReference {
3636
}
3737

3838
export type UploadInput =
39-
// Blob, File, ReadableStreamDefaultReader are available in browsers and Node.js
39+
// available in all environments
4040
| Blob // includes File
4141
| ArrayBuffer
4242
| SharedArrayBuffer
4343
| ArrayBufferView // includes Node.js' Buffer
44-
// TODO: Should we keep the Pick<> here?
45-
// TODO: Should we also accept ReadableStreamBYOBReader? What about ReadableStream?
44+
// TODO: We should accept a ReadableStream instead of a reader
4645
| Pick<ReadableStreamDefaultReader, 'read'>
47-
// Buffer, stream.Readable, fs.ReadStream are available in Node.js
48-
| Readable // TODO: Replace this with our own interface based on https://github.com/DefinitelyTyped/DefinitelyTyped/blob/3634b01d50c10ce1afaae63e41d39e7da309d8e3/types/node/globals.d.ts#L399
46+
// available in Node.js
47+
| NodeReadableStream
4948
| PathReference
49+
// available in React Native
5050
| ReactNativeFile
5151

5252
export interface UploadOptions {
@@ -120,6 +120,9 @@ export interface FileSource {
120120
close(): void
121121
}
122122

123+
// TODO: Allow Web Streams' ReadableStream as well
124+
export type SliceType = Blob | ArrayBufferView | NodeReadableStream
125+
123126
export type SliceResult =
124127
| {
125128
done: true
@@ -128,9 +131,11 @@ export type SliceResult =
128131
}
129132
| {
130133
done: boolean
131-
// Platform-specific data type which must be usable by the HTTP stack as a body.
132-
// TODO: This shouldn't be unknown, but precise values.
133-
value: NonNullable<unknown>
134+
value: NonNullable<SliceType>
135+
// TODO: How should sizes be handled? If we want to allow `slice()` to return
136+
// streams without buffering them before, this cannot return a known size.
137+
// Should size be returned by the HTTP stack based on the number of uploaded bytes?
138+
// It would make sense since it likely already counts progress.
134139
size: number
135140
}
136141

@@ -150,7 +155,7 @@ export interface HttpRequest {
150155

151156
setProgressHandler(handler: HttpProgressHandler): void
152157
// TODO: Should this be something like { value: unknown, size: number }?
153-
send(body?: unknown): Promise<HttpResponse>
158+
send(body?: SliceType): Promise<HttpResponse>
154159
abort(): Promise<void>
155160

156161
// Return an environment specific object, e.g. the XMLHttpRequest object in browsers.

‎lib/upload.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
PROTOCOL_IETF_DRAFT_05,
1313
PROTOCOL_TUS_V1,
1414
type PreviousUpload,
15+
type SliceType,
1516
type UploadInput,
1617
type UploadOptions,
1718
} from './options.js'
@@ -981,7 +982,7 @@ export class BaseUpload {
981982
*
982983
* @api private
983984
*/
984-
_sendRequest(req: HttpRequest, body?: unknown): Promise<HttpResponse> {
985+
_sendRequest(req: HttpRequest, body?: SliceType): Promise<HttpResponse> {
985986
return sendRequest(req, body, this.options)
986987
}
987988
}
@@ -1041,7 +1042,7 @@ function openRequest(method: string, url: string, options: UploadOptions): HttpR
10411042
*/
10421043
async function sendRequest(
10431044
req: HttpRequest,
1044-
body: unknown | undefined,
1045+
body: SliceType | undefined,
10451046
options: UploadOptions,
10461047
): Promise<HttpResponse> {
10471048
if (typeof options.onBeforeRequest === 'function') {

0 commit comments

Comments
 (0)
Please sign in to comment.