@effing/ffs 0.16.0 → 0.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -164,7 +164,8 @@ async function createServerContext(options) {
|
|
|
164
164
|
skipValidation: !!process.env.FFS_SKIP_VALIDATION && process.env.FFS_SKIP_VALIDATION !== "false",
|
|
165
165
|
warmupConcurrency: parseInt(process.env.FFS_WARMUP_CONCURRENCY || "4", 10),
|
|
166
166
|
warmupBackendResolver: options?.warmupBackendResolver,
|
|
167
|
-
renderBackendResolver: options?.renderBackendResolver
|
|
167
|
+
renderBackendResolver: options?.renderBackendResolver,
|
|
168
|
+
onRenderComplete: options?.onRenderComplete
|
|
168
169
|
};
|
|
169
170
|
}
|
|
170
171
|
function parseEffieData(body, skipValidation) {
|
|
@@ -751,6 +752,7 @@ async function streamRenderProgress(req, res, ctx) {
|
|
|
751
752
|
keepalivePhase = "render";
|
|
752
753
|
if (job.upload) {
|
|
753
754
|
keepalivePhase = "upload";
|
|
755
|
+
const renderStart = performance.now();
|
|
754
756
|
if (renderBackend) {
|
|
755
757
|
const videoJob = {
|
|
756
758
|
effie: job.effie,
|
|
@@ -793,6 +795,17 @@ async function streamRenderProgress(req, res, ctx) {
|
|
|
793
795
|
timings
|
|
794
796
|
);
|
|
795
797
|
}
|
|
798
|
+
if (ctx.onRenderComplete) {
|
|
799
|
+
try {
|
|
800
|
+
await ctx.onRenderComplete({
|
|
801
|
+
effie: job.effie,
|
|
802
|
+
metadata: job.metadata,
|
|
803
|
+
wallClockTime: performance.now() - renderStart
|
|
804
|
+
});
|
|
805
|
+
} catch (err) {
|
|
806
|
+
console.error("onRenderComplete hook error:", err);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
796
809
|
sendEvent("complete", { status: "done" });
|
|
797
810
|
} else {
|
|
798
811
|
const videoJob = {
|
|
@@ -847,6 +860,7 @@ async function streamRenderVideo(req, res, ctx) {
|
|
|
847
860
|
videoJob.metadata
|
|
848
861
|
);
|
|
849
862
|
if (backend) {
|
|
863
|
+
const renderStart = performance.now();
|
|
850
864
|
const backendUrl = `${backend.baseUrl}/render/${jobId}/video`;
|
|
851
865
|
const response = await ffsFetch(backendUrl, {
|
|
852
866
|
headers: backend.apiKey ? { Authorization: `Bearer ${backend.apiKey}` } : void 0
|
|
@@ -861,6 +875,17 @@ async function streamRenderVideo(req, res, ctx) {
|
|
|
861
875
|
return;
|
|
862
876
|
}
|
|
863
877
|
await proxyBinaryStream(response, res);
|
|
878
|
+
if (ctx.onRenderComplete) {
|
|
879
|
+
try {
|
|
880
|
+
await ctx.onRenderComplete({
|
|
881
|
+
effie: videoJob.effie,
|
|
882
|
+
metadata: videoJob.metadata,
|
|
883
|
+
wallClockTime: performance.now() - renderStart
|
|
884
|
+
});
|
|
885
|
+
} catch (err) {
|
|
886
|
+
console.error("onRenderComplete hook error:", err);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
864
889
|
return;
|
|
865
890
|
}
|
|
866
891
|
}
|
|
@@ -876,19 +901,38 @@ async function streamRenderVideo(req, res, ctx) {
|
|
|
876
901
|
}
|
|
877
902
|
}
|
|
878
903
|
async function streamRenderDirect(res, job, ctx) {
|
|
904
|
+
const renderStart = performance.now();
|
|
879
905
|
const { EffieRenderer } = await import("./render-764Y6ARD.js");
|
|
880
906
|
const renderer = new EffieRenderer(job.effie, {
|
|
881
907
|
transientStore: ctx.transientStore,
|
|
882
908
|
httpProxy: ctx.httpProxy
|
|
883
909
|
});
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
910
|
+
try {
|
|
911
|
+
const videoStream = await renderer.render(job.scale);
|
|
912
|
+
res.on("close", () => {
|
|
913
|
+
videoStream.destroy();
|
|
914
|
+
});
|
|
915
|
+
res.set("Content-Type", "video/mp4");
|
|
916
|
+
res.set("Cache-Control", "public, immutable, max-age=86400");
|
|
917
|
+
await new Promise((resolve, reject) => {
|
|
918
|
+
videoStream.pipe(res);
|
|
919
|
+
res.on("finish", resolve);
|
|
920
|
+
res.on("error", reject);
|
|
921
|
+
});
|
|
922
|
+
} finally {
|
|
887
923
|
renderer.close();
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
924
|
+
}
|
|
925
|
+
if (ctx.onRenderComplete) {
|
|
926
|
+
try {
|
|
927
|
+
await ctx.onRenderComplete({
|
|
928
|
+
effie: job.effie,
|
|
929
|
+
metadata: job.metadata,
|
|
930
|
+
wallClockTime: performance.now() - renderStart
|
|
931
|
+
});
|
|
932
|
+
} catch (err) {
|
|
933
|
+
console.error("onRenderComplete hook error:", err);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
892
936
|
}
|
|
893
937
|
async function uploadRenderedVideo(videoBuffer, effie, upload, sendEvent) {
|
|
894
938
|
const timings = {};
|
|
@@ -989,4 +1033,4 @@ export {
|
|
|
989
1033
|
streamRenderProgress,
|
|
990
1034
|
streamRenderVideo
|
|
991
1035
|
};
|
|
992
|
-
//# sourceMappingURL=chunk-
|
|
1036
|
+
//# sourceMappingURL=chunk-54HNSHSL.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/handlers/shared.ts","../src/proxy.ts","../src/handlers/errors.ts","../src/handlers/caching.ts","../src/handlers/rendering.ts"],"sourcesContent":["import express from \"express\";\nimport type { Response as UndiciResponse } from \"undici\";\nimport type { TransientStore } from \"../storage\";\nimport { createTransientStore } from \"../storage\";\nimport { HttpProxy } from \"../proxy\";\nimport { ffsFetch } from \"../fetch\";\nimport type { TypedEventSender, EventSender } from \"../sse\";\nexport type { EventSender } from \"../sse\";\nimport type {\n EffieData,\n EffieSources,\n EffieSourceWithType,\n} from \"@effing/effie\";\nimport { effieDataSchema } from \"@effing/effie\";\nimport { ErrorCode } from \"./errors\";\nimport type { ErrorCode as ErrorCodeType } from \"./errors\";\n\nexport type OnRenderComplete = (result: {\n effie: EffieData<EffieSources>;\n metadata?: Record<string, unknown>;\n wallClockTime: number;\n}) => void | Promise<void>;\n\nexport type UploadOptions = {\n videoUrl: string;\n coverUrl?: string;\n};\n\nexport type BackendConfig = {\n baseUrl: string;\n apiKey?: string;\n};\n\nexport type WarmupBackendResolver = (\n sources: EffieSourceWithType[],\n metadata?: Record<string, unknown>,\n) => BackendConfig | null;\n\nexport type RenderBackendResolver = (\n effie: EffieData<EffieSources>,\n metadata?: Record<string, unknown>,\n) => BackendConfig | null;\n\nexport type WarmupJob = {\n sources: EffieSourceWithType[];\n metadata?: Record<string, unknown>;\n};\n\nexport type ResolvedRenderJob = {\n kind: \"resolved\";\n effie: EffieData<EffieSources>;\n sources: EffieSourceWithType[];\n scale: number;\n upload?: UploadOptions;\n purge?: boolean;\n warmupJobId: string;\n createdAt: number;\n metadata?: Record<string, unknown>;\n};\n\nexport type DeferredRenderJob = {\n kind: \"deferred\";\n effieUrl: string;\n scale: number;\n upload?: UploadOptions;\n purge?: boolean;\n warmupJobId: string;\n createdAt: number;\n metadata?: Record<string, unknown>;\n};\n\nexport type RenderJob = ResolvedRenderJob | DeferredRenderJob;\n\nexport type VideoJob = {\n effie: EffieData<EffieSources>;\n scale: number;\n metadata?: Record<string, unknown>;\n};\n\nexport type ServerContext = {\n transientStore: TransientStore;\n httpProxy?: HttpProxy;\n baseUrl: string;\n skipValidation: boolean;\n warmupConcurrency: number;\n warmupBackendResolver?: WarmupBackendResolver;\n renderBackendResolver?: RenderBackendResolver;\n onRenderComplete?: OnRenderComplete;\n};\n\nexport type ParseEffieResult =\n | { effie: EffieData<EffieSources> }\n | {\n error: string;\n code: ErrorCodeType;\n issues?: Array<{ path: string; message: string }>;\n };\n\n/**\n * Create the server context with configuration from environment variables\n */\nexport async function createServerContext(options?: {\n warmupBackendResolver?: WarmupBackendResolver;\n renderBackendResolver?: RenderBackendResolver;\n httpProxy?: boolean;\n onRenderComplete?: OnRenderComplete;\n}): Promise<ServerContext> {\n const port = process.env.FFS_PORT || process.env.PORT || 2000;\n const enableHttpProxy = options?.httpProxy ?? true;\n let httpProxy: HttpProxy | undefined;\n if (enableHttpProxy) {\n httpProxy = new HttpProxy();\n await httpProxy.start();\n }\n return {\n transientStore: createTransientStore(),\n httpProxy,\n baseUrl: process.env.FFS_BASE_URL || `http://localhost:${port}`,\n skipValidation:\n !!process.env.FFS_SKIP_VALIDATION &&\n process.env.FFS_SKIP_VALIDATION !== \"false\",\n warmupConcurrency: parseInt(process.env.FFS_WARMUP_CONCURRENCY || \"4\", 10),\n warmupBackendResolver: options?.warmupBackendResolver,\n renderBackendResolver: options?.renderBackendResolver,\n onRenderComplete: options?.onRenderComplete,\n };\n}\n\n/**\n * Parse and validate Effie data from request body\n */\nexport function parseEffieData(\n body: unknown,\n skipValidation: boolean,\n): ParseEffieResult {\n // Wrapped format has `effie` property\n const isWrapped =\n typeof body === \"object\" && body !== null && \"effie\" in body;\n const rawEffieData = isWrapped ? (body as { effie: unknown }).effie : body;\n\n if (!skipValidation) {\n const result = effieDataSchema.safeParse(rawEffieData);\n if (!result.success) {\n return {\n error: \"Invalid effie data\",\n code: ErrorCode.INVALID_EFFIE,\n issues: result.error.issues.map((issue) => ({\n path: issue.path.join(\".\"),\n message: issue.message,\n })),\n };\n }\n return { effie: result.data };\n } else {\n const effie = rawEffieData as EffieData<EffieSources>;\n if (!effie?.segments) {\n return {\n error: \"Invalid effie data: missing segments\",\n code: ErrorCode.INVALID_EFFIE,\n };\n }\n return { effie };\n }\n}\n\n/**\n * Set up CORS headers for public endpoints\n */\nexport function setupCORSHeaders(res: express.Response): void {\n res.setHeader(\"Access-Control-Allow-Origin\", \"*\");\n res.setHeader(\"Access-Control-Allow-Methods\", \"GET\");\n}\n\n/**\n * Set up SSE response headers\n */\nexport function setupSSEResponse(res: express.Response): void {\n res.setHeader(\"Content-Type\", \"text/event-stream\");\n res.setHeader(\"Cache-Control\", \"no-cache\");\n res.setHeader(\"Connection\", \"keep-alive\");\n res.flushHeaders();\n}\n\n/**\n * Create an SSE event sender function for a response\n */\nexport function createEventSender(res: express.Response): EventSender;\nexport function createEventSender<TMap extends Record<string, unknown>>(\n res: express.Response,\n): TypedEventSender<TMap>;\nexport function createEventSender(res: express.Response): EventSender {\n return (event: string, data: object) => {\n res.write(`event: ${event}\\ndata: ${JSON.stringify(data)}\\n\\n`);\n };\n}\n\n/**\n * Create a prefixed event sender that adds a prefix to event names\n */\nexport function prefixEventSender<TMap extends Record<string, unknown>>(\n sendEvent: EventSender,\n prefix: string,\n): TypedEventSender<TMap> {\n return ((event: string, data: object) => {\n sendEvent(`${prefix}${event}`, data);\n }) as TypedEventSender<TMap>;\n}\n\n/**\n * Proxy SSE events from a remote backend, prefixing event names\n */\nexport async function proxyRemoteSSE(\n url: string,\n sendEvent: EventSender,\n prefix: string,\n res: express.Response,\n headers?: Record<string, string>,\n): Promise<void> {\n const response = await ffsFetch(url, {\n headers: {\n Accept: \"text/event-stream\",\n ...headers,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Remote backend error: ${response.status}`);\n }\n\n const reader = response.body?.getReader();\n if (!reader) {\n throw new Error(\"No response body from remote backend\");\n }\n\n const decoder = new TextDecoder();\n let buffer = \"\";\n let currentEvent = \"\";\n let currentData = \"\";\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n // Check if client disconnected\n if (res.destroyed) {\n reader.cancel();\n break;\n }\n\n buffer += decoder.decode(value, { stream: true });\n\n // Parse SSE events from buffer\n const lines = buffer.split(\"\\n\");\n buffer = lines.pop() || \"\"; // Keep incomplete line in buffer\n\n for (const line of lines) {\n if (line.startsWith(\"event: \")) {\n currentEvent = line.slice(7);\n } else if (line.startsWith(\"data: \")) {\n currentData = line.slice(6);\n } else if (line === \"\" && currentEvent && currentData) {\n // End of event, forward it with prefix\n try {\n const data = JSON.parse(currentData);\n sendEvent(`${prefix}${currentEvent}`, data);\n } catch {\n // Skip malformed JSON\n }\n currentEvent = \"\";\n currentData = \"\";\n }\n }\n }\n } finally {\n reader.releaseLock();\n }\n}\n\n/**\n * Proxy a binary stream (e.g., video) from a fetch Response to an Express response.\n * Forwards Content-Type and Content-Length headers.\n */\nexport async function proxyBinaryStream(\n response: UndiciResponse,\n res: express.Response,\n): Promise<void> {\n const contentType = response.headers.get(\"content-type\");\n if (contentType) res.set(\"Content-Type\", contentType);\n\n const contentLength = response.headers.get(\"content-length\");\n if (contentLength) res.set(\"Content-Length\", contentLength);\n\n const reader = response.body?.getReader();\n if (!reader) {\n throw new Error(\"No response body\");\n }\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n if (res.destroyed) {\n reader.cancel();\n break;\n }\n\n res.write(value);\n }\n } finally {\n reader.releaseLock();\n res.end();\n }\n}\n","import http from \"http\";\nimport type { AddressInfo, Server } from \"net\";\nimport { Readable } from \"stream\";\nimport { ffsFetch } from \"./fetch\";\n\n/**\n * HTTP proxy for FFmpeg URL handling.\n *\n * Static FFmpeg binaries can have DNS resolution issues on Alpine Linux (musl libc).\n * This proxy lets Node.js handle DNS lookups instead of FFmpeg by proxying HTTP\n * requests through localhost.\n *\n * URL scheme (M3U8-compatible):\n * - Original: https://cdn.example.com/path/to/stream.m3u8\n * - Proxy: http://127.0.0.1:{port}/https://cdn.example.com/path/to/stream.m3u8\n * - Relative: segment-0.ts → http://127.0.0.1:{port}/https://cdn.example.com/path/to/segment-0.ts\n */\nexport class HttpProxy {\n private server: Server | null = null;\n private _port: number | null = null;\n private startPromise: Promise<void> | null = null;\n\n get port(): number | null {\n return this._port;\n }\n\n /**\n * Transform a URL to go through the proxy.\n * @throws Error if proxy not started\n */\n transformUrl(url: string): string {\n if (this._port === null) throw new Error(\"Proxy not started\");\n return `http://127.0.0.1:${this._port}/${url}`;\n }\n\n /**\n * Start the proxy server. Safe to call multiple times.\n */\n async start(): Promise<void> {\n if (this._port !== null) return;\n if (this.startPromise) {\n await this.startPromise;\n return;\n }\n this.startPromise = this.doStart();\n await this.startPromise;\n }\n\n private async doStart(): Promise<void> {\n this.server = http.createServer(async (req, res) => {\n try {\n const originalUrl = this.parseProxyPath(req.url || \"\");\n if (!originalUrl) {\n res.writeHead(400, { \"Content-Type\": \"text/plain\" });\n res.end(\"Bad Request: invalid proxy path\");\n return;\n }\n\n const response = await ffsFetch(originalUrl, {\n method: req.method as \"GET\" | \"HEAD\" | undefined,\n headers: this.filterHeaders(req.headers),\n bodyTimeout: 0, // No timeout for streaming\n });\n\n // Convert response headers to plain object\n const headers: Record<string, string> = {};\n response.headers.forEach((value, key) => {\n headers[key] = value;\n });\n\n res.writeHead(response.status, headers);\n\n if (response.body) {\n const nodeStream = Readable.fromWeb(response.body);\n nodeStream.pipe(res);\n nodeStream.on(\"error\", (err) => {\n console.error(\"Proxy stream error:\", err);\n res.destroy();\n });\n } else {\n res.end();\n }\n } catch (err) {\n console.error(\"Proxy request error:\", err);\n if (!res.headersSent) {\n res.writeHead(502, { \"Content-Type\": \"text/plain\" });\n res.end(\"Bad Gateway\");\n } else {\n res.destroy();\n }\n }\n });\n\n await new Promise<void>((resolve) => {\n this.server!.listen(0, \"127.0.0.1\", () => {\n this._port = (this.server!.address() as AddressInfo).port;\n resolve();\n });\n });\n }\n\n /**\n * Parse the proxy path to extract the original URL.\n * Path format: /{originalUrl}\n */\n private parseProxyPath(path: string): string | null {\n if (!path.startsWith(\"/http://\") && !path.startsWith(\"/https://\")) {\n return null;\n }\n return path.slice(1); // Remove leading /\n }\n\n /**\n * Filter headers to forward to the upstream server.\n * Removes hop-by-hop headers that shouldn't be forwarded.\n */\n private filterHeaders(\n headers: http.IncomingHttpHeaders,\n ): Record<string, string> {\n const skip = new Set([\n \"host\",\n \"connection\",\n \"keep-alive\",\n \"transfer-encoding\",\n \"te\",\n \"trailer\",\n \"upgrade\",\n \"proxy-authorization\",\n \"proxy-authenticate\",\n ]);\n\n const result: Record<string, string> = {};\n for (const [key, value] of Object.entries(headers)) {\n if (!skip.has(key.toLowerCase()) && typeof value === \"string\") {\n result[key] = value;\n }\n }\n return result;\n }\n\n /**\n * Close the proxy server and reset state.\n */\n close(): void {\n this.server?.close();\n this.server = null;\n this._port = null;\n this.startPromise = null;\n }\n}\n","import type express from \"express\";\n\nexport const ErrorCode = {\n UNAUTHORIZED: \"UNAUTHORIZED\",\n INVALID_EFFIE: \"INVALID_EFFIE\",\n NOT_FOUND: \"NOT_FOUND\",\n BACKEND_FAILED: \"BACKEND_FAILED\",\n INTERNAL_ERROR: \"INTERNAL_ERROR\",\n FETCH_FAILED: \"FETCH_FAILED\",\n} as const;\n\nexport type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode];\n\nexport type ApiError = {\n error: string;\n code: ErrorCode;\n issues?: Array<{ path: string; message: string }>;\n};\n\nexport function sendError(\n res: express.Response,\n status: number,\n code: ErrorCode,\n message: string,\n issues?: Array<{ path: string; message: string }>,\n): void {\n if (res.headersSent) return;\n const body: ApiError = { error: message, code };\n if (issues) body.issues = issues;\n res.status(status).json(body);\n}\n","import express from \"express\";\nimport { Readable, Transform } from \"stream\";\nimport { randomUUID } from \"crypto\";\nimport type { TransientStore } from \"../storage\";\nimport { storeKeys } from \"../storage\";\nimport { ffsFetch } from \"../fetch\";\nimport {\n extractEffieSources,\n extractEffieSourcesWithTypes,\n} from \"@effing/effie\";\nimport type { EffieSourceWithType } from \"@effing/effie\";\nimport type { WarmupEventMap, WarmupEventSender } from \"../sse\";\nimport type { ServerContext, WarmupJob } from \"./shared\";\nimport {\n parseEffieData,\n setupCORSHeaders,\n setupSSEResponse,\n createEventSender,\n} from \"./shared\";\nimport { proxyRemoteSSE } from \"./shared\";\nimport { sendError, ErrorCode } from \"./errors\";\n\n/**\n * Check if a source should be skipped during warmup.\n * Video/audio sources are passed directly to FFmpeg and don't need caching.\n */\nfunction shouldSkipWarmup(source: EffieSourceWithType): boolean {\n return source.type === \"video\" || source.type === \"audio\";\n}\n\n// Track in-flight fetches to avoid duplicate fetches within the same instance\nconst inFlightFetches = new Map<string, Promise<void>>();\n\n/**\n * POST /warmup - Create a warmup job\n * Stores the source list in cache and returns a job ID for SSE streaming\n */\nexport async function createWarmupJob(\n req: express.Request,\n res: express.Response,\n ctx: ServerContext,\n options?: {\n metadata?: Record<string, unknown>;\n timings?: Record<string, number>;\n },\n): Promise<void> {\n try {\n const validationStart = performance.now();\n const parseResult = parseEffieData(req.body, ctx.skipValidation);\n if (options?.timings) {\n options.timings.validation = performance.now() - validationStart;\n }\n if (\"error\" in parseResult) {\n res.status(400).json(parseResult);\n return;\n }\n\n const sources = extractEffieSourcesWithTypes(parseResult.effie);\n const jobId = randomUUID();\n\n const job: WarmupJob = { sources, metadata: options?.metadata };\n const storeJobStart = performance.now();\n await ctx.transientStore.putJson(\n storeKeys.warmupJob(jobId),\n job,\n ctx.transientStore.ttlMs,\n );\n if (options?.timings) {\n options.timings.storeJob = performance.now() - storeJobStart;\n }\n\n res.json({\n id: jobId,\n progressUrl: `${ctx.baseUrl}/warmup/${jobId}/progress`,\n });\n } catch (error) {\n console.error(\"Error creating warmup job:\", error);\n sendError(\n res,\n 500,\n ErrorCode.INTERNAL_ERROR,\n \"Failed to create warmup job\",\n );\n }\n}\n\n/**\n * GET /warmup/:id/progress - Stream warmup progress via SSE\n * Fetches and caches sources, emitting progress events\n */\nexport async function streamWarmupProgress(\n req: express.Request,\n res: express.Response,\n ctx: ServerContext,\n): Promise<void> {\n try {\n setupCORSHeaders(res);\n\n const jobId = req.params.id;\n\n const jobStoreKey = storeKeys.warmupJob(jobId);\n const job = await ctx.transientStore.getJson<WarmupJob>(jobStoreKey);\n\n if (!job) {\n sendError(res, 404, ErrorCode.NOT_FOUND, \"Job not found\");\n return;\n }\n\n // Proxy to warmup backend if resolver is configured\n if (ctx.warmupBackendResolver) {\n const backend = ctx.warmupBackendResolver(job.sources, job.metadata);\n if (backend) {\n setupSSEResponse(res);\n const sendEvent = createEventSender(res);\n try {\n await proxyRemoteSSE(\n `${backend.baseUrl}/warmup/${jobId}/progress`,\n sendEvent,\n \"\",\n res,\n backend.apiKey\n ? { Authorization: `Bearer ${backend.apiKey}` }\n : undefined,\n );\n } finally {\n res.end();\n }\n return;\n }\n }\n\n // Local warmup — only allow the warmup job to run once\n ctx.transientStore.delete(jobStoreKey);\n\n setupSSEResponse(res);\n const sendEvent = createEventSender<WarmupEventMap>(res);\n\n try {\n await warmupSources(job.sources, sendEvent, ctx);\n sendEvent(\"complete\", { status: \"ready\" });\n } catch (error) {\n sendEvent(\"error\", { message: String(error) });\n } finally {\n res.end();\n }\n } catch (error) {\n console.error(\"Error in warmup streaming:\", error);\n if (!res.headersSent) {\n sendError(res, 500, ErrorCode.INTERNAL_ERROR, \"Warmup streaming failed\");\n } else {\n res.end();\n }\n }\n}\n\n/**\n * Purge cached sources by URL list.\n * Returns the number purged and total.\n */\nexport async function purgeCachedSources(\n urls: string[],\n store: TransientStore,\n): Promise<{ purged: number; total: number }> {\n let purged = 0;\n for (const url of urls) {\n const ck = storeKeys.source(url);\n if (await store.exists(ck)) {\n await store.delete(ck);\n purged++;\n }\n }\n return { purged, total: urls.length };\n}\n\n/**\n * POST /purge - Purge cached sources for an Effie composition\n */\nexport async function purgeCache(\n req: express.Request,\n res: express.Response,\n ctx: ServerContext,\n options?: { timings?: Record<string, number> },\n): Promise<void> {\n try {\n const validationStart = performance.now();\n const parseResult = parseEffieData(req.body, ctx.skipValidation);\n if (options?.timings) {\n options.timings.validation = performance.now() - validationStart;\n }\n if (\"error\" in parseResult) {\n res.status(400).json(parseResult);\n return;\n }\n\n const sources = extractEffieSources(parseResult.effie);\n const purgeStart = performance.now();\n const result = await purgeCachedSources(sources, ctx.transientStore);\n if (options?.timings) {\n options.timings.purge = performance.now() - purgeStart;\n }\n\n res.json(result);\n } catch (error) {\n console.error(\"Error purging cache:\", error);\n sendError(res, 500, ErrorCode.INTERNAL_ERROR, \"Failed to purge cache\");\n }\n}\n\n/**\n * Warm up sources by fetching and caching them.\n * HTTP(S) video/audio sources are skipped as they are passed directly to FFmpeg.\n */\nexport async function warmupSources(\n sources: EffieSourceWithType[],\n sendEvent: WarmupEventSender,\n ctx: ServerContext,\n): Promise<void> {\n const total = sources.length;\n\n sendEvent(\"start\", { total });\n\n let cached = 0;\n let failed = 0;\n let skipped = 0;\n\n // Separate sources that need caching from those that should be skipped\n const sourcesToCache: EffieSourceWithType[] = [];\n for (const source of sources) {\n if (shouldSkipWarmup(source)) {\n skipped++;\n sendEvent(\"progress\", {\n url: source.url,\n status: \"skipped\",\n reason: \"http-video-audio-passthrough\",\n cached,\n failed,\n skipped,\n total,\n });\n } else {\n sourcesToCache.push(source);\n }\n }\n\n // Check what's already cached\n const sourceCacheKeys = sourcesToCache.map((s) => storeKeys.source(s.url));\n const existsMap = await ctx.transientStore.existsMany(sourceCacheKeys);\n\n // Report hits immediately\n for (let i = 0; i < sourcesToCache.length; i++) {\n if (existsMap.get(sourceCacheKeys[i])) {\n cached++;\n sendEvent(\"progress\", {\n url: sourcesToCache[i].url,\n status: \"hit\",\n cached,\n failed,\n skipped,\n total,\n });\n }\n }\n\n // Filter to uncached sources\n const uncached = sourcesToCache.filter(\n (_, i) => !existsMap.get(sourceCacheKeys[i]),\n );\n\n if (uncached.length === 0) {\n sendEvent(\"summary\", { cached, failed, skipped, total });\n return;\n }\n\n // Keepalive interval for long-running fetches\n const keepalive = setInterval(() => {\n sendEvent(\"keepalive\", { cached, failed, skipped, total });\n }, 25_000);\n\n // Fetch uncached sources with concurrency limit\n const queue = [...uncached];\n const workers = Array.from(\n { length: Math.min(ctx.warmupConcurrency, queue.length) },\n async () => {\n while (queue.length > 0) {\n const source = queue.shift()!;\n const cacheKey = storeKeys.source(source.url);\n const startTime = Date.now();\n\n try {\n // Check if another worker is already fetching this\n let fetchPromise = inFlightFetches.get(cacheKey);\n if (!fetchPromise) {\n fetchPromise = fetchAndCache(source.url, cacheKey, sendEvent, ctx);\n inFlightFetches.set(cacheKey, fetchPromise);\n }\n\n await fetchPromise;\n inFlightFetches.delete(cacheKey);\n\n cached++;\n sendEvent(\"progress\", {\n url: source.url,\n status: \"cached\",\n cached,\n failed,\n skipped,\n total,\n ms: Date.now() - startTime,\n });\n } catch (error) {\n inFlightFetches.delete(cacheKey);\n failed++;\n sendEvent(\"progress\", {\n url: source.url,\n status: \"error\",\n error: String(error),\n cached,\n failed,\n skipped,\n total,\n ms: Date.now() - startTime,\n });\n }\n }\n },\n );\n\n await Promise.all(workers);\n clearInterval(keepalive);\n\n sendEvent(\"summary\", { cached, failed, skipped, total });\n}\n\n/**\n * Fetch a source and cache it, with streaming progress events\n */\nexport async function fetchAndCache(\n url: string,\n cacheKey: string,\n sendEvent: WarmupEventSender,\n ctx: ServerContext,\n): Promise<void> {\n const response = await ffsFetch(url, {\n headersTimeout: 10 * 60 * 1000, // 10 minutes\n bodyTimeout: 20 * 60 * 1000, // 20 minutes\n });\n\n if (!response.ok) {\n throw new Error(`${response.status} ${response.statusText}`);\n }\n\n sendEvent(\"downloading\", { url, status: \"started\", bytesReceived: 0 });\n\n // Stream through a progress tracker\n const sourceStream = Readable.fromWeb(\n response.body as import(\"stream/web\").ReadableStream,\n );\n\n let totalBytes = 0;\n let lastEventTime = Date.now();\n const PROGRESS_INTERVAL = 10_000; // 10 seconds\n\n const progressStream = new Transform({\n transform(chunk, _encoding, callback) {\n totalBytes += chunk.length;\n const now = Date.now();\n if (now - lastEventTime >= PROGRESS_INTERVAL) {\n sendEvent(\"downloading\", {\n url,\n status: \"downloading\",\n bytesReceived: totalBytes,\n });\n lastEventTime = now;\n }\n callback(null, chunk);\n },\n });\n\n // Pipe through progress tracker to cache storage with source TTL\n const trackedStream = sourceStream.pipe(progressStream);\n await ctx.transientStore.put(\n cacheKey,\n trackedStream,\n ctx.transientStore.ttlMs,\n );\n}\n","import express from \"express\";\nimport { randomUUID } from \"crypto\";\nimport { storeKeys } from \"../storage\";\nimport { ffsFetch } from \"../fetch\";\nimport {\n extractEffieSourcesWithTypes,\n extractEffieSources,\n} from \"@effing/effie\";\nimport type { EffieData, EffieSources } from \"@effing/effie\";\nimport type { RenderEventMap, RenderEventSender, WarmupEventMap } from \"../sse\";\nimport type {\n ServerContext,\n RenderJob,\n ResolvedRenderJob,\n DeferredRenderJob,\n VideoJob,\n UploadOptions,\n} from \"./shared\";\nimport {\n parseEffieData,\n setupCORSHeaders,\n setupSSEResponse,\n createEventSender,\n prefixEventSender,\n proxyRemoteSSE,\n proxyBinaryStream,\n} from \"./shared\";\nimport { warmupSources, purgeCachedSources } from \"./caching\";\nimport { sendError, ErrorCode } from \"./errors\";\n\n/**\n * POST /render - Create a render job (warmup + render, optional purge)\n * Returns a job ID and progress URL for SSE streaming\n */\nexport async function createRenderJob(\n req: express.Request,\n res: express.Response,\n ctx: ServerContext,\n options?: {\n metadata?: Record<string, unknown>;\n timings?: Record<string, number>;\n },\n): Promise<void> {\n try {\n // Parse request body\n const body = req.body as Record<string, unknown>;\n\n const scale =\n (body.scale as number | undefined) ??\n (req.query?.scale ? parseFloat(req.query.scale as string) : undefined) ??\n 1;\n const purge =\n (body.purge as boolean | undefined) ??\n (req.query?.purge === \"true\" ? true : undefined) ??\n false;\n const upload = body.upload as UploadOptions | undefined;\n\n // Create IDs\n const jobId = randomUUID();\n const warmupJobId = randomUUID();\n\n // URL handling: defer fetch to progress stream\n if (typeof body.effie === \"string\") {\n const job: DeferredRenderJob = {\n kind: \"deferred\",\n effieUrl: body.effie,\n scale,\n upload,\n purge,\n warmupJobId,\n createdAt: Date.now(),\n metadata: options?.metadata,\n };\n\n const storeJobStart = performance.now();\n await ctx.transientStore.putJson(\n storeKeys.renderJob(jobId),\n job,\n ctx.transientStore.ttlMs,\n );\n if (options?.timings) {\n options.timings.storeJob = performance.now() - storeJobStart;\n }\n\n res.json({\n id: jobId,\n progressUrl: `${ctx.baseUrl}/render/${jobId}/progress`,\n });\n return;\n }\n\n // Parse & validate effie data (supports both wrapped and raw formats)\n const validationStart = performance.now();\n const parseResult = parseEffieData(body, ctx.skipValidation);\n if (options?.timings) {\n options.timings.validation = performance.now() - validationStart;\n }\n if (\"error\" in parseResult) {\n sendError(\n res,\n 400,\n parseResult.code,\n parseResult.error,\n parseResult.issues,\n );\n return;\n }\n const effie = parseResult.effie;\n\n const sources = extractEffieSourcesWithTypes(effie);\n\n // Store the render job\n const job: ResolvedRenderJob = {\n kind: \"resolved\",\n effie,\n sources,\n scale,\n upload,\n purge,\n warmupJobId,\n createdAt: Date.now(),\n metadata: options?.metadata,\n };\n\n const storeJobStart = performance.now();\n await ctx.transientStore.putJson(\n storeKeys.renderJob(jobId),\n job,\n ctx.transientStore.ttlMs,\n );\n\n // Store warmup sub-job for backend execution\n await ctx.transientStore.putJson(\n storeKeys.warmupJob(warmupJobId),\n { sources, metadata: options?.metadata },\n ctx.transientStore.ttlMs,\n );\n if (options?.timings) {\n options.timings.storeJob = performance.now() - storeJobStart;\n }\n\n res.json({\n id: jobId,\n progressUrl: `${ctx.baseUrl}/render/${jobId}/progress`,\n });\n } catch (error) {\n console.error(\"Error creating render job:\", error);\n sendError(\n res,\n 500,\n ErrorCode.INTERNAL_ERROR,\n \"Failed to create render job\",\n );\n }\n}\n\n/**\n * Resolve a deferred Effie URL: fetch, parse, validate, extract sources.\n * Emits effie:fetching/effie:fetched SSE events. Throws on failure.\n */\nasync function resolveEffieUrl(\n deferred: DeferredRenderJob,\n sendEvent: ReturnType<typeof createEventSender<RenderEventMap>>,\n ctx: ServerContext,\n): Promise<ResolvedRenderJob> {\n const url = deferred.effieUrl;\n sendEvent(\"effie:fetching\", { url });\n\n let response;\n try {\n response = await ffsFetch(url);\n } catch (error) {\n throw new Error(\n `Failed to fetch Effie data: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n if (!response.ok) {\n throw new Error(\n `Failed to fetch Effie data: ${response.status} ${response.statusText}`,\n );\n }\n\n const body = { effie: await response.json() };\n const parseResult = parseEffieData(body, ctx.skipValidation);\n if (\"error\" in parseResult) {\n throw new Error(parseResult.error);\n }\n\n const effie = parseResult.effie;\n const sources = extractEffieSourcesWithTypes(effie);\n\n sendEvent(\"effie:fetched\", { url });\n\n return {\n kind: \"resolved\",\n effie,\n sources,\n scale: deferred.scale,\n upload: deferred.upload,\n purge: deferred.purge,\n warmupJobId: deferred.warmupJobId,\n createdAt: deferred.createdAt,\n metadata: deferred.metadata,\n };\n}\n\n/**\n * GET /render/:id/progress - Stream render progress via SSE\n * Orchestrates warmup (local or remote) followed by render (local or remote)\n */\nexport async function streamRenderProgress(\n req: express.Request,\n res: express.Response,\n ctx: ServerContext,\n): Promise<void> {\n try {\n setupCORSHeaders(res);\n\n const jobId = req.params.id;\n const jobStoreKey = storeKeys.renderJob(jobId);\n const storedJob = await ctx.transientStore.getJson<RenderJob>(jobStoreKey);\n\n if (!storedJob) {\n sendError(res, 404, ErrorCode.NOT_FOUND, \"Job not found\");\n return;\n }\n\n // Only allow the job to run once\n ctx.transientStore.delete(jobStoreKey);\n\n setupSSEResponse(res);\n const sendEvent = createEventSender<RenderEventMap>(res);\n const rawSendEvent = createEventSender(res);\n\n // Keepalive interval for long-running operations\n let keepalivePhase: \"effie\" | \"warmup\" | \"render\" | \"upload\" =\n storedJob.kind === \"deferred\" ? \"effie\" : \"warmup\";\n const keepalive = setInterval(() => {\n sendEvent(\"keepalive\", { phase: keepalivePhase });\n }, 25_000);\n\n try {\n // Phase -1: Resolve deferred Effie URL if needed\n // Backward compat: jobs without `kind` (from before deploy) are treated as resolved\n let job: ResolvedRenderJob;\n if (storedJob.kind === \"deferred\") {\n job = await resolveEffieUrl(storedJob, sendEvent, ctx);\n\n // Store warmup sub-job now that we have sources\n await ctx.transientStore.putJson(\n storeKeys.warmupJob(job.warmupJobId),\n { sources: job.sources, metadata: job.metadata },\n ctx.transientStore.ttlMs,\n );\n\n keepalivePhase = \"warmup\";\n } else {\n // resolved or legacy (no kind field)\n job = storedJob as ResolvedRenderJob;\n }\n\n // Resolve backends up front\n const warmupBackend = ctx.warmupBackendResolver\n ? ctx.warmupBackendResolver(job.sources, job.metadata)\n : null;\n const renderBackend = ctx.renderBackendResolver\n ? ctx.renderBackendResolver(job.effie, job.metadata)\n : null;\n\n // Phase 0: Purge (if requested)\n if (job.purge) {\n const sourceUrls = extractEffieSources(job.effie);\n const purgeResult = await purgeCachedSources(\n sourceUrls,\n ctx.transientStore,\n );\n sendEvent(\"purge:complete\", purgeResult);\n }\n\n // Phase 1: Warmup\n if (warmupBackend) {\n // Proxy warmup from remote backend\n await proxyRemoteSSE(\n `${warmupBackend.baseUrl}/warmup/${job.warmupJobId}/progress`,\n rawSendEvent,\n \"warmup:\",\n res,\n warmupBackend.apiKey\n ? { Authorization: `Bearer ${warmupBackend.apiKey}` }\n : undefined,\n );\n } else {\n // Local warmup execution\n const warmupSender = prefixEventSender<WarmupEventMap>(\n rawSendEvent,\n \"warmup:\",\n );\n await warmupSources(job.sources, warmupSender, ctx);\n warmupSender(\"complete\", { status: \"ready\" });\n }\n\n // Phase 2: Render\n keepalivePhase = \"render\";\n\n if (job.upload) {\n keepalivePhase = \"upload\";\n const renderStart = performance.now();\n if (renderBackend) {\n // Upload + backend: store VideoJob for backend to render,\n // fetch binary video from backend, upload locally.\n const videoJob: VideoJob = {\n effie: job.effie,\n scale: job.scale,\n metadata: job.metadata,\n };\n await ctx.transientStore.putJson(\n storeKeys.videoJob(jobId),\n videoJob,\n ctx.transientStore.ttlMs,\n );\n\n const backendUrl = `${renderBackend.baseUrl}/render/${jobId}/video`;\n const response = await ffsFetch(backendUrl, {\n headers: renderBackend.apiKey\n ? { Authorization: `Bearer ${renderBackend.apiKey}` }\n : undefined,\n });\n if (!response.ok) {\n throw new Error(`Backend render failed: ${response.status}`);\n }\n const videoBuffer = Buffer.from(await response.arrayBuffer());\n\n const timings = await uploadRenderedVideo(\n videoBuffer,\n job.effie,\n job.upload,\n sendEvent,\n );\n sendEvent(\n \"render:complete\",\n timings as RenderEventMap[\"render:complete\"],\n );\n } else {\n // Upload + no backend: render and upload locally (no VideoJob stored)\n const timings = await renderAndUploadInternal(\n job.effie,\n job.scale,\n job.upload,\n sendEvent,\n ctx,\n );\n sendEvent(\n \"render:complete\",\n timings as RenderEventMap[\"render:complete\"],\n );\n }\n\n if (ctx.onRenderComplete) {\n try {\n await ctx.onRenderComplete({\n effie: job.effie,\n metadata: job.metadata,\n wallClockTime: performance.now() - renderStart,\n });\n } catch (err) {\n console.error(\"onRenderComplete hook error:\", err);\n }\n }\n\n sendEvent(\"complete\", { status: \"done\" });\n } else {\n // Non-upload mode: store VideoJob for on-demand fetch via /render/:id/video\n const videoJob: VideoJob = {\n effie: job.effie,\n scale: job.scale,\n metadata: job.metadata,\n };\n await ctx.transientStore.putJson(\n storeKeys.videoJob(jobId),\n videoJob,\n ctx.transientStore.ttlMs,\n );\n const videoUrl = `${ctx.baseUrl}/render/${jobId}/video`;\n sendEvent(\"ready\", { videoUrl });\n }\n } catch (error) {\n sendEvent(\"error\", {\n phase: keepalivePhase,\n message: String(error),\n });\n } finally {\n clearInterval(keepalive);\n res.end();\n }\n } catch (error) {\n console.error(\"Error in render progress streaming:\", error);\n if (!res.headersSent) {\n sendError(\n res,\n 500,\n ErrorCode.INTERNAL_ERROR,\n \"Render progress streaming failed\",\n );\n } else {\n res.end();\n }\n }\n}\n\n/**\n * GET /render/:id/video - Stream rendered video\n * Reads the video sub-job from the store, deletes it (one-time use), and streams the MP4.\n */\nexport async function streamRenderVideo(\n req: express.Request,\n res: express.Response,\n ctx: ServerContext,\n): Promise<void> {\n try {\n setupCORSHeaders(res);\n\n const jobId = req.params.id;\n const videoJobKey = storeKeys.videoJob(jobId);\n const videoJob = await ctx.transientStore.getJson<VideoJob>(videoJobKey);\n\n if (!videoJob) {\n sendError(res, 404, ErrorCode.NOT_FOUND, \"Video not found or expired\");\n return;\n }\n\n // Proxy to render backend if resolver is configured\n // Don't delete — the backend reads/deletes the VideoJob from shared store\n if (ctx.renderBackendResolver) {\n const backend = ctx.renderBackendResolver(\n videoJob.effie,\n videoJob.metadata,\n );\n if (backend) {\n const renderStart = performance.now();\n const backendUrl = `${backend.baseUrl}/render/${jobId}/video`;\n const response = await ffsFetch(backendUrl, {\n headers: backend.apiKey\n ? { Authorization: `Bearer ${backend.apiKey}` }\n : undefined,\n });\n\n if (!response.ok) {\n sendError(\n res,\n response.status,\n ErrorCode.BACKEND_FAILED,\n \"Backend render failed\",\n );\n return;\n }\n\n await proxyBinaryStream(response, res);\n\n if (ctx.onRenderComplete) {\n try {\n await ctx.onRenderComplete({\n effie: videoJob.effie,\n metadata: videoJob.metadata,\n wallClockTime: performance.now() - renderStart,\n });\n } catch (err) {\n console.error(\"onRenderComplete hook error:\", err);\n }\n }\n return;\n }\n }\n\n // Local render — safe to delete the video job (one-time use)\n ctx.transientStore.delete(videoJobKey);\n\n // Render locally\n await streamRenderDirect(res, videoJob, ctx);\n } catch (error) {\n console.error(\"Error streaming video:\", error);\n if (!res.headersSent) {\n sendError(res, 500, ErrorCode.INTERNAL_ERROR, \"Video streaming failed\");\n } else {\n res.end();\n }\n }\n}\n\n/**\n * Stream video directly to the response (no upload)\n */\nasync function streamRenderDirect(\n res: express.Response,\n job: VideoJob,\n ctx: ServerContext,\n): Promise<void> {\n const renderStart = performance.now();\n const { EffieRenderer } = await import(\"../render\");\n const renderer = new EffieRenderer(job.effie, {\n transientStore: ctx.transientStore,\n httpProxy: ctx.httpProxy,\n });\n\n try {\n const videoStream = await renderer.render(job.scale);\n\n res.on(\"close\", () => {\n videoStream.destroy();\n });\n\n res.set(\"Content-Type\", \"video/mp4\");\n res.set(\"Cache-Control\", \"public, immutable, max-age=86400\");\n\n await new Promise<void>((resolve, reject) => {\n videoStream.pipe(res);\n res.on(\"finish\", resolve);\n res.on(\"error\", reject);\n });\n } finally {\n renderer.close();\n }\n\n if (ctx.onRenderComplete) {\n try {\n await ctx.onRenderComplete({\n effie: job.effie,\n metadata: job.metadata,\n wallClockTime: performance.now() - renderStart,\n });\n } catch (err) {\n console.error(\"onRenderComplete hook error:\", err);\n }\n }\n}\n\n/**\n * Upload a rendered video buffer (and optional cover) to presigned URLs.\n * Shared between local render+upload and backend render+upload flows.\n */\nasync function uploadRenderedVideo(\n videoBuffer: Buffer,\n effie: EffieData<EffieSources>,\n upload: UploadOptions,\n sendEvent: RenderEventSender,\n): Promise<Record<string, number>> {\n const timings: Record<string, number> = {};\n\n // Fetch and upload cover if coverUrl provided\n if (upload.coverUrl) {\n const fetchCoverStartTime = Date.now();\n let coverBuffer: Buffer;\n if (effie.cover.startsWith(\"data:\")) {\n const commaIndex = effie.cover.indexOf(\",\");\n if (commaIndex === -1) {\n throw new Error(\"Invalid cover data URL\");\n }\n const meta = effie.cover.slice(5, commaIndex); // after \"data:\"\n const isBase64 = meta.endsWith(\";base64\");\n const data = effie.cover.slice(commaIndex + 1);\n coverBuffer = isBase64\n ? Buffer.from(data, \"base64\")\n : Buffer.from(decodeURIComponent(data));\n } else {\n const coverFetchResponse = await ffsFetch(effie.cover);\n if (!coverFetchResponse.ok) {\n throw new Error(\n `Failed to fetch cover image: ${coverFetchResponse.status} ${coverFetchResponse.statusText}`,\n );\n }\n coverBuffer = Buffer.from(await coverFetchResponse.arrayBuffer());\n }\n timings.fetchCoverTime = Date.now() - fetchCoverStartTime;\n\n const uploadCoverStartTime = Date.now();\n const uploadCoverResponse = await ffsFetch(upload.coverUrl, {\n method: \"PUT\",\n body: coverBuffer,\n headers: {\n \"Content-Type\": \"image/png\",\n \"Content-Length\": coverBuffer.length.toString(),\n },\n });\n if (!uploadCoverResponse.ok) {\n throw new Error(\n `Failed to upload cover: ${uploadCoverResponse.status} ${uploadCoverResponse.statusText}`,\n );\n }\n timings.uploadCoverTime = Date.now() - uploadCoverStartTime;\n }\n\n // Update keepalive status for upload phase\n sendEvent(\"keepalive\", { phase: \"upload\" });\n\n // Upload rendered video\n const uploadStartTime = Date.now();\n const uploadResponse = await ffsFetch(upload.videoUrl, {\n method: \"PUT\",\n body: videoBuffer,\n headers: {\n \"Content-Type\": \"video/mp4\",\n \"Content-Length\": videoBuffer.length.toString(),\n },\n });\n if (!uploadResponse.ok) {\n throw new Error(\n `Failed to upload rendered video: ${uploadResponse.status} ${uploadResponse.statusText}`,\n );\n }\n timings.uploadTime = Date.now() - uploadStartTime;\n\n return timings;\n}\n\n/**\n * Internal render and upload logic\n * Returns timings for the SSE complete event\n */\nexport async function renderAndUploadInternal(\n effie: EffieData<EffieSources>,\n scale: number,\n upload: UploadOptions,\n sendEvent: RenderEventSender,\n ctx: ServerContext,\n): Promise<Record<string, number>> {\n // Render effie data to video buffer\n const renderStartTime = Date.now();\n const { EffieRenderer } = await import(\"../render\");\n const renderer = new EffieRenderer(effie, {\n transientStore: ctx.transientStore,\n httpProxy: ctx.httpProxy,\n });\n try {\n const videoStream = await renderer.render(scale);\n const chunks: Buffer[] = [];\n for await (const chunk of videoStream) {\n chunks.push(Buffer.from(chunk));\n }\n const videoBuffer = Buffer.concat(chunks);\n const renderTime = Date.now() - renderStartTime;\n\n // Upload video (and cover)\n const timings = await uploadRenderedVideo(\n videoBuffer,\n effie,\n upload,\n sendEvent,\n );\n timings.renderTime = renderTime;\n\n return timings;\n } finally {\n renderer.close();\n }\n}\n"],"mappings":"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","names":["Readable","sendEvent","Readable","randomUUID","extractEffieSourcesWithTypes","extractEffieSources","randomUUID","job","storeJobStart","extractEffieSourcesWithTypes","extractEffieSources"]}
|
package/dist/handlers/index.d.ts
CHANGED
|
@@ -27,6 +27,11 @@ declare function sendError(res: express.Response, status: number, code: ErrorCod
|
|
|
27
27
|
message: string;
|
|
28
28
|
}>): void;
|
|
29
29
|
|
|
30
|
+
type OnRenderComplete = (result: {
|
|
31
|
+
effie: EffieData<EffieSources>;
|
|
32
|
+
metadata?: Record<string, unknown>;
|
|
33
|
+
wallClockTime: number;
|
|
34
|
+
}) => void | Promise<void>;
|
|
30
35
|
type UploadOptions = {
|
|
31
36
|
videoUrl: string;
|
|
32
37
|
coverUrl?: string;
|
|
@@ -76,6 +81,7 @@ type ServerContext = {
|
|
|
76
81
|
warmupConcurrency: number;
|
|
77
82
|
warmupBackendResolver?: WarmupBackendResolver;
|
|
78
83
|
renderBackendResolver?: RenderBackendResolver;
|
|
84
|
+
onRenderComplete?: OnRenderComplete;
|
|
79
85
|
};
|
|
80
86
|
/**
|
|
81
87
|
* Create the server context with configuration from environment variables
|
|
@@ -84,6 +90,7 @@ declare function createServerContext(options?: {
|
|
|
84
90
|
warmupBackendResolver?: WarmupBackendResolver;
|
|
85
91
|
renderBackendResolver?: RenderBackendResolver;
|
|
86
92
|
httpProxy?: boolean;
|
|
93
|
+
onRenderComplete?: OnRenderComplete;
|
|
87
94
|
}): Promise<ServerContext>;
|
|
88
95
|
/**
|
|
89
96
|
* Proxy SSE events from a remote backend, prefixing event names
|
|
@@ -134,4 +141,4 @@ declare function streamRenderProgress(req: express.Request, res: express.Respons
|
|
|
134
141
|
*/
|
|
135
142
|
declare function streamRenderVideo(req: express.Request, res: express.Response, ctx: ServerContext): Promise<void>;
|
|
136
143
|
|
|
137
|
-
export { type ApiError, type BackendConfig, type DeferredRenderJob, ErrorCode, EventSender, type RenderBackendResolver, type RenderJob, type ResolvedRenderJob, type ServerContext, type UploadOptions, type VideoJob, type WarmupBackendResolver, type WarmupJob, createRenderJob, createServerContext, createWarmupJob, proxyBinaryStream, proxyRemoteSSE, purgeCache, sendError, streamRenderProgress, streamRenderVideo, streamWarmupProgress };
|
|
144
|
+
export { type ApiError, type BackendConfig, type DeferredRenderJob, ErrorCode, EventSender, type OnRenderComplete, type RenderBackendResolver, type RenderJob, type ResolvedRenderJob, type ServerContext, type UploadOptions, type VideoJob, type WarmupBackendResolver, type WarmupJob, createRenderJob, createServerContext, createWarmupJob, proxyBinaryStream, proxyRemoteSSE, purgeCache, sendError, streamRenderProgress, streamRenderVideo, streamWarmupProgress };
|
package/dist/handlers/index.js
CHANGED
package/dist/server.js
CHANGED
|
@@ -169,7 +169,8 @@ async function createServerContext(options) {
|
|
|
169
169
|
skipValidation: !!process.env.FFS_SKIP_VALIDATION && process.env.FFS_SKIP_VALIDATION !== "false",
|
|
170
170
|
warmupConcurrency: parseInt(process.env.FFS_WARMUP_CONCURRENCY || "4", 10),
|
|
171
171
|
warmupBackendResolver: options?.warmupBackendResolver,
|
|
172
|
-
renderBackendResolver: options?.renderBackendResolver
|
|
172
|
+
renderBackendResolver: options?.renderBackendResolver,
|
|
173
|
+
onRenderComplete: options?.onRenderComplete
|
|
173
174
|
};
|
|
174
175
|
}
|
|
175
176
|
function parseEffieData(body, skipValidation) {
|
|
@@ -756,6 +757,7 @@ async function streamRenderProgress(req, res, ctx2) {
|
|
|
756
757
|
keepalivePhase = "render";
|
|
757
758
|
if (job.upload) {
|
|
758
759
|
keepalivePhase = "upload";
|
|
760
|
+
const renderStart = performance.now();
|
|
759
761
|
if (renderBackend) {
|
|
760
762
|
const videoJob = {
|
|
761
763
|
effie: job.effie,
|
|
@@ -798,6 +800,17 @@ async function streamRenderProgress(req, res, ctx2) {
|
|
|
798
800
|
timings
|
|
799
801
|
);
|
|
800
802
|
}
|
|
803
|
+
if (ctx2.onRenderComplete) {
|
|
804
|
+
try {
|
|
805
|
+
await ctx2.onRenderComplete({
|
|
806
|
+
effie: job.effie,
|
|
807
|
+
metadata: job.metadata,
|
|
808
|
+
wallClockTime: performance.now() - renderStart
|
|
809
|
+
});
|
|
810
|
+
} catch (err) {
|
|
811
|
+
console.error("onRenderComplete hook error:", err);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
801
814
|
sendEvent("complete", { status: "done" });
|
|
802
815
|
} else {
|
|
803
816
|
const videoJob = {
|
|
@@ -852,6 +865,7 @@ async function streamRenderVideo(req, res, ctx2) {
|
|
|
852
865
|
videoJob.metadata
|
|
853
866
|
);
|
|
854
867
|
if (backend) {
|
|
868
|
+
const renderStart = performance.now();
|
|
855
869
|
const backendUrl = `${backend.baseUrl}/render/${jobId}/video`;
|
|
856
870
|
const response = await ffsFetch(backendUrl, {
|
|
857
871
|
headers: backend.apiKey ? { Authorization: `Bearer ${backend.apiKey}` } : void 0
|
|
@@ -866,6 +880,17 @@ async function streamRenderVideo(req, res, ctx2) {
|
|
|
866
880
|
return;
|
|
867
881
|
}
|
|
868
882
|
await proxyBinaryStream(response, res);
|
|
883
|
+
if (ctx2.onRenderComplete) {
|
|
884
|
+
try {
|
|
885
|
+
await ctx2.onRenderComplete({
|
|
886
|
+
effie: videoJob.effie,
|
|
887
|
+
metadata: videoJob.metadata,
|
|
888
|
+
wallClockTime: performance.now() - renderStart
|
|
889
|
+
});
|
|
890
|
+
} catch (err) {
|
|
891
|
+
console.error("onRenderComplete hook error:", err);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
869
894
|
return;
|
|
870
895
|
}
|
|
871
896
|
}
|
|
@@ -881,19 +906,38 @@ async function streamRenderVideo(req, res, ctx2) {
|
|
|
881
906
|
}
|
|
882
907
|
}
|
|
883
908
|
async function streamRenderDirect(res, job, ctx2) {
|
|
909
|
+
const renderStart = performance.now();
|
|
884
910
|
const { EffieRenderer } = await import("./render-7O2KKH3F.js");
|
|
885
911
|
const renderer = new EffieRenderer(job.effie, {
|
|
886
912
|
transientStore: ctx2.transientStore,
|
|
887
913
|
httpProxy: ctx2.httpProxy
|
|
888
914
|
});
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
915
|
+
try {
|
|
916
|
+
const videoStream = await renderer.render(job.scale);
|
|
917
|
+
res.on("close", () => {
|
|
918
|
+
videoStream.destroy();
|
|
919
|
+
});
|
|
920
|
+
res.set("Content-Type", "video/mp4");
|
|
921
|
+
res.set("Cache-Control", "public, immutable, max-age=86400");
|
|
922
|
+
await new Promise((resolve, reject) => {
|
|
923
|
+
videoStream.pipe(res);
|
|
924
|
+
res.on("finish", resolve);
|
|
925
|
+
res.on("error", reject);
|
|
926
|
+
});
|
|
927
|
+
} finally {
|
|
892
928
|
renderer.close();
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
929
|
+
}
|
|
930
|
+
if (ctx2.onRenderComplete) {
|
|
931
|
+
try {
|
|
932
|
+
await ctx2.onRenderComplete({
|
|
933
|
+
effie: job.effie,
|
|
934
|
+
metadata: job.metadata,
|
|
935
|
+
wallClockTime: performance.now() - renderStart
|
|
936
|
+
});
|
|
937
|
+
} catch (err) {
|
|
938
|
+
console.error("onRenderComplete hook error:", err);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
897
941
|
}
|
|
898
942
|
async function uploadRenderedVideo(videoBuffer, effie, upload, sendEvent) {
|
|
899
943
|
const timings = {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effing/ffs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.1",
|
|
4
4
|
"description": "FFmpeg-based effie rendering service",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -36,10 +36,10 @@
|
|
|
36
36
|
"tar-stream": "^3.1.7",
|
|
37
37
|
"undici": "^7.3.0",
|
|
38
38
|
"zod": "^3.25.76",
|
|
39
|
-
"@effing/effie": "0.
|
|
39
|
+
"@effing/effie": "0.17.1"
|
|
40
40
|
},
|
|
41
41
|
"optionalDependencies": {
|
|
42
|
-
"@effing/ffmpeg": "0.
|
|
42
|
+
"@effing/ffmpeg": "0.17.1"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@types/body-parser": "^1.19.5",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/handlers/shared.ts","../src/proxy.ts","../src/handlers/errors.ts","../src/handlers/caching.ts","../src/handlers/rendering.ts"],"sourcesContent":["import express from \"express\";\nimport type { Response as UndiciResponse } from \"undici\";\nimport type { TransientStore } from \"../storage\";\nimport { createTransientStore } from \"../storage\";\nimport { HttpProxy } from \"../proxy\";\nimport { ffsFetch } from \"../fetch\";\nimport type { TypedEventSender, EventSender } from \"../sse\";\nexport type { EventSender } from \"../sse\";\nimport type {\n EffieData,\n EffieSources,\n EffieSourceWithType,\n} from \"@effing/effie\";\nimport { effieDataSchema } from \"@effing/effie\";\nimport { ErrorCode } from \"./errors\";\nimport type { ErrorCode as ErrorCodeType } from \"./errors\";\n\nexport type UploadOptions = {\n videoUrl: string;\n coverUrl?: string;\n};\n\nexport type BackendConfig = {\n baseUrl: string;\n apiKey?: string;\n};\n\nexport type WarmupBackendResolver = (\n sources: EffieSourceWithType[],\n metadata?: Record<string, unknown>,\n) => BackendConfig | null;\n\nexport type RenderBackendResolver = (\n effie: EffieData<EffieSources>,\n metadata?: Record<string, unknown>,\n) => BackendConfig | null;\n\nexport type WarmupJob = {\n sources: EffieSourceWithType[];\n metadata?: Record<string, unknown>;\n};\n\nexport type ResolvedRenderJob = {\n kind: \"resolved\";\n effie: EffieData<EffieSources>;\n sources: EffieSourceWithType[];\n scale: number;\n upload?: UploadOptions;\n purge?: boolean;\n warmupJobId: string;\n createdAt: number;\n metadata?: Record<string, unknown>;\n};\n\nexport type DeferredRenderJob = {\n kind: \"deferred\";\n effieUrl: string;\n scale: number;\n upload?: UploadOptions;\n purge?: boolean;\n warmupJobId: string;\n createdAt: number;\n metadata?: Record<string, unknown>;\n};\n\nexport type RenderJob = ResolvedRenderJob | DeferredRenderJob;\n\nexport type VideoJob = {\n effie: EffieData<EffieSources>;\n scale: number;\n metadata?: Record<string, unknown>;\n};\n\nexport type ServerContext = {\n transientStore: TransientStore;\n httpProxy?: HttpProxy;\n baseUrl: string;\n skipValidation: boolean;\n warmupConcurrency: number;\n warmupBackendResolver?: WarmupBackendResolver;\n renderBackendResolver?: RenderBackendResolver;\n};\n\nexport type ParseEffieResult =\n | { effie: EffieData<EffieSources> }\n | {\n error: string;\n code: ErrorCodeType;\n issues?: Array<{ path: string; message: string }>;\n };\n\n/**\n * Create the server context with configuration from environment variables\n */\nexport async function createServerContext(options?: {\n warmupBackendResolver?: WarmupBackendResolver;\n renderBackendResolver?: RenderBackendResolver;\n httpProxy?: boolean;\n}): Promise<ServerContext> {\n const port = process.env.FFS_PORT || process.env.PORT || 2000;\n const enableHttpProxy = options?.httpProxy ?? true;\n let httpProxy: HttpProxy | undefined;\n if (enableHttpProxy) {\n httpProxy = new HttpProxy();\n await httpProxy.start();\n }\n return {\n transientStore: createTransientStore(),\n httpProxy,\n baseUrl: process.env.FFS_BASE_URL || `http://localhost:${port}`,\n skipValidation:\n !!process.env.FFS_SKIP_VALIDATION &&\n process.env.FFS_SKIP_VALIDATION !== \"false\",\n warmupConcurrency: parseInt(process.env.FFS_WARMUP_CONCURRENCY || \"4\", 10),\n warmupBackendResolver: options?.warmupBackendResolver,\n renderBackendResolver: options?.renderBackendResolver,\n };\n}\n\n/**\n * Parse and validate Effie data from request body\n */\nexport function parseEffieData(\n body: unknown,\n skipValidation: boolean,\n): ParseEffieResult {\n // Wrapped format has `effie` property\n const isWrapped =\n typeof body === \"object\" && body !== null && \"effie\" in body;\n const rawEffieData = isWrapped ? (body as { effie: unknown }).effie : body;\n\n if (!skipValidation) {\n const result = effieDataSchema.safeParse(rawEffieData);\n if (!result.success) {\n return {\n error: \"Invalid effie data\",\n code: ErrorCode.INVALID_EFFIE,\n issues: result.error.issues.map((issue) => ({\n path: issue.path.join(\".\"),\n message: issue.message,\n })),\n };\n }\n return { effie: result.data };\n } else {\n const effie = rawEffieData as EffieData<EffieSources>;\n if (!effie?.segments) {\n return {\n error: \"Invalid effie data: missing segments\",\n code: ErrorCode.INVALID_EFFIE,\n };\n }\n return { effie };\n }\n}\n\n/**\n * Set up CORS headers for public endpoints\n */\nexport function setupCORSHeaders(res: express.Response): void {\n res.setHeader(\"Access-Control-Allow-Origin\", \"*\");\n res.setHeader(\"Access-Control-Allow-Methods\", \"GET\");\n}\n\n/**\n * Set up SSE response headers\n */\nexport function setupSSEResponse(res: express.Response): void {\n res.setHeader(\"Content-Type\", \"text/event-stream\");\n res.setHeader(\"Cache-Control\", \"no-cache\");\n res.setHeader(\"Connection\", \"keep-alive\");\n res.flushHeaders();\n}\n\n/**\n * Create an SSE event sender function for a response\n */\nexport function createEventSender(res: express.Response): EventSender;\nexport function createEventSender<TMap extends Record<string, unknown>>(\n res: express.Response,\n): TypedEventSender<TMap>;\nexport function createEventSender(res: express.Response): EventSender {\n return (event: string, data: object) => {\n res.write(`event: ${event}\\ndata: ${JSON.stringify(data)}\\n\\n`);\n };\n}\n\n/**\n * Create a prefixed event sender that adds a prefix to event names\n */\nexport function prefixEventSender<TMap extends Record<string, unknown>>(\n sendEvent: EventSender,\n prefix: string,\n): TypedEventSender<TMap> {\n return ((event: string, data: object) => {\n sendEvent(`${prefix}${event}`, data);\n }) as TypedEventSender<TMap>;\n}\n\n/**\n * Proxy SSE events from a remote backend, prefixing event names\n */\nexport async function proxyRemoteSSE(\n url: string,\n sendEvent: EventSender,\n prefix: string,\n res: express.Response,\n headers?: Record<string, string>,\n): Promise<void> {\n const response = await ffsFetch(url, {\n headers: {\n Accept: \"text/event-stream\",\n ...headers,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Remote backend error: ${response.status}`);\n }\n\n const reader = response.body?.getReader();\n if (!reader) {\n throw new Error(\"No response body from remote backend\");\n }\n\n const decoder = new TextDecoder();\n let buffer = \"\";\n let currentEvent = \"\";\n let currentData = \"\";\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n // Check if client disconnected\n if (res.destroyed) {\n reader.cancel();\n break;\n }\n\n buffer += decoder.decode(value, { stream: true });\n\n // Parse SSE events from buffer\n const lines = buffer.split(\"\\n\");\n buffer = lines.pop() || \"\"; // Keep incomplete line in buffer\n\n for (const line of lines) {\n if (line.startsWith(\"event: \")) {\n currentEvent = line.slice(7);\n } else if (line.startsWith(\"data: \")) {\n currentData = line.slice(6);\n } else if (line === \"\" && currentEvent && currentData) {\n // End of event, forward it with prefix\n try {\n const data = JSON.parse(currentData);\n sendEvent(`${prefix}${currentEvent}`, data);\n } catch {\n // Skip malformed JSON\n }\n currentEvent = \"\";\n currentData = \"\";\n }\n }\n }\n } finally {\n reader.releaseLock();\n }\n}\n\n/**\n * Proxy a binary stream (e.g., video) from a fetch Response to an Express response.\n * Forwards Content-Type and Content-Length headers.\n */\nexport async function proxyBinaryStream(\n response: UndiciResponse,\n res: express.Response,\n): Promise<void> {\n const contentType = response.headers.get(\"content-type\");\n if (contentType) res.set(\"Content-Type\", contentType);\n\n const contentLength = response.headers.get(\"content-length\");\n if (contentLength) res.set(\"Content-Length\", contentLength);\n\n const reader = response.body?.getReader();\n if (!reader) {\n throw new Error(\"No response body\");\n }\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n if (res.destroyed) {\n reader.cancel();\n break;\n }\n\n res.write(value);\n }\n } finally {\n reader.releaseLock();\n res.end();\n }\n}\n","import http from \"http\";\nimport type { AddressInfo, Server } from \"net\";\nimport { Readable } from \"stream\";\nimport { ffsFetch } from \"./fetch\";\n\n/**\n * HTTP proxy for FFmpeg URL handling.\n *\n * Static FFmpeg binaries can have DNS resolution issues on Alpine Linux (musl libc).\n * This proxy lets Node.js handle DNS lookups instead of FFmpeg by proxying HTTP\n * requests through localhost.\n *\n * URL scheme (M3U8-compatible):\n * - Original: https://cdn.example.com/path/to/stream.m3u8\n * - Proxy: http://127.0.0.1:{port}/https://cdn.example.com/path/to/stream.m3u8\n * - Relative: segment-0.ts → http://127.0.0.1:{port}/https://cdn.example.com/path/to/segment-0.ts\n */\nexport class HttpProxy {\n private server: Server | null = null;\n private _port: number | null = null;\n private startPromise: Promise<void> | null = null;\n\n get port(): number | null {\n return this._port;\n }\n\n /**\n * Transform a URL to go through the proxy.\n * @throws Error if proxy not started\n */\n transformUrl(url: string): string {\n if (this._port === null) throw new Error(\"Proxy not started\");\n return `http://127.0.0.1:${this._port}/${url}`;\n }\n\n /**\n * Start the proxy server. Safe to call multiple times.\n */\n async start(): Promise<void> {\n if (this._port !== null) return;\n if (this.startPromise) {\n await this.startPromise;\n return;\n }\n this.startPromise = this.doStart();\n await this.startPromise;\n }\n\n private async doStart(): Promise<void> {\n this.server = http.createServer(async (req, res) => {\n try {\n const originalUrl = this.parseProxyPath(req.url || \"\");\n if (!originalUrl) {\n res.writeHead(400, { \"Content-Type\": \"text/plain\" });\n res.end(\"Bad Request: invalid proxy path\");\n return;\n }\n\n const response = await ffsFetch(originalUrl, {\n method: req.method as \"GET\" | \"HEAD\" | undefined,\n headers: this.filterHeaders(req.headers),\n bodyTimeout: 0, // No timeout for streaming\n });\n\n // Convert response headers to plain object\n const headers: Record<string, string> = {};\n response.headers.forEach((value, key) => {\n headers[key] = value;\n });\n\n res.writeHead(response.status, headers);\n\n if (response.body) {\n const nodeStream = Readable.fromWeb(response.body);\n nodeStream.pipe(res);\n nodeStream.on(\"error\", (err) => {\n console.error(\"Proxy stream error:\", err);\n res.destroy();\n });\n } else {\n res.end();\n }\n } catch (err) {\n console.error(\"Proxy request error:\", err);\n if (!res.headersSent) {\n res.writeHead(502, { \"Content-Type\": \"text/plain\" });\n res.end(\"Bad Gateway\");\n } else {\n res.destroy();\n }\n }\n });\n\n await new Promise<void>((resolve) => {\n this.server!.listen(0, \"127.0.0.1\", () => {\n this._port = (this.server!.address() as AddressInfo).port;\n resolve();\n });\n });\n }\n\n /**\n * Parse the proxy path to extract the original URL.\n * Path format: /{originalUrl}\n */\n private parseProxyPath(path: string): string | null {\n if (!path.startsWith(\"/http://\") && !path.startsWith(\"/https://\")) {\n return null;\n }\n return path.slice(1); // Remove leading /\n }\n\n /**\n * Filter headers to forward to the upstream server.\n * Removes hop-by-hop headers that shouldn't be forwarded.\n */\n private filterHeaders(\n headers: http.IncomingHttpHeaders,\n ): Record<string, string> {\n const skip = new Set([\n \"host\",\n \"connection\",\n \"keep-alive\",\n \"transfer-encoding\",\n \"te\",\n \"trailer\",\n \"upgrade\",\n \"proxy-authorization\",\n \"proxy-authenticate\",\n ]);\n\n const result: Record<string, string> = {};\n for (const [key, value] of Object.entries(headers)) {\n if (!skip.has(key.toLowerCase()) && typeof value === \"string\") {\n result[key] = value;\n }\n }\n return result;\n }\n\n /**\n * Close the proxy server and reset state.\n */\n close(): void {\n this.server?.close();\n this.server = null;\n this._port = null;\n this.startPromise = null;\n }\n}\n","import type express from \"express\";\n\nexport const ErrorCode = {\n UNAUTHORIZED: \"UNAUTHORIZED\",\n INVALID_EFFIE: \"INVALID_EFFIE\",\n NOT_FOUND: \"NOT_FOUND\",\n BACKEND_FAILED: \"BACKEND_FAILED\",\n INTERNAL_ERROR: \"INTERNAL_ERROR\",\n FETCH_FAILED: \"FETCH_FAILED\",\n} as const;\n\nexport type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode];\n\nexport type ApiError = {\n error: string;\n code: ErrorCode;\n issues?: Array<{ path: string; message: string }>;\n};\n\nexport function sendError(\n res: express.Response,\n status: number,\n code: ErrorCode,\n message: string,\n issues?: Array<{ path: string; message: string }>,\n): void {\n if (res.headersSent) return;\n const body: ApiError = { error: message, code };\n if (issues) body.issues = issues;\n res.status(status).json(body);\n}\n","import express from \"express\";\nimport { Readable, Transform } from \"stream\";\nimport { randomUUID } from \"crypto\";\nimport type { TransientStore } from \"../storage\";\nimport { storeKeys } from \"../storage\";\nimport { ffsFetch } from \"../fetch\";\nimport {\n extractEffieSources,\n extractEffieSourcesWithTypes,\n} from \"@effing/effie\";\nimport type { EffieSourceWithType } from \"@effing/effie\";\nimport type { WarmupEventMap, WarmupEventSender } from \"../sse\";\nimport type { ServerContext, WarmupJob } from \"./shared\";\nimport {\n parseEffieData,\n setupCORSHeaders,\n setupSSEResponse,\n createEventSender,\n} from \"./shared\";\nimport { proxyRemoteSSE } from \"./shared\";\nimport { sendError, ErrorCode } from \"./errors\";\n\n/**\n * Check if a source should be skipped during warmup.\n * Video/audio sources are passed directly to FFmpeg and don't need caching.\n */\nfunction shouldSkipWarmup(source: EffieSourceWithType): boolean {\n return source.type === \"video\" || source.type === \"audio\";\n}\n\n// Track in-flight fetches to avoid duplicate fetches within the same instance\nconst inFlightFetches = new Map<string, Promise<void>>();\n\n/**\n * POST /warmup - Create a warmup job\n * Stores the source list in cache and returns a job ID for SSE streaming\n */\nexport async function createWarmupJob(\n req: express.Request,\n res: express.Response,\n ctx: ServerContext,\n options?: {\n metadata?: Record<string, unknown>;\n timings?: Record<string, number>;\n },\n): Promise<void> {\n try {\n const validationStart = performance.now();\n const parseResult = parseEffieData(req.body, ctx.skipValidation);\n if (options?.timings) {\n options.timings.validation = performance.now() - validationStart;\n }\n if (\"error\" in parseResult) {\n res.status(400).json(parseResult);\n return;\n }\n\n const sources = extractEffieSourcesWithTypes(parseResult.effie);\n const jobId = randomUUID();\n\n const job: WarmupJob = { sources, metadata: options?.metadata };\n const storeJobStart = performance.now();\n await ctx.transientStore.putJson(\n storeKeys.warmupJob(jobId),\n job,\n ctx.transientStore.ttlMs,\n );\n if (options?.timings) {\n options.timings.storeJob = performance.now() - storeJobStart;\n }\n\n res.json({\n id: jobId,\n progressUrl: `${ctx.baseUrl}/warmup/${jobId}/progress`,\n });\n } catch (error) {\n console.error(\"Error creating warmup job:\", error);\n sendError(\n res,\n 500,\n ErrorCode.INTERNAL_ERROR,\n \"Failed to create warmup job\",\n );\n }\n}\n\n/**\n * GET /warmup/:id/progress - Stream warmup progress via SSE\n * Fetches and caches sources, emitting progress events\n */\nexport async function streamWarmupProgress(\n req: express.Request,\n res: express.Response,\n ctx: ServerContext,\n): Promise<void> {\n try {\n setupCORSHeaders(res);\n\n const jobId = req.params.id;\n\n const jobStoreKey = storeKeys.warmupJob(jobId);\n const job = await ctx.transientStore.getJson<WarmupJob>(jobStoreKey);\n\n if (!job) {\n sendError(res, 404, ErrorCode.NOT_FOUND, \"Job not found\");\n return;\n }\n\n // Proxy to warmup backend if resolver is configured\n if (ctx.warmupBackendResolver) {\n const backend = ctx.warmupBackendResolver(job.sources, job.metadata);\n if (backend) {\n setupSSEResponse(res);\n const sendEvent = createEventSender(res);\n try {\n await proxyRemoteSSE(\n `${backend.baseUrl}/warmup/${jobId}/progress`,\n sendEvent,\n \"\",\n res,\n backend.apiKey\n ? { Authorization: `Bearer ${backend.apiKey}` }\n : undefined,\n );\n } finally {\n res.end();\n }\n return;\n }\n }\n\n // Local warmup — only allow the warmup job to run once\n ctx.transientStore.delete(jobStoreKey);\n\n setupSSEResponse(res);\n const sendEvent = createEventSender<WarmupEventMap>(res);\n\n try {\n await warmupSources(job.sources, sendEvent, ctx);\n sendEvent(\"complete\", { status: \"ready\" });\n } catch (error) {\n sendEvent(\"error\", { message: String(error) });\n } finally {\n res.end();\n }\n } catch (error) {\n console.error(\"Error in warmup streaming:\", error);\n if (!res.headersSent) {\n sendError(res, 500, ErrorCode.INTERNAL_ERROR, \"Warmup streaming failed\");\n } else {\n res.end();\n }\n }\n}\n\n/**\n * Purge cached sources by URL list.\n * Returns the number purged and total.\n */\nexport async function purgeCachedSources(\n urls: string[],\n store: TransientStore,\n): Promise<{ purged: number; total: number }> {\n let purged = 0;\n for (const url of urls) {\n const ck = storeKeys.source(url);\n if (await store.exists(ck)) {\n await store.delete(ck);\n purged++;\n }\n }\n return { purged, total: urls.length };\n}\n\n/**\n * POST /purge - Purge cached sources for an Effie composition\n */\nexport async function purgeCache(\n req: express.Request,\n res: express.Response,\n ctx: ServerContext,\n options?: { timings?: Record<string, number> },\n): Promise<void> {\n try {\n const validationStart = performance.now();\n const parseResult = parseEffieData(req.body, ctx.skipValidation);\n if (options?.timings) {\n options.timings.validation = performance.now() - validationStart;\n }\n if (\"error\" in parseResult) {\n res.status(400).json(parseResult);\n return;\n }\n\n const sources = extractEffieSources(parseResult.effie);\n const purgeStart = performance.now();\n const result = await purgeCachedSources(sources, ctx.transientStore);\n if (options?.timings) {\n options.timings.purge = performance.now() - purgeStart;\n }\n\n res.json(result);\n } catch (error) {\n console.error(\"Error purging cache:\", error);\n sendError(res, 500, ErrorCode.INTERNAL_ERROR, \"Failed to purge cache\");\n }\n}\n\n/**\n * Warm up sources by fetching and caching them.\n * HTTP(S) video/audio sources are skipped as they are passed directly to FFmpeg.\n */\nexport async function warmupSources(\n sources: EffieSourceWithType[],\n sendEvent: WarmupEventSender,\n ctx: ServerContext,\n): Promise<void> {\n const total = sources.length;\n\n sendEvent(\"start\", { total });\n\n let cached = 0;\n let failed = 0;\n let skipped = 0;\n\n // Separate sources that need caching from those that should be skipped\n const sourcesToCache: EffieSourceWithType[] = [];\n for (const source of sources) {\n if (shouldSkipWarmup(source)) {\n skipped++;\n sendEvent(\"progress\", {\n url: source.url,\n status: \"skipped\",\n reason: \"http-video-audio-passthrough\",\n cached,\n failed,\n skipped,\n total,\n });\n } else {\n sourcesToCache.push(source);\n }\n }\n\n // Check what's already cached\n const sourceCacheKeys = sourcesToCache.map((s) => storeKeys.source(s.url));\n const existsMap = await ctx.transientStore.existsMany(sourceCacheKeys);\n\n // Report hits immediately\n for (let i = 0; i < sourcesToCache.length; i++) {\n if (existsMap.get(sourceCacheKeys[i])) {\n cached++;\n sendEvent(\"progress\", {\n url: sourcesToCache[i].url,\n status: \"hit\",\n cached,\n failed,\n skipped,\n total,\n });\n }\n }\n\n // Filter to uncached sources\n const uncached = sourcesToCache.filter(\n (_, i) => !existsMap.get(sourceCacheKeys[i]),\n );\n\n if (uncached.length === 0) {\n sendEvent(\"summary\", { cached, failed, skipped, total });\n return;\n }\n\n // Keepalive interval for long-running fetches\n const keepalive = setInterval(() => {\n sendEvent(\"keepalive\", { cached, failed, skipped, total });\n }, 25_000);\n\n // Fetch uncached sources with concurrency limit\n const queue = [...uncached];\n const workers = Array.from(\n { length: Math.min(ctx.warmupConcurrency, queue.length) },\n async () => {\n while (queue.length > 0) {\n const source = queue.shift()!;\n const cacheKey = storeKeys.source(source.url);\n const startTime = Date.now();\n\n try {\n // Check if another worker is already fetching this\n let fetchPromise = inFlightFetches.get(cacheKey);\n if (!fetchPromise) {\n fetchPromise = fetchAndCache(source.url, cacheKey, sendEvent, ctx);\n inFlightFetches.set(cacheKey, fetchPromise);\n }\n\n await fetchPromise;\n inFlightFetches.delete(cacheKey);\n\n cached++;\n sendEvent(\"progress\", {\n url: source.url,\n status: \"cached\",\n cached,\n failed,\n skipped,\n total,\n ms: Date.now() - startTime,\n });\n } catch (error) {\n inFlightFetches.delete(cacheKey);\n failed++;\n sendEvent(\"progress\", {\n url: source.url,\n status: \"error\",\n error: String(error),\n cached,\n failed,\n skipped,\n total,\n ms: Date.now() - startTime,\n });\n }\n }\n },\n );\n\n await Promise.all(workers);\n clearInterval(keepalive);\n\n sendEvent(\"summary\", { cached, failed, skipped, total });\n}\n\n/**\n * Fetch a source and cache it, with streaming progress events\n */\nexport async function fetchAndCache(\n url: string,\n cacheKey: string,\n sendEvent: WarmupEventSender,\n ctx: ServerContext,\n): Promise<void> {\n const response = await ffsFetch(url, {\n headersTimeout: 10 * 60 * 1000, // 10 minutes\n bodyTimeout: 20 * 60 * 1000, // 20 minutes\n });\n\n if (!response.ok) {\n throw new Error(`${response.status} ${response.statusText}`);\n }\n\n sendEvent(\"downloading\", { url, status: \"started\", bytesReceived: 0 });\n\n // Stream through a progress tracker\n const sourceStream = Readable.fromWeb(\n response.body as import(\"stream/web\").ReadableStream,\n );\n\n let totalBytes = 0;\n let lastEventTime = Date.now();\n const PROGRESS_INTERVAL = 10_000; // 10 seconds\n\n const progressStream = new Transform({\n transform(chunk, _encoding, callback) {\n totalBytes += chunk.length;\n const now = Date.now();\n if (now - lastEventTime >= PROGRESS_INTERVAL) {\n sendEvent(\"downloading\", {\n url,\n status: \"downloading\",\n bytesReceived: totalBytes,\n });\n lastEventTime = now;\n }\n callback(null, chunk);\n },\n });\n\n // Pipe through progress tracker to cache storage with source TTL\n const trackedStream = sourceStream.pipe(progressStream);\n await ctx.transientStore.put(\n cacheKey,\n trackedStream,\n ctx.transientStore.ttlMs,\n );\n}\n","import express from \"express\";\nimport { randomUUID } from \"crypto\";\nimport { storeKeys } from \"../storage\";\nimport { ffsFetch } from \"../fetch\";\nimport {\n extractEffieSourcesWithTypes,\n extractEffieSources,\n} from \"@effing/effie\";\nimport type { EffieData, EffieSources } from \"@effing/effie\";\nimport type { RenderEventMap, RenderEventSender, WarmupEventMap } from \"../sse\";\nimport type {\n ServerContext,\n RenderJob,\n ResolvedRenderJob,\n DeferredRenderJob,\n VideoJob,\n UploadOptions,\n} from \"./shared\";\nimport {\n parseEffieData,\n setupCORSHeaders,\n setupSSEResponse,\n createEventSender,\n prefixEventSender,\n proxyRemoteSSE,\n proxyBinaryStream,\n} from \"./shared\";\nimport { warmupSources, purgeCachedSources } from \"./caching\";\nimport { sendError, ErrorCode } from \"./errors\";\n\n/**\n * POST /render - Create a render job (warmup + render, optional purge)\n * Returns a job ID and progress URL for SSE streaming\n */\nexport async function createRenderJob(\n req: express.Request,\n res: express.Response,\n ctx: ServerContext,\n options?: {\n metadata?: Record<string, unknown>;\n timings?: Record<string, number>;\n },\n): Promise<void> {\n try {\n // Parse request body\n const body = req.body as Record<string, unknown>;\n\n const scale =\n (body.scale as number | undefined) ??\n (req.query?.scale ? parseFloat(req.query.scale as string) : undefined) ??\n 1;\n const purge =\n (body.purge as boolean | undefined) ??\n (req.query?.purge === \"true\" ? true : undefined) ??\n false;\n const upload = body.upload as UploadOptions | undefined;\n\n // Create IDs\n const jobId = randomUUID();\n const warmupJobId = randomUUID();\n\n // URL handling: defer fetch to progress stream\n if (typeof body.effie === \"string\") {\n const job: DeferredRenderJob = {\n kind: \"deferred\",\n effieUrl: body.effie,\n scale,\n upload,\n purge,\n warmupJobId,\n createdAt: Date.now(),\n metadata: options?.metadata,\n };\n\n const storeJobStart = performance.now();\n await ctx.transientStore.putJson(\n storeKeys.renderJob(jobId),\n job,\n ctx.transientStore.ttlMs,\n );\n if (options?.timings) {\n options.timings.storeJob = performance.now() - storeJobStart;\n }\n\n res.json({\n id: jobId,\n progressUrl: `${ctx.baseUrl}/render/${jobId}/progress`,\n });\n return;\n }\n\n // Parse & validate effie data (supports both wrapped and raw formats)\n const validationStart = performance.now();\n const parseResult = parseEffieData(body, ctx.skipValidation);\n if (options?.timings) {\n options.timings.validation = performance.now() - validationStart;\n }\n if (\"error\" in parseResult) {\n sendError(\n res,\n 400,\n parseResult.code,\n parseResult.error,\n parseResult.issues,\n );\n return;\n }\n const effie = parseResult.effie;\n\n const sources = extractEffieSourcesWithTypes(effie);\n\n // Store the render job\n const job: ResolvedRenderJob = {\n kind: \"resolved\",\n effie,\n sources,\n scale,\n upload,\n purge,\n warmupJobId,\n createdAt: Date.now(),\n metadata: options?.metadata,\n };\n\n const storeJobStart = performance.now();\n await ctx.transientStore.putJson(\n storeKeys.renderJob(jobId),\n job,\n ctx.transientStore.ttlMs,\n );\n\n // Store warmup sub-job for backend execution\n await ctx.transientStore.putJson(\n storeKeys.warmupJob(warmupJobId),\n { sources, metadata: options?.metadata },\n ctx.transientStore.ttlMs,\n );\n if (options?.timings) {\n options.timings.storeJob = performance.now() - storeJobStart;\n }\n\n res.json({\n id: jobId,\n progressUrl: `${ctx.baseUrl}/render/${jobId}/progress`,\n });\n } catch (error) {\n console.error(\"Error creating render job:\", error);\n sendError(\n res,\n 500,\n ErrorCode.INTERNAL_ERROR,\n \"Failed to create render job\",\n );\n }\n}\n\n/**\n * Resolve a deferred Effie URL: fetch, parse, validate, extract sources.\n * Emits effie:fetching/effie:fetched SSE events. Throws on failure.\n */\nasync function resolveEffieUrl(\n deferred: DeferredRenderJob,\n sendEvent: ReturnType<typeof createEventSender<RenderEventMap>>,\n ctx: ServerContext,\n): Promise<ResolvedRenderJob> {\n const url = deferred.effieUrl;\n sendEvent(\"effie:fetching\", { url });\n\n let response;\n try {\n response = await ffsFetch(url);\n } catch (error) {\n throw new Error(\n `Failed to fetch Effie data: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n if (!response.ok) {\n throw new Error(\n `Failed to fetch Effie data: ${response.status} ${response.statusText}`,\n );\n }\n\n const body = { effie: await response.json() };\n const parseResult = parseEffieData(body, ctx.skipValidation);\n if (\"error\" in parseResult) {\n throw new Error(parseResult.error);\n }\n\n const effie = parseResult.effie;\n const sources = extractEffieSourcesWithTypes(effie);\n\n sendEvent(\"effie:fetched\", { url });\n\n return {\n kind: \"resolved\",\n effie,\n sources,\n scale: deferred.scale,\n upload: deferred.upload,\n purge: deferred.purge,\n warmupJobId: deferred.warmupJobId,\n createdAt: deferred.createdAt,\n metadata: deferred.metadata,\n };\n}\n\n/**\n * GET /render/:id/progress - Stream render progress via SSE\n * Orchestrates warmup (local or remote) followed by render (local or remote)\n */\nexport async function streamRenderProgress(\n req: express.Request,\n res: express.Response,\n ctx: ServerContext,\n): Promise<void> {\n try {\n setupCORSHeaders(res);\n\n const jobId = req.params.id;\n const jobStoreKey = storeKeys.renderJob(jobId);\n const storedJob = await ctx.transientStore.getJson<RenderJob>(jobStoreKey);\n\n if (!storedJob) {\n sendError(res, 404, ErrorCode.NOT_FOUND, \"Job not found\");\n return;\n }\n\n // Only allow the job to run once\n ctx.transientStore.delete(jobStoreKey);\n\n setupSSEResponse(res);\n const sendEvent = createEventSender<RenderEventMap>(res);\n const rawSendEvent = createEventSender(res);\n\n // Keepalive interval for long-running operations\n let keepalivePhase: \"effie\" | \"warmup\" | \"render\" | \"upload\" =\n storedJob.kind === \"deferred\" ? \"effie\" : \"warmup\";\n const keepalive = setInterval(() => {\n sendEvent(\"keepalive\", { phase: keepalivePhase });\n }, 25_000);\n\n try {\n // Phase -1: Resolve deferred Effie URL if needed\n // Backward compat: jobs without `kind` (from before deploy) are treated as resolved\n let job: ResolvedRenderJob;\n if (storedJob.kind === \"deferred\") {\n job = await resolveEffieUrl(storedJob, sendEvent, ctx);\n\n // Store warmup sub-job now that we have sources\n await ctx.transientStore.putJson(\n storeKeys.warmupJob(job.warmupJobId),\n { sources: job.sources, metadata: job.metadata },\n ctx.transientStore.ttlMs,\n );\n\n keepalivePhase = \"warmup\";\n } else {\n // resolved or legacy (no kind field)\n job = storedJob as ResolvedRenderJob;\n }\n\n // Resolve backends up front\n const warmupBackend = ctx.warmupBackendResolver\n ? ctx.warmupBackendResolver(job.sources, job.metadata)\n : null;\n const renderBackend = ctx.renderBackendResolver\n ? ctx.renderBackendResolver(job.effie, job.metadata)\n : null;\n\n // Phase 0: Purge (if requested)\n if (job.purge) {\n const sourceUrls = extractEffieSources(job.effie);\n const purgeResult = await purgeCachedSources(\n sourceUrls,\n ctx.transientStore,\n );\n sendEvent(\"purge:complete\", purgeResult);\n }\n\n // Phase 1: Warmup\n if (warmupBackend) {\n // Proxy warmup from remote backend\n await proxyRemoteSSE(\n `${warmupBackend.baseUrl}/warmup/${job.warmupJobId}/progress`,\n rawSendEvent,\n \"warmup:\",\n res,\n warmupBackend.apiKey\n ? { Authorization: `Bearer ${warmupBackend.apiKey}` }\n : undefined,\n );\n } else {\n // Local warmup execution\n const warmupSender = prefixEventSender<WarmupEventMap>(\n rawSendEvent,\n \"warmup:\",\n );\n await warmupSources(job.sources, warmupSender, ctx);\n warmupSender(\"complete\", { status: \"ready\" });\n }\n\n // Phase 2: Render\n keepalivePhase = \"render\";\n\n if (job.upload) {\n keepalivePhase = \"upload\";\n if (renderBackend) {\n // Upload + backend: store VideoJob for backend to render,\n // fetch binary video from backend, upload locally.\n const videoJob: VideoJob = {\n effie: job.effie,\n scale: job.scale,\n metadata: job.metadata,\n };\n await ctx.transientStore.putJson(\n storeKeys.videoJob(jobId),\n videoJob,\n ctx.transientStore.ttlMs,\n );\n\n const backendUrl = `${renderBackend.baseUrl}/render/${jobId}/video`;\n const response = await ffsFetch(backendUrl, {\n headers: renderBackend.apiKey\n ? { Authorization: `Bearer ${renderBackend.apiKey}` }\n : undefined,\n });\n if (!response.ok) {\n throw new Error(`Backend render failed: ${response.status}`);\n }\n const videoBuffer = Buffer.from(await response.arrayBuffer());\n\n const timings = await uploadRenderedVideo(\n videoBuffer,\n job.effie,\n job.upload,\n sendEvent,\n );\n sendEvent(\n \"render:complete\",\n timings as RenderEventMap[\"render:complete\"],\n );\n } else {\n // Upload + no backend: render and upload locally (no VideoJob stored)\n const timings = await renderAndUploadInternal(\n job.effie,\n job.scale,\n job.upload,\n sendEvent,\n ctx,\n );\n sendEvent(\n \"render:complete\",\n timings as RenderEventMap[\"render:complete\"],\n );\n }\n sendEvent(\"complete\", { status: \"done\" });\n } else {\n // Non-upload mode: store VideoJob for on-demand fetch via /render/:id/video\n const videoJob: VideoJob = {\n effie: job.effie,\n scale: job.scale,\n metadata: job.metadata,\n };\n await ctx.transientStore.putJson(\n storeKeys.videoJob(jobId),\n videoJob,\n ctx.transientStore.ttlMs,\n );\n const videoUrl = `${ctx.baseUrl}/render/${jobId}/video`;\n sendEvent(\"ready\", { videoUrl });\n }\n } catch (error) {\n sendEvent(\"error\", {\n phase: keepalivePhase,\n message: String(error),\n });\n } finally {\n clearInterval(keepalive);\n res.end();\n }\n } catch (error) {\n console.error(\"Error in render progress streaming:\", error);\n if (!res.headersSent) {\n sendError(\n res,\n 500,\n ErrorCode.INTERNAL_ERROR,\n \"Render progress streaming failed\",\n );\n } else {\n res.end();\n }\n }\n}\n\n/**\n * GET /render/:id/video - Stream rendered video\n * Reads the video sub-job from the store, deletes it (one-time use), and streams the MP4.\n */\nexport async function streamRenderVideo(\n req: express.Request,\n res: express.Response,\n ctx: ServerContext,\n): Promise<void> {\n try {\n setupCORSHeaders(res);\n\n const jobId = req.params.id;\n const videoJobKey = storeKeys.videoJob(jobId);\n const videoJob = await ctx.transientStore.getJson<VideoJob>(videoJobKey);\n\n if (!videoJob) {\n sendError(res, 404, ErrorCode.NOT_FOUND, \"Video not found or expired\");\n return;\n }\n\n // Proxy to render backend if resolver is configured\n // Don't delete — the backend reads/deletes the VideoJob from shared store\n if (ctx.renderBackendResolver) {\n const backend = ctx.renderBackendResolver(\n videoJob.effie,\n videoJob.metadata,\n );\n if (backend) {\n const backendUrl = `${backend.baseUrl}/render/${jobId}/video`;\n const response = await ffsFetch(backendUrl, {\n headers: backend.apiKey\n ? { Authorization: `Bearer ${backend.apiKey}` }\n : undefined,\n });\n\n if (!response.ok) {\n sendError(\n res,\n response.status,\n ErrorCode.BACKEND_FAILED,\n \"Backend render failed\",\n );\n return;\n }\n\n await proxyBinaryStream(response, res);\n return;\n }\n }\n\n // Local render — safe to delete the video job (one-time use)\n ctx.transientStore.delete(videoJobKey);\n\n // Render locally\n await streamRenderDirect(res, videoJob, ctx);\n } catch (error) {\n console.error(\"Error streaming video:\", error);\n if (!res.headersSent) {\n sendError(res, 500, ErrorCode.INTERNAL_ERROR, \"Video streaming failed\");\n } else {\n res.end();\n }\n }\n}\n\n/**\n * Stream video directly to the response (no upload)\n */\nasync function streamRenderDirect(\n res: express.Response,\n job: VideoJob,\n ctx: ServerContext,\n): Promise<void> {\n const { EffieRenderer } = await import(\"../render\");\n const renderer = new EffieRenderer(job.effie, {\n transientStore: ctx.transientStore,\n httpProxy: ctx.httpProxy,\n });\n const videoStream = await renderer.render(job.scale);\n\n res.on(\"close\", () => {\n videoStream.destroy();\n renderer.close();\n });\n\n res.set(\"Content-Type\", \"video/mp4\");\n res.set(\"Cache-Control\", \"public, immutable, max-age=86400\");\n videoStream.pipe(res);\n}\n\n/**\n * Upload a rendered video buffer (and optional cover) to presigned URLs.\n * Shared between local render+upload and backend render+upload flows.\n */\nasync function uploadRenderedVideo(\n videoBuffer: Buffer,\n effie: EffieData<EffieSources>,\n upload: UploadOptions,\n sendEvent: RenderEventSender,\n): Promise<Record<string, number>> {\n const timings: Record<string, number> = {};\n\n // Fetch and upload cover if coverUrl provided\n if (upload.coverUrl) {\n const fetchCoverStartTime = Date.now();\n let coverBuffer: Buffer;\n if (effie.cover.startsWith(\"data:\")) {\n const commaIndex = effie.cover.indexOf(\",\");\n if (commaIndex === -1) {\n throw new Error(\"Invalid cover data URL\");\n }\n const meta = effie.cover.slice(5, commaIndex); // after \"data:\"\n const isBase64 = meta.endsWith(\";base64\");\n const data = effie.cover.slice(commaIndex + 1);\n coverBuffer = isBase64\n ? Buffer.from(data, \"base64\")\n : Buffer.from(decodeURIComponent(data));\n } else {\n const coverFetchResponse = await ffsFetch(effie.cover);\n if (!coverFetchResponse.ok) {\n throw new Error(\n `Failed to fetch cover image: ${coverFetchResponse.status} ${coverFetchResponse.statusText}`,\n );\n }\n coverBuffer = Buffer.from(await coverFetchResponse.arrayBuffer());\n }\n timings.fetchCoverTime = Date.now() - fetchCoverStartTime;\n\n const uploadCoverStartTime = Date.now();\n const uploadCoverResponse = await ffsFetch(upload.coverUrl, {\n method: \"PUT\",\n body: coverBuffer,\n headers: {\n \"Content-Type\": \"image/png\",\n \"Content-Length\": coverBuffer.length.toString(),\n },\n });\n if (!uploadCoverResponse.ok) {\n throw new Error(\n `Failed to upload cover: ${uploadCoverResponse.status} ${uploadCoverResponse.statusText}`,\n );\n }\n timings.uploadCoverTime = Date.now() - uploadCoverStartTime;\n }\n\n // Update keepalive status for upload phase\n sendEvent(\"keepalive\", { phase: \"upload\" });\n\n // Upload rendered video\n const uploadStartTime = Date.now();\n const uploadResponse = await ffsFetch(upload.videoUrl, {\n method: \"PUT\",\n body: videoBuffer,\n headers: {\n \"Content-Type\": \"video/mp4\",\n \"Content-Length\": videoBuffer.length.toString(),\n },\n });\n if (!uploadResponse.ok) {\n throw new Error(\n `Failed to upload rendered video: ${uploadResponse.status} ${uploadResponse.statusText}`,\n );\n }\n timings.uploadTime = Date.now() - uploadStartTime;\n\n return timings;\n}\n\n/**\n * Internal render and upload logic\n * Returns timings for the SSE complete event\n */\nexport async function renderAndUploadInternal(\n effie: EffieData<EffieSources>,\n scale: number,\n upload: UploadOptions,\n sendEvent: RenderEventSender,\n ctx: ServerContext,\n): Promise<Record<string, number>> {\n // Render effie data to video buffer\n const renderStartTime = Date.now();\n const { EffieRenderer } = await import(\"../render\");\n const renderer = new EffieRenderer(effie, {\n transientStore: ctx.transientStore,\n httpProxy: ctx.httpProxy,\n });\n try {\n const videoStream = await renderer.render(scale);\n const chunks: Buffer[] = [];\n for await (const chunk of videoStream) {\n chunks.push(Buffer.from(chunk));\n }\n const videoBuffer = Buffer.concat(chunks);\n const renderTime = Date.now() - renderStartTime;\n\n // Upload video (and cover)\n const timings = await uploadRenderedVideo(\n videoBuffer,\n effie,\n upload,\n sendEvent,\n );\n timings.renderTime = renderTime;\n\n return timings;\n } finally {\n renderer.close();\n }\n}\n"],"mappings":";;;;;;;AAAA,OAAoB;;;ACApB,OAAO,UAAU;AAEjB,SAAS,gBAAgB;AAelB,IAAM,YAAN,MAAgB;AAAA,EACb,SAAwB;AAAA,EACxB,QAAuB;AAAA,EACvB,eAAqC;AAAA,EAE7C,IAAI,OAAsB;AACxB,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,aAAa,KAAqB;AAChC,QAAI,KAAK,UAAU,KAAM,OAAM,IAAI,MAAM,mBAAmB;AAC5D,WAAO,oBAAoB,KAAK,KAAK,IAAI,GAAG;AAAA,EAC9C;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,QAAuB;AAC3B,QAAI,KAAK,UAAU,KAAM;AACzB,QAAI,KAAK,cAAc;AACrB,YAAM,KAAK;AACX;AAAA,IACF;AACA,SAAK,eAAe,KAAK,QAAQ;AACjC,UAAM,KAAK;AAAA,EACb;AAAA,EAEA,MAAc,UAAyB;AACrC,SAAK,SAAS,KAAK,aAAa,OAAO,KAAK,QAAQ;AAClD,UAAI;AACF,cAAM,cAAc,KAAK,eAAe,IAAI,OAAO,EAAE;AACrD,YAAI,CAAC,aAAa;AAChB,cAAI,UAAU,KAAK,EAAE,gBAAgB,aAAa,CAAC;AACnD,cAAI,IAAI,iCAAiC;AACzC;AAAA,QACF;AAEA,cAAM,WAAW,MAAM,SAAS,aAAa;AAAA,UAC3C,QAAQ,IAAI;AAAA,UACZ,SAAS,KAAK,cAAc,IAAI,OAAO;AAAA,UACvC,aAAa;AAAA;AAAA,QACf,CAAC;AAGD,cAAM,UAAkC,CAAC;AACzC,iBAAS,QAAQ,QAAQ,CAAC,OAAO,QAAQ;AACvC,kBAAQ,GAAG,IAAI;AAAA,QACjB,CAAC;AAED,YAAI,UAAU,SAAS,QAAQ,OAAO;AAEtC,YAAI,SAAS,MAAM;AACjB,gBAAM,aAAa,SAAS,QAAQ,SAAS,IAAI;AACjD,qBAAW,KAAK,GAAG;AACnB,qBAAW,GAAG,SAAS,CAAC,QAAQ;AAC9B,oBAAQ,MAAM,uBAAuB,GAAG;AACxC,gBAAI,QAAQ;AAAA,UACd,CAAC;AAAA,QACH,OAAO;AACL,cAAI,IAAI;AAAA,QACV;AAAA,MACF,SAAS,KAAK;AACZ,gBAAQ,MAAM,wBAAwB,GAAG;AACzC,YAAI,CAAC,IAAI,aAAa;AACpB,cAAI,UAAU,KAAK,EAAE,gBAAgB,aAAa,CAAC;AACnD,cAAI,IAAI,aAAa;AAAA,QACvB,OAAO;AACL,cAAI,QAAQ;AAAA,QACd;AAAA,MACF;AAAA,IACF,CAAC;AAED,UAAM,IAAI,QAAc,CAAC,YAAY;AACnC,WAAK,OAAQ,OAAO,GAAG,aAAa,MAAM;AACxC,aAAK,QAAS,KAAK,OAAQ,QAAQ,EAAkB;AACrD,gBAAQ;AAAA,MACV,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,eAAe,MAA6B;AAClD,QAAI,CAAC,KAAK,WAAW,UAAU,KAAK,CAAC,KAAK,WAAW,WAAW,GAAG;AACjE,aAAO;AAAA,IACT;AACA,WAAO,KAAK,MAAM,CAAC;AAAA,EACrB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,cACN,SACwB;AACxB,UAAM,OAAO,oBAAI,IAAI;AAAA,MACnB;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF,CAAC;AAED,UAAM,SAAiC,CAAC;AACxC,eAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,OAAO,GAAG;AAClD,UAAI,CAAC,KAAK,IAAI,IAAI,YAAY,CAAC,KAAK,OAAO,UAAU,UAAU;AAC7D,eAAO,GAAG,IAAI;AAAA,MAChB;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,QAAc;AACZ,SAAK,QAAQ,MAAM;AACnB,SAAK,SAAS;AACd,SAAK,QAAQ;AACb,SAAK,eAAe;AAAA,EACtB;AACF;;;ADxIA,SAAS,uBAAuB;;;AEXzB,IAAM,YAAY;AAAA,EACvB,cAAc;AAAA,EACd,eAAe;AAAA,EACf,WAAW;AAAA,EACX,gBAAgB;AAAA,EAChB,gBAAgB;AAAA,EAChB,cAAc;AAChB;AAUO,SAAS,UACd,KACA,QACA,MACA,SACA,QACM;AACN,MAAI,IAAI,YAAa;AACrB,QAAM,OAAiB,EAAE,OAAO,SAAS,KAAK;AAC9C,MAAI,OAAQ,MAAK,SAAS;AAC1B,MAAI,OAAO,MAAM,EAAE,KAAK,IAAI;AAC9B;;;AFgEA,eAAsB,oBAAoB,SAIf;AACzB,QAAM,OAAO,QAAQ,IAAI,YAAY,QAAQ,IAAI,QAAQ;AACzD,QAAM,kBAAkB,SAAS,aAAa;AAC9C,MAAI;AACJ,MAAI,iBAAiB;AACnB,gBAAY,IAAI,UAAU;AAC1B,UAAM,UAAU,MAAM;AAAA,EACxB;AACA,SAAO;AAAA,IACL,gBAAgB,qBAAqB;AAAA,IACrC;AAAA,IACA,SAAS,QAAQ,IAAI,gBAAgB,oBAAoB,IAAI;AAAA,IAC7D,gBACE,CAAC,CAAC,QAAQ,IAAI,uBACd,QAAQ,IAAI,wBAAwB;AAAA,IACtC,mBAAmB,SAAS,QAAQ,IAAI,0BAA0B,KAAK,EAAE;AAAA,IACzE,uBAAuB,SAAS;AAAA,IAChC,uBAAuB,SAAS;AAAA,EAClC;AACF;AAKO,SAAS,eACd,MACA,gBACkB;AAElB,QAAM,YACJ,OAAO,SAAS,YAAY,SAAS,QAAQ,WAAW;AAC1D,QAAM,eAAe,YAAa,KAA4B,QAAQ;AAEtE,MAAI,CAAC,gBAAgB;AACnB,UAAM,SAAS,gBAAgB,UAAU,YAAY;AACrD,QAAI,CAAC,OAAO,SAAS;AACnB,aAAO;AAAA,QACL,OAAO;AAAA,QACP,MAAM,UAAU;AAAA,QAChB,QAAQ,OAAO,MAAM,OAAO,IAAI,CAAC,WAAW;AAAA,UAC1C,MAAM,MAAM,KAAK,KAAK,GAAG;AAAA,UACzB,SAAS,MAAM;AAAA,QACjB,EAAE;AAAA,MACJ;AAAA,IACF;AACA,WAAO,EAAE,OAAO,OAAO,KAAK;AAAA,EAC9B,OAAO;AACL,UAAM,QAAQ;AACd,QAAI,CAAC,OAAO,UAAU;AACpB,aAAO;AAAA,QACL,OAAO;AAAA,QACP,MAAM,UAAU;AAAA,MAClB;AAAA,IACF;AACA,WAAO,EAAE,MAAM;AAAA,EACjB;AACF;AAKO,SAAS,iBAAiB,KAA6B;AAC5D,MAAI,UAAU,+BAA+B,GAAG;AAChD,MAAI,UAAU,gCAAgC,KAAK;AACrD;AAKO,SAAS,iBAAiB,KAA6B;AAC5D,MAAI,UAAU,gBAAgB,mBAAmB;AACjD,MAAI,UAAU,iBAAiB,UAAU;AACzC,MAAI,UAAU,cAAc,YAAY;AACxC,MAAI,aAAa;AACnB;AASO,SAAS,kBAAkB,KAAoC;AACpE,SAAO,CAAC,OAAe,SAAiB;AACtC,QAAI,MAAM,UAAU,KAAK;AAAA,QAAW,KAAK,UAAU,IAAI,CAAC;AAAA;AAAA,CAAM;AAAA,EAChE;AACF;AAKO,SAAS,kBACd,WACA,QACwB;AACxB,UAAQ,CAAC,OAAe,SAAiB;AACvC,cAAU,GAAG,MAAM,GAAG,KAAK,IAAI,IAAI;AAAA,EACrC;AACF;AAKA,eAAsB,eACpB,KACA,WACA,QACA,KACA,SACe;AACf,QAAM,WAAW,MAAM,SAAS,KAAK;AAAA,IACnC,SAAS;AAAA,MACP,QAAQ;AAAA,MACR,GAAG;AAAA,IACL;AAAA,EACF,CAAC;AAED,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,IAAI,MAAM,yBAAyB,SAAS,MAAM,EAAE;AAAA,EAC5D;AAEA,QAAM,SAAS,SAAS,MAAM,UAAU;AACxC,MAAI,CAAC,QAAQ;AACX,UAAM,IAAI,MAAM,sCAAsC;AAAA,EACxD;AAEA,QAAM,UAAU,IAAI,YAAY;AAChC,MAAI,SAAS;AACb,MAAI,eAAe;AACnB,MAAI,cAAc;AAElB,MAAI;AACF,WAAO,MAAM;AACX,YAAM,EAAE,MAAM,MAAM,IAAI,MAAM,OAAO,KAAK;AAC1C,UAAI,KAAM;AAGV,UAAI,IAAI,WAAW;AACjB,eAAO,OAAO;AACd;AAAA,MACF;AAEA,gBAAU,QAAQ,OAAO,OAAO,EAAE,QAAQ,KAAK,CAAC;AAGhD,YAAM,QAAQ,OAAO,MAAM,IAAI;AAC/B,eAAS,MAAM,IAAI,KAAK;AAExB,iBAAW,QAAQ,OAAO;AACxB,YAAI,KAAK,WAAW,SAAS,GAAG;AAC9B,yBAAe,KAAK,MAAM,CAAC;AAAA,QAC7B,WAAW,KAAK,WAAW,QAAQ,GAAG;AACpC,wBAAc,KAAK,MAAM,CAAC;AAAA,QAC5B,WAAW,SAAS,MAAM,gBAAgB,aAAa;AAErD,cAAI;AACF,kBAAM,OAAO,KAAK,MAAM,WAAW;AACnC,sBAAU,GAAG,MAAM,GAAG,YAAY,IAAI,IAAI;AAAA,UAC5C,QAAQ;AAAA,UAER;AACA,yBAAe;AACf,wBAAc;AAAA,QAChB;AAAA,MACF;AAAA,IACF;AAAA,EACF,UAAE;AACA,WAAO,YAAY;AAAA,EACrB;AACF;AAMA,eAAsB,kBACpB,UACA,KACe;AACf,QAAM,cAAc,SAAS,QAAQ,IAAI,cAAc;AACvD,MAAI,YAAa,KAAI,IAAI,gBAAgB,WAAW;AAEpD,QAAM,gBAAgB,SAAS,QAAQ,IAAI,gBAAgB;AAC3D,MAAI,cAAe,KAAI,IAAI,kBAAkB,aAAa;AAE1D,QAAM,SAAS,SAAS,MAAM,UAAU;AACxC,MAAI,CAAC,QAAQ;AACX,UAAM,IAAI,MAAM,kBAAkB;AAAA,EACpC;AAEA,MAAI;AACF,WAAO,MAAM;AACX,YAAM,EAAE,MAAM,MAAM,IAAI,MAAM,OAAO,KAAK;AAC1C,UAAI,KAAM;AAEV,UAAI,IAAI,WAAW;AACjB,eAAO,OAAO;AACd;AAAA,MACF;AAEA,UAAI,MAAM,KAAK;AAAA,IACjB;AAAA,EACF,UAAE;AACA,WAAO,YAAY;AACnB,QAAI,IAAI;AAAA,EACV;AACF;;;AGjTA,OAAoB;AACpB,SAAS,YAAAA,WAAU,iBAAiB;AACpC,SAAS,kBAAkB;AAI3B;AAAA,EACE;AAAA,EACA;AAAA,OACK;AAiBP,SAAS,iBAAiB,QAAsC;AAC9D,SAAO,OAAO,SAAS,WAAW,OAAO,SAAS;AACpD;AAGA,IAAM,kBAAkB,oBAAI,IAA2B;AAMvD,eAAsB,gBACpB,KACA,KACA,KACA,SAIe;AACf,MAAI;AACF,UAAM,kBAAkB,YAAY,IAAI;AACxC,UAAM,cAAc,eAAe,IAAI,MAAM,IAAI,cAAc;AAC/D,QAAI,SAAS,SAAS;AACpB,cAAQ,QAAQ,aAAa,YAAY,IAAI,IAAI;AAAA,IACnD;AACA,QAAI,WAAW,aAAa;AAC1B,UAAI,OAAO,GAAG,EAAE,KAAK,WAAW;AAChC;AAAA,IACF;AAEA,UAAM,UAAU,6BAA6B,YAAY,KAAK;AAC9D,UAAM,QAAQ,WAAW;AAEzB,UAAM,MAAiB,EAAE,SAAS,UAAU,SAAS,SAAS;AAC9D,UAAM,gBAAgB,YAAY,IAAI;AACtC,UAAM,IAAI,eAAe;AAAA,MACvB,UAAU,UAAU,KAAK;AAAA,MACzB;AAAA,MACA,IAAI,eAAe;AAAA,IACrB;AACA,QAAI,SAAS,SAAS;AACpB,cAAQ,QAAQ,WAAW,YAAY,IAAI,IAAI;AAAA,IACjD;AAEA,QAAI,KAAK;AAAA,MACP,IAAI;AAAA,MACJ,aAAa,GAAG,IAAI,OAAO,WAAW,KAAK;AAAA,IAC7C,CAAC;AAAA,EACH,SAAS,OAAO;AACd,YAAQ,MAAM,8BAA8B,KAAK;AACjD;AAAA,MACE;AAAA,MACA;AAAA,MACA,UAAU;AAAA,MACV;AAAA,IACF;AAAA,EACF;AACF;AAMA,eAAsB,qBACpB,KACA,KACA,KACe;AACf,MAAI;AACF,qBAAiB,GAAG;AAEpB,UAAM,QAAQ,IAAI,OAAO;AAEzB,UAAM,cAAc,UAAU,UAAU,KAAK;AAC7C,UAAM,MAAM,MAAM,IAAI,eAAe,QAAmB,WAAW;AAEnE,QAAI,CAAC,KAAK;AACR,gBAAU,KAAK,KAAK,UAAU,WAAW,eAAe;AACxD;AAAA,IACF;AAGA,QAAI,IAAI,uBAAuB;AAC7B,YAAM,UAAU,IAAI,sBAAsB,IAAI,SAAS,IAAI,QAAQ;AACnE,UAAI,SAAS;AACX,yBAAiB,GAAG;AACpB,cAAMC,aAAY,kBAAkB,GAAG;AACvC,YAAI;AACF,gBAAM;AAAA,YACJ,GAAG,QAAQ,OAAO,WAAW,KAAK;AAAA,YAClCA;AAAA,YACA;AAAA,YACA;AAAA,YACA,QAAQ,SACJ,EAAE,eAAe,UAAU,QAAQ,MAAM,GAAG,IAC5C;AAAA,UACN;AAAA,QACF,UAAE;AACA,cAAI,IAAI;AAAA,QACV;AACA;AAAA,MACF;AAAA,IACF;AAGA,QAAI,eAAe,OAAO,WAAW;AAErC,qBAAiB,GAAG;AACpB,UAAM,YAAY,kBAAkC,GAAG;AAEvD,QAAI;AACF,YAAM,cAAc,IAAI,SAAS,WAAW,GAAG;AAC/C,gBAAU,YAAY,EAAE,QAAQ,QAAQ,CAAC;AAAA,IAC3C,SAAS,OAAO;AACd,gBAAU,SAAS,EAAE,SAAS,OAAO,KAAK,EAAE,CAAC;AAAA,IAC/C,UAAE;AACA,UAAI,IAAI;AAAA,IACV;AAAA,EACF,SAAS,OAAO;AACd,YAAQ,MAAM,8BAA8B,KAAK;AACjD,QAAI,CAAC,IAAI,aAAa;AACpB,gBAAU,KAAK,KAAK,UAAU,gBAAgB,yBAAyB;AAAA,IACzE,OAAO;AACL,UAAI,IAAI;AAAA,IACV;AAAA,EACF;AACF;AAMA,eAAsB,mBACpB,MACA,OAC4C;AAC5C,MAAI,SAAS;AACb,aAAW,OAAO,MAAM;AACtB,UAAM,KAAK,UAAU,OAAO,GAAG;AAC/B,QAAI,MAAM,MAAM,OAAO,EAAE,GAAG;AAC1B,YAAM,MAAM,OAAO,EAAE;AACrB;AAAA,IACF;AAAA,EACF;AACA,SAAO,EAAE,QAAQ,OAAO,KAAK,OAAO;AACtC;AAKA,eAAsB,WACpB,KACA,KACA,KACA,SACe;AACf,MAAI;AACF,UAAM,kBAAkB,YAAY,IAAI;AACxC,UAAM,cAAc,eAAe,IAAI,MAAM,IAAI,cAAc;AAC/D,QAAI,SAAS,SAAS;AACpB,cAAQ,QAAQ,aAAa,YAAY,IAAI,IAAI;AAAA,IACnD;AACA,QAAI,WAAW,aAAa;AAC1B,UAAI,OAAO,GAAG,EAAE,KAAK,WAAW;AAChC;AAAA,IACF;AAEA,UAAM,UAAU,oBAAoB,YAAY,KAAK;AACrD,UAAM,aAAa,YAAY,IAAI;AACnC,UAAM,SAAS,MAAM,mBAAmB,SAAS,IAAI,cAAc;AACnE,QAAI,SAAS,SAAS;AACpB,cAAQ,QAAQ,QAAQ,YAAY,IAAI,IAAI;AAAA,IAC9C;AAEA,QAAI,KAAK,MAAM;AAAA,EACjB,SAAS,OAAO;AACd,YAAQ,MAAM,wBAAwB,KAAK;AAC3C,cAAU,KAAK,KAAK,UAAU,gBAAgB,uBAAuB;AAAA,EACvE;AACF;AAMA,eAAsB,cACpB,SACA,WACA,KACe;AACf,QAAM,QAAQ,QAAQ;AAEtB,YAAU,SAAS,EAAE,MAAM,CAAC;AAE5B,MAAI,SAAS;AACb,MAAI,SAAS;AACb,MAAI,UAAU;AAGd,QAAM,iBAAwC,CAAC;AAC/C,aAAW,UAAU,SAAS;AAC5B,QAAI,iBAAiB,MAAM,GAAG;AAC5B;AACA,gBAAU,YAAY;AAAA,QACpB,KAAK,OAAO;AAAA,QACZ,QAAQ;AAAA,QACR,QAAQ;AAAA,QACR;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF,CAAC;AAAA,IACH,OAAO;AACL,qBAAe,KAAK,MAAM;AAAA,IAC5B;AAAA,EACF;AAGA,QAAM,kBAAkB,eAAe,IAAI,CAAC,MAAM,UAAU,OAAO,EAAE,GAAG,CAAC;AACzE,QAAM,YAAY,MAAM,IAAI,eAAe,WAAW,eAAe;AAGrE,WAAS,IAAI,GAAG,IAAI,eAAe,QAAQ,KAAK;AAC9C,QAAI,UAAU,IAAI,gBAAgB,CAAC,CAAC,GAAG;AACrC;AACA,gBAAU,YAAY;AAAA,QACpB,KAAK,eAAe,CAAC,EAAE;AAAA,QACvB,QAAQ;AAAA,QACR;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF;AAGA,QAAM,WAAW,eAAe;AAAA,IAC9B,CAAC,GAAG,MAAM,CAAC,UAAU,IAAI,gBAAgB,CAAC,CAAC;AAAA,EAC7C;AAEA,MAAI,SAAS,WAAW,GAAG;AACzB,cAAU,WAAW,EAAE,QAAQ,QAAQ,SAAS,MAAM,CAAC;AACvD;AAAA,EACF;AAGA,QAAM,YAAY,YAAY,MAAM;AAClC,cAAU,aAAa,EAAE,QAAQ,QAAQ,SAAS,MAAM,CAAC;AAAA,EAC3D,GAAG,IAAM;AAGT,QAAM,QAAQ,CAAC,GAAG,QAAQ;AAC1B,QAAM,UAAU,MAAM;AAAA,IACpB,EAAE,QAAQ,KAAK,IAAI,IAAI,mBAAmB,MAAM,MAAM,EAAE;AAAA,IACxD,YAAY;AACV,aAAO,MAAM,SAAS,GAAG;AACvB,cAAM,SAAS,MAAM,MAAM;AAC3B,cAAM,WAAW,UAAU,OAAO,OAAO,GAAG;AAC5C,cAAM,YAAY,KAAK,IAAI;AAE3B,YAAI;AAEF,cAAI,eAAe,gBAAgB,IAAI,QAAQ;AAC/C,cAAI,CAAC,cAAc;AACjB,2BAAe,cAAc,OAAO,KAAK,UAAU,WAAW,GAAG;AACjE,4BAAgB,IAAI,UAAU,YAAY;AAAA,UAC5C;AAEA,gBAAM;AACN,0BAAgB,OAAO,QAAQ;AAE/B;AACA,oBAAU,YAAY;AAAA,YACpB,KAAK,OAAO;AAAA,YACZ,QAAQ;AAAA,YACR;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA,IAAI,KAAK,IAAI,IAAI;AAAA,UACnB,CAAC;AAAA,QACH,SAAS,OAAO;AACd,0BAAgB,OAAO,QAAQ;AAC/B;AACA,oBAAU,YAAY;AAAA,YACpB,KAAK,OAAO;AAAA,YACZ,QAAQ;AAAA,YACR,OAAO,OAAO,KAAK;AAAA,YACnB;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA,IAAI,KAAK,IAAI,IAAI;AAAA,UACnB,CAAC;AAAA,QACH;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,QAAM,QAAQ,IAAI,OAAO;AACzB,gBAAc,SAAS;AAEvB,YAAU,WAAW,EAAE,QAAQ,QAAQ,SAAS,MAAM,CAAC;AACzD;AAKA,eAAsB,cACpB,KACA,UACA,WACA,KACe;AACf,QAAM,WAAW,MAAM,SAAS,KAAK;AAAA,IACnC,gBAAgB,KAAK,KAAK;AAAA;AAAA,IAC1B,aAAa,KAAK,KAAK;AAAA;AAAA,EACzB,CAAC;AAED,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,IAAI,MAAM,GAAG,SAAS,MAAM,IAAI,SAAS,UAAU,EAAE;AAAA,EAC7D;AAEA,YAAU,eAAe,EAAE,KAAK,QAAQ,WAAW,eAAe,EAAE,CAAC;AAGrE,QAAM,eAAeC,UAAS;AAAA,IAC5B,SAAS;AAAA,EACX;AAEA,MAAI,aAAa;AACjB,MAAI,gBAAgB,KAAK,IAAI;AAC7B,QAAM,oBAAoB;AAE1B,QAAM,iBAAiB,IAAI,UAAU;AAAA,IACnC,UAAU,OAAO,WAAW,UAAU;AACpC,oBAAc,MAAM;AACpB,YAAM,MAAM,KAAK,IAAI;AACrB,UAAI,MAAM,iBAAiB,mBAAmB;AAC5C,kBAAU,eAAe;AAAA,UACvB;AAAA,UACA,QAAQ;AAAA,UACR,eAAe;AAAA,QACjB,CAAC;AACD,wBAAgB;AAAA,MAClB;AACA,eAAS,MAAM,KAAK;AAAA,IACtB;AAAA,EACF,CAAC;AAGD,QAAM,gBAAgB,aAAa,KAAK,cAAc;AACtD,QAAM,IAAI,eAAe;AAAA,IACvB;AAAA,IACA;AAAA,IACA,IAAI,eAAe;AAAA,EACrB;AACF;;;ACjYA,OAAoB;AACpB,SAAS,cAAAC,mBAAkB;AAG3B;AAAA,EACE,gCAAAC;AAAA,EACA,uBAAAC;AAAA,OACK;AA2BP,eAAsB,gBACpB,KACA,KACA,KACA,SAIe;AACf,MAAI;AAEF,UAAM,OAAO,IAAI;AAEjB,UAAM,QACH,KAAK,UACL,IAAI,OAAO,QAAQ,WAAW,IAAI,MAAM,KAAe,IAAI,WAC5D;AACF,UAAM,QACH,KAAK,UACL,IAAI,OAAO,UAAU,SAAS,OAAO,WACtC;AACF,UAAM,SAAS,KAAK;AAGpB,UAAM,QAAQC,YAAW;AACzB,UAAM,cAAcA,YAAW;AAG/B,QAAI,OAAO,KAAK,UAAU,UAAU;AAClC,YAAMC,OAAyB;AAAA,QAC7B,MAAM;AAAA,QACN,UAAU,KAAK;AAAA,QACf;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA,WAAW,KAAK,IAAI;AAAA,QACpB,UAAU,SAAS;AAAA,MACrB;AAEA,YAAMC,iBAAgB,YAAY,IAAI;AACtC,YAAM,IAAI,eAAe;AAAA,QACvB,UAAU,UAAU,KAAK;AAAA,QACzBD;AAAA,QACA,IAAI,eAAe;AAAA,MACrB;AACA,UAAI,SAAS,SAAS;AACpB,gBAAQ,QAAQ,WAAW,YAAY,IAAI,IAAIC;AAAA,MACjD;AAEA,UAAI,KAAK;AAAA,QACP,IAAI;AAAA,QACJ,aAAa,GAAG,IAAI,OAAO,WAAW,KAAK;AAAA,MAC7C,CAAC;AACD;AAAA,IACF;AAGA,UAAM,kBAAkB,YAAY,IAAI;AACxC,UAAM,cAAc,eAAe,MAAM,IAAI,cAAc;AAC3D,QAAI,SAAS,SAAS;AACpB,cAAQ,QAAQ,aAAa,YAAY,IAAI,IAAI;AAAA,IACnD;AACA,QAAI,WAAW,aAAa;AAC1B;AAAA,QACE;AAAA,QACA;AAAA,QACA,YAAY;AAAA,QACZ,YAAY;AAAA,QACZ,YAAY;AAAA,MACd;AACA;AAAA,IACF;AACA,UAAM,QAAQ,YAAY;AAE1B,UAAM,UAAUC,8BAA6B,KAAK;AAGlD,UAAM,MAAyB;AAAA,MAC7B,MAAM;AAAA,MACN;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,WAAW,KAAK,IAAI;AAAA,MACpB,UAAU,SAAS;AAAA,IACrB;AAEA,UAAM,gBAAgB,YAAY,IAAI;AACtC,UAAM,IAAI,eAAe;AAAA,MACvB,UAAU,UAAU,KAAK;AAAA,MACzB;AAAA,MACA,IAAI,eAAe;AAAA,IACrB;AAGA,UAAM,IAAI,eAAe;AAAA,MACvB,UAAU,UAAU,WAAW;AAAA,MAC/B,EAAE,SAAS,UAAU,SAAS,SAAS;AAAA,MACvC,IAAI,eAAe;AAAA,IACrB;AACA,QAAI,SAAS,SAAS;AACpB,cAAQ,QAAQ,WAAW,YAAY,IAAI,IAAI;AAAA,IACjD;AAEA,QAAI,KAAK;AAAA,MACP,IAAI;AAAA,MACJ,aAAa,GAAG,IAAI,OAAO,WAAW,KAAK;AAAA,IAC7C,CAAC;AAAA,EACH,SAAS,OAAO;AACd,YAAQ,MAAM,8BAA8B,KAAK;AACjD;AAAA,MACE;AAAA,MACA;AAAA,MACA,UAAU;AAAA,MACV;AAAA,IACF;AAAA,EACF;AACF;AAMA,eAAe,gBACb,UACA,WACA,KAC4B;AAC5B,QAAM,MAAM,SAAS;AACrB,YAAU,kBAAkB,EAAE,IAAI,CAAC;AAEnC,MAAI;AACJ,MAAI;AACF,eAAW,MAAM,SAAS,GAAG;AAAA,EAC/B,SAAS,OAAO;AACd,UAAM,IAAI;AAAA,MACR,+BAA+B,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC;AAAA,IACvF;AAAA,EACF;AACA,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,IAAI;AAAA,MACR,+BAA+B,SAAS,MAAM,IAAI,SAAS,UAAU;AAAA,IACvE;AAAA,EACF;AAEA,QAAM,OAAO,EAAE,OAAO,MAAM,SAAS,KAAK,EAAE;AAC5C,QAAM,cAAc,eAAe,MAAM,IAAI,cAAc;AAC3D,MAAI,WAAW,aAAa;AAC1B,UAAM,IAAI,MAAM,YAAY,KAAK;AAAA,EACnC;AAEA,QAAM,QAAQ,YAAY;AAC1B,QAAM,UAAUA,8BAA6B,KAAK;AAElD,YAAU,iBAAiB,EAAE,IAAI,CAAC;AAElC,SAAO;AAAA,IACL,MAAM;AAAA,IACN;AAAA,IACA;AAAA,IACA,OAAO,SAAS;AAAA,IAChB,QAAQ,SAAS;AAAA,IACjB,OAAO,SAAS;AAAA,IAChB,aAAa,SAAS;AAAA,IACtB,WAAW,SAAS;AAAA,IACpB,UAAU,SAAS;AAAA,EACrB;AACF;AAMA,eAAsB,qBACpB,KACA,KACA,KACe;AACf,MAAI;AACF,qBAAiB,GAAG;AAEpB,UAAM,QAAQ,IAAI,OAAO;AACzB,UAAM,cAAc,UAAU,UAAU,KAAK;AAC7C,UAAM,YAAY,MAAM,IAAI,eAAe,QAAmB,WAAW;AAEzE,QAAI,CAAC,WAAW;AACd,gBAAU,KAAK,KAAK,UAAU,WAAW,eAAe;AACxD;AAAA,IACF;AAGA,QAAI,eAAe,OAAO,WAAW;AAErC,qBAAiB,GAAG;AACpB,UAAM,YAAY,kBAAkC,GAAG;AACvD,UAAM,eAAe,kBAAkB,GAAG;AAG1C,QAAI,iBACF,UAAU,SAAS,aAAa,UAAU;AAC5C,UAAM,YAAY,YAAY,MAAM;AAClC,gBAAU,aAAa,EAAE,OAAO,eAAe,CAAC;AAAA,IAClD,GAAG,IAAM;AAET,QAAI;AAGF,UAAI;AACJ,UAAI,UAAU,SAAS,YAAY;AACjC,cAAM,MAAM,gBAAgB,WAAW,WAAW,GAAG;AAGrD,cAAM,IAAI,eAAe;AAAA,UACvB,UAAU,UAAU,IAAI,WAAW;AAAA,UACnC,EAAE,SAAS,IAAI,SAAS,UAAU,IAAI,SAAS;AAAA,UAC/C,IAAI,eAAe;AAAA,QACrB;AAEA,yBAAiB;AAAA,MACnB,OAAO;AAEL,cAAM;AAAA,MACR;AAGA,YAAM,gBAAgB,IAAI,wBACtB,IAAI,sBAAsB,IAAI,SAAS,IAAI,QAAQ,IACnD;AACJ,YAAM,gBAAgB,IAAI,wBACtB,IAAI,sBAAsB,IAAI,OAAO,IAAI,QAAQ,IACjD;AAGJ,UAAI,IAAI,OAAO;AACb,cAAM,aAAaC,qBAAoB,IAAI,KAAK;AAChD,cAAM,cAAc,MAAM;AAAA,UACxB;AAAA,UACA,IAAI;AAAA,QACN;AACA,kBAAU,kBAAkB,WAAW;AAAA,MACzC;AAGA,UAAI,eAAe;AAEjB,cAAM;AAAA,UACJ,GAAG,cAAc,OAAO,WAAW,IAAI,WAAW;AAAA,UAClD;AAAA,UACA;AAAA,UACA;AAAA,UACA,cAAc,SACV,EAAE,eAAe,UAAU,cAAc,MAAM,GAAG,IAClD;AAAA,QACN;AAAA,MACF,OAAO;AAEL,cAAM,eAAe;AAAA,UACnB;AAAA,UACA;AAAA,QACF;AACA,cAAM,cAAc,IAAI,SAAS,cAAc,GAAG;AAClD,qBAAa,YAAY,EAAE,QAAQ,QAAQ,CAAC;AAAA,MAC9C;AAGA,uBAAiB;AAEjB,UAAI,IAAI,QAAQ;AACd,yBAAiB;AACjB,YAAI,eAAe;AAGjB,gBAAM,WAAqB;AAAA,YACzB,OAAO,IAAI;AAAA,YACX,OAAO,IAAI;AAAA,YACX,UAAU,IAAI;AAAA,UAChB;AACA,gBAAM,IAAI,eAAe;AAAA,YACvB,UAAU,SAAS,KAAK;AAAA,YACxB;AAAA,YACA,IAAI,eAAe;AAAA,UACrB;AAEA,gBAAM,aAAa,GAAG,cAAc,OAAO,WAAW,KAAK;AAC3D,gBAAM,WAAW,MAAM,SAAS,YAAY;AAAA,YAC1C,SAAS,cAAc,SACnB,EAAE,eAAe,UAAU,cAAc,MAAM,GAAG,IAClD;AAAA,UACN,CAAC;AACD,cAAI,CAAC,SAAS,IAAI;AAChB,kBAAM,IAAI,MAAM,0BAA0B,SAAS,MAAM,EAAE;AAAA,UAC7D;AACA,gBAAM,cAAc,OAAO,KAAK,MAAM,SAAS,YAAY,CAAC;AAE5D,gBAAM,UAAU,MAAM;AAAA,YACpB;AAAA,YACA,IAAI;AAAA,YACJ,IAAI;AAAA,YACJ;AAAA,UACF;AACA;AAAA,YACE;AAAA,YACA;AAAA,UACF;AAAA,QACF,OAAO;AAEL,gBAAM,UAAU,MAAM;AAAA,YACpB,IAAI;AAAA,YACJ,IAAI;AAAA,YACJ,IAAI;AAAA,YACJ;AAAA,YACA;AAAA,UACF;AACA;AAAA,YACE;AAAA,YACA;AAAA,UACF;AAAA,QACF;AACA,kBAAU,YAAY,EAAE,QAAQ,OAAO,CAAC;AAAA,MAC1C,OAAO;AAEL,cAAM,WAAqB;AAAA,UACzB,OAAO,IAAI;AAAA,UACX,OAAO,IAAI;AAAA,UACX,UAAU,IAAI;AAAA,QAChB;AACA,cAAM,IAAI,eAAe;AAAA,UACvB,UAAU,SAAS,KAAK;AAAA,UACxB;AAAA,UACA,IAAI,eAAe;AAAA,QACrB;AACA,cAAM,WAAW,GAAG,IAAI,OAAO,WAAW,KAAK;AAC/C,kBAAU,SAAS,EAAE,SAAS,CAAC;AAAA,MACjC;AAAA,IACF,SAAS,OAAO;AACd,gBAAU,SAAS;AAAA,QACjB,OAAO;AAAA,QACP,SAAS,OAAO,KAAK;AAAA,MACvB,CAAC;AAAA,IACH,UAAE;AACA,oBAAc,SAAS;AACvB,UAAI,IAAI;AAAA,IACV;AAAA,EACF,SAAS,OAAO;AACd,YAAQ,MAAM,uCAAuC,KAAK;AAC1D,QAAI,CAAC,IAAI,aAAa;AACpB;AAAA,QACE;AAAA,QACA;AAAA,QACA,UAAU;AAAA,QACV;AAAA,MACF;AAAA,IACF,OAAO;AACL,UAAI,IAAI;AAAA,IACV;AAAA,EACF;AACF;AAMA,eAAsB,kBACpB,KACA,KACA,KACe;AACf,MAAI;AACF,qBAAiB,GAAG;AAEpB,UAAM,QAAQ,IAAI,OAAO;AACzB,UAAM,cAAc,UAAU,SAAS,KAAK;AAC5C,UAAM,WAAW,MAAM,IAAI,eAAe,QAAkB,WAAW;AAEvE,QAAI,CAAC,UAAU;AACb,gBAAU,KAAK,KAAK,UAAU,WAAW,4BAA4B;AACrE;AAAA,IACF;AAIA,QAAI,IAAI,uBAAuB;AAC7B,YAAM,UAAU,IAAI;AAAA,QAClB,SAAS;AAAA,QACT,SAAS;AAAA,MACX;AACA,UAAI,SAAS;AACX,cAAM,aAAa,GAAG,QAAQ,OAAO,WAAW,KAAK;AACrD,cAAM,WAAW,MAAM,SAAS,YAAY;AAAA,UAC1C,SAAS,QAAQ,SACb,EAAE,eAAe,UAAU,QAAQ,MAAM,GAAG,IAC5C;AAAA,QACN,CAAC;AAED,YAAI,CAAC,SAAS,IAAI;AAChB;AAAA,YACE;AAAA,YACA,SAAS;AAAA,YACT,UAAU;AAAA,YACV;AAAA,UACF;AACA;AAAA,QACF;AAEA,cAAM,kBAAkB,UAAU,GAAG;AACrC;AAAA,MACF;AAAA,IACF;AAGA,QAAI,eAAe,OAAO,WAAW;AAGrC,UAAM,mBAAmB,KAAK,UAAU,GAAG;AAAA,EAC7C,SAAS,OAAO;AACd,YAAQ,MAAM,0BAA0B,KAAK;AAC7C,QAAI,CAAC,IAAI,aAAa;AACpB,gBAAU,KAAK,KAAK,UAAU,gBAAgB,wBAAwB;AAAA,IACxE,OAAO;AACL,UAAI,IAAI;AAAA,IACV;AAAA,EACF;AACF;AAKA,eAAe,mBACb,KACA,KACA,KACe;AACf,QAAM,EAAE,cAAc,IAAI,MAAM,OAAO,sBAAW;AAClD,QAAM,WAAW,IAAI,cAAc,IAAI,OAAO;AAAA,IAC5C,gBAAgB,IAAI;AAAA,IACpB,WAAW,IAAI;AAAA,EACjB,CAAC;AACD,QAAM,cAAc,MAAM,SAAS,OAAO,IAAI,KAAK;AAEnD,MAAI,GAAG,SAAS,MAAM;AACpB,gBAAY,QAAQ;AACpB,aAAS,MAAM;AAAA,EACjB,CAAC;AAED,MAAI,IAAI,gBAAgB,WAAW;AACnC,MAAI,IAAI,iBAAiB,kCAAkC;AAC3D,cAAY,KAAK,GAAG;AACtB;AAMA,eAAe,oBACb,aACA,OACA,QACA,WACiC;AACjC,QAAM,UAAkC,CAAC;AAGzC,MAAI,OAAO,UAAU;AACnB,UAAM,sBAAsB,KAAK,IAAI;AACrC,QAAI;AACJ,QAAI,MAAM,MAAM,WAAW,OAAO,GAAG;AACnC,YAAM,aAAa,MAAM,MAAM,QAAQ,GAAG;AAC1C,UAAI,eAAe,IAAI;AACrB,cAAM,IAAI,MAAM,wBAAwB;AAAA,MAC1C;AACA,YAAM,OAAO,MAAM,MAAM,MAAM,GAAG,UAAU;AAC5C,YAAM,WAAW,KAAK,SAAS,SAAS;AACxC,YAAM,OAAO,MAAM,MAAM,MAAM,aAAa,CAAC;AAC7C,oBAAc,WACV,OAAO,KAAK,MAAM,QAAQ,IAC1B,OAAO,KAAK,mBAAmB,IAAI,CAAC;AAAA,IAC1C,OAAO;AACL,YAAM,qBAAqB,MAAM,SAAS,MAAM,KAAK;AACrD,UAAI,CAAC,mBAAmB,IAAI;AAC1B,cAAM,IAAI;AAAA,UACR,gCAAgC,mBAAmB,MAAM,IAAI,mBAAmB,UAAU;AAAA,QAC5F;AAAA,MACF;AACA,oBAAc,OAAO,KAAK,MAAM,mBAAmB,YAAY,CAAC;AAAA,IAClE;AACA,YAAQ,iBAAiB,KAAK,IAAI,IAAI;AAEtC,UAAM,uBAAuB,KAAK,IAAI;AACtC,UAAM,sBAAsB,MAAM,SAAS,OAAO,UAAU;AAAA,MAC1D,QAAQ;AAAA,MACR,MAAM;AAAA,MACN,SAAS;AAAA,QACP,gBAAgB;AAAA,QAChB,kBAAkB,YAAY,OAAO,SAAS;AAAA,MAChD;AAAA,IACF,CAAC;AACD,QAAI,CAAC,oBAAoB,IAAI;AAC3B,YAAM,IAAI;AAAA,QACR,2BAA2B,oBAAoB,MAAM,IAAI,oBAAoB,UAAU;AAAA,MACzF;AAAA,IACF;AACA,YAAQ,kBAAkB,KAAK,IAAI,IAAI;AAAA,EACzC;AAGA,YAAU,aAAa,EAAE,OAAO,SAAS,CAAC;AAG1C,QAAM,kBAAkB,KAAK,IAAI;AACjC,QAAM,iBAAiB,MAAM,SAAS,OAAO,UAAU;AAAA,IACrD,QAAQ;AAAA,IACR,MAAM;AAAA,IACN,SAAS;AAAA,MACP,gBAAgB;AAAA,MAChB,kBAAkB,YAAY,OAAO,SAAS;AAAA,IAChD;AAAA,EACF,CAAC;AACD,MAAI,CAAC,eAAe,IAAI;AACtB,UAAM,IAAI;AAAA,MACR,oCAAoC,eAAe,MAAM,IAAI,eAAe,UAAU;AAAA,IACxF;AAAA,EACF;AACA,UAAQ,aAAa,KAAK,IAAI,IAAI;AAElC,SAAO;AACT;AAMA,eAAsB,wBACpB,OACA,OACA,QACA,WACA,KACiC;AAEjC,QAAM,kBAAkB,KAAK,IAAI;AACjC,QAAM,EAAE,cAAc,IAAI,MAAM,OAAO,sBAAW;AAClD,QAAM,WAAW,IAAI,cAAc,OAAO;AAAA,IACxC,gBAAgB,IAAI;AAAA,IACpB,WAAW,IAAI;AAAA,EACjB,CAAC;AACD,MAAI;AACF,UAAM,cAAc,MAAM,SAAS,OAAO,KAAK;AAC/C,UAAM,SAAmB,CAAC;AAC1B,qBAAiB,SAAS,aAAa;AACrC,aAAO,KAAK,OAAO,KAAK,KAAK,CAAC;AAAA,IAChC;AACA,UAAM,cAAc,OAAO,OAAO,MAAM;AACxC,UAAM,aAAa,KAAK,IAAI,IAAI;AAGhC,UAAM,UAAU,MAAM;AAAA,MACpB;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AACA,YAAQ,aAAa;AAErB,WAAO;AAAA,EACT,UAAE;AACA,aAAS,MAAM;AAAA,EACjB;AACF;","names":["Readable","sendEvent","Readable","randomUUID","extractEffieSourcesWithTypes","extractEffieSources","randomUUID","job","storeJobStart","extractEffieSourcesWithTypes","extractEffieSources"]}
|