UNPKG

turbo-stream

Version:

A streaming data transport format that aims to support built-in features such as Promises, Dates, RegExps, Maps, Sets and more.

453 lines (452 loc) 18.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.encodeSync = exports.encode = void 0; const shared_js_1 = require("./shared.js"); let { NEGATIVE_INFINITY, POSITIVE_INFINITY, isNaN: nan } = Number; const ASYNC_FRAME_TYPE_PROMISE = 1; const ASYNC_FRAME_TYPE_ITERABLE = 2; class ChunkBuffer { controller; chunks = []; size = 0; highWaterMark; constructor(controller, highWaterMark) { this.controller = controller; this.highWaterMark = highWaterMark; } push(...chunks) { for (let i = 0; i < chunks.length; i++) { const chunk = chunks[i]; if (chunk == null) continue; const stringified = typeof chunk === "string" ? chunk : String(chunk); this.chunks.push(stringified); this.size += stringified.length; if (this.size >= this.highWaterMark) { this.flush(); } } } flush(suffix = "") { if (this.chunks.length > 0 || suffix.length > 0) { this.controller.enqueue(this.chunks.join("") + suffix); this.chunks.length = 0; this.size = 0; } } } function encode(value, { plugins = [], redactErrors = true, signal, highWaterMark = 16 * 1024, } = {}) { const aborted = () => signal?.aborted ?? false; const waitForAbort = new Promise((_, reject) => { signal?.addEventListener("abort", (reason) => { reject(new DOMException("Aborted", "AbortError")); }); }); return new ReadableStream({ async start(controller) { let refCache = new WeakMap(); let asyncCache = new WeakMap(); let counters = { refId: 0, promiseId: 0 }; let wg = new shared_js_1.WaitGroup(); let chunks = new ChunkBuffer(controller, highWaterMark); let encode = (value) => { encodeSync(value, chunks, refCache, asyncCache, promises, counters, plugins, redactErrors); chunks.flush("\n"); }; let handlePromiseResolved = (id, value) => { wg.done(); if (aborted()) return; chunks.push(`${id}${shared_js_1.STR_SUCCESS}`); encode(value); }; let handlePromiseRejected = (id, error) => { wg.done(); if (aborted()) return; chunks.push(`${id}${shared_js_1.STR_FAILURE}`); encode(error); }; let promises = { push: (...promiseFrames) => { for (let [type, id, promise] of promiseFrames) { wg.add(); if (type === ASYNC_FRAME_TYPE_PROMISE) { Promise.race([promise, waitForAbort]).then(handlePromiseResolved.bind(null, id), handlePromiseRejected.bind(null, id)); } else { (async () => { let iterator = promise[Symbol.asyncIterator](); let result; do { result = await iterator.next(); if (aborted()) return; if (!result.done) { chunks.push(`${id}${shared_js_1.STR_SUCCESS}`); encode(result.value); } } while (!result.done); })() .then(() => { if (aborted()) return; chunks.push(`${id}\n`); chunks.flush(); }, (error) => { if (aborted()) return; chunks.push(`${id}${shared_js_1.STR_FAILURE}`); encode(error); }) .finally(() => { wg.done(); }); } } }, }; try { encode(value); do { await Promise.race([wg.wait(), waitForAbort]); } while (wg.p > 0); controller.close(); } catch (error) { controller.error(error); } }, }); } exports.encode = encode; const ENCODE_FRAME_TYPE_NEEDS_ENCODING = 1; const ENCODE_FRAME_TYPE_ALREADY_ENCODED = 2; class EncodeFrame { type; prefix; value; constructor(type, prefix, value) { this.type = type; this.prefix = prefix; this.value = value; } } function encodeSync(value, chunks, refs, promises, asyncFrames, counters, plugins, redactErrors) { let encodeStack = [ new EncodeFrame(ENCODE_FRAME_TYPE_NEEDS_ENCODING, "", value), ]; let frame; encodeLoop: while ((frame = encodeStack.pop()) !== undefined) { if (frame.type === ENCODE_FRAME_TYPE_ALREADY_ENCODED) { chunks.push(frame.prefix); continue; } let { prefix, value } = frame; chunks.push(prefix); if (value === undefined) { chunks.push(shared_js_1.STR_UNDEFINED); continue; } if (value === null) { chunks.push(shared_js_1.STR_NULL); continue; } if (value === true) { chunks.push(shared_js_1.STR_TRUE); continue; } if (value === false) { chunks.push(shared_js_1.STR_FALSE); continue; } const typeOfValue = typeof value; if (typeOfValue === "object") { if (value instanceof Promise || typeof value.then === "function") { let existingId = promises.get(value); if (existingId !== undefined) { chunks.push(shared_js_1.STR_PROMISE, existingId.toString()); continue; } let promiseId = counters.promiseId++; promises.set(value, promiseId); chunks.push(shared_js_1.STR_PROMISE, promiseId.toString()); asyncFrames.push([ ASYNC_FRAME_TYPE_PROMISE, promiseId, value, ]); continue; } if (value instanceof ReadableStream) { let existingId = promises.get(value); if (existingId !== undefined) { chunks.push(shared_js_1.STR_READABLE_STREAM, existingId.toString()); continue; } let iterableId = counters.promiseId++; promises.set(value, iterableId); chunks.push(shared_js_1.STR_READABLE_STREAM, iterableId.toString()); asyncFrames.push([ ASYNC_FRAME_TYPE_ITERABLE, iterableId, { [Symbol.asyncIterator]: async function* () { let reader = value.getReader(); try { while (true) { let { done, value } = await reader.read(); if (done) { return; } yield value; } } finally { reader.releaseLock(); } }, }, ]); continue; } if (typeof value[Symbol.asyncIterator] === "function") { let existingId = promises.get(value); if (existingId !== undefined) { chunks.push(shared_js_1.STR_ASYNC_ITERABLE, existingId.toString()); continue; } let iterableId = counters.promiseId++; promises.set(value, iterableId); chunks.push(shared_js_1.STR_ASYNC_ITERABLE, iterableId.toString()); asyncFrames.push([ ASYNC_FRAME_TYPE_ITERABLE, iterableId, value, ]); continue; } { let existingId = refs.get(value); if (existingId !== undefined) { chunks.push(shared_js_1.STR_REFERENCE_SYMBOL, existingId.toString()); continue; } refs.set(value, counters.refId++); } if (value instanceof Date) { chunks.push(shared_js_1.STR_DATE, '"', value.toJSON(), '"'); } else if (value instanceof RegExp) { chunks.push(shared_js_1.STR_REGEXP, JSON.stringify([value.source, value.flags])); } else if (value instanceof URL) { chunks.push(shared_js_1.STR_URL, JSON.stringify(value)); } else if (value instanceof ArrayBuffer) { chunks.push(shared_js_1.STR_ARRAY_BUFFER); stringifyTypedArray(chunks, new Uint8Array(value)); } else if (value instanceof Int8Array) { chunks.push(shared_js_1.STR_INT_8_ARRAY); stringifyTypedArray(chunks, value); } else if (value instanceof Uint8Array) { chunks.push(shared_js_1.STR_UINT_8_ARRAY); stringifyTypedArray(chunks, value); } else if (value instanceof Uint8ClampedArray) { chunks.push(shared_js_1.STR_UINT_8_ARRAY_CLAMPED); stringifyTypedArray(chunks, value); } else if (value instanceof Int16Array) { chunks.push(shared_js_1.STR_INT_16_ARRAY); stringifyTypedArray(chunks, value); } else if (value instanceof Uint16Array) { chunks.push(shared_js_1.STR_UINT_16_ARRAY); stringifyTypedArray(chunks, value); } else if (value instanceof Int32Array) { chunks.push(shared_js_1.STR_INT_32_ARRAY); stringifyTypedArray(chunks, value); } else if (value instanceof Uint32Array) { chunks.push(shared_js_1.STR_UINT_32_ARRAY); stringifyTypedArray(chunks, value); } else if (value instanceof Float32Array) { chunks.push(shared_js_1.STR_FLOAT_32_ARRAY); stringifyTypedArray(chunks, value); } else if (value instanceof Float64Array) { chunks.push(shared_js_1.STR_FLOAT_64_ARRAY); stringifyTypedArray(chunks, value); } else if (value instanceof BigInt64Array) { chunks.push(shared_js_1.STR_BIG_INT_64_ARRAY); stringifyTypedArray(chunks, value); } else if (value instanceof BigUint64Array) { chunks.push(shared_js_1.STR_BIG_UINT_64_ARRAY); stringifyTypedArray(chunks, value); } else if (value instanceof DataView) { chunks.push(shared_js_1.STR_DATA_VIEW); stringifyTypedArray(chunks, value); } else if (value instanceof FormData) { encodeStack.push(new EncodeFrame(ENCODE_FRAME_TYPE_NEEDS_ENCODING, shared_js_1.STR_FORM_DATA, Array.from(value.entries()))); } else if (shared_js_1.SUPPORTS_FILE && value instanceof File) { encodeStack.push(new EncodeFrame(ENCODE_FRAME_TYPE_NEEDS_ENCODING, shared_js_1.STR_FILE, { promise: value.arrayBuffer(), size: value.size, type: value.type, name: value.name, lastModified: value.lastModified, })); } else if (value instanceof Blob) { encodeStack.push(new EncodeFrame(ENCODE_FRAME_TYPE_NEEDS_ENCODING, shared_js_1.STR_BLOB, { promise: value.arrayBuffer(), size: value.size, type: value.type, })); } else if (value instanceof Error) { encodeStack.push(new EncodeFrame(ENCODE_FRAME_TYPE_NEEDS_ENCODING, shared_js_1.STR_ERROR, prepareErrorForEncoding(value, redactErrors))); } else if (typeof value.toJSON === "function") { const newValue = value.toJSON(); encodeStack.push(new EncodeFrame(ENCODE_FRAME_TYPE_NEEDS_ENCODING, "", newValue)); if (typeof newValue === "object") { counters.refId--; } else { refs.delete(value); } } else { { let isIterable = typeof value[Symbol.iterator] === "function"; if (isIterable) { let isArray = Array.isArray(value); let toEncode = isArray ? value : Array.from(value); encodeStack.push(new EncodeFrame(ENCODE_FRAME_TYPE_ALREADY_ENCODED, "]", undefined)); for (let i = toEncode.length - 1; i >= 0; i--) { encodeStack.push(new EncodeFrame(ENCODE_FRAME_TYPE_NEEDS_ENCODING, i === 0 ? "" : ",", toEncode[i])); } chunks.push(isArray ? "[" : value instanceof Set ? `${shared_js_1.STR_SET}[` : value instanceof Map ? `${shared_js_1.STR_MAP}[` : "["); continue; } } { let pluginsLength = plugins.length; for (let i = 0; i < pluginsLength; i++) { let result = plugins[i](value); if (Array.isArray(result)) { encodeStack.push(new EncodeFrame(ENCODE_FRAME_TYPE_NEEDS_ENCODING, shared_js_1.STR_PLUGIN, result)); counters.refId--; refs.delete(value); continue encodeLoop; } } } encodeStack.push(new EncodeFrame(ENCODE_FRAME_TYPE_ALREADY_ENCODED, "}", undefined)); { let keys = Object.keys(value); let end = keys.length; let encodeFrames = new Array(end); end -= 1; for (let i = keys.length - 1; i >= 0; i--) { let key = keys[i]; let prefix = i > 0 ? "," : ""; encodeFrames[end - i] = new EncodeFrame(ENCODE_FRAME_TYPE_NEEDS_ENCODING, `${prefix}${JSON.stringify(key)}:`, value[key]); } encodeStack.push(...encodeFrames); } chunks.push("{"); } } else if (typeOfValue === "string") { chunks.push(JSON.stringify(value)); } else if (typeOfValue === "number") { if (nan(value)) { chunks.push(shared_js_1.STR_NaN); } else if (value === POSITIVE_INFINITY) { chunks.push(shared_js_1.STR_INFINITY); } else if (value === NEGATIVE_INFINITY) { chunks.push(shared_js_1.STR_NEGATIVE_INFINITY); } else if (Object.is(value, -0)) { chunks.push(shared_js_1.STR_NEGATIVE_ZERO); } else { chunks.push(value.toString()); } } else if (typeOfValue === "bigint") { chunks.push(shared_js_1.STR_BIGINT, value.toString()); } else if (typeOfValue === "symbol") { let symbolKey = Symbol.keyFor(value); if (typeof symbolKey === "string") { chunks.push(shared_js_1.STR_SYMBOL, JSON.stringify(symbolKey)); } else { chunks.push(shared_js_1.STR_UNDEFINED); } } else { let pluginsLength = plugins.length; for (let i = 0; i < pluginsLength; i++) { let result = plugins[i](value); if (Array.isArray(result)) { encodeStack.push(new EncodeFrame(ENCODE_FRAME_TYPE_NEEDS_ENCODING, shared_js_1.STR_PLUGIN, result)); continue encodeLoop; } } chunks.push(shared_js_1.STR_UNDEFINED); } } } exports.encodeSync = encodeSync; function prepareErrorForEncoding(error, redactErrors) { const shouldRedact = redactErrors === true || typeof redactErrors === "string" || typeof redactErrors === "undefined"; const redacted = typeof redactErrors === "string" ? redactErrors : shared_js_1.STR_REDACTED; return { name: shouldRedact ? "Error" : error.name, message: shouldRedact ? redacted : error.message, stack: shouldRedact ? undefined : error.stack, cause: error.cause, }; } function stringifyTypedArray(chunks, content) { const view = new Uint8Array(content.buffer, content.byteOffset, content.byteLength); chunks.push('"'); const chunkSize = 65535 - (65535 % 3); for (let i = 0; i < view.length; i += chunkSize) { const sub = view.subarray(i, i + chunkSize); let binary = ""; for (let j = 0; j < sub.length; j += 8192) { binary += String.fromCharCode.apply(null, sub.subarray(j, j + 8192)); } chunks.push(btoa(binary)); } chunks.push('"'); }