tus-js-client-stall-detection
Version:
A pure JavaScript client for the tus resumable upload protocol (fork with stall detection)
295 lines • 11.9 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.NodeHttpStack = void 0;
// The url.parse method is superseeded by the url.URL constructor,
// but it is still included in Node.js
const http = __importStar(require("node:http"));
const https = __importStar(require("node:https"));
const node_stream_1 = require("node:stream");
const node_url_1 = require("node:url");
const is_stream_1 = __importDefault(require("is-stream"));
const lodash_throttle_1 = __importDefault(require("lodash.throttle"));
class NodeHttpStack {
constructor(requestOptions = {}) {
this._requestOptions = requestOptions;
}
createRequest(method, url) {
return new Request(method, url, this._requestOptions);
}
getName() {
return 'NodeHttpStack';
}
supportsProgressEvents() {
// Node.js HTTP stack supports progress tracking through streams
return true;
}
}
exports.NodeHttpStack = NodeHttpStack;
class Request {
constructor(method, url, options) {
this._headers = {};
this._request = null;
this._progressHandler = () => { };
this._method = method;
this._url = url;
this._requestOptions = options;
}
getMethod() {
return this._method;
}
getURL() {
return this._url;
}
setHeader(header, value) {
this._headers[header] = value;
}
getHeader(header) {
return this._headers[header];
}
setProgressHandler(progressHandler) {
this._progressHandler = progressHandler;
}
async send(body) {
var _a;
let nodeBody;
if (body != null) {
if (body instanceof Blob) {
nodeBody = new Uint8Array(await body.arrayBuffer());
}
else if (body instanceof Uint8Array) {
nodeBody = body;
}
else if (ArrayBuffer.isView(body)) {
// Any typed array other than Uint8Array or a DataVew
nodeBody = new Uint8Array(body.buffer, body.byteOffset, body.byteLength);
}
else if (is_stream_1.default.readable(body)) {
nodeBody = body;
}
else {
throw new Error(
// @ts-expect-error According to the types, this case cannot happen. But
// we still want to try logging the constructor if this code is reached by accident.
`Unsupported HTTP request body type in Node.js HTTP stack: ${typeof body} (constructor: ${(_a = body === null || body === void 0 ? void 0 : body.constructor) === null || _a === void 0 ? void 0 : _a.name})`);
}
}
return new Promise((resolve, reject) => {
const options = {
...(0, node_url_1.parse)(this._url),
...this._requestOptions,
method: this._method,
headers: {
...(this._requestOptions.headers || {}),
...this._headers,
},
};
// TODO: What to do here?
// @ts-expect-error We still have to type `size` for `body`
if (body === null || body === void 0 ? void 0 : body.size) {
// @ts-expect-error We still have to type `size` for `body`
options.headers['Content-Length'] = body.size;
}
const httpModule = options.protocol === 'https:' ? https : http;
this._request = httpModule.request(options);
const req = this._request;
req.on('response', (res) => {
const resChunks = [];
res.on('data', (data) => {
resChunks.push(data);
});
res.on('end', () => {
const responseText = Buffer.concat(resChunks).toString('utf8');
resolve(new Response(res, responseText));
});
});
req.on('error', (err) => {
reject(err);
});
if (nodeBody instanceof node_stream_1.Readable) {
// Readable stream are piped through a PassThrough instance, which
// counts the number of bytes passed through. This is used, for example,
// when an fs.ReadStream is provided to tus-js-client.
nodeBody.pipe(new ProgressEmitter(this._progressHandler)).pipe(req);
}
else if (nodeBody instanceof Uint8Array) {
// For Buffers and Uint8Arrays (in Node.js all buffers are instances of Uint8Array),
// we write chunks of the buffer to the stream and use that to track the progress.
// This is used when either a Buffer or a normal readable stream is provided
// to tus-js-client.
writeBufferToStreamWithProgress(req, nodeBody, this._progressHandler);
}
else {
req.end();
}
});
}
abort() {
if (this._request != null)
this._request.abort();
return Promise.resolve();
}
getUnderlyingObject() {
return this._request;
}
}
class Response {
constructor(res, body) {
this._response = res;
this._body = body;
}
getStatus() {
if (this._response.statusCode === undefined) {
throw new Error('no status code available yet');
}
return this._response.statusCode;
}
getHeader(header) {
const values = this._response.headers[header.toLowerCase()];
if (Array.isArray(values)) {
return values.join(', ');
}
return values;
}
getBody() {
return this._body;
}
getUnderlyingObject() {
return this._response;
}
}
// ProgressEmitter is a simple PassThrough-style transform stream which keeps
// track of the number of bytes which have been piped through it and will
// invoke the `onprogress` function whenever new number are available.
class ProgressEmitter extends node_stream_1.Transform {
constructor(onprogress) {
super();
this._position = 0;
// The _onprogress property will be invoked, whenever a chunk is piped
// through this transformer. Since chunks are usually quite small (64kb),
// these calls can occur frequently, especially when you have a good
// connection to the remote server. Therefore, we are throtteling them to
// prevent excessive function calls. We use trailing: true to ensure
// the final progress event is always reported.
this._onprogress = (0, lodash_throttle_1.default)(onprogress, 100, {
leading: true,
trailing: true,
});
// Report initial progress immediately
this._onprogress(0);
}
_transform(chunk, _encoding, callback) {
this._position += chunk.length;
this._onprogress(this._position);
callback(null, chunk);
}
}
// writeBufferToStreamWithProgress writes chunks from `source` (either a
// Buffer or Uint8Array) to the readable stream `stream`.
// The size of the chunk depends on the stream's highWaterMark to fill the
// stream's internal buffer as best as possible.
// If the internal buffer is full, the callback `onprogress` will be invoked
// to notify about the write progress. Writing will be resumed once the internal
// buffer is empty, as indicated by the emitted `drain` event.
// See https://nodejs.org/docs/latest/api/stream.html#buffering for more details
// on the buffering behavior of streams.
function writeBufferToStreamWithProgress(stream, source, onprogress) {
// Don't throttle progress events here to ensure stall detector gets accurate updates
// The user-facing progress events can handle their own throttling if needed
let offset = 0;
let lastReportedBytes = 0;
// Report initial progress
onprogress(0);
// For HTTP requests, we can track actual bytes sent via the socket
const request = stream;
// Set up socket monitoring to track actual network progress
function setupSocketMonitoring(socket) {
// Poll the socket's bytesWritten to track actual network progress
const progressInterval = setInterval(() => {
const currentBytes = socket.bytesWritten;
// Only report progress if bytes have actually been sent
if (currentBytes > lastReportedBytes) {
lastReportedBytes = currentBytes;
// Report the actual bytes sent, capped by our total size
const progress = Math.min(currentBytes, source.length);
onprogress(progress);
}
// Stop monitoring once we've sent all data
if (currentBytes >= source.length || socket.destroyed) {
clearInterval(progressInterval);
}
}, 50); // Check every 50ms for progress
// Clean up on socket close
socket.once('close', () => {
clearInterval(progressInterval);
});
}
// Monitor the socket for actual network progress
if (request.socket) {
setupSocketMonitoring(request.socket);
}
else {
request.once('socket', (socket) => {
setupSocketMonitoring(socket);
});
}
function writeNextChunk() {
// Take at most the amount of bytes from highWaterMark. This should fill the streams
// internal buffer already.
const chunkSize = Math.min(stream.writableHighWaterMark, source.length - offset);
// Note: We use subarray instead of slice because it works without copying data for
// Buffers and Uint8Arrays.
const chunk = source.subarray(offset, offset + chunkSize);
offset += chunk.length;
const canContinue = stream.write(chunk);
if (!canContinue) {
// The stream buffer is full. Wait for the drain event before writing more data.
stream.once('drain', writeNextChunk);
}
else if (offset < source.length) {
// Use setImmediate to avoid blocking the event loop
setImmediate(writeNextChunk);
}
else {
// All data has been written to the stream buffer
stream.end();
}
}
writeNextChunk();
}
//# sourceMappingURL=NodeHttpStack.js.map