Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 0 additions & 125 deletions src/rendering/orchestrator/ssr-orchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,6 @@ import type { HTMLGenerationContext, HTMLGenerator } from "./html.ts";
import type { LayoutOrchestrator } from "./layout.ts";
import type { RenderOptions } from "./types.ts";
import { runWithHeadCollector } from "#veryfront/react/head-collector.ts";
import { getWorkerPool, isSSRIsolationEnabled } from "#veryfront/security/sandbox/worker-pool.ts";
import type { WorkerResponse } from "#veryfront/security/sandbox/worker-types.ts";
import { requireActiveSourceIntegrationPolicy } from "#veryfront/integrations/source-policy-context.ts";
import {
endRenderSession,
hasRenderSession,
Expand All @@ -36,24 +33,6 @@ export interface SSRRenderingResult {
ssrHash: string;
}

/**
* Options for isolated SSR rendering through the Worker pool.
* When provided and SSR isolation is enabled, the rendering happens
* in a per-project Worker instead of the main process.
*/
export interface SSRIsolationOptions {
/** Temp file path for the page component module */
pageModulePath: string;
/** Ordered layout module temp paths (innermost to outermost) */
layoutModulePaths: string[];
/** Page component props */
pageProps: Record<string, unknown>;
/** Layout props (one entry per layout, matching layoutModulePaths order) */
layoutProps: Record<string, unknown>[];
/** Project directory for worker scoping */
projectDir: string;
}

function getElementTypeName(el: React.ReactElement | null | undefined): string {
if (!el?.type) return "unknown";
if (typeof el.type === "string") return el.type;
Expand Down Expand Up @@ -93,21 +72,7 @@ export class SSROrchestrator {
pageElement: React.ReactElement,
generationContext: Omit<HTMLGenerationContext, "html" | "ssrHash">,
options?: RenderOptions,
isolationOptions?: SSRIsolationOptions,
): Promise<SSRRenderingResult> {
// Isolated SSR path: render in per-project Worker
if (
isSSRIsolationEnabled() &&
isolationOptions?.pageModulePath &&
isolationOptions?.projectDir
) {
// NOTE: the app-router error.tsx catch below is scoped to the main-process
// render path. Under SSR isolation (per-project Worker) a page throw is not
// yet routed to error.tsx — a follow-up, isolation being off by default.
return this.performIsolatedSSR(generationContext, options, isolationOptions);
}

// Default path: render in main process
logger.debug("performSSRRendering called", {
elementType: getElementTypeName(pageElement),
hasChildren: !!(pageElement.props as Record<string, unknown>)?.children,
Expand Down Expand Up @@ -299,96 +264,6 @@ export class SSROrchestrator {
return { result: rendered.result, head: rendered.head, errorPath: errorInfo.path };
}

/**
* Perform SSR rendering in an isolated per-project Worker.
*
* The Worker imports user modules from their temp file paths,
* constructs the React element tree, and renders to HTML.
* For streaming, the Worker sends chunks via postMessage.
*/
private async performIsolatedSSR(
generationContext: Omit<HTMLGenerationContext, "html" | "ssrHash">,
options: RenderOptions | undefined,
isolation: SSRIsolationOptions,
): Promise<SSRRenderingResult> {
const wantsStream = options?.delivery === "stream";
const pool = getWorkerPool();
const requestId = crypto.randomUUID();

return withSpan(
"ssr.isolated_render",
async () => {
const worker = pool.getOrCreateWorker(isolation.projectDir, [isolation.projectDir]);

if (wantsStream) {
// Streaming mode: get a ReadableStream of chunks from the Worker
const stream = worker.executeStream({
type: "render-ssr",
id: requestId,
pageModulePath: isolation.pageModulePath,
layoutModulePaths: isolation.layoutModulePaths,
pageProps: isolation.pageProps,
layoutProps: isolation.layoutProps,
delivery: "stream",
sourceIntegrationPolicy: requireActiveSourceIntegrationPolicy(),
});

const ssrHash = `stream-isolated-${Date.now()}`;

// Generate HTML stream using the framework's HTML generator
const finalStream = await this.config.htmlGenerator.generateHTMLStream(stream, {
...generationContext,
ssrHash,
options: { ...generationContext.options, ...options },
collectedHead: undefined,
});

return { fullHtml: "", finalStream, ssrHash };
}

// String mode: render to HTML in Worker, get result back
const workerResponse: WorkerResponse = await worker.execute({
type: "render-ssr",
id: requestId,
pageModulePath: isolation.pageModulePath,
layoutModulePaths: isolation.layoutModulePaths,
pageProps: isolation.pageProps,
layoutProps: isolation.layoutProps,
delivery: "string",
sourceIntegrationPolicy: requireActiveSourceIntegrationPolicy(),
});

if (workerResponse.type === "error") {
const err = new Error(workerResponse.error.message);
err.name = workerResponse.error.name;
throw err;
}

if (workerResponse.type !== "ssr-result") {
throw new Error(`Unexpected worker response type: ${workerResponse.type}`);
}

const html = workerResponse.html;
const ssrHash = await computeHash(html);

const fullHtml = await this.config.htmlGenerator.generateFullHTML({
...generationContext,
html,
ssrHash,
options: { ...generationContext.options, ...options },
collectedHead: undefined,
});

return { fullHtml, finalStream: null, ssrHash };
},
{
"ssr.isolated": true,
"ssr.wants_stream": wantsStream,
"ssr.project_dir": isolation.projectDir,
},
);
}

private createStream(html: string): ReadableStream | null {
try {
return new Response(html).body ?? null;
Expand Down
54 changes: 0 additions & 54 deletions src/security/sandbox/project-worker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ const TEST_WORKER_SCRIPT_URL = `data:application/typescript,${
return;
}
if (msg.type === "clear-cache") return;
if (msg.type === "render-ssr") return;
self.postMessage({
type: "error",
id: msg.id,
Expand Down Expand Up @@ -432,15 +431,6 @@ testSuite("ProjectWorker - real worker request isolation", () => {
url: serializedRequest.url,
},
},
{
type: "render-ssr",
id: "ssr",
pageModulePath: modulePath,
layoutModulePaths: [],
pageProps: {},
layoutProps: [],
delivery: "string",
},
];

worker.start();
Expand Down Expand Up @@ -1354,47 +1344,3 @@ testSuite("ProjectWorker - real worker request isolation", () => {
}
});
});

testSuite("ProjectWorker - executeStream", () => {
it("throws when worker is not started", () => {
const worker = createTestWorker("test-stream");
let threw = false;
try {
worker.executeStream({
type: "render-ssr",
id: "test-id",
pageModulePath: "/nonexistent.ts",
layoutModulePaths: [],
pageProps: {},
layoutProps: [],
delivery: "stream",
sourceIntegrationPolicy: TEST_SOURCE_INTEGRATION_POLICY,
});
} catch {
threw = true;
}
assert(threw, "should throw when worker is not available");
});

it("returns a ReadableStream when worker is started", () => {
const worker = createTestWorker("test-stream");
worker.start();
try {
const stream = worker.executeStream({
type: "render-ssr",
id: "test-id",
pageModulePath: "/nonexistent.ts",
layoutModulePaths: [],
pageProps: {},
layoutProps: [],
delivery: "stream",
sourceIntegrationPolicy: TEST_SOURCE_INTEGRATION_POLICY,
});
assert(stream instanceof ReadableStream, "should return a ReadableStream");
// Cancel the stream to clean up
stream.cancel();
} finally {
worker.terminate();
}
});
});
Loading