All files / build-worker queue-handler.ts

100% Statements 29/29
100% Branches 12/12
100% Functions 2/2
100% Lines 29/29

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109                                                                                      8x   8x 9x 9x       9x 9x 1x     8x         9x 9x   9x 9x               6x 6x       6x     3x 3x 3x 2x 2x 2x   1x     2x     2x   1x 1x     3x 3x     3x        
/**
 * Queue Handler for Website Build Jobs
 *
 * Handles messages from BUILD_QUEUE and routes them to the
 * ProSiteBuilder Durable Object — same path for all environments.
 *
 * Architecture (unified):
 * Queue → Handler → Durable Object → Container
 *
 * In local dev, `wrangler dev` manages the Docker container.
 * In production, Cloudflare Containers manages it.
 */
 
import type { ProSiteBuilder } from "./durable-object";
 
// Message format from the API's BUILD_QUEUE
export interface BuildQueueMessage {
	jobId: number;
	proId: string;
	proWebsiteId: number;
	subdomain: string;
	projectName: string;
	timestamp: number;
}
 
// Environment type for queue handler
export interface BuildQueueEnv {
	PRO_SITE_BUILDER?: DurableObjectNamespace<ProSiteBuilder>;
	ENVIRONMENT?: string;
}
 
/**
 * Handle queue messages for website builds
 *
 * This is the main entry point for queue-driven builds.
 * It receives messages when pros click "Publish" in the Portal.
 * Always routes through the Durable Object — zero environment branching.
 */
export async function handleBuildQueue(
	batch: MessageBatch<BuildQueueMessage>,
	env: BuildQueueEnv,
	_ctx: ExecutionContext,
): Promise<void> {
	console.log(`**** Received ${batch.messages.length} build message(s)`);
 
	for (const message of batch.messages) {
		const { jobId, proId, subdomain, projectName } = message.body;
		console.log(
			`Processing build job #${jobId} for ${subdomain} (pro: ${proId}, project: ${projectName})`,
		);
 
		try {
			if (!env.PRO_SITE_BUILDER) {
				throw new Error("PRO_SITE_BUILDER binding not available");
			}
 
			console.log(
				`[Queue] Routing job #${jobId} to Durable Object (env: ${env.ENVIRONMENT || "unknown"})`,
			);
 
			// Always route through the Durable Object
			const id = env.PRO_SITE_BUILDER.idFromName("build-worker");
			const stub = env.PRO_SITE_BUILDER.get(id);
 
			console.log(`[Queue] Sending build request to DO for job #${jobId}...`);
			const response = await stub.fetch(
				new Request(`http://container/build/${jobId}`, {
					method: "POST",
					headers: { "Content-Type": "application/json" },
					body: JSON.stringify({ projectName }),
				}),
			);
 
			const responseBody = await response.text();
			console.log(
				`[Queue] DO response for job #${jobId}: status=${response.status}, body=${responseBody}`,
			);
 
			if (response.status === 202) {
				// Build dispatched — the DO runs it in the background.
				// The container updates job status via the API when done.
				console.log(`[Queue] Build job #${jobId} dispatched to container`);
				message.ack();
			} else if (!response.ok) {
				const errorData = (() => {
					try {
						return JSON.parse(responseBody) as { error?: string };
					} catch {
						return {};
					}
				})();
				console.error(
					`[Queue] Build job #${jobId} failed: ${errorData.error || response.statusText}`,
				);
				message.retry();
			} else {
				console.log(`[Queue] Build job #${jobId} completed successfully`);
				message.ack();
			}
		} catch (error) {
			const errorMsg = error instanceof Error ? error.message : String(error);
			console.error(
				`[Queue] Error processing build job #${jobId}: ${errorMsg}`,
			);
			message.retry();
		}
	}
}