@effing/ffs 0.16.0 → 0.17.0

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