@phryneas/turbo-stream
Version:
A streaming data transport format that aims to support built-in features such as Promises, Dates, RegExps, Maps, Sets and more.
322 lines (321 loc) • 14.6 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.encode = exports.decode = void 0;
const flatten_js_1 = require("./flatten.js");
const unflatten_js_1 = require("./unflatten.js");
const utils_js_1 = require("./utils.js");
async function decode(readable, options) {
const { plugins } = options ?? {};
const done = new utils_js_1.Deferred();
const reader = readable
.pipeThrough((0, utils_js_1.createLineSplittingTransform)())
.getReader();
const decoder = {
values: [],
hydrated: [],
deferred: {},
streams: {},
plugins,
};
const decoded = await decodeInitial.call(decoder, reader);
let donePromise = done.promise;
if (decoded.done) {
done.resolve();
}
else {
donePromise = decodeDeferred
.call(decoder, reader)
.then(done.resolve)
.catch((reason) => {
for (const deferred of Object.values(decoder.deferred)) {
deferred.reject(reason);
}
done.reject(reason);
});
}
return {
done: donePromise.then(() => reader.closed),
value: decoded.value,
};
}
exports.decode = decode;
async function decodeInitial(reader) {
const read = await reader.read();
if (!read.value) {
throw new SyntaxError();
}
let line;
try {
line = JSON.parse(read.value);
}
catch (reason) {
throw new SyntaxError();
}
return {
done: read.done,
value: unflatten_js_1.unflatten.call(this, line),
};
}
async function decodeDeferred(reader) {
let read = await reader.read();
while (!read.done) {
if (!read.value)
continue;
const line = read.value;
switch (line[0]) {
case utils_js_1.TYPE_PROMISE: {
const isError = line[1] === utils_js_1.TYPE_ERROR;
const startIndex = isError ? 2 : 1;
const colonIndex = line.indexOf(":");
const deferredId = Number(line.slice(startIndex, colonIndex));
const deferred = this.deferred[deferredId];
if (!deferred) {
throw new Error(`Deferred ID ${deferredId} not found in stream`);
}
const lineData = line.slice(colonIndex + 1);
let jsonLine;
try {
jsonLine = JSON.parse(lineData);
}
catch (reason) {
throw new SyntaxError();
}
const value = unflatten_js_1.unflatten.call(this, jsonLine);
if (isError) {
deferred.reject(value);
}
else {
deferred.resolve(value);
}
break;
}
case utils_js_1.TYPE_STREAM: {
const isError = line[1] === utils_js_1.TYPE_ERROR;
const isDone = line[1] === utils_js_1.TYPE_DONE;
const startIndex = isError || isDone ? 2 : 1;
const colonIndex = line.indexOf(":");
const streamId = Number(line.slice(startIndex, colonIndex));
const stream = this.streams[streamId];
if (!stream) {
throw new Error(`ReadableStream ID ${streamId} not found in stream`);
}
if (isDone) {
try {
stream.close();
}
catch {
/** stream could already be closed externally, we want to prevent any potential error */
}
break;
}
const lineData = line.slice(colonIndex + 1);
let jsonLine;
try {
jsonLine = JSON.parse(lineData);
}
catch (reason) {
throw new SyntaxError();
}
const value = unflatten_js_1.unflatten.call(this, jsonLine);
if (isError) {
stream.error(value);
}
else {
try {
stream.enqueue(value);
}
catch {
/** stream could already be closed externally, we want to prevent any potential error */
}
}
break;
}
default:
throw new SyntaxError();
}
read = await reader.read();
}
}
function encode(input, options) {
const { plugins, postPlugins, signal } = options ?? {};
const encoder = {
deferred: {},
streams: {},
index: 0,
indices: new Map(),
stringified: [],
plugins,
postPlugins,
signal,
};
const textEncoder = new TextEncoder();
let lastSentIndex = 0;
const readable = new ReadableStream({
async start(controller) {
const id = flatten_js_1.flatten.call(encoder, input);
if (Array.isArray(id)) {
throw new Error("This should never happen");
}
if (id < 0) {
controller.enqueue(textEncoder.encode(`${id}\n`));
}
else {
controller.enqueue(textEncoder.encode(`[${encoder.stringified.join(",")}]\n`));
lastSentIndex = encoder.stringified.length - 1;
}
const seenPromises = new WeakSet();
if (Object.keys(encoder.deferred).length ||
Object.keys(encoder.streams).length) {
let raceDone;
const racePromise = new Promise((resolve, reject) => {
raceDone = resolve;
if (signal) {
const rejectPromise = () => reject(signal.reason || new Error("Signal was aborted."));
if (signal.aborted) {
rejectPromise();
}
else {
signal.addEventListener("abort", (event) => {
rejectPromise();
});
}
}
});
while (Object.keys(encoder.deferred).length > 0 ||
Object.keys(encoder.streams).length > 0) {
for (const [deferredId, deferred] of Object.entries(encoder.deferred)) {
if (seenPromises.has(deferred))
continue;
seenPromises.add(
// biome-ignore lint/suspicious/noAssignInExpressions: <explanation>
(encoder.deferred[Number(deferredId)] = Promise.race([
racePromise,
deferred,
])
.then((resolved) => {
const id = flatten_js_1.flatten.call(encoder, resolved);
if (Array.isArray(id)) {
controller.enqueue(textEncoder.encode(`${utils_js_1.TYPE_PROMISE}${deferredId}:[["${utils_js_1.TYPE_PREVIOUS_RESOLVED}",${id[0]}]]\n`));
encoder.index++;
lastSentIndex++;
}
else if (id < 0) {
controller.enqueue(textEncoder.encode(`${utils_js_1.TYPE_PROMISE}${deferredId}:${id}\n`));
}
else {
const values = encoder.stringified
.slice(lastSentIndex + 1)
.join(",");
controller.enqueue(textEncoder.encode(`${utils_js_1.TYPE_PROMISE}${deferredId}:[${values}]\n`));
lastSentIndex = encoder.stringified.length - 1;
}
}, (reason) => {
if (!reason ||
typeof reason !== "object" ||
!(reason instanceof Error)) {
reason = new Error("An unknown error occurred");
}
const id = flatten_js_1.flatten.call(encoder, reason);
if (Array.isArray(id)) {
controller.enqueue(textEncoder.encode(`${utils_js_1.TYPE_PROMISE}${utils_js_1.TYPE_ERROR}${deferredId}:[["${utils_js_1.TYPE_PREVIOUS_RESOLVED}",${id[0]}]]\n`));
encoder.index++;
lastSentIndex++;
}
else if (id < 0) {
controller.enqueue(textEncoder.encode(`${utils_js_1.TYPE_PROMISE}${utils_js_1.TYPE_ERROR}${deferredId}:${id}\n`));
}
else {
const values = encoder.stringified
.slice(lastSentIndex + 1)
.join(",");
controller.enqueue(textEncoder.encode(`${utils_js_1.TYPE_PROMISE}${utils_js_1.TYPE_ERROR}${deferredId}:[${values}]\n`));
lastSentIndex = encoder.stringified.length - 1;
}
})
.finally(() => {
delete encoder.deferred[Number(deferredId)];
})));
}
for (const [streamId, stream] of Object.entries(encoder.streams)) {
if (stream.finish)
continue;
const { resolve, promise } = new utils_js_1.Deferred();
stream.finish = promise;
promise.finally(() => {
delete encoder.streams[Number(streamId)];
});
const reader = stream.getReader();
consumeChunk();
function consumeChunk() {
reader
.read()
.then(({ done, value }) => {
if (signal?.aborted) {
throw signal.reason || new Error("Signal was aborted.");
}
if (done) {
controller.enqueue(textEncoder.encode(`${utils_js_1.TYPE_STREAM}${utils_js_1.TYPE_DONE}${streamId}:[]\n`));
resolve();
}
else {
const id = flatten_js_1.flatten.call(encoder, value);
if (Array.isArray(id)) {
controller.enqueue(textEncoder.encode(`${utils_js_1.TYPE_STREAM}${streamId}:[["${utils_js_1.TYPE_PREVIOUS_RESOLVED}",${id[0]}]]\n`));
encoder.index++;
lastSentIndex++;
}
else if (id < 0) {
controller.enqueue(textEncoder.encode(`${utils_js_1.TYPE_STREAM}${streamId}:${id}\n`));
}
else {
const values = encoder.stringified
.slice(lastSentIndex + 1)
.join(",");
controller.enqueue(textEncoder.encode(`${utils_js_1.TYPE_STREAM}${streamId}:[${values}]\n`));
lastSentIndex = encoder.stringified.length - 1;
}
return consumeChunk();
}
})
.catch((reason) => {
if (!reason ||
typeof reason !== "object" ||
!(reason instanceof Error)) {
reason = new Error("An unknown error occurred");
}
const id = flatten_js_1.flatten.call(encoder, reason);
if (Array.isArray(id)) {
controller.enqueue(textEncoder.encode(`${utils_js_1.TYPE_STREAM}${utils_js_1.TYPE_ERROR}${streamId}:[["${utils_js_1.TYPE_PREVIOUS_RESOLVED}",${id[0]}]]\n`));
encoder.index++;
lastSentIndex++;
}
else if (id < 0) {
controller.enqueue(textEncoder.encode(`${utils_js_1.TYPE_STREAM}${utils_js_1.TYPE_ERROR}${streamId}:[${id}]\n`));
}
else {
const values = encoder.stringified
.slice(lastSentIndex + 1)
.join(",");
controller.enqueue(textEncoder.encode(`${utils_js_1.TYPE_STREAM}${utils_js_1.TYPE_ERROR}${streamId}:[${values}]\n`));
lastSentIndex = encoder.stringified.length - 1;
}
resolve();
});
}
}
await Promise.race([
...Object.values(encoder.deferred),
...Object.values(encoder.streams)
.map((stream) => stream.finish)
.filter((x) => !!x),
]);
}
raceDone();
}
await Promise.all(Object.values(encoder.deferred));
controller.close();
},
});
return readable;
}
exports.encode = encode;