UNPKG

vike

Version:

The Framework *You* Control - Next.js & Nuxt alternative for unprecedented flexibility and dependability.

830 lines (829 loc) 30.5 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.processStream = processStream; exports.streamToString = streamToString; exports.stampPipe = stampPipe; exports.pipeStream = pipeStream; exports.pipeWebStream = pipeWebStream; exports.pipeNodeStream = pipeNodeStream; exports.getStreamReadableNode = getStreamReadableNode; exports.getStreamReadableWeb = getStreamReadableWeb; exports.pipeToStreamWritableNode = pipeToStreamWritableNode; exports.pipeToStreamWritableWeb = pipeToStreamWritableWeb; exports.isStream = isStream; exports.isStreamPipeWeb = isStreamPipeWeb; exports.isStreamPipeNode = isStreamPipeNode; exports.isStreamReadableWeb = isStreamReadableWeb; exports.isStreamReadableNode = isStreamReadableNode; exports.getStreamName = getStreamName; exports.inferStreamName = inferStreamName; exports.streamReadableWebToString = streamReadableWebToString; exports.streamPipeNodeToString = streamPipeNodeToString; exports.isStreamWritableWeb = isStreamWritableWeb; exports.isStreamWritableNode = isStreamWritableNode; const utils_js_1 = require("../utils.js"); const react_streaming_js_1 = require("./stream/react-streaming.js"); const import_1 = require("@brillout/import"); const picocolors_1 = __importDefault(require("@brillout/picocolors")); const debug = (0, utils_js_1.createDebugger)('vike:stream'); function isStreamReadableWeb(thing) { return typeof ReadableStream !== 'undefined' && thing instanceof ReadableStream; } function isStreamWritableWeb(thing) { return typeof WritableStream !== 'undefined' && thing instanceof WritableStream; } function isStreamReadableNode(thing) { if (isStreamReadableWeb(thing)) { return false; } // https://stackoverflow.com/questions/17009975/how-to-test-if-an-object-is-a-stream-in-nodejs/37022523#37022523 return (0, utils_js_1.hasProp)(thing, 'read', 'function'); } function isStreamWritableNode(thing) { if (isStreamWritableWeb(thing)) { return false; } // https://stackoverflow.com/questions/17009975/how-to-test-if-an-object-is-a-stream-in-nodejs/37022523#37022523 return (0, utils_js_1.hasProp)(thing, 'write', 'function'); } async function streamReadableNodeToString(readableNode) { // Copied from: https://stackoverflow.com/questions/10623798/how-do-i-read-the-contents-of-a-node-js-stream-into-a-string-variable/49428486#49428486 const chunks = []; return new Promise((resolve, reject) => { readableNode.on('data', (chunk) => chunks.push(Buffer.from(chunk))); readableNode.on('error', (err) => reject(err)); readableNode.on('end', () => resolve(Buffer.concat(chunks).toString('utf8'))); }); } async function streamReadableWebToString(readableWeb) { const reader = readableWeb.getReader(); const { decode, getClosingChunk } = decodeChunks(); let str = ''; while (true) { const { done, value } = await reader.read(); if (done) break; str += decode(value); } str += getClosingChunk(); return str; } async function stringToStreamReadableNode(str) { const { Readable } = await loadStreamNodeModule(); return Readable.from(str); } function stringToStreamReadableWeb(str) { // ReadableStream.from() spec discussion: https://github.com/whatwg/streams/issues/1018 assertReadableStreamConstructor(); const readableStream = new ReadableStream({ start(controller) { controller.enqueue(encodeForWebStream(str)); controller.close(); }, }); return readableStream; } function stringToStreamPipeNode(str) { return (writable) => { writable.write(str); writable.end(); }; } function stringToStreamPipeWeb(str) { return (writable) => { const writer = writable.getWriter(); writer.write(encodeForWebStream(str)); writer.close(); }; } async function streamPipeNodeToString(streamPipeNode) { let str = ''; let resolve; let reject; const promise = new Promise((resolve_, reject_) => { resolve = () => resolve_(str); reject = reject_; }); const { Writable } = await loadStreamNodeModule(); const writable = new Writable({ write(chunk, _encoding, callback) { const s = chunk.toString(); (0, utils_js_1.assert)(typeof s === 'string'); str += s; callback(); }, final(callback) { resolve(); callback(); }, destroy(err) { if (err) { reject(err); } else { resolve(); } }, }); streamPipeNode(writable); return promise; } function streamPipeWebToString(streamPipeWeb) { const { decode, getClosingChunk } = decodeChunks(); let str = ''; let resolve; const promise = new Promise((r) => (resolve = r)); const writable = new WritableStream({ write(chunk) { str += decode(chunk); }, close() { str += getClosingChunk(); resolve(str); }, }); streamPipeWeb(writable); return promise; } async function getStreamReadableNode(htmlRender) { if (typeof htmlRender === 'string') { return stringToStreamReadableNode(htmlRender); } if (isStreamReadableNode(htmlRender)) { return htmlRender; } return null; } function getStreamReadableWeb(htmlRender) { if (typeof htmlRender === 'string') { return stringToStreamReadableWeb(htmlRender); } if (isStreamReadableWeb(htmlRender)) { return htmlRender; } if (isStreamPipeWeb(htmlRender)) { const streamPipeWeb = getStreamPipeWeb(htmlRender); (0, utils_js_1.assert)(streamPipeWeb); const { readable, writable } = new TransformStream(); streamPipeWeb(writable); return readable; } return null; } function pipeToStreamWritableWeb(htmlRender, writable) { if (typeof htmlRender === 'string') { const streamPipeWeb = stringToStreamPipeWeb(htmlRender); streamPipeWeb(writable); return true; } if (isStreamReadableWeb(htmlRender)) { htmlRender.pipeTo(writable); return true; } if (isStreamPipeWeb(htmlRender)) { const streamPipeWeb = getStreamPipeWeb(htmlRender); (0, utils_js_1.assert)(streamPipeWeb); streamPipeWeb(writable); return true; } if (isStreamReadableNode(htmlRender) || isStreamPipeNode(htmlRender)) { return false; } (0, utils_js_1.checkType)(htmlRender); (0, utils_js_1.assert)(false); } function pipeToStreamWritableNode(htmlRender, writable) { if (typeof htmlRender === 'string') { const streamPipeNode = stringToStreamPipeNode(htmlRender); streamPipeNode(writable); return true; } if (isStreamReadableNode(htmlRender)) { htmlRender.pipe(writable); return true; } if (isStreamPipeNode(htmlRender)) { const streamPipeNode = getStreamPipeNode(htmlRender); (0, utils_js_1.assert)(streamPipeNode); streamPipeNode(writable); return true; } if (isStreamReadableWeb(htmlRender) || isStreamPipeWeb(htmlRender)) { return false; } (0, utils_js_1.checkType)(htmlRender); (0, utils_js_1.assert)(false); } async function processStream(streamOriginal, { injectStringAtBegin, injectStringAfterFirstChunk, injectStringAtEnd, onErrorWhileStreaming, enableEagerStreaming, }) { const buffer = []; let streamOriginalHasStartedEmitting = false; let streamOriginalEnded = false; let streamClosed = false; let onEndWasCalled = false; let isReadyToWrite = false; let wrapperCreated = false; let shouldFlushStream = false; let resolve; let reject; let promiseHasResolved = false; let injectStringAfterFirstChunk_done = false; const streamWrapperPromise = new Promise((resolve_, reject_) => { resolve = (streamWrapper) => { promiseHasResolved = true; resolve_(streamWrapper); }; reject = (err) => { promiseHasResolved = true; reject_(err); }; }); let resolveReadyToWrite; const promiseReadyToWrite = new Promise((r) => (resolveReadyToWrite = r)); if (injectStringAtBegin) { const injectedChunk = await injectStringAtBegin(); writeStream(injectedChunk); // Adds injectedChunk to buffer flushStream(); // Sets shouldFlushStream to true } // We call onStreamEvent() also when the stream ends in order to properly handle the situation when the stream didn't emit any data const onStreamDataOrEnd = (cb) => { (0, utils_js_1.assert)(streamOriginalEnded === false); streamOriginalHasStartedEmitting = true; cb(); if (wrapperCreated) resolvePromise(); }; const { streamWrapper, streamWrapperOperations } = await createStreamWrapper({ streamOriginal, onReadyToWrite() { debug('stream begin'); isReadyToWrite = true; flushBuffer(); resolveReadyToWrite(); }, onError(err) { if (!promiseHasResolved) { reject(err); } else { onErrorWhileStreaming(err); } }, onData(chunk) { onStreamDataOrEnd(() => { writeStream(chunk); if (injectStringAfterFirstChunk && !injectStringAfterFirstChunk_done) { const injectedChunk = injectStringAfterFirstChunk(); if (injectedChunk !== null) writeStream(injectedChunk); injectStringAfterFirstChunk_done = true; } }); }, async onEnd( // Should we use this `isCancel`? Maybe we can skip `injectStringAtEnd()`? isCancel) { try { (0, utils_js_1.assert)(!onEndWasCalled); onEndWasCalled = true; debug('stream end'); // We call onStreamEvent() also here in case the stream didn't emit any data onStreamDataOrEnd(() => { streamOriginalEnded = true; }); if (injectStringAtEnd) { const injectedChunk = await injectStringAtEnd(); writeStream(injectedChunk); } await promiseReadyToWrite; // E.g. if the user calls the pipe wrapper after the original writable has ended (0, utils_js_1.assert)(isReady()); flushBuffer(); streamClosed = true; debug('stream ended'); } catch (err) { // Ideally, we should catch and gracefully handle user land errors, as any error thrown here kills the server. (I assume that the fact it kills the server is a Node.js bug?) // Show "[vike][Bug] You stumbled upon a bug in Vike's source code" to user while printing original error if (!(0, utils_js_1.isBug)(err)) { console.error(err); (0, utils_js_1.assert)(false); } throw err; } }, onFlush() { flushStream(); }, }); wrapperCreated = true; flushBuffer(); // In case onReadyToWrite() was already called (the flushBuffer() of onReadyToWrite() wasn't called because `wrapperCreated === false`) if (!delayStreamStart()) resolvePromise(); return streamWrapperPromise; function writeStream(chunk) { buffer.push(chunk); flushBuffer(); } function flushBuffer() { if (!isReady()) return; (0, utils_js_1.assert)(!streamClosed); buffer.forEach((chunk) => { streamWrapperOperations.writeChunk(chunk); }); buffer.length = 0; if (shouldFlushStream) flushStream(); } function resolvePromise() { (0, utils_js_1.assert)(!delayStreamStart()); // The stream promise shouldn't resolve before delayStreamStart() (0, utils_js_1.assert)(wrapperCreated); // Doesn't make sense to resolve streamWrapper if it isn't defined yet debug('stream promise resolved'); resolve(streamWrapper); } function flushStream() { if (!isReady()) { shouldFlushStream = true; return; } if (streamWrapperOperations.flushStream === null) return; streamWrapperOperations.flushStream(); shouldFlushStream = false; debug('stream flushed'); } function isReady() { /* console.log('isReadyToWrite', isReadyToWrite) console.log('wrapperCreated', wrapperCreated) console.log('!delayStreamStart()', !delayStreamStart()) */ return (isReadyToWrite && // We can't use streamWrapperOperations.writeChunk() if it isn't defined yet wrapperCreated && // See comment below !delayStreamStart()); } // Delay streaming, so that if the page shell fails then Vike is able to render the error page. // - We can't erase the previously written stream data => we need to delay streaming if we want to be able to restart rendering anew for the error page // - This is what React expects. // - Does this make sense for UI frameworks other than React? // - We don't need this anymore if we implement a client-side recover mechanism. // - I.e. rendering the error page on the client-side if there is an error during the stream. // - We cannot do this with Server Routing // - Emitting the wrong status code doesn't matter with libraries like react-streaming which automatically disable streaming for bots. (Emitting the right status code only matters for bots.) function delayStreamStart() { return !enableEagerStreaming && !streamOriginalHasStartedEmitting; } } async function createStreamWrapper({ streamOriginal, onError, onData, onEnd, onFlush, onReadyToWrite, }) { if ((0, react_streaming_js_1.isStreamFromReactStreamingPackage)(streamOriginal)) { debug(`onRenderHtml() hook returned ${picocolors_1.default.cyan('react-streaming')} result`); const stream = (0, react_streaming_js_1.getStreamOfReactStreamingPackage)(streamOriginal); streamOriginal = stream; } if (isStreamPipeNode(streamOriginal)) { debug('onRenderHtml() hook returned Node.js Stream Pipe'); let writableOriginal = null; const pipeProxy = (writable_) => { writableOriginal = writable_; debug('original Node.js Writable received'); onReadyToWrite(); if (hasEnded) { // onReadyToWrite() already wrote everything; we can close the stream right away writableOriginal.end(); } }; stampPipe(pipeProxy, 'node-stream'); const writeChunk = (chunk) => { (0, utils_js_1.assert)(writableOriginal); writableOriginal.write(chunk); debugWithChunk('data written (Node.js Writable)', chunk); }; // For libraries such as https://www.npmjs.com/package/compression // - React calls writable.flush() when available // - https://github.com/vikejs/vike/issues/466#issuecomment-1269601710 const flushStream = () => { (0, utils_js_1.assert)(writableOriginal); if (typeof writableOriginal.flush === 'function') { writableOriginal.flush(); debug('stream flush() performed (Node.js Writable)'); } }; let hasEnded = false; const endStream = () => { hasEnded = true; if (writableOriginal) { writableOriginal.end(); } }; const { Writable } = await loadStreamNodeModule(); const writableProxy = new Writable({ async write(chunk, _encoding, callback) { onData(chunk); callback(); }, async destroy(err, callback) { if (err) { onError(err); } else { await onEnd(); } callback(err); endStream(); }, }); // Forward the flush() call (0, utils_js_1.objectAssign)(writableProxy, { flush: () => { onFlush(); }, }); (0, utils_js_1.assert)(typeof writableProxy.flush === 'function'); const pipeOriginal = getStreamPipeNode(streamOriginal); pipeOriginal(writableProxy); return { streamWrapper: pipeProxy, streamWrapperOperations: { writeChunk, flushStream } }; } if (isStreamPipeWeb(streamOriginal)) { debug('onRenderHtml() hook returned Web Stream Pipe'); let writerOriginal = null; const pipeProxy = (writableOriginal) => { writerOriginal = writableOriginal.getWriter(); debug('original Web Writable received'); (async () => { // CloudFlare Workers does not implement `ready` property // - https://github.com/vuejs/vue-next/issues/4287 try { await writerOriginal.ready; } catch (e) { } onReadyToWrite(); if (hasEnded) { // onReadyToWrite() already wrote everything; we can close the stream right away writerOriginal.close(); } })(); }; stampPipe(pipeProxy, 'web-stream'); const writeChunk = (chunk) => { (0, utils_js_1.assert)(writerOriginal); writerOriginal.write(encodeForWebStream(chunk)); debugWithChunk('data written (Web Writable)', chunk); }; // Web Streams have compression built-in // - https://developer.mozilla.org/en-US/docs/Web/API/Compression_Streams_API // - It seems that there is no flush interface? Flushing just works automagically? const flushStream = null; let hasEnded = false; const endStream = () => { hasEnded = true; if (writerOriginal) { writerOriginal.close(); } }; let writableProxy; if (typeof ReadableStream !== 'function') { writableProxy = new WritableStream({ write(chunk) { onData(chunk); }, async close() { await onEnd(); endStream(); }, abort(err) { onError(err); endStream(); }, }); } else { const { readable, writable } = new TransformStream(); writableProxy = writable; handleReadableWeb(readable, { onData, onError(err) { onError(err); endStream(); }, async onEnd() { await onEnd(); endStream(); }, }); } const pipeOriginal = getStreamPipeWeb(streamOriginal); pipeOriginal(writableProxy); return { streamWrapper: pipeProxy, streamWrapperOperations: { writeChunk, flushStream } }; } if (isStreamReadableWeb(streamOriginal)) { debug('onRenderHtml() hook returned Web Readable'); const readableOriginal = streamOriginal; let isClosed = false; let isCancel = false; const closeStream = async () => { if (isClosed) return; isClosed = true; await onEnd(isCancel); controllerProxy.close(); }; let controllerProxy; assertReadableStreamConstructor(); const readableProxy = new ReadableStream({ start(controller) { controllerProxy = controller; onReadyToWrite(); handleReadableWeb(readableOriginal, { onData, onError(err) { onError(err); controllerProxy.close(); }, async onEnd() { await closeStream(); }, }); }, async cancel(...args) { isCancel = true; await readableOriginal.cancel(...args); // If readableOriginal has implemented readableOriginal.cancel() then the onEnd() callback and therefore closeStream() may already have been called at this point await closeStream(); }, }); const writeChunk = (chunk) => { if ( // If readableOriginal doesn't implement readableOriginal.cancel() then it may still emit data after we close the stream. We therefore need to check whether the steam is closed. !isClosed) { controllerProxy.enqueue(encodeForWebStream(chunk)); debugWithChunk('data written (Web Readable)', chunk); } else { debugWithChunk('data emitted but not written (Web Readable)', chunk); } }; // Readables don't have the notion of flushing const flushStream = null; return { streamWrapper: readableProxy, streamWrapperOperations: { writeChunk, flushStream }, }; } if (isStreamReadableNode(streamOriginal)) { debug('onRenderHtml() hook returned Node.js Readable'); const readableOriginal = streamOriginal; const { Readable } = await loadStreamNodeModule(); // Vue doesn't always set the read() handler: https://github.com/vikejs/vike/issues/138#issuecomment-934743375 if (readableOriginal._read === Readable.prototype._read) { readableOriginal._read = function () { }; } const writeChunk = (chunk) => { readableProxy.push(chunk); debugWithChunk('data written (Node.js Readable)', chunk); }; // Readables don't have the notion of flushing const flushStream = null; const closeProxy = () => { readableProxy.push(null); }; const readableProxy = new Readable({ read() { } }); onReadyToWrite(); readableOriginal.on('data', (chunk) => { onData(chunk); }); readableOriginal.on('error', (err) => { onError(err); closeProxy(); }); readableOriginal.on('end', async () => { await onEnd(); closeProxy(); }); return { streamWrapper: readableProxy, streamWrapperOperations: { writeChunk, flushStream }, }; } (0, utils_js_1.assert)(false); } async function handleReadableWeb(readable, { onData, onError, onEnd, }) { const reader = readable.getReader(); while (true) { let result; try { result = await reader.read(); } catch (err) { onError(err); return; } const { value, done } = result; if (done) { break; } onData(value); } await onEnd(); } function isStream(something) { if (isStreamReadableWeb(something) || isStreamReadableNode(something) || isStreamPipeNode(something) || isStreamPipeWeb(something) || (0, react_streaming_js_1.isStreamFromReactStreamingPackage)(something)) { (0, utils_js_1.checkType)(something); return true; } return false; } const __streamPipeWeb = '__streamPipeWeb'; /** @deprecated */ function pipeWebStream(pipe) { (0, utils_js_1.assertWarning)(false, 'pipeWebStream() is outdated, use stampPipe() instead. See https://vike.dev/streaming', { onlyOnce: true, showStackTrace: true, }); return { [__streamPipeWeb]: pipe }; } function getStreamPipeWeb(thing) { if (!isStreamPipeWeb(thing)) { return null; } if ((0, utils_js_1.isObject)(thing)) { // pipeWebStream() (0, utils_js_1.assert)(__streamPipeWeb && thing); return thing[__streamPipeWeb]; } else { // stampPipe() (0, utils_js_1.assert)((0, utils_js_1.isCallable)(thing) && 'isWebStreamPipe' in thing); return thing; } } function isStreamPipeWeb(thing) { // pipeWebStream() if ((0, utils_js_1.isObject)(thing) && __streamPipeWeb in thing) { return true; } // stampPipe() if ((0, utils_js_1.isCallable)(thing) && 'isWebStreamPipe' in thing) { return true; } return false; } const __streamPipeNode = '__streamPipeNode'; /** @deprecated */ function pipeNodeStream(pipe) { (0, utils_js_1.assertWarning)(false, 'pipeNodeStream() is outdated, use stampPipe() instead. See https://vike.dev/streaming', { onlyOnce: true, showStackTrace: true, }); return { [__streamPipeNode]: pipe }; } function getStreamPipeNode(thing) { if (!isStreamPipeNode(thing)) { return null; } if ((0, utils_js_1.isObject)(thing)) { // pipeNodeStream() (0, utils_js_1.assert)(__streamPipeNode in thing); return thing[__streamPipeNode]; } else { // stampPipe() (0, utils_js_1.assert)((0, utils_js_1.isCallable)(thing) && 'isNodeStreamPipe' in thing); return thing; } } function isStreamPipeNode(thing) { // pipeNodeStream() if ((0, utils_js_1.isObject)(thing) && __streamPipeNode in thing) { return true; } // stampPipe() if ((0, utils_js_1.isCallable)(thing) && 'isNodeStreamPipe' in thing) { return true; } return false; } function stampPipe(pipe, pipeType) { (0, utils_js_1.assertUsage)(pipeType, `stampPipe(pipe, pipeType): argument ${picocolors_1.default.cyan('pipeType')} is missing.)`, { showStackTrace: true, }); (0, utils_js_1.assertUsage)(['web-stream', 'node-stream'].includes(pipeType), `stampPipe(pipe, pipeType): argument ${picocolors_1.default.cyan('pipeType')} should be either ${picocolors_1.default.cyan("'web-stream'")} or ${picocolors_1.default.cyan("'node-stream'")}.`, { showStackTrace: true }); if (pipeType === 'node-stream') { Object.assign(pipe, { isNodeStreamPipe: true }); } else { Object.assign(pipe, { isWebStreamPipe: true }); } } const __streamPipe = '__streamPipe'; function pipeStream(pipe) { return { [__streamPipe]: pipe }; } async function streamToString(stream) { if (isStreamReadableWeb(stream)) { return await streamReadableWebToString(stream); } if (isStreamReadableNode(stream)) { return await streamReadableNodeToString(stream); } if (isStreamPipeNode(stream)) { return await streamPipeNodeToString(getStreamPipeNode(stream)); } if (isStreamPipeWeb(stream)) { return await streamPipeWebToString(getStreamPipeWeb(stream)); } if ((0, react_streaming_js_1.isStreamFromReactStreamingPackage)(stream)) { return await (0, react_streaming_js_1.streamFromReactStreamingPackageToString)(stream); } (0, utils_js_1.assert)(false); } function assertReadableStreamConstructor() { (0, utils_js_1.assertUsage)(typeof ReadableStream === 'function', // Error message copied from vue's renderToWebStream() implementation "ReadableStream constructor isn't available in the global scope. " + 'If the target environment does support web streams, consider using ' + 'pipeToWebWritable() with an existing WritableStream instance instead.'); } let encoder; function encodeForWebStream(thing) { if (!encoder) { encoder = new TextEncoder(); } if (typeof thing === 'string') { return encoder.encode(thing); } return thing; } // Because of Cloudflare Workers, we cannot statically import the `stream` module, instead we dynamically import it. async function loadStreamNodeModule() { const streamModule = (await (0, import_1.import_)('stream')).default; const { Readable, Writable } = streamModule; return { Readable, Writable }; } function getStreamName(kind, type) { let typeName = (0, utils_js_1.capitalizeFirstLetter)(type); if (typeName === 'Node') { typeName = 'Node.js'; } const kindName = (0, utils_js_1.capitalizeFirstLetter)(kind); if (kind !== 'pipe') { return `a ${kindName} ${typeName} Stream`; } if (kind === 'pipe') { return `a ${typeName} Stream Pipe`; } (0, utils_js_1.assert)(false); } function inferStreamName(stream) { if (isStreamReadableWeb(stream)) { return getStreamName('readable', 'web'); } if (isStreamReadableNode(stream)) { return getStreamName('readable', 'node'); } if (isStreamPipeNode(stream)) { return getStreamName('pipe', 'node'); } if (isStreamPipeWeb(stream)) { return getStreamName('pipe', 'web'); } (0, utils_js_1.assert)(false); } function decodeChunks() { const decoder = new TextDecoder(); const decode = (chunk) => { if (typeof chunk === 'string') { return chunk; } else if (chunk instanceof Uint8Array) { return decoder.decode(chunk, { stream: true }); } else { (0, utils_js_1.assert)(false); } }; // https://github.com/vikejs/vike/pull/1799#discussion_r1713554096 const getClosingChunk = () => { return decoder.decode(); }; return { decode, getClosingChunk }; } function debugWithChunk(msg, chunk) { if (!debug.isActivated) return; let chunkStr; try { chunkStr = new TextDecoder().decode(chunk); } catch (err) { chunkStr = String(chunk); } debug(msg, chunkStr); }