Page MenuHomeSealhub

test.ts
No OneTemporary

import Koa, { BaseContext } from "koa";
import http from "http";
import assert from "assert";
import { promisify } from "util";
import { FlatTemplatable, Templatable, tempstream } from ".";
import streamToString from "./tostring";
import { PassThrough } from "stream";
import { prettify } from "./prettify";
const st = (time: number, cb: () => void) => setTimeout(cb, time);
const sleep = promisify(st);
// template`hello ${world}, and ${name}`.pipe(process.stdout);
describe("tempstream", () => {
it("renders properly in the basic case", async () => {
const list_items = Promise.resolve(
["one", "two", "three"].map((e) => `<li>${e}</li>`)
);
const title = "Changed page title";
const slept = sleep(100).then(() => "slept");
const result = await streamToString(tempstream`<!DOCTYPE html>
<head>
<meta charset="utf-8" />
<title>${title}</title>
</head>
<body>
hello World, I ${slept}.
<ul>
${list_items}
</ul>
</body> `);
assert.strictEqual(
result,
`<!DOCTYPE html>
<head>
<meta charset="utf-8" />
<title>Changed page title</title>
</head>
<body>
hello World, I slept.
<ul>
<li>one</li><li>two</li><li>three</li>
</ul>
</body> `
);
});
it("handles stream within a stream", async () => {
const item_elements = Promise.resolve(["one", "two", "three"]);
const inside_stream = tempstream`Here's a list: ${item_elements}`;
const outside_stream = tempstream`<div>${inside_stream}</div>`;
const result = await streamToString(outside_stream);
assert.strictEqual(result, `<div>Here's a list: onetwothree</div>`);
});
it("handles an array of promises", async () => {
async function process(text: string) {
return "processed " + text + "\n";
}
const result = await streamToString(
tempstream`${["a", "b", "c"].map(process)}`
);
assert.strictEqual(
result,
`processed a
processed b
processed c
`
);
});
it("handles an array of promises of streams", async () => {
async function process(text: string) {
return tempstream`processed ${text}\n`;
}
const result = await streamToString(
tempstream`${["a", "b", "c"].map(process)}`
);
assert.strictEqual(
result,
`processed a
processed b
processed c
`
);
});
it("handles an array of promises of streams nested within a stream", async () => {
async function process(text: string) {
return tempstream`processed ${text}\n`;
}
const result = await streamToString(
tempstream`PREFIX: ${tempstream`${["a", "b", "c"].map(process)}`}`
);
assert.strictEqual(
result,
`PREFIX: processed a
processed b
processed c
`
);
});
it("handles an array of delayed promises of streams nested within a stream", async () => {
async function process(text: string) {
await sleep(100);
return tempstream`processed ${text}\n`;
}
const result = await streamToString(
tempstream`PREFIX: ${tempstream`${["a", "b", "c"].map(process)}`}`
);
assert.strictEqual(
result,
`PREFIX: processed a
processed b
processed c
`
);
});
it("sends the first byte as soon as possible when dealing with delayed promises of streams nested within a stream", async () => {
async function process(text: string) {
await sleep(100);
return tempstream`processed ${text}\n`;
}
const stream_start_ts = Date.now();
const stream = tempstream`PREFIX: ${tempstream`${["a", "b", "c"].map(
process
)}`}`;
let result = "";
let ttfb: number | null = null;
await new Promise((resolve) => {
stream.on("data", (newdata) => {
result += newdata.toString();
if (ttfb === null) {
ttfb = Date.now() - stream_start_ts;
}
});
stream.on("end", () => resolve(result));
});
assert(ttfb !== null && ttfb < 10);
assert.strictEqual(
result,
`PREFIX: processed a
processed b
processed c
`
);
});
it("properly renders `(Templatable | Promise<Templatable>)[]`", async () => {
const items = [
tempstream`hello `,
tempstream`world `,
Promise.resolve(tempstream`I am `) as Promise<Templatable>,
"testing",
];
const result = await streamToString(
tempstream`${items as Templatable}`
);
assert.strictEqual(result, `hello world I am testing`);
});
it("properly renders `Promise<FlatTemplatable[]>`", async () => {
const items = Promise.resolve([
tempstream`hello `,
tempstream`world `,
Promise.resolve(tempstream`I am `) as Promise<FlatTemplatable>,
"testing",
] as FlatTemplatable[]);
const result = await streamToString(tempstream`${items}`);
assert.strictEqual(result, `hello world I am testing`);
});
it("properly closes the stream to prevent premature ending when sending over HTTP", async () => {
const app = new Koa();
async function generateOptions() {
const ret = {} as Record<string, unknown>;
for (let i = 0; i <= 4000; i++) {
ret[i.toString()] = i;
}
return ret;
}
app.use(async (ctx: BaseContext) => {
ctx.body = tempstream/* HTML */ `<select>
${Promise.resolve(generateOptions()).then((options) =>
Object.entries(options).map(
([value]) => `<option value="${value}"></option>`
)
)}
</select>`;
});
const port = 3787;
const server = app.listen(port);
await sleep(100);
const response = (await new Promise((resolve) => {
http.get(`http://127.0.0.1:${port}`, {}, (res) => {
resolve(streamToString(res));
// res.on("end", resolve);
});
})) as string;
server.close();
assert(
response.endsWith("</select>"),
"Response should be transmitted in full, and not cut before it ends"
);
});
it("Allows to catch an error thrown by an awaited promise within the template", async () => {
const app = new Koa();
let caught = false;
app.use(async (ctx, next) => {
await next();
ctx.body.waitUntilFinished().then(() => {
if (ctx.body.has_errors) {
caught = true;
}
});
});
app.use(async (ctx: BaseContext) => {
ctx.body = tempstream/* HTML */ `<div>
${sleep(100).then(() => {
throw new Error("SOME ERROR!");
})}
</div>`;
});
const port = 3787;
const server = app.listen(port);
await sleep(100);
(await new Promise((resolve) => {
http.get(`http://127.0.0.1:${port}`, {}, (res) => {
resolve(streamToString(res));
// res.on("end", resolve);
});
})) as string;
server.close();
assert(caught);
});
it("Allows to await the finishing of the stream", async () => {
const app = new Koa();
let got_success_signal = false;
app.use(async (ctx, next) => {
await next();
ctx.body.waitUntilFinished().then(() => {
got_success_signal = true;
});
});
app.use(async (ctx: BaseContext) => {
ctx.body = tempstream/* HTML */ `<div>
${sleep(100).then(() => {
return "Everything's ok";
})}
</div>`;
});
const port = 3787;
const server = app.listen(port);
await sleep(100);
(await new Promise((resolve) => {
http.get(`http://127.0.0.1:${port}`, {}, (res) => {
resolve(streamToString(res));
// res.on("end", resolve);
});
})) as string;
server.close();
assert(got_success_signal);
});
it("doesn't leave uncaught promises within nested streams", async () => {
const app = new Koa();
let got_error_signal = false;
app.use(async (ctx, next) => {
await next();
ctx.body.waitUntilFinished().then(() => {
got_error_signal = true;
});
});
app.use(async (ctx: BaseContext) => {
const sleep1 = sleep(100).then(() => {
return "Everything's ok";
});
const sleep2 = sleep(200).then(() => {
throw new Error("SOME ERROR");
});
ctx.body = tempstream/* HTML */ `${tempstream`<div>${sleep1}, ${sleep2}</div>`}`;
});
const port = 3787;
const server = app.listen(port);
await sleep(100);
(await new Promise((resolve) => {
http.get(`http://127.0.0.1:${port}`, {}, (res) => {
resolve(streamToString(res));
// res.on("end", resolve);
});
})) as string;
server.close();
assert(got_error_signal);
});
it("doesn't leave uncaught rejections for promises within the stream", async () => {
let got_unhandled_rejection = false;
let got_error_signal = false;
const unhandled_listener = (reason: Error, _: unknown) => {
console.log("GOT UNHANDLED REJECTION!", reason.message);
got_unhandled_rejection = true;
};
process
.on("unhandledRejection", unhandled_listener)
.on("uncaughtException", unhandled_listener);
const app = new Koa();
app.use(async (ctx, next) => {
// this approach has the downside of blocking TTFB until the stream
// is finished, kind of defeating the purpose of using streams in
// the first place. BUT! A big TODO would be to not close the
// new_stream when the pipe ends
// (https://nodejs.org/api/stream.html#readablepipedestination-options)
// and not await the waitUntilFinishedPromise, but just append some
// error handling directives to the new stream on `waitUntilFinished().then()`.
await next();
if (ctx.body.waitUntilFinished) {
const original_stream = ctx.body;
const new_stream = new PassThrough();
ctx.body = new_stream;
original_stream.pipe(new_stream);
await original_stream.waitUntilFinished();
if (original_stream.has_errors) {
got_error_signal = true;
}
}
});
app.use(async (ctx: BaseContext) => {
const makeResponse = async () => {
function simpleMessage(t: FlatTemplatable): FlatTemplatable {
return tempstream`t: ${t}`;
}
async function getAd() {
await sleep(200);
throw new Error("syntetic ERROR!");
}
const ad = getAd();
return simpleMessage(
tempstream`Some text before: ${ad.then(
(_) => "success"
)}; and then some text after`
);
};
ctx.body = await makeResponse();
});
const port = 3787;
const server = app.listen(port);
await sleep(100);
(await new Promise((resolve) => {
http.get(`http://127.0.0.1:${port}`, {}, (res) => {
resolve(streamToString(res));
// res.on("end", resolve);
});
})) as string;
server.close();
assert(!got_unhandled_rejection);
assert(got_error_signal);
process
.removeListener("unhandledRejection", unhandled_listener)
.removeListener("uncaughtException", unhandled_listener);
});
it.skip("doesn't leave unaught promise rejections from promises rejected before the stream starts", async () => {
// the difference from the test above is that we're letting the promise
// reject before tempstream has a chance to set up any error listeners
// passing this test might actually be impossible. Maybe some eslint
// rule is able to catch that?
// Instead, tempstream will set up unhandledRejection listeners to
// explain in the console what went wrong.
// unskip this test scenario to test this behavior
let got_unhandled_rejection = false;
let got_error_signal = false;
const unhandled_listener = (reason: Error, _: unknown) => {
console.log("GOT UNHANDLED REJECTION!", reason.message);
got_unhandled_rejection = true;
};
process
.on("unhandledRejection", unhandled_listener)
.on("uncaughtException", unhandled_listener);
const app = new Koa();
app.use(async (ctx, next) => {
// this approach has the downside of blocking TTFB until the stream
// is finished, kind of defeating the purpose of using streams in
// the first place. BUT! A big TODO would be to not close the
// new_stream when the pipe ends
// (https://nodejs.org/api/stream.html#readablepipedestination-options)
// and not await the waitUntilFinishedPromise, but just append some
// error handling directives to the new stream on `waitUntilFinished().then()`.
await next();
if (ctx.body.waitUntilFinished) {
const original_stream = ctx.body;
const new_stream = new PassThrough();
ctx.body = new_stream;
original_stream.pipe(new_stream);
await original_stream.waitUntilFinished();
if (original_stream.has_errors) {
got_error_signal = true;
}
}
});
app.use(async (ctx: BaseContext) => {
const makeResponse = async () => {
function simpleMessage(t: FlatTemplatable): FlatTemplatable {
return tempstream`t: ${t}`;
}
async function getAd() {
await sleep(100);
throw new Error("syntetic ERROR!");
}
const ad = getAd();
await sleep(200); // <=============== here's the difference to the other, similar test
return simpleMessage(
tempstream`Some text before: ${ad.then(
() => "success"
)}; and then some text after`
);
};
ctx.body = await makeResponse();
});
const port = 3787;
const server = app.listen(port);
await sleep(100);
await new Promise((resolve) => {
http.get(`http://127.0.0.1:${port}`, {}, (res) => {
resolve(streamToString(res));
// res.on("end", resolve);
});
});
server.close();
assert(!got_unhandled_rejection);
assert(got_error_signal);
process
.removeListener("unhandledRejection", unhandled_listener)
.removeListener("uncaughtException", unhandled_listener);
});
it("handles null values properly", async () => {
const stream = tempstream`some ${null} value`;
const result = await streamToString(stream);
assert.strictEqual(result, "some value");
});
it("handles number values properly", async () => {
const stream = tempstream`some ${5} value`;
const result = await streamToString(stream);
assert.strictEqual(result, "some 5 value");
});
it("handles stringifiable values properly", async () => {
const object = { toString: () => "hehe" };
const stream = tempstream`some ${object as any} value`;
const result = await streamToString(stream);
assert.strictEqual(result, "some hehe value");
});
});

File Metadata

Mime Type
text/html
Expires
Fri, Nov 28, 14:49 (2 h, 44 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1075913
Default Alt Text
test.ts (13 KB)

Event Timeline