=== NEW === - studio/ — MCPEngine Studio scaffold (Next.js monorepo, build plan) - docs/FACTORY-V2.md — Factory v2 architecture doc - docs/CALENDLY_MCP_BUILD_SUMMARY.md — Calendly MCP build report === UPDATED SERVERS === - fieldedge: Added jobs-tools, UI build script, main entry update - lightspeed: Updated main + server entry points - squarespace: Added collection-browser + page-manager apps - toast: Added main + server entry points === INFRA === - infra/command-center/state.json — Updated pipeline state - infra/command-center/FACTORY-V2.md — Factory v2 operator playbook
90 lines
2.6 KiB
TypeScript
90 lines
2.6 KiB
TypeScript
// SSE Helper — converts async generator of PipelineEvents to Server-Sent Events Response
|
|
|
|
import type { PipelineEvent } from '../types';
|
|
|
|
/**
|
|
* Create a Server-Sent Events Response from a PipelineEvent async generator.
|
|
* Compatible with Next.js App Router API routes.
|
|
*/
|
|
export function createSSEResponse(
|
|
generator: AsyncGenerator<PipelineEvent>
|
|
): Response {
|
|
const encoder = new TextEncoder();
|
|
|
|
const stream = new ReadableStream({
|
|
async start(controller) {
|
|
try {
|
|
for await (const event of generator) {
|
|
const data = `data: ${JSON.stringify(event)}\n\n`;
|
|
controller.enqueue(encoder.encode(data));
|
|
}
|
|
// Send done event
|
|
controller.enqueue(encoder.encode('data: [DONE]\n\n'));
|
|
} catch (error) {
|
|
const errorEvent: PipelineEvent = {
|
|
type: 'error',
|
|
message: error instanceof Error ? error.message : String(error),
|
|
recoverable: false,
|
|
};
|
|
controller.enqueue(
|
|
encoder.encode(`data: ${JSON.stringify(errorEvent)}\n\n`)
|
|
);
|
|
} finally {
|
|
controller.close();
|
|
}
|
|
},
|
|
});
|
|
|
|
return new Response(stream, {
|
|
headers: {
|
|
'Content-Type': 'text/event-stream',
|
|
'Cache-Control': 'no-cache, no-transform',
|
|
Connection: 'keep-alive',
|
|
'X-Accel-Buffering': 'no', // Disable nginx buffering
|
|
},
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Create a named SSE event (with event: field).
|
|
* Useful when clients want to use EventSource.addEventListener().
|
|
*/
|
|
export function createNamedSSEResponse(
|
|
generator: AsyncGenerator<PipelineEvent>
|
|
): Response {
|
|
const encoder = new TextEncoder();
|
|
|
|
const stream = new ReadableStream({
|
|
async start(controller) {
|
|
try {
|
|
for await (const event of generator) {
|
|
const eventType = event.type.replace(':', '_');
|
|
const line = `event: ${eventType}\ndata: ${JSON.stringify(event)}\n\n`;
|
|
controller.enqueue(encoder.encode(line));
|
|
}
|
|
controller.enqueue(encoder.encode('event: done\ndata: {}\n\n'));
|
|
} catch (error) {
|
|
const errorEvent: PipelineEvent = {
|
|
type: 'error',
|
|
message: error instanceof Error ? error.message : String(error),
|
|
recoverable: false,
|
|
};
|
|
controller.enqueue(
|
|
encoder.encode(`event: error\ndata: ${JSON.stringify(errorEvent)}\n\n`)
|
|
);
|
|
} finally {
|
|
controller.close();
|
|
}
|
|
},
|
|
});
|
|
|
|
return new Response(stream, {
|
|
headers: {
|
|
'Content-Type': 'text/event-stream',
|
|
'Cache-Control': 'no-cache, no-transform',
|
|
Connection: 'keep-alive',
|
|
'X-Accel-Buffering': 'no',
|
|
},
|
|
});
|
|
}
|