better error handling and full jsx support
This commit is contained in:
parent
11be5e979c
commit
5197e3316d
7 changed files with 258 additions and 85 deletions
13
src/app.tsx
13
src/app.tsx
|
|
@ -3,11 +3,24 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { close, open } from "interest/jsx-runtime";
|
||||||
|
|
||||||
|
async function* Fruits() {
|
||||||
|
const fruits = ["TSX", "Apple", "Banana", "Cherry"];
|
||||||
|
yield open("ol");
|
||||||
|
for (const fruit of fruits) {
|
||||||
|
await new Promise((r) => setTimeout(r, 500));
|
||||||
|
yield <li>{fruit}</li>;
|
||||||
|
}
|
||||||
|
yield close("ol");
|
||||||
|
}
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h1>JSX Page</h1>
|
<h1>JSX Page</h1>
|
||||||
<p class="oh hey">meowing chunk by chunk</p>
|
<p class="oh hey">meowing chunk by chunk</p>
|
||||||
|
<Fruits />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
33
src/global.d.ts
vendored
33
src/global.d.ts
vendored
|
|
@ -5,16 +5,37 @@
|
||||||
|
|
||||||
/// <reference types="interest/jsx-runtime" />
|
/// <reference types="interest/jsx-runtime" />
|
||||||
|
|
||||||
import type { ChunkedStream } from "./stream.ts";
|
import type { JsxElement } from "interest/jsx-runtime";
|
||||||
|
|
||||||
|
type HTMLAttributeMap<T = HTMLElement> = Partial<
|
||||||
|
Omit<T, keyof Element | "children" | "style"> & {
|
||||||
|
style?: string;
|
||||||
|
class?: string;
|
||||||
|
children?: any;
|
||||||
|
[key: `data-${string}`]: string | number | boolean | null | undefined;
|
||||||
|
[key: `aria-${string}`]: string | number | boolean | null | undefined;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
namespace JSX {
|
namespace JSX {
|
||||||
type Element = (chunks: ChunkedStream<string>) => Promise<void>;
|
type Element = JsxElement;
|
||||||
interface IntrinsicElements {
|
|
||||||
[key: string]: ElementProps;
|
export interface ElementChildrenAttribute {
|
||||||
|
// deno-lint-ignore ban-types
|
||||||
|
children: {};
|
||||||
}
|
}
|
||||||
interface ElementProps {
|
|
||||||
[key: string]: any;
|
export type IntrinsicElements =
|
||||||
|
& {
|
||||||
|
[K in keyof HTMLElementTagNameMap]: HTMLAttributeMap<
|
||||||
|
HTMLElementTagNameMap[K]
|
||||||
|
>;
|
||||||
}
|
}
|
||||||
|
& {
|
||||||
|
[K in keyof SVGElementTagNameMap]: HTMLAttributeMap<
|
||||||
|
SVGElementTagNameMap[K]
|
||||||
|
>;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
37
src/html.ts
37
src/html.ts
|
|
@ -6,9 +6,12 @@
|
||||||
import { type Chunk } from "./http.ts";
|
import { type Chunk } from "./http.ts";
|
||||||
import { ChunkedStream } from "./stream.ts";
|
import { ChunkedStream } from "./stream.ts";
|
||||||
|
|
||||||
type Attrs = Record<string, string | number | boolean>;
|
export type Attrs = Record<
|
||||||
|
string,
|
||||||
|
string | number | boolean | null | undefined
|
||||||
|
>;
|
||||||
|
|
||||||
const VOID_TAGS = new Set([
|
export const VOID_TAGS = new Set([
|
||||||
"area",
|
"area",
|
||||||
"base",
|
"base",
|
||||||
"br",
|
"br",
|
||||||
|
|
@ -66,6 +69,7 @@ type TagRes = void | Promise<void>;
|
||||||
|
|
||||||
type TagFn = {
|
type TagFn = {
|
||||||
(attrs: Attrs, ...children: Chunk[]): TagRes;
|
(attrs: Attrs, ...children: Chunk[]): TagRes;
|
||||||
|
(attrs: Attrs, fn: () => any): TagRes;
|
||||||
(...children: Chunk[]): TagRes;
|
(...children: Chunk[]): TagRes;
|
||||||
(template: TemplateStringsArray, ...values: Chunk[]): TagRes;
|
(template: TemplateStringsArray, ...values: Chunk[]): TagRes;
|
||||||
(fn: () => any): TagRes;
|
(fn: () => any): TagRes;
|
||||||
|
|
@ -73,30 +77,33 @@ type TagFn = {
|
||||||
|
|
||||||
export type HtmlProxy = { [K in keyof HTMLElementTagNameMap]: TagFn } & {
|
export type HtmlProxy = { [K in keyof HTMLElementTagNameMap]: TagFn } & {
|
||||||
[key: string]: TagFn;
|
[key: string]: TagFn;
|
||||||
} & { render(child: any): Promise<void> };
|
};
|
||||||
|
|
||||||
const isTemplateLiteral = (arg: any): arg is TemplateStringsArray =>
|
const isTemplateLiteral = (arg: any): arg is TemplateStringsArray =>
|
||||||
Array.isArray(arg) && "raw" in arg;
|
Array.isArray(arg) && "raw" in arg;
|
||||||
|
|
||||||
const isAttributes = (arg: any): arg is Record<string, any> =>
|
const isAttributes = (arg: any): arg is Record<string, any> =>
|
||||||
arg && typeof arg === "object" && !isTemplateLiteral(arg);
|
arg && typeof arg === "object" && !isTemplateLiteral(arg) &&
|
||||||
|
!Array.isArray(arg) && !(arg instanceof Promise);
|
||||||
|
|
||||||
async function render(child: any): Promise<string> {
|
async function render(child: unknown): Promise<string> {
|
||||||
if (child == null) return "";
|
if (child == null) return "";
|
||||||
if (typeof child === "string" || typeof child === "number") {
|
|
||||||
return String(child);
|
if (typeof child === "string") return escape(child);
|
||||||
}
|
if (typeof child === "number") return String(child);
|
||||||
|
if (typeof child === "boolean") return String(Number(child));
|
||||||
|
|
||||||
if (child instanceof Promise) return render(await child);
|
if (child instanceof Promise) return render(await child);
|
||||||
|
|
||||||
if (Array.isArray(child)) {
|
if (Array.isArray(child)) {
|
||||||
return (await Promise.all(child.map(render))).join("");
|
return (await Promise.all(child.map(render))).join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof child === "function") return render(await child());
|
if (typeof child === "function") return render(await child());
|
||||||
return String(child);
|
return escape(String(child));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function html(
|
export function html(chunks: ChunkedStream<string>): HtmlProxy {
|
||||||
chunks: ChunkedStream<string>,
|
|
||||||
): HtmlProxy {
|
|
||||||
const cache = new Map<string, TagFn>();
|
const cache = new Map<string, TagFn>();
|
||||||
const write = (buf: string) => !chunks.closed && chunks.write(buf);
|
const write = (buf: string) => !chunks.closed && chunks.write(buf);
|
||||||
|
|
||||||
|
|
@ -105,11 +112,11 @@ export function html(
|
||||||
let fn = cache.get(tag);
|
let fn = cache.get(tag);
|
||||||
if (fn) return fn;
|
if (fn) return fn;
|
||||||
|
|
||||||
fn = async (...args: any[]) => {
|
fn = async (...args: unknown[]) => {
|
||||||
const attrs = isAttributes(args[0]) ? args.shift() : undefined;
|
const attrs = isAttributes(args[0]) ? args.shift() : undefined;
|
||||||
|
|
||||||
const isVoid = VOID_TAGS.has(tag.toLowerCase());
|
const isVoid = VOID_TAGS.has(tag.toLowerCase());
|
||||||
const attributes = serialize(attrs);
|
const attributes = serialize(attrs as Attrs);
|
||||||
|
|
||||||
write(`<${tag}${attributes}${isVoid ? "/" : ""}>`);
|
write(`<${tag}${attributes}${isVoid ? "/" : ""}>`);
|
||||||
if (isVoid) return;
|
if (isVoid) return;
|
||||||
|
|
@ -126,7 +133,5 @@ export function html(
|
||||||
};
|
};
|
||||||
|
|
||||||
const proxy = new Proxy({}, handler) as HtmlProxy;
|
const proxy = new Proxy({}, handler) as HtmlProxy;
|
||||||
proxy.render = async (stuff: any) => void write(await render(stuff));
|
|
||||||
|
|
||||||
return proxy;
|
return proxy;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
31
src/http.ts
31
src/http.ts
|
|
@ -15,7 +15,9 @@ export type Chunk =
|
||||||
| string
|
| string
|
||||||
| AsyncIterable<string>
|
| AsyncIterable<string>
|
||||||
| Promise<string>
|
| Promise<string>
|
||||||
| Iterable<string>;
|
| Iterable<string>
|
||||||
|
| null
|
||||||
|
| undefined;
|
||||||
|
|
||||||
async function* normalize(
|
async function* normalize(
|
||||||
value: Chunk | undefined | null,
|
value: Chunk | undefined | null,
|
||||||
|
|
@ -23,7 +25,6 @@ async function* normalize(
|
||||||
if (value == null) return;
|
if (value == null) return;
|
||||||
|
|
||||||
if (typeof value === "string") {
|
if (typeof value === "string") {
|
||||||
3;
|
|
||||||
yield value;
|
yield value;
|
||||||
} else if (value instanceof Promise) {
|
} else if (value instanceof Promise) {
|
||||||
const resolved = await value;
|
const resolved = await value;
|
||||||
|
|
@ -31,7 +32,6 @@ async function* normalize(
|
||||||
} else if (Symbol.asyncIterator in value || Symbol.iterator in value) {
|
} else if (Symbol.asyncIterator in value || Symbol.iterator in value) {
|
||||||
for await (const chunk of value as AsyncIterable<string>) {
|
for await (const chunk of value as AsyncIterable<string>) {
|
||||||
if (chunk != null) yield String(chunk);
|
if (chunk != null) yield String(chunk);
|
||||||
1;
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
yield String(value);
|
yield String(value);
|
||||||
|
|
@ -52,6 +52,7 @@ export const makeChunkWriter =
|
||||||
|
|
||||||
for (let i = 0; i < strings.length; i++) {
|
for (let i = 0; i < strings.length; i++) {
|
||||||
strings[i] && emit(strings[i]);
|
strings[i] && emit(strings[i]);
|
||||||
|
|
||||||
for await (const chunk of normalize(values[i])) {
|
for await (const chunk of normalize(values[i])) {
|
||||||
emit(chunk);
|
emit(chunk);
|
||||||
}
|
}
|
||||||
|
|
@ -64,10 +65,14 @@ export function chunkedHtml() {
|
||||||
const stream = new ReadableStream<Uint8Array>({
|
const stream = new ReadableStream<Uint8Array>({
|
||||||
async start(controller) {
|
async start(controller) {
|
||||||
const encoder = new TextEncoder();
|
const encoder = new TextEncoder();
|
||||||
|
try {
|
||||||
for await (const chunk of chunks) {
|
for await (const chunk of chunks) {
|
||||||
controller.enqueue(encoder.encode(chunk));
|
controller.enqueue(encoder.encode(chunk));
|
||||||
}
|
}
|
||||||
controller.close();
|
controller.close();
|
||||||
|
} catch (error) {
|
||||||
|
controller.error(error);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
cancel: chunks.close,
|
cancel: chunks.close,
|
||||||
});
|
});
|
||||||
|
|
@ -82,7 +87,18 @@ const HEAD_END = "</head><body";
|
||||||
const BODY_END = ">";
|
const BODY_END = ">";
|
||||||
const HTML_END = "</body></html>";
|
const HTML_END = "</body></html>";
|
||||||
|
|
||||||
export async function createHtmlStream(options: StreamOptions = {}) {
|
export interface HtmlStream {
|
||||||
|
write: ChunkedWriter;
|
||||||
|
blob: ReadableStream<Uint8Array>;
|
||||||
|
chunks: ChunkedStream<string>;
|
||||||
|
close(): void;
|
||||||
|
error(err: Error): void;
|
||||||
|
readonly response: Response;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createHtmlStream(
|
||||||
|
options: StreamOptions = {},
|
||||||
|
): Promise<HtmlStream> {
|
||||||
const { chunks, stream } = chunkedHtml();
|
const { chunks, stream } = chunkedHtml();
|
||||||
const writer = makeChunkWriter(chunks);
|
const writer = makeChunkWriter(chunks);
|
||||||
|
|
||||||
|
|
@ -103,15 +119,14 @@ export async function createHtmlStream(options: StreamOptions = {}) {
|
||||||
chunks.close();
|
chunks.close();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
get response(): Response {
|
error: chunks.error,
|
||||||
return new Response(stream, {
|
response: new Response(stream, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "text/html; charset=utf-8",
|
"Content-Type": "text/html; charset=utf-8",
|
||||||
"Transfer-Encoding": "chunked",
|
"Transfer-Encoding": "chunked",
|
||||||
"Cache-Control": "no-cache",
|
"Cache-Control": "no-cache",
|
||||||
"Connection": "keep-alive",
|
"Connection": "keep-alive",
|
||||||
},
|
},
|
||||||
});
|
}),
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
131
src/jsx.ts
131
src/jsx.ts
|
|
@ -3,27 +3,120 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { html, type HtmlProxy } from "./html.ts";
|
import { Attrs, escape, html, VOID_TAGS } from "./html.ts";
|
||||||
import { ChunkedStream } from "./stream.ts";
|
import { ChunkedStream } from "./stream.ts";
|
||||||
|
|
||||||
let context;
|
|
||||||
|
|
||||||
export function jsx(
|
|
||||||
tag: string | typeof Fragment,
|
|
||||||
{ children }: Record<string, any> = {},
|
|
||||||
) {
|
|
||||||
return async (chunks: ChunkedStream<string>) => {
|
|
||||||
if (tag === Fragment) {
|
|
||||||
context = html(chunks);
|
|
||||||
for (const child of children) {
|
|
||||||
await context.render?.(child);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await (context ||= html(chunks))[tag](...children);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Fragment = Symbol("Fragment");
|
export const Fragment = Symbol("Fragment");
|
||||||
|
|
||||||
|
type Props = Attrs & { children?: any };
|
||||||
|
type Component = (props: Props) => JsxElement | AsyncGenerator<JsxElement>;
|
||||||
|
|
||||||
|
export type JsxElement =
|
||||||
|
| ((chunks: ChunkedStream<string>) => Promise<void>)
|
||||||
|
| AsyncGenerator<JsxElement, void, unknown>;
|
||||||
|
|
||||||
|
async function render(
|
||||||
|
child: any,
|
||||||
|
chunks: ChunkedStream<string>,
|
||||||
|
context: ReturnType<typeof html>,
|
||||||
|
): Promise<void> {
|
||||||
|
if (child == null || child === false || child === true) return;
|
||||||
|
|
||||||
|
if (typeof child === "string") return chunks.write(escape(child));
|
||||||
|
if (typeof child === "number") return chunks.write(String(child));
|
||||||
|
|
||||||
|
if (typeof child === "function") return await child(chunks);
|
||||||
|
if (child instanceof Promise) {
|
||||||
|
return await render(await child, chunks, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof child === "object" && Symbol.asyncIterator in child) {
|
||||||
|
(async () => {
|
||||||
|
for await (const item of child) {
|
||||||
|
await render(item, chunks, context);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(child)) {
|
||||||
|
for (const item of child) await render(item, chunks, context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
chunks.write(escape(String(child)));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function jsx(
|
||||||
|
tag: string | Component | typeof Fragment,
|
||||||
|
props: Props | null = {},
|
||||||
|
): JsxElement {
|
||||||
|
props ||= {};
|
||||||
|
|
||||||
|
return async (chunks: ChunkedStream<string>) => {
|
||||||
|
const context = html(chunks);
|
||||||
|
const { children, ...attrs } = props;
|
||||||
|
|
||||||
|
if (tag === Fragment) {
|
||||||
|
if (!Array.isArray(children)) {
|
||||||
|
return await render([children], chunks, context);
|
||||||
|
}
|
||||||
|
for (const child of children) {
|
||||||
|
await render(child, chunks, context);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof tag === "function") {
|
||||||
|
return await render(tag(props), chunks, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
const childr = children == null ? [] : [].concat(children);
|
||||||
|
const attributes = Object.keys(attrs).length ? attrs : {};
|
||||||
|
|
||||||
|
if (!childr.length || VOID_TAGS.has(tag)) {
|
||||||
|
await context[tag](childr);
|
||||||
|
} else {
|
||||||
|
await context[tag](attributes, async () => {
|
||||||
|
for (const child of childr) {
|
||||||
|
await render(child, chunks, context);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export const jsxs = jsx;
|
export const jsxs = jsx;
|
||||||
|
|
||||||
|
async function renderJsx(
|
||||||
|
element: JsxElement | JsxElement[],
|
||||||
|
chunks: ChunkedStream<string>,
|
||||||
|
): Promise<void> {
|
||||||
|
if (Array.isArray(element)) {
|
||||||
|
for (const el of element) {
|
||||||
|
await renderJsx(el, chunks);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof element === "object" && Symbol.asyncIterator in element) {
|
||||||
|
for await (const item of element) {
|
||||||
|
await renderJsx(item, chunks);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof element === "function") {
|
||||||
|
await element(chunks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const raw =
|
||||||
|
(html: string): JsxElement => async (chunks: ChunkedStream<string>) =>
|
||||||
|
void (!chunks.closed && chunks.write(html));
|
||||||
|
|
||||||
|
export const open = <K extends keyof HTMLElementTagNameMap>(tag: K) =>
|
||||||
|
raw(`<${tag}>`);
|
||||||
|
|
||||||
|
export const close = <K extends keyof HTMLElementTagNameMap>(tag: K) =>
|
||||||
|
raw(`</${tag}>`);
|
||||||
|
|
||||||
|
export { renderJsx as render };
|
||||||
|
|
|
||||||
15
src/main.ts
15
src/main.ts
|
|
@ -3,26 +3,15 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { html } from "./html.ts";
|
|
||||||
import { createHtmlStream } from "./http.ts";
|
import { createHtmlStream } from "./http.ts";
|
||||||
import App from "./app.tsx";
|
import App from "./app.tsx";
|
||||||
|
import { render } from "interest/jsx-runtime";
|
||||||
|
|
||||||
Deno.serve({
|
Deno.serve({
|
||||||
port: 8080,
|
port: 8080,
|
||||||
async handler() {
|
async handler() {
|
||||||
const stream = await createHtmlStream({ lang: "en" });
|
const stream = await createHtmlStream({ lang: "en" });
|
||||||
const { ol, li } = html(stream.chunks);
|
await render(App(), stream.chunks);
|
||||||
|
|
||||||
await App()(stream.chunks);
|
|
||||||
|
|
||||||
ol(async () => {
|
|
||||||
const fruits = ["TSX support", "Apple", "Banana", "Cherry"];
|
|
||||||
for (const fruit of fruits) {
|
|
||||||
await new Promise((r) => setTimeout(r, 500));
|
|
||||||
await li(fruit);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return stream.response;
|
return stream.response;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,11 @@
|
||||||
|
|
||||||
export class ChunkedStream<T> implements AsyncIterable<T> {
|
export class ChunkedStream<T> implements AsyncIterable<T> {
|
||||||
private readonly chunks: T[] = [];
|
private readonly chunks: T[] = [];
|
||||||
|
|
||||||
private readonly resolvers: ((result: IteratorResult<T>) => void)[] = [];
|
private readonly resolvers: ((result: IteratorResult<T>) => void)[] = [];
|
||||||
|
private readonly rejectors: ((error: Error) => void)[] = [];
|
||||||
|
|
||||||
|
private _error: Error | null = null;
|
||||||
private _closed = false;
|
private _closed = false;
|
||||||
|
|
||||||
get closed(): boolean {
|
get closed(): boolean {
|
||||||
|
|
@ -17,6 +21,7 @@ export class ChunkedStream<T> implements AsyncIterable<T> {
|
||||||
|
|
||||||
const resolver = this.resolvers.shift();
|
const resolver = this.resolvers.shift();
|
||||||
if (resolver) {
|
if (resolver) {
|
||||||
|
this.rejectors.shift();
|
||||||
resolver({ value: chunk, done: false });
|
resolver({ value: chunk, done: false });
|
||||||
} else {
|
} else {
|
||||||
this.chunks.push(chunk);
|
this.chunks.push(chunk);
|
||||||
|
|
@ -26,16 +31,37 @@ export class ChunkedStream<T> implements AsyncIterable<T> {
|
||||||
close(): void {
|
close(): void {
|
||||||
this._closed = true;
|
this._closed = true;
|
||||||
while (this.resolvers.length) {
|
while (this.resolvers.length) {
|
||||||
|
this.rejectors.shift();
|
||||||
this.resolvers.shift()!({ value: undefined! as any, done: true });
|
this.resolvers.shift()!({ value: undefined! as any, done: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
error(err: Error): void {
|
||||||
|
if (this._closed) return;
|
||||||
|
|
||||||
|
this._error = err;
|
||||||
|
this._closed = true;
|
||||||
|
|
||||||
|
while (this.rejectors.length) {
|
||||||
|
this.rejectors.shift()!(err);
|
||||||
|
this.resolvers.shift();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async next(): Promise<IteratorResult<T>> {
|
async next(): Promise<IteratorResult<T>> {
|
||||||
|
if (this._error) {
|
||||||
|
throw this._error;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.chunks.length) {
|
if (this.chunks.length) {
|
||||||
return { value: this.chunks.shift()!, done: false };
|
return { value: this.chunks.shift()!, done: false };
|
||||||
}
|
}
|
||||||
if (this._closed) return { value: undefined as any, done: true };
|
if (this._closed) return { value: undefined as any, done: true };
|
||||||
return new Promise((resolve) => this.resolvers.push(resolve));
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.resolvers.push(resolve);
|
||||||
|
this.rejectors.push(reject);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[Symbol.asyncIterator](): AsyncIterableIterator<T> {
|
[Symbol.asyncIterator](): AsyncIterableIterator<T> {
|
||||||
|
|
@ -44,20 +70,20 @@ export class ChunkedStream<T> implements AsyncIterable<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const mapStream = <T, U>(
|
export const mapStream = <T, U>(
|
||||||
fn: (chunk: T, i: number) => U | Promise<U>,
|
fn: (chunk: T, index: number) => U | Promise<U>,
|
||||||
) =>
|
) =>
|
||||||
async function* (source: AsyncIterable<T>): AsyncIterable<U> {
|
async function* (source: AsyncIterable<T>): AsyncIterable<U> {
|
||||||
let i = 0;
|
let index = 0;
|
||||||
for await (const chunk of source) yield await fn(chunk, i++);
|
for await (const chunk of source) yield await fn(chunk, index++);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const filterStream = <T>(
|
export const filterStream = <T>(
|
||||||
pred: (chunk: T, i: number) => boolean | Promise<boolean>,
|
pred: (chunk: T, index: number) => boolean | Promise<boolean>,
|
||||||
) =>
|
) =>
|
||||||
async function* (source: AsyncIterable<T>): AsyncIterable<T> {
|
async function* (source: AsyncIterable<T>): AsyncIterable<T> {
|
||||||
let i = 0;
|
let index = 0;
|
||||||
for await (const chunk of source) {
|
for await (const chunk of source) {
|
||||||
if (await pred(chunk, i++)) yield chunk;
|
if (await pred(chunk, index++)) yield chunk;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -72,9 +98,9 @@ export const takeStream = <T>(count: number) =>
|
||||||
|
|
||||||
export const skipStream = <T>(count: number) =>
|
export const skipStream = <T>(count: number) =>
|
||||||
async function* (source: AsyncIterable<T>): AsyncIterable<T> {
|
async function* (source: AsyncIterable<T>): AsyncIterable<T> {
|
||||||
let i = 0;
|
let index = 0;
|
||||||
for await (const chunk of source) {
|
for await (const chunk of source) {
|
||||||
if (i++ >= count) yield chunk;
|
if (index++ >= count) yield chunk;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -88,17 +114,28 @@ export const batchStream = <T>(size: number) =>
|
||||||
batch = [];
|
batch = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
batch.length && (yield batch);
|
if (batch.length) yield batch;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const tapStream = <T>(
|
export const tapStream = <T>(
|
||||||
fn: (chunk: T, i: number) => void | Promise<void>,
|
fn: (chunk: T, index: number) => void | Promise<void>,
|
||||||
) =>
|
) =>
|
||||||
async function* (source: AsyncIterable<T>): AsyncIterable<T> {
|
async function* (source: AsyncIterable<T>): AsyncIterable<T> {
|
||||||
let i = 0;
|
let index = 0;
|
||||||
for await (const chunk of source) {
|
for await (const chunk of source) {
|
||||||
yield chunk;
|
yield chunk;
|
||||||
await fn(chunk, i++);
|
await fn(chunk, index++);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const catchStream = <T>(
|
||||||
|
handler: (error: Error) => void | Promise<void>,
|
||||||
|
) =>
|
||||||
|
async function* (source: AsyncIterable<T>): AsyncIterable<T> {
|
||||||
|
try {
|
||||||
|
for await (const chunk of source) yield chunk;
|
||||||
|
} catch (err) {
|
||||||
|
await handler(err instanceof Error ? err : new Error(String(err)));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue