@effing/ffs 0.17.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.
|
@@ -711,13 +711,9 @@ async function streamRenderProgress(req, res, ctx) {
|
|
|
711
711
|
sendEvent("keepalive", { phase: keepalivePhase });
|
|
712
712
|
}, 25e3);
|
|
713
713
|
try {
|
|
714
|
-
const progressStart = performance.now();
|
|
715
|
-
const timings = {};
|
|
716
714
|
let job;
|
|
717
715
|
if (storedJob.kind === "deferred") {
|
|
718
|
-
const effieFetchStart = performance.now();
|
|
719
716
|
job = await resolveEffieUrl(storedJob, sendEvent, ctx);
|
|
720
|
-
timings.effieFetch = performance.now() - effieFetchStart;
|
|
721
717
|
await ctx.transientStore.putJson(
|
|
722
718
|
storeKeys.warmupJob(job.warmupJobId),
|
|
723
719
|
{ sources: job.sources, metadata: job.metadata },
|
|
@@ -737,7 +733,6 @@ async function streamRenderProgress(req, res, ctx) {
|
|
|
737
733
|
);
|
|
738
734
|
sendEvent("purge:complete", purgeResult);
|
|
739
735
|
}
|
|
740
|
-
const warmupStart = performance.now();
|
|
741
736
|
if (warmupBackend) {
|
|
742
737
|
await proxyRemoteSSE(
|
|
743
738
|
`${warmupBackend.baseUrl}/warmup/${job.warmupJobId}/progress`,
|
|
@@ -754,10 +749,10 @@ async function streamRenderProgress(req, res, ctx) {
|
|
|
754
749
|
await warmupSources(job.sources, warmupSender, ctx);
|
|
755
750
|
warmupSender("complete", { status: "ready" });
|
|
756
751
|
}
|
|
757
|
-
timings.warmup = performance.now() - warmupStart;
|
|
758
752
|
keepalivePhase = "render";
|
|
759
753
|
if (job.upload) {
|
|
760
754
|
keepalivePhase = "upload";
|
|
755
|
+
const renderStart = performance.now();
|
|
761
756
|
if (renderBackend) {
|
|
762
757
|
const videoJob = {
|
|
763
758
|
effie: job.effie,
|
|
@@ -777,38 +772,35 @@ async function streamRenderProgress(req, res, ctx) {
|
|
|
777
772
|
throw new Error(`Backend render failed: ${response.status}`);
|
|
778
773
|
}
|
|
779
774
|
const videoBuffer = Buffer.from(await response.arrayBuffer());
|
|
780
|
-
const
|
|
775
|
+
const timings = await uploadRenderedVideo(
|
|
781
776
|
videoBuffer,
|
|
782
777
|
job.effie,
|
|
783
778
|
job.upload,
|
|
784
779
|
sendEvent
|
|
785
780
|
);
|
|
786
|
-
Object.assign(timings, phaseTimings);
|
|
787
781
|
sendEvent(
|
|
788
782
|
"render:complete",
|
|
789
|
-
|
|
783
|
+
timings
|
|
790
784
|
);
|
|
791
785
|
} else {
|
|
792
|
-
const
|
|
786
|
+
const timings = await renderAndUploadInternal(
|
|
793
787
|
job.effie,
|
|
794
788
|
job.scale,
|
|
795
789
|
job.upload,
|
|
796
790
|
sendEvent,
|
|
797
791
|
ctx
|
|
798
792
|
);
|
|
799
|
-
Object.assign(timings, phaseTimings);
|
|
800
793
|
sendEvent(
|
|
801
794
|
"render:complete",
|
|
802
|
-
|
|
795
|
+
timings
|
|
803
796
|
);
|
|
804
797
|
}
|
|
805
|
-
timings.total = performance.now() - progressStart;
|
|
806
798
|
if (ctx.onRenderComplete) {
|
|
807
799
|
try {
|
|
808
800
|
await ctx.onRenderComplete({
|
|
809
801
|
effie: job.effie,
|
|
810
802
|
metadata: job.metadata,
|
|
811
|
-
|
|
803
|
+
wallClockTime: performance.now() - renderStart
|
|
812
804
|
});
|
|
813
805
|
} catch (err) {
|
|
814
806
|
console.error("onRenderComplete hook error:", err);
|
|
@@ -827,18 +819,6 @@ async function streamRenderProgress(req, res, ctx) {
|
|
|
827
819
|
ctx.transientStore.ttlMs
|
|
828
820
|
);
|
|
829
821
|
const videoUrl = `${ctx.baseUrl}/render/${jobId}/video`;
|
|
830
|
-
timings.total = performance.now() - progressStart;
|
|
831
|
-
if (ctx.onRenderComplete) {
|
|
832
|
-
try {
|
|
833
|
-
await ctx.onRenderComplete({
|
|
834
|
-
effie: job.effie,
|
|
835
|
-
metadata: job.metadata,
|
|
836
|
-
timings
|
|
837
|
-
});
|
|
838
|
-
} catch (err) {
|
|
839
|
-
console.error("onRenderComplete hook error:", err);
|
|
840
|
-
}
|
|
841
|
-
}
|
|
842
822
|
sendEvent("ready", { videoUrl });
|
|
843
823
|
}
|
|
844
824
|
} catch (error) {
|
|
@@ -880,6 +860,7 @@ async function streamRenderVideo(req, res, ctx) {
|
|
|
880
860
|
videoJob.metadata
|
|
881
861
|
);
|
|
882
862
|
if (backend) {
|
|
863
|
+
const renderStart = performance.now();
|
|
883
864
|
const backendUrl = `${backend.baseUrl}/render/${jobId}/video`;
|
|
884
865
|
const response = await ffsFetch(backendUrl, {
|
|
885
866
|
headers: backend.apiKey ? { Authorization: `Bearer ${backend.apiKey}` } : void 0
|
|
@@ -894,6 +875,17 @@ async function streamRenderVideo(req, res, ctx) {
|
|
|
894
875
|
return;
|
|
895
876
|
}
|
|
896
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
|
+
}
|
|
897
889
|
return;
|
|
898
890
|
}
|
|
899
891
|
}
|
|
@@ -909,19 +901,38 @@ async function streamRenderVideo(req, res, ctx) {
|
|
|
909
901
|
}
|
|
910
902
|
}
|
|
911
903
|
async function streamRenderDirect(res, job, ctx) {
|
|
904
|
+
const renderStart = performance.now();
|
|
912
905
|
const { EffieRenderer } = await import("./render-764Y6ARD.js");
|
|
913
906
|
const renderer = new EffieRenderer(job.effie, {
|
|
914
907
|
transientStore: ctx.transientStore,
|
|
915
908
|
httpProxy: ctx.httpProxy
|
|
916
909
|
});
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
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 {
|
|
920
923
|
renderer.close();
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
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
|
+
}
|
|
925
936
|
}
|
|
926
937
|
async function uploadRenderedVideo(videoBuffer, effie, upload, sendEvent) {
|
|
927
938
|
const timings = {};
|
|
@@ -1022,4 +1033,4 @@ export {
|
|
|
1022
1033
|
streamRenderProgress,
|
|
1023
1034
|
streamRenderVideo
|
|
1024
1035
|
};
|
|
1025
|
-
//# 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
|
@@ -30,7 +30,7 @@ declare function sendError(res: express.Response, status: number, code: ErrorCod
|
|
|
30
30
|
type OnRenderComplete = (result: {
|
|
31
31
|
effie: EffieData<EffieSources>;
|
|
32
32
|
metadata?: Record<string, unknown>;
|
|
33
|
-
|
|
33
|
+
wallClockTime: number;
|
|
34
34
|
}) => void | Promise<void>;
|
|
35
35
|
type UploadOptions = {
|
|
36
36
|
videoUrl: string;
|
package/dist/handlers/index.js
CHANGED
package/dist/server.js
CHANGED
|
@@ -716,13 +716,9 @@ async function streamRenderProgress(req, res, ctx2) {
|
|
|
716
716
|
sendEvent("keepalive", { phase: keepalivePhase });
|
|
717
717
|
}, 25e3);
|
|
718
718
|
try {
|
|
719
|
-
const progressStart = performance.now();
|
|
720
|
-
const timings = {};
|
|
721
719
|
let job;
|
|
722
720
|
if (storedJob.kind === "deferred") {
|
|
723
|
-
const effieFetchStart = performance.now();
|
|
724
721
|
job = await resolveEffieUrl(storedJob, sendEvent, ctx2);
|
|
725
|
-
timings.effieFetch = performance.now() - effieFetchStart;
|
|
726
722
|
await ctx2.transientStore.putJson(
|
|
727
723
|
storeKeys.warmupJob(job.warmupJobId),
|
|
728
724
|
{ sources: job.sources, metadata: job.metadata },
|
|
@@ -742,7 +738,6 @@ async function streamRenderProgress(req, res, ctx2) {
|
|
|
742
738
|
);
|
|
743
739
|
sendEvent("purge:complete", purgeResult);
|
|
744
740
|
}
|
|
745
|
-
const warmupStart = performance.now();
|
|
746
741
|
if (warmupBackend) {
|
|
747
742
|
await proxyRemoteSSE(
|
|
748
743
|
`${warmupBackend.baseUrl}/warmup/${job.warmupJobId}/progress`,
|
|
@@ -759,10 +754,10 @@ async function streamRenderProgress(req, res, ctx2) {
|
|
|
759
754
|
await warmupSources(job.sources, warmupSender, ctx2);
|
|
760
755
|
warmupSender("complete", { status: "ready" });
|
|
761
756
|
}
|
|
762
|
-
timings.warmup = performance.now() - warmupStart;
|
|
763
757
|
keepalivePhase = "render";
|
|
764
758
|
if (job.upload) {
|
|
765
759
|
keepalivePhase = "upload";
|
|
760
|
+
const renderStart = performance.now();
|
|
766
761
|
if (renderBackend) {
|
|
767
762
|
const videoJob = {
|
|
768
763
|
effie: job.effie,
|
|
@@ -782,38 +777,35 @@ async function streamRenderProgress(req, res, ctx2) {
|
|
|
782
777
|
throw new Error(`Backend render failed: ${response.status}`);
|
|
783
778
|
}
|
|
784
779
|
const videoBuffer = Buffer.from(await response.arrayBuffer());
|
|
785
|
-
const
|
|
780
|
+
const timings = await uploadRenderedVideo(
|
|
786
781
|
videoBuffer,
|
|
787
782
|
job.effie,
|
|
788
783
|
job.upload,
|
|
789
784
|
sendEvent
|
|
790
785
|
);
|
|
791
|
-
Object.assign(timings, phaseTimings);
|
|
792
786
|
sendEvent(
|
|
793
787
|
"render:complete",
|
|
794
|
-
|
|
788
|
+
timings
|
|
795
789
|
);
|
|
796
790
|
} else {
|
|
797
|
-
const
|
|
791
|
+
const timings = await renderAndUploadInternal(
|
|
798
792
|
job.effie,
|
|
799
793
|
job.scale,
|
|
800
794
|
job.upload,
|
|
801
795
|
sendEvent,
|
|
802
796
|
ctx2
|
|
803
797
|
);
|
|
804
|
-
Object.assign(timings, phaseTimings);
|
|
805
798
|
sendEvent(
|
|
806
799
|
"render:complete",
|
|
807
|
-
|
|
800
|
+
timings
|
|
808
801
|
);
|
|
809
802
|
}
|
|
810
|
-
timings.total = performance.now() - progressStart;
|
|
811
803
|
if (ctx2.onRenderComplete) {
|
|
812
804
|
try {
|
|
813
805
|
await ctx2.onRenderComplete({
|
|
814
806
|
effie: job.effie,
|
|
815
807
|
metadata: job.metadata,
|
|
816
|
-
|
|
808
|
+
wallClockTime: performance.now() - renderStart
|
|
817
809
|
});
|
|
818
810
|
} catch (err) {
|
|
819
811
|
console.error("onRenderComplete hook error:", err);
|
|
@@ -832,18 +824,6 @@ async function streamRenderProgress(req, res, ctx2) {
|
|
|
832
824
|
ctx2.transientStore.ttlMs
|
|
833
825
|
);
|
|
834
826
|
const videoUrl = `${ctx2.baseUrl}/render/${jobId}/video`;
|
|
835
|
-
timings.total = performance.now() - progressStart;
|
|
836
|
-
if (ctx2.onRenderComplete) {
|
|
837
|
-
try {
|
|
838
|
-
await ctx2.onRenderComplete({
|
|
839
|
-
effie: job.effie,
|
|
840
|
-
metadata: job.metadata,
|
|
841
|
-
timings
|
|
842
|
-
});
|
|
843
|
-
} catch (err) {
|
|
844
|
-
console.error("onRenderComplete hook error:", err);
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
827
|
sendEvent("ready", { videoUrl });
|
|
848
828
|
}
|
|
849
829
|
} catch (error) {
|
|
@@ -885,6 +865,7 @@ async function streamRenderVideo(req, res, ctx2) {
|
|
|
885
865
|
videoJob.metadata
|
|
886
866
|
);
|
|
887
867
|
if (backend) {
|
|
868
|
+
const renderStart = performance.now();
|
|
888
869
|
const backendUrl = `${backend.baseUrl}/render/${jobId}/video`;
|
|
889
870
|
const response = await ffsFetch(backendUrl, {
|
|
890
871
|
headers: backend.apiKey ? { Authorization: `Bearer ${backend.apiKey}` } : void 0
|
|
@@ -899,6 +880,17 @@ async function streamRenderVideo(req, res, ctx2) {
|
|
|
899
880
|
return;
|
|
900
881
|
}
|
|
901
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
|
+
}
|
|
902
894
|
return;
|
|
903
895
|
}
|
|
904
896
|
}
|
|
@@ -914,19 +906,38 @@ async function streamRenderVideo(req, res, ctx2) {
|
|
|
914
906
|
}
|
|
915
907
|
}
|
|
916
908
|
async function streamRenderDirect(res, job, ctx2) {
|
|
909
|
+
const renderStart = performance.now();
|
|
917
910
|
const { EffieRenderer } = await import("./render-7O2KKH3F.js");
|
|
918
911
|
const renderer = new EffieRenderer(job.effie, {
|
|
919
912
|
transientStore: ctx2.transientStore,
|
|
920
913
|
httpProxy: ctx2.httpProxy
|
|
921
914
|
});
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
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 {
|
|
925
928
|
renderer.close();
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
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
|
+
}
|
|
930
941
|
}
|
|
931
942
|
async function uploadRenderedVideo(videoBuffer, effie, upload, sendEvent) {
|
|
932
943
|
const timings = {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effing/ffs",
|
|
3
|
-
"version": "0.17.
|
|
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.17.
|
|
39
|
+
"@effing/effie": "0.17.1"
|
|
40
40
|
},
|
|
41
41
|
"optionalDependencies": {
|
|
42
|
-
"@effing/ffmpeg": "0.17.
|
|
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 OnRenderComplete = (result: {\n effie: EffieData<EffieSources>;\n metadata?: Record<string, unknown>;\n timings: Record<string, 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 const progressStart = performance.now();\n const timings: Record<string, number> = {};\n\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 const effieFetchStart = performance.now();\n job = await resolveEffieUrl(storedJob, sendEvent, ctx);\n timings.effieFetch = performance.now() - effieFetchStart;\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 const warmupStart = performance.now();\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 timings.warmup = performance.now() - warmupStart;\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 phaseTimings = await uploadRenderedVideo(\n videoBuffer,\n job.effie,\n job.upload,\n sendEvent,\n );\n Object.assign(timings, phaseTimings);\n sendEvent(\n \"render:complete\",\n phaseTimings as RenderEventMap[\"render:complete\"],\n );\n } else {\n // Upload + no backend: render and upload locally (no VideoJob stored)\n const phaseTimings = await renderAndUploadInternal(\n job.effie,\n job.scale,\n job.upload,\n sendEvent,\n ctx,\n );\n Object.assign(timings, phaseTimings);\n sendEvent(\n \"render:complete\",\n phaseTimings as RenderEventMap[\"render:complete\"],\n );\n }\n\n timings.total = performance.now() - progressStart;\n if (ctx.onRenderComplete) {\n try {\n await ctx.onRenderComplete({\n effie: job.effie,\n metadata: job.metadata,\n timings,\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\n timings.total = performance.now() - progressStart;\n if (ctx.onRenderComplete) {\n try {\n await ctx.onRenderComplete({\n effie: job.effie,\n metadata: job.metadata,\n timings,\n });\n } catch (err) {\n console.error(\"onRenderComplete hook error:\", err);\n }\n }\n\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;;;AFuEA,eAAsB,oBAAoB,SAKf;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,IAChC,kBAAkB,SAAS;AAAA,EAC7B;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;;;AG1TA,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;AACF,YAAM,gBAAgB,YAAY,IAAI;AACtC,YAAM,UAAkC,CAAC;AAIzC,UAAI;AACJ,UAAI,UAAU,SAAS,YAAY;AACjC,cAAM,kBAAkB,YAAY,IAAI;AACxC,cAAM,MAAM,gBAAgB,WAAW,WAAW,GAAG;AACrD,gBAAQ,aAAa,YAAY,IAAI,IAAI;AAGzC,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,YAAM,cAAc,YAAY,IAAI;AACpC,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;AACA,cAAQ,SAAS,YAAY,IAAI,IAAI;AAGrC,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,eAAe,MAAM;AAAA,YACzB;AAAA,YACA,IAAI;AAAA,YACJ,IAAI;AAAA,YACJ;AAAA,UACF;AACA,iBAAO,OAAO,SAAS,YAAY;AACnC;AAAA,YACE;AAAA,YACA;AAAA,UACF;AAAA,QACF,OAAO;AAEL,gBAAM,eAAe,MAAM;AAAA,YACzB,IAAI;AAAA,YACJ,IAAI;AAAA,YACJ,IAAI;AAAA,YACJ;AAAA,YACA;AAAA,UACF;AACA,iBAAO,OAAO,SAAS,YAAY;AACnC;AAAA,YACE;AAAA,YACA;AAAA,UACF;AAAA,QACF;AAEA,gBAAQ,QAAQ,YAAY,IAAI,IAAI;AACpC,YAAI,IAAI,kBAAkB;AACxB,cAAI;AACF,kBAAM,IAAI,iBAAiB;AAAA,cACzB,OAAO,IAAI;AAAA,cACX,UAAU,IAAI;AAAA,cACd;AAAA,YACF,CAAC;AAAA,UACH,SAAS,KAAK;AACZ,oBAAQ,MAAM,gCAAgC,GAAG;AAAA,UACnD;AAAA,QACF;AAEA,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;AAE/C,gBAAQ,QAAQ,YAAY,IAAI,IAAI;AACpC,YAAI,IAAI,kBAAkB;AACxB,cAAI;AACF,kBAAM,IAAI,iBAAiB;AAAA,cACzB,OAAO,IAAI;AAAA,cACX,UAAU,IAAI;AAAA,cACd;AAAA,YACF,CAAC;AAAA,UACH,SAAS,KAAK;AACZ,oBAAQ,MAAM,gCAAgC,GAAG;AAAA,UACnD;AAAA,QACF;AAEA,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"]}
|