674 lines
20 KiB
JavaScript
674 lines
20 KiB
JavaScript
// src/utils.ts
|
|
var HOLE = -1;
|
|
var NAN = -2;
|
|
var NEGATIVE_INFINITY = -3;
|
|
var NEGATIVE_ZERO = -4;
|
|
var NULL = -5;
|
|
var POSITIVE_INFINITY = -6;
|
|
var UNDEFINED = -7;
|
|
var TYPE_BIGINT = "B";
|
|
var TYPE_DATE = "D";
|
|
var TYPE_ERROR = "E";
|
|
var TYPE_MAP = "M";
|
|
var TYPE_NULL_OBJECT = "N";
|
|
var TYPE_PROMISE = "P";
|
|
var TYPE_REGEXP = "R";
|
|
var TYPE_SET = "S";
|
|
var TYPE_SYMBOL = "Y";
|
|
var TYPE_URL = "U";
|
|
var TYPE_PREVIOUS_RESOLVED = "Z";
|
|
var Deferred = class {
|
|
promise;
|
|
resolve;
|
|
reject;
|
|
constructor() {
|
|
this.promise = new Promise((resolve, reject) => {
|
|
this.resolve = resolve;
|
|
this.reject = reject;
|
|
});
|
|
}
|
|
};
|
|
function createLineSplittingTransform() {
|
|
const decoder = new TextDecoder();
|
|
let leftover = "";
|
|
return new TransformStream({
|
|
transform(chunk, controller) {
|
|
const str = decoder.decode(chunk, { stream: true });
|
|
const parts = (leftover + str).split("\n");
|
|
leftover = parts.pop() || "";
|
|
for (const part of parts) {
|
|
controller.enqueue(part);
|
|
}
|
|
},
|
|
flush(controller) {
|
|
if (leftover) {
|
|
controller.enqueue(leftover);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// src/flatten.ts
|
|
function flatten(input) {
|
|
const { indices } = this;
|
|
const existing = indices.get(input);
|
|
if (existing)
|
|
return [existing];
|
|
if (input === void 0)
|
|
return UNDEFINED;
|
|
if (input === null)
|
|
return NULL;
|
|
if (Number.isNaN(input))
|
|
return NAN;
|
|
if (input === Number.POSITIVE_INFINITY)
|
|
return POSITIVE_INFINITY;
|
|
if (input === Number.NEGATIVE_INFINITY)
|
|
return NEGATIVE_INFINITY;
|
|
if (input === 0 && 1 / input < 0)
|
|
return NEGATIVE_ZERO;
|
|
const index = this.index++;
|
|
indices.set(input, index);
|
|
stringify.call(this, input, index);
|
|
return index;
|
|
}
|
|
function stringify(input, index) {
|
|
const { deferred, plugins, postPlugins } = this;
|
|
const str = this.stringified;
|
|
const stack = [[input, index]];
|
|
while (stack.length > 0) {
|
|
const [input2, index2] = stack.pop();
|
|
const partsForObj = (obj) => Object.keys(obj).map((k) => `"_${flatten.call(this, k)}":${flatten.call(this, obj[k])}`).join(",");
|
|
let error = null;
|
|
switch (typeof input2) {
|
|
case "boolean":
|
|
case "number":
|
|
case "string":
|
|
str[index2] = JSON.stringify(input2);
|
|
break;
|
|
case "bigint":
|
|
str[index2] = `["${TYPE_BIGINT}","${input2}"]`;
|
|
break;
|
|
case "symbol": {
|
|
const keyFor = Symbol.keyFor(input2);
|
|
if (!keyFor) {
|
|
error = new Error(
|
|
"Cannot encode symbol unless created with Symbol.for()"
|
|
);
|
|
} else {
|
|
str[index2] = `["${TYPE_SYMBOL}",${JSON.stringify(keyFor)}]`;
|
|
}
|
|
break;
|
|
}
|
|
case "object": {
|
|
if (!input2) {
|
|
str[index2] = `${NULL}`;
|
|
break;
|
|
}
|
|
const isArray = Array.isArray(input2);
|
|
let pluginHandled = false;
|
|
if (!isArray && plugins) {
|
|
for (const plugin of plugins) {
|
|
const pluginResult = plugin(input2);
|
|
if (Array.isArray(pluginResult)) {
|
|
pluginHandled = true;
|
|
const [pluginIdentifier, ...rest] = pluginResult;
|
|
str[index2] = `[${JSON.stringify(pluginIdentifier)}`;
|
|
if (rest.length > 0) {
|
|
str[index2] += `,${rest.map((v) => flatten.call(this, v)).join(",")}`;
|
|
}
|
|
str[index2] += "]";
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (!pluginHandled) {
|
|
let result = isArray ? "[" : "{";
|
|
if (isArray) {
|
|
for (let i = 0; i < input2.length; i++)
|
|
result += (i ? "," : "") + (i in input2 ? flatten.call(this, input2[i]) : HOLE);
|
|
str[index2] = `${result}]`;
|
|
} else if (input2 instanceof Date) {
|
|
str[index2] = `["${TYPE_DATE}",${input2.getTime()}]`;
|
|
} else if (input2 instanceof URL) {
|
|
str[index2] = `["${TYPE_URL}",${JSON.stringify(input2.href)}]`;
|
|
} else if (input2 instanceof RegExp) {
|
|
str[index2] = `["${TYPE_REGEXP}",${JSON.stringify(
|
|
input2.source
|
|
)},${JSON.stringify(input2.flags)}]`;
|
|
} else if (input2 instanceof Set) {
|
|
if (input2.size > 0) {
|
|
str[index2] = `["${TYPE_SET}",${[...input2].map((val) => flatten.call(this, val)).join(",")}]`;
|
|
} else {
|
|
str[index2] = `["${TYPE_SET}"]`;
|
|
}
|
|
} else if (input2 instanceof Map) {
|
|
if (input2.size > 0) {
|
|
str[index2] = `["${TYPE_MAP}",${[...input2].flatMap(([k, v]) => [
|
|
flatten.call(this, k),
|
|
flatten.call(this, v)
|
|
]).join(",")}]`;
|
|
} else {
|
|
str[index2] = `["${TYPE_MAP}"]`;
|
|
}
|
|
} else if (input2 instanceof Promise) {
|
|
str[index2] = `["${TYPE_PROMISE}",${index2}]`;
|
|
deferred[index2] = input2;
|
|
} else if (input2 instanceof Error) {
|
|
str[index2] = `["${TYPE_ERROR}",${JSON.stringify(input2.message)}`;
|
|
if (input2.name !== "Error") {
|
|
str[index2] += `,${JSON.stringify(input2.name)}`;
|
|
}
|
|
str[index2] += "]";
|
|
} else if (Object.getPrototypeOf(input2) === null) {
|
|
str[index2] = `["${TYPE_NULL_OBJECT}",{${partsForObj(input2)}}]`;
|
|
} else if (isPlainObject(input2)) {
|
|
str[index2] = `{${partsForObj(input2)}}`;
|
|
} else {
|
|
error = new Error("Cannot encode object with prototype");
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
default: {
|
|
const isArray = Array.isArray(input2);
|
|
let pluginHandled = false;
|
|
if (!isArray && plugins) {
|
|
for (const plugin of plugins) {
|
|
const pluginResult = plugin(input2);
|
|
if (Array.isArray(pluginResult)) {
|
|
pluginHandled = true;
|
|
const [pluginIdentifier, ...rest] = pluginResult;
|
|
str[index2] = `[${JSON.stringify(pluginIdentifier)}`;
|
|
if (rest.length > 0) {
|
|
str[index2] += `,${rest.map((v) => flatten.call(this, v)).join(",")}`;
|
|
}
|
|
str[index2] += "]";
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (!pluginHandled) {
|
|
error = new Error("Cannot encode function or unexpected type");
|
|
}
|
|
}
|
|
}
|
|
if (error) {
|
|
let pluginHandled = false;
|
|
if (postPlugins) {
|
|
for (const plugin of postPlugins) {
|
|
const pluginResult = plugin(input2);
|
|
if (Array.isArray(pluginResult)) {
|
|
pluginHandled = true;
|
|
const [pluginIdentifier, ...rest] = pluginResult;
|
|
str[index2] = `[${JSON.stringify(pluginIdentifier)}`;
|
|
if (rest.length > 0) {
|
|
str[index2] += `,${rest.map((v) => flatten.call(this, v)).join(",")}`;
|
|
}
|
|
str[index2] += "]";
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (!pluginHandled) {
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
var objectProtoNames = Object.getOwnPropertyNames(Object.prototype).sort().join("\0");
|
|
function isPlainObject(thing) {
|
|
const proto = Object.getPrototypeOf(thing);
|
|
return proto === Object.prototype || proto === null || Object.getOwnPropertyNames(proto).sort().join("\0") === objectProtoNames;
|
|
}
|
|
|
|
// src/unflatten.ts
|
|
var globalObj = typeof window !== "undefined" ? window : typeof globalThis !== "undefined" ? globalThis : void 0;
|
|
function unflatten(parsed) {
|
|
const { hydrated, values } = this;
|
|
if (typeof parsed === "number")
|
|
return hydrate.call(this, parsed);
|
|
if (!Array.isArray(parsed) || !parsed.length)
|
|
throw new SyntaxError();
|
|
const startIndex = values.length;
|
|
for (const value of parsed) {
|
|
values.push(value);
|
|
}
|
|
hydrated.length = values.length;
|
|
return hydrate.call(this, startIndex);
|
|
}
|
|
function hydrate(index) {
|
|
const { hydrated, values, deferred, plugins } = this;
|
|
let result;
|
|
const stack = [
|
|
[
|
|
index,
|
|
(v) => {
|
|
result = v;
|
|
}
|
|
]
|
|
];
|
|
let postRun = [];
|
|
while (stack.length > 0) {
|
|
const [index2, set] = stack.pop();
|
|
switch (index2) {
|
|
case UNDEFINED:
|
|
set(void 0);
|
|
continue;
|
|
case NULL:
|
|
set(null);
|
|
continue;
|
|
case NAN:
|
|
set(NaN);
|
|
continue;
|
|
case POSITIVE_INFINITY:
|
|
set(Infinity);
|
|
continue;
|
|
case NEGATIVE_INFINITY:
|
|
set(-Infinity);
|
|
continue;
|
|
case NEGATIVE_ZERO:
|
|
set(-0);
|
|
continue;
|
|
}
|
|
if (hydrated[index2]) {
|
|
set(hydrated[index2]);
|
|
continue;
|
|
}
|
|
const value = values[index2];
|
|
if (!value || typeof value !== "object") {
|
|
hydrated[index2] = value;
|
|
set(value);
|
|
continue;
|
|
}
|
|
if (Array.isArray(value)) {
|
|
if (typeof value[0] === "string") {
|
|
const [type, b, c] = value;
|
|
switch (type) {
|
|
case TYPE_DATE:
|
|
set(hydrated[index2] = new Date(b));
|
|
continue;
|
|
case TYPE_URL:
|
|
set(hydrated[index2] = new URL(b));
|
|
continue;
|
|
case TYPE_BIGINT:
|
|
set(hydrated[index2] = BigInt(b));
|
|
continue;
|
|
case TYPE_REGEXP:
|
|
set(hydrated[index2] = new RegExp(b, c));
|
|
continue;
|
|
case TYPE_SYMBOL:
|
|
set(hydrated[index2] = Symbol.for(b));
|
|
continue;
|
|
case TYPE_SET:
|
|
const newSet = /* @__PURE__ */ new Set();
|
|
hydrated[index2] = newSet;
|
|
for (let i = 1; i < value.length; i++)
|
|
stack.push([
|
|
value[i],
|
|
(v) => {
|
|
newSet.add(v);
|
|
}
|
|
]);
|
|
set(newSet);
|
|
continue;
|
|
case TYPE_MAP:
|
|
const map = /* @__PURE__ */ new Map();
|
|
hydrated[index2] = map;
|
|
for (let i = 1; i < value.length; i += 2) {
|
|
const r = [];
|
|
stack.push([
|
|
value[i + 1],
|
|
(v) => {
|
|
r[1] = v;
|
|
}
|
|
]);
|
|
stack.push([
|
|
value[i],
|
|
(k) => {
|
|
r[0] = k;
|
|
}
|
|
]);
|
|
postRun.push(() => {
|
|
map.set(r[0], r[1]);
|
|
});
|
|
}
|
|
set(map);
|
|
continue;
|
|
case TYPE_NULL_OBJECT:
|
|
const obj = /* @__PURE__ */ Object.create(null);
|
|
hydrated[index2] = obj;
|
|
for (const key of Object.keys(b).reverse()) {
|
|
const r = [];
|
|
stack.push([
|
|
b[key],
|
|
(v) => {
|
|
r[1] = v;
|
|
}
|
|
]);
|
|
stack.push([
|
|
Number(key.slice(1)),
|
|
(k) => {
|
|
r[0] = k;
|
|
}
|
|
]);
|
|
postRun.push(() => {
|
|
obj[r[0]] = r[1];
|
|
});
|
|
}
|
|
set(obj);
|
|
continue;
|
|
case TYPE_PROMISE:
|
|
if (hydrated[b]) {
|
|
set(hydrated[index2] = hydrated[b]);
|
|
} else {
|
|
const d = new Deferred();
|
|
deferred[b] = d;
|
|
set(hydrated[index2] = d.promise);
|
|
}
|
|
continue;
|
|
case TYPE_ERROR:
|
|
const [, message, errorType] = value;
|
|
let error = errorType && globalObj && globalObj[errorType] ? new globalObj[errorType](message) : new Error(message);
|
|
hydrated[index2] = error;
|
|
set(error);
|
|
continue;
|
|
case TYPE_PREVIOUS_RESOLVED:
|
|
set(hydrated[index2] = hydrated[b]);
|
|
continue;
|
|
default:
|
|
if (Array.isArray(plugins)) {
|
|
const r = [];
|
|
const vals = value.slice(1);
|
|
for (let i = 0; i < vals.length; i++) {
|
|
const v = vals[i];
|
|
stack.push([
|
|
v,
|
|
(v2) => {
|
|
r[i] = v2;
|
|
}
|
|
]);
|
|
}
|
|
postRun.push(() => {
|
|
for (const plugin of plugins) {
|
|
const result2 = plugin(value[0], ...r);
|
|
if (result2) {
|
|
set(hydrated[index2] = result2.value);
|
|
return;
|
|
}
|
|
}
|
|
throw new SyntaxError();
|
|
});
|
|
continue;
|
|
}
|
|
throw new SyntaxError();
|
|
}
|
|
} else {
|
|
const array = [];
|
|
hydrated[index2] = array;
|
|
for (let i = 0; i < value.length; i++) {
|
|
const n = value[i];
|
|
if (n !== HOLE) {
|
|
stack.push([
|
|
n,
|
|
(v) => {
|
|
array[i] = v;
|
|
}
|
|
]);
|
|
}
|
|
}
|
|
set(array);
|
|
continue;
|
|
}
|
|
} else {
|
|
const object = {};
|
|
hydrated[index2] = object;
|
|
for (const key of Object.keys(value).reverse()) {
|
|
const r = [];
|
|
stack.push([
|
|
value[key],
|
|
(v) => {
|
|
r[1] = v;
|
|
}
|
|
]);
|
|
stack.push([
|
|
Number(key.slice(1)),
|
|
(k) => {
|
|
r[0] = k;
|
|
}
|
|
]);
|
|
postRun.push(() => {
|
|
object[r[0]] = r[1];
|
|
});
|
|
}
|
|
set(object);
|
|
continue;
|
|
}
|
|
}
|
|
while (postRun.length > 0) {
|
|
postRun.pop()();
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// src/turbo-stream.ts
|
|
async function decode(readable, options) {
|
|
const { plugins } = options ?? {};
|
|
const done = new Deferred();
|
|
const reader = readable.pipeThrough(createLineSplittingTransform()).getReader();
|
|
const decoder = {
|
|
values: [],
|
|
hydrated: [],
|
|
deferred: {},
|
|
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
|
|
};
|
|
}
|
|
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.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 TYPE_PROMISE: {
|
|
const colonIndex = line.indexOf(":");
|
|
const deferredId = Number(line.slice(1, 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.call(this, jsonLine);
|
|
deferred.resolve(value);
|
|
break;
|
|
}
|
|
case TYPE_ERROR: {
|
|
const colonIndex = line.indexOf(":");
|
|
const deferredId = Number(line.slice(1, 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.call(this, jsonLine);
|
|
deferred.reject(value);
|
|
break;
|
|
}
|
|
default:
|
|
throw new SyntaxError();
|
|
}
|
|
read = await reader.read();
|
|
}
|
|
}
|
|
function encode(input, options) {
|
|
const { plugins, postPlugins, signal } = options ?? {};
|
|
const encoder = {
|
|
deferred: {},
|
|
index: 0,
|
|
indices: /* @__PURE__ */ new Map(),
|
|
stringified: [],
|
|
plugins,
|
|
postPlugins,
|
|
signal
|
|
};
|
|
const textEncoder = new TextEncoder();
|
|
let lastSentIndex = 0;
|
|
const readable = new ReadableStream({
|
|
async start(controller) {
|
|
const id = flatten.call(encoder, input);
|
|
if (Array.isArray(id)) {
|
|
throw new Error("This should never happen");
|
|
}
|
|
if (id < 0) {
|
|
controller.enqueue(textEncoder.encode(`${id}
|
|
`));
|
|
} else {
|
|
controller.enqueue(
|
|
textEncoder.encode(`[${encoder.stringified.join(",")}]
|
|
`)
|
|
);
|
|
lastSentIndex = encoder.stringified.length - 1;
|
|
}
|
|
const seenPromises = /* @__PURE__ */ new WeakSet();
|
|
while (Object.keys(encoder.deferred).length > 0) {
|
|
for (const [deferredId, deferred] of Object.entries(encoder.deferred)) {
|
|
if (seenPromises.has(deferred))
|
|
continue;
|
|
seenPromises.add(
|
|
encoder.deferred[Number(deferredId)] = raceSignal(
|
|
deferred,
|
|
encoder.signal
|
|
).then(
|
|
(resolved) => {
|
|
const id2 = flatten.call(encoder, resolved);
|
|
if (Array.isArray(id2)) {
|
|
controller.enqueue(
|
|
textEncoder.encode(
|
|
`${TYPE_PROMISE}${deferredId}:[["${TYPE_PREVIOUS_RESOLVED}",${id2[0]}]]
|
|
`
|
|
)
|
|
);
|
|
encoder.index++;
|
|
lastSentIndex++;
|
|
} else if (id2 < 0) {
|
|
controller.enqueue(
|
|
textEncoder.encode(`${TYPE_PROMISE}${deferredId}:${id2}
|
|
`)
|
|
);
|
|
} else {
|
|
const values = encoder.stringified.slice(lastSentIndex + 1).join(",");
|
|
controller.enqueue(
|
|
textEncoder.encode(
|
|
`${TYPE_PROMISE}${deferredId}:[${values}]
|
|
`
|
|
)
|
|
);
|
|
lastSentIndex = encoder.stringified.length - 1;
|
|
}
|
|
},
|
|
(reason) => {
|
|
if (!reason || typeof reason !== "object" || !(reason instanceof Error)) {
|
|
reason = new Error("An unknown error occurred");
|
|
}
|
|
const id2 = flatten.call(encoder, reason);
|
|
if (Array.isArray(id2)) {
|
|
controller.enqueue(
|
|
textEncoder.encode(
|
|
`${TYPE_ERROR}${deferredId}:[["${TYPE_PREVIOUS_RESOLVED}",${id2[0]}]]
|
|
`
|
|
)
|
|
);
|
|
encoder.index++;
|
|
lastSentIndex++;
|
|
} else if (id2 < 0) {
|
|
controller.enqueue(
|
|
textEncoder.encode(`${TYPE_ERROR}${deferredId}:${id2}
|
|
`)
|
|
);
|
|
} else {
|
|
const values = encoder.stringified.slice(lastSentIndex + 1).join(",");
|
|
controller.enqueue(
|
|
textEncoder.encode(
|
|
`${TYPE_ERROR}${deferredId}:[${values}]
|
|
`
|
|
)
|
|
);
|
|
lastSentIndex = encoder.stringified.length - 1;
|
|
}
|
|
}
|
|
).finally(() => {
|
|
delete encoder.deferred[Number(deferredId)];
|
|
})
|
|
);
|
|
}
|
|
await Promise.race(Object.values(encoder.deferred));
|
|
}
|
|
await Promise.all(Object.values(encoder.deferred));
|
|
controller.close();
|
|
}
|
|
});
|
|
return readable;
|
|
}
|
|
function raceSignal(promise, signal) {
|
|
if (!signal)
|
|
return promise;
|
|
if (signal.aborted)
|
|
return Promise.reject(signal.reason || new Error("Signal was aborted."));
|
|
const abort = new Promise((resolve, reject) => {
|
|
signal.addEventListener("abort", (event) => {
|
|
reject(signal.reason || new Error("Signal was aborted."));
|
|
});
|
|
promise.then(resolve).catch(reject);
|
|
});
|
|
abort.catch(() => {
|
|
});
|
|
return Promise.race([abort, promise]);
|
|
}
|
|
export {
|
|
decode,
|
|
encode
|
|
};
|