Created
October 4, 2024 19:54
-
-
Save subtleGradient/7d31f0d31ef08e7d30617fa2fd92bdf3 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// <reference types="@frens/reframe/micro-apps" /> | |
import ui, { clientManifest, missingClientComponents as todo } from "@frens/reframe/Micro.ClientComponents" | |
import React from "react" | |
import ReactServerDOMServer, { type BundlerConfig } from "react-server-dom-webpack/server" | |
import { PassThrough } from "stream" | |
import { withAsyncIterableTimeout } from "../AsyncIterableTimeout" | |
type GenUI = (props: { ui: typeof ui; todo: typeof todo }) => React.ReactNode | |
const CONFIG = { | |
timeoutMs: 500, | |
} | |
export async function* renderFromServer(RootView: GenUI, config = CONFIG) { | |
const root = <RootView ui={ui} todo={todo} /> | |
yield* withAsyncIterableTimeout(renderToAsyncIterable(root, clientManifest), config.timeoutMs) | |
} | |
function renderToPipeableStream(renderable: any, clientManifest: BundlerConfig) { | |
return ReactServerDOMServer.renderToPipeableStream(renderable, clientManifest, { | |
onError: (err: unknown) => console.error("ReactServerDOMServer.renderToPipeableStream onError", err), | |
onPostpone: (reason: unknown) => console.warn("ReactServerDOMServer.renderToPipeableStream onPostpone", reason), | |
}) | |
} | |
export async function* renderToAsyncIterable(renderable: any, clientManifest: BundlerConfig): AsyncIterable<string> { | |
const pipeableStream = renderToPipeableStream(renderable, clientManifest) | |
const passThrough = new PassThrough() | |
pipeableStream.pipe(passThrough) | |
const textDecoder = new TextDecoder("utf-8") | |
try { | |
for await (const chunk of passThrough) { | |
yield textDecoder.decode(chunk, { stream: true }) | |
} | |
} finally { | |
yield textDecoder.decode(undefined, { stream: false }) | |
} | |
} | |
export async function Await({ children: promise }: { children: React.ReactNode | Promise<React.ReactNode> }) { | |
"server only" | |
return await promise | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* eslint-disable no-inner-declarations */ | |
class AsyncIterableTimeoutError extends Error { | |
constructor(message: string) { | |
super(message) | |
this.name = "TimeoutError" | |
} | |
} | |
export function withAsyncIterableTimeout<T>( | |
asyncIterable: AsyncIterable<T>, | |
timeoutMs: number, | |
abortController = new AbortController(), | |
): AsyncIterable<T> { | |
return { | |
async *[Symbol.asyncIterator]() { | |
const iterator = asyncIterable[Symbol.asyncIterator]() | |
while (abortController.signal.aborted === false) { | |
let timeoutId: ReturnType<typeof setTimeout> | undefined | |
const timeoutPromise = new Promise<never>((_, reject) => { | |
timeoutId = setTimeout(() => { | |
abortController.abort() | |
reject(new AsyncIterableTimeoutError(`Iteration timed out after ${timeoutMs}ms`)) | |
}, timeoutMs) | |
}) | |
try { | |
const nextPromise = iterator.next() | |
const racePromise = Promise.race([timeoutPromise, nextPromise]) | |
const result = await racePromise | |
clearTimeout(timeoutId) | |
if (result.done) return | |
yield result.value | |
} catch (error) { | |
if (error instanceof AsyncIterableTimeoutError) return // Exit the generator on timeout | |
throw error // Re-throw other errors | |
} finally { | |
if (abortController.signal.aborted) { | |
if (typeof iterator.return === "function") await iterator.return() | |
// eslint-disable-next-line no-unsafe-finally | |
return // Exit the generator if aborted | |
} | |
} | |
} | |
}, | |
} | |
} | |
// Usage examples: | |
// if (require.main === module) { | |
// // Example 1: Basic usage | |
// async function* slowCounter() { | |
// for (let i = 1; i <= 5; i++) { | |
// await new Promise((resolve) => setTimeout(resolve, 1000)) | |
// yield i | |
// } | |
// } | |
// async function example1() { | |
// const timeoutIterable = withAsyncIterableTimeout(slowCounter(), 2500) | |
// for await (const item of timeoutIterable) { | |
// console.log("example1 received:", item) | |
// } | |
// } | |
// // Example 2: Using with array-like structures | |
// async function example2() { | |
// const asyncArray = (async function* () { | |
// for (const item of [1, 2, 3, 4, 5]) { | |
// yield item | |
// } | |
// })() | |
// const timeoutIterable = withAsyncIterableTimeout(asyncArray, 1000) | |
// const result = await Array.fromAsync(timeoutIterable) | |
// console.log("example2 result:", result) | |
// } | |
// // Example 3: Handling timeout error | |
// async function example3() { | |
// const infiniteIterable = (async function* () { | |
// let count = 0 | |
// while (true) { | |
// await new Promise((resolve) => setTimeout(resolve, 1000)) | |
// const value = Math.random() | |
// yield value | |
// count++ | |
// } | |
// })() | |
// try { | |
// const timeoutIterable = withAsyncIterableTimeout(infiniteIterable, 3000) | |
// for await (const item of timeoutIterable) { | |
// console.log("example3 received:", item) | |
// } | |
// } catch (error) { | |
// if (error instanceof AsyncIterableTimeoutError) { | |
// console.log("Iteration timed out:", error.message) | |
// } else { | |
// console.error("Unexpected error:", error) | |
// } | |
// } | |
// } | |
// // Run examples | |
// async function test() { | |
// console.log("Example 1:") | |
// await example1() | |
// console.log("\nExample 2:") | |
// await example2() | |
// console.log("\nExample 3:") | |
// await example3() | |
// } | |
// await test() | |
// } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment