Skip to main content
Glama
deleonio
by deleonio
renderer.ts7.03 kB
import { IsolatedHydrateRenderer } from './resource-manager.js'; import type { HydrateRenderer, HydrateRendererOptions, RenderResponsePayload } from './types.js'; export const isPlainObject = (value: unknown): value is Record<string, unknown> => Boolean(value) && typeof value === 'object' && !Array.isArray(value); const normalizeDiagnostics = (diagnostics: unknown): unknown[] => { if (!diagnostics) { return []; } if (Array.isArray(diagnostics)) { return diagnostics; } return [diagnostics]; }; const coerceComponents = (components: unknown): string[] => { if (!Array.isArray(components)) { return []; } return components.filter((component): component is string => typeof component === 'string'); }; const coerceHydratedCount = (hydratedCount: unknown): number => { return typeof hydratedCount === 'number' && Number.isFinite(hydratedCount) ? hydratedCount : 0; }; const DEFAULT_RENDERER_OPTIONS: Readonly<HydrateRendererOptions> = Object.freeze({ clientHydrateAnnotations: false, destroyDocument: true, destroyWindow: true, prettyHtml: false, removeAttributeQuotes: false, removeBooleanAttributeQuotes: true, removeEmptyAttributes: false, removeHtmlComments: true, removeUnusedStyles: true, serializeToHtml: true, timeout: 5000, // Keep original timeout for backward compatibility }); const DEFAULT_TIMEOUT = 5000; const mergeHydrateOptions = (base: Record<string, unknown> | undefined, overrides: Record<string, unknown> | undefined): HydrateRendererOptions => ({ ...DEFAULT_RENDERER_OPTIONS, ...(base ?? {}), ...(overrides ?? {}), }); class AsyncMutex { private queue: Array<() => void> = []; private locked = false; public async acquire(): Promise<() => void> { return new Promise<() => void>((resolve) => { const release = this.createReleaseCallback(); if (!this.locked) { this.locked = true; resolve(release); return; } this.queue.push(() => resolve(release)); }); } public async runExclusive<T>(callback: () => Promise<T>): Promise<T>; public async runExclusive<T>(callback: () => T): Promise<T>; public async runExclusive<T>(callback: () => Promise<T> | T): Promise<T> { const release = await this.acquire(); try { return await callback(); } finally { release(); } } private createReleaseCallback(): () => void { let released = false; return () => { if (released) { return; } released = true; const next = this.queue.shift(); if (next) { next(); return; } this.locked = false; }; } } const rendererMutex = new AsyncMutex(); // Global isolated renderer instance for resource management let globalIsolatedRenderer: IsolatedHydrateRenderer | null = null; /** * Initialize or get the global isolated renderer */ export const getIsolatedRenderer = (baseRenderer: HydrateRenderer): IsolatedHydrateRenderer => { if (!globalIsolatedRenderer) { globalIsolatedRenderer = new IsolatedHydrateRenderer(baseRenderer); } return globalIsolatedRenderer; }; /** * Cleanup the global isolated renderer */ export const cleanupGlobalRenderer = (): void => { const cleanupPromise = rendererMutex.runExclusive(() => { if (globalIsolatedRenderer) { globalIsolatedRenderer.destroy(); globalIsolatedRenderer = null; } }); void cleanupPromise.catch((error) => { if (typeof queueMicrotask === 'function') { queueMicrotask(() => { throw error; }); return; } setTimeout(() => { throw error; }, 0); }); }; /** * Server-optimized hydrate function that reuses isolated renderer instance */ export const hydrateFragmentForServer = async ( renderer: HydrateRenderer, html: string, options?: unknown, baseOptions?: Record<string, unknown>, ): Promise<RenderResponsePayload> => { if (typeof html !== 'string' || html.trim().length === 0) { throw new TypeError('HTML input must be a non-empty string'); } const normalizedOptions = isPlainObject(options) ? (options as Record<string, unknown>) : undefined; const normalizedBaseOptions = isPlainObject(baseOptions) ? baseOptions : undefined; const effectiveOptions = mergeHydrateOptions(normalizedBaseOptions, normalizedOptions); return rendererMutex.runExclusive(async () => { // Use shared isolated renderer for server efficiency const isolatedRenderer = getIsolatedRenderer(renderer); try { // Race between rendering and timeout to prevent hanging const renderPromise = isolatedRenderer.render(html, effectiveOptions); const timeoutPromise = new Promise<never>((_, reject) => { setTimeout(() => { reject(new Error(`Hydration timeout after ${DEFAULT_TIMEOUT}ms`)); }, DEFAULT_TIMEOUT); }); const result = await Promise.race([renderPromise, timeoutPromise]); return { html: typeof result.html === 'string' ? result.html : html, components: coerceComponents(result.components), hydratedCount: coerceHydratedCount(result.hydratedCount), diagnostics: normalizeDiagnostics(result.diagnostics), }; } catch (error) { // Log renderer stats for debugging const stats = isolatedRenderer.getStats(); console.warn(`Hydration failed with ${stats.activeTimers} active timers:`, error); // Force cleanup on timeout/error isolatedRenderer.destroy(); globalIsolatedRenderer = null; throw error; } }); }; export const hydrateFragment = async ( renderer: HydrateRenderer, html: string, options?: unknown, baseOptions?: Record<string, unknown>, ): Promise<RenderResponsePayload> => { if (typeof html !== 'string' || html.trim().length === 0) { throw new TypeError('HTML input must be a non-empty string'); } const normalizedOptions = isPlainObject(options) ? (options as Record<string, unknown>) : undefined; const normalizedBaseOptions = isPlainObject(baseOptions) ? baseOptions : undefined; const effectiveOptions = mergeHydrateOptions(normalizedBaseOptions, normalizedOptions); // Create a new isolated renderer for each call to ensure test isolation const isolatedRenderer = new IsolatedHydrateRenderer(renderer); try { // Race between rendering and timeout to prevent hanging const renderPromise = isolatedRenderer.render(html, effectiveOptions); const timeoutPromise = new Promise<never>((_, reject) => { setTimeout(() => { reject(new Error(`Hydration timeout after ${DEFAULT_TIMEOUT}ms`)); }, DEFAULT_TIMEOUT); }); const result = await Promise.race([renderPromise, timeoutPromise]); // Clean up after successful rendering isolatedRenderer.destroy(); return { html: typeof result.html === 'string' ? result.html : html, components: coerceComponents(result.components), hydratedCount: coerceHydratedCount(result.hydratedCount), diagnostics: normalizeDiagnostics(result.diagnostics), }; } catch (error) { // Log renderer stats for debugging const stats = isolatedRenderer.getStats(); console.warn(`Hydration failed with ${stats.activeTimers} active timers:`, error); // Force cleanup on timeout/error isolatedRenderer.destroy(); throw error; } };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/deleonio/public-ui-kolibri'

If you have feedback or need assistance with the MCP directory API, please join our Discord server