@effing/ffs 0.5.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,7 +2,7 @@ import {
2
2
  createTransientStore,
3
3
  ffsFetch,
4
4
  storeKeys
5
- } from "./chunk-5SGOYTM2.js";
5
+ } from "./chunk-4N2GLGC5.js";
6
6
 
7
7
  // src/handlers/shared.ts
8
8
  import "express";
@@ -131,11 +131,11 @@ var HttpProxy = class {
131
131
 
132
132
  // src/handlers/shared.ts
133
133
  import { effieDataSchema } from "@effing/effie";
134
- async function createServerContext() {
134
+ async function createServerContext(options) {
135
135
  const port = process.env.FFS_PORT || process.env.PORT || 2e3;
136
- const renderBackendBaseUrl = process.env.FFS_RENDER_BACKEND_BASE_URL;
136
+ const enableHttpProxy = options?.httpProxy ?? !options?.renderBackendResolver;
137
137
  let httpProxy;
138
- if (!renderBackendBaseUrl) {
138
+ if (enableHttpProxy) {
139
139
  httpProxy = new HttpProxy();
140
140
  await httpProxy.start();
141
141
  }
@@ -145,10 +145,8 @@ async function createServerContext() {
145
145
  baseUrl: process.env.FFS_BASE_URL || `http://localhost:${port}`,
146
146
  skipValidation: !!process.env.FFS_SKIP_VALIDATION && process.env.FFS_SKIP_VALIDATION !== "false",
147
147
  warmupConcurrency: parseInt(process.env.FFS_WARMUP_CONCURRENCY || "4", 10),
148
- warmupBackendBaseUrl: process.env.FFS_WARMUP_BACKEND_BASE_URL,
149
- renderBackendBaseUrl: process.env.FFS_RENDER_BACKEND_BASE_URL,
150
- warmupBackendApiKey: process.env.FFS_WARMUP_BACKEND_API_KEY,
151
- renderBackendApiKey: process.env.FFS_RENDER_BACKEND_API_KEY
148
+ warmupBackendResolver: options?.warmupBackendResolver,
149
+ renderBackendResolver: options?.renderBackendResolver
152
150
  };
153
151
  }
154
152
  function parseEffieData(body, skipValidation) {
@@ -211,16 +209,16 @@ import { extractEffieSourcesWithTypes, effieDataSchema as effieDataSchema3 } fro
211
209
  import "express";
212
210
  import { randomUUID } from "crypto";
213
211
  import { effieDataSchema as effieDataSchema2 } from "@effing/effie";
214
- async function createRenderJob(req, res, ctx) {
212
+ async function createRenderJob(req, res, ctx, options) {
215
213
  try {
216
214
  const isWrapped = "effie" in req.body;
217
215
  let rawEffieData;
218
216
  let scale;
219
217
  let upload;
220
218
  if (isWrapped) {
221
- const options = req.body;
222
- if (typeof options.effie === "string") {
223
- const response = await ffsFetch(options.effie);
219
+ const options2 = req.body;
220
+ if (typeof options2.effie === "string") {
221
+ const response = await ffsFetch(options2.effie);
224
222
  if (!response.ok) {
225
223
  throw new Error(
226
224
  `Failed to fetch Effie data: ${response.status} ${response.statusText}`
@@ -228,10 +226,10 @@ async function createRenderJob(req, res, ctx) {
228
226
  }
229
227
  rawEffieData = await response.json();
230
228
  } else {
231
- rawEffieData = options.effie;
229
+ rawEffieData = options2.effie;
232
230
  }
233
- scale = options.scale ?? 1;
234
- upload = options.upload;
231
+ scale = options2.scale ?? 1;
232
+ upload = options2.upload;
235
233
  } else {
236
234
  rawEffieData = req.body;
237
235
  scale = parseFloat(req.query.scale?.toString() || "1");
@@ -263,12 +261,13 @@ async function createRenderJob(req, res, ctx) {
263
261
  effie,
264
262
  scale,
265
263
  upload,
266
- createdAt: Date.now()
264
+ createdAt: Date.now(),
265
+ metadata: options?.metadata
267
266
  };
268
267
  await ctx.transientStore.putJson(
269
268
  storeKeys.renderJob(jobId),
270
269
  job,
271
- ctx.transientStore.jobMetadataTtlMs
270
+ ctx.transientStore.jobDataTtlMs
272
271
  );
273
272
  res.json({
274
273
  id: jobId,
@@ -283,17 +282,20 @@ async function streamRenderJob(req, res, ctx) {
283
282
  try {
284
283
  setupCORSHeaders(res);
285
284
  const jobId = req.params.id;
286
- if (ctx.renderBackendBaseUrl) {
287
- await proxyRenderFromBackend(res, jobId, ctx);
288
- return;
289
- }
290
- const jobCacheKey = storeKeys.renderJob(jobId);
291
- const job = await ctx.transientStore.getJson(jobCacheKey);
292
- ctx.transientStore.delete(jobCacheKey);
285
+ const jobStoreKey = storeKeys.renderJob(jobId);
286
+ const job = await ctx.transientStore.getJson(jobStoreKey);
293
287
  if (!job) {
294
288
  res.status(404).json({ error: "Job not found or expired" });
295
289
  return;
296
290
  }
291
+ if (ctx.renderBackendResolver) {
292
+ const backend = ctx.renderBackendResolver(job.effie, job.metadata);
293
+ if (backend) {
294
+ await proxyRenderFromBackend(res, jobId, backend);
295
+ return;
296
+ }
297
+ }
298
+ ctx.transientStore.delete(jobStoreKey);
297
299
  if (job.upload) {
298
300
  await streamRenderWithUpload(res, job, ctx);
299
301
  } else {
@@ -309,7 +311,7 @@ async function streamRenderJob(req, res, ctx) {
309
311
  }
310
312
  }
311
313
  async function streamRenderDirect(res, job, ctx) {
312
- const { EffieRenderer } = await import("./render-NEDCS65O.js");
314
+ const { EffieRenderer } = await import("./render-IKGZZOBP.js");
313
315
  const renderer = new EffieRenderer(job.effie, {
314
316
  transientStore: ctx.transientStore,
315
317
  httpProxy: ctx.httpProxy
@@ -320,6 +322,7 @@ async function streamRenderDirect(res, job, ctx) {
320
322
  renderer.close();
321
323
  });
322
324
  res.set("Content-Type", "video/mp4");
325
+ res.set("Cache-Control", "public, immutable, max-age=86400");
323
326
  videoStream.pipe(res);
324
327
  }
325
328
  async function streamRenderWithUpload(res, job, ctx) {
@@ -374,7 +377,7 @@ async function renderAndUploadInternal(effie, scale, upload, sendEvent, ctx) {
374
377
  timings.uploadCoverTime = Date.now() - uploadCoverStartTime;
375
378
  }
376
379
  const renderStartTime = Date.now();
377
- const { EffieRenderer } = await import("./render-NEDCS65O.js");
380
+ const { EffieRenderer } = await import("./render-IKGZZOBP.js");
378
381
  const renderer = new EffieRenderer(effie, {
379
382
  transientStore: ctx.transientStore,
380
383
  httpProxy: ctx.httpProxy
@@ -404,10 +407,10 @@ async function renderAndUploadInternal(effie, scale, upload, sendEvent, ctx) {
404
407
  timings.uploadTime = Date.now() - uploadStartTime;
405
408
  return timings;
406
409
  }
407
- async function proxyRenderFromBackend(res, jobId, ctx) {
408
- const backendUrl = `${ctx.renderBackendBaseUrl}/render/${jobId}`;
410
+ async function proxyRenderFromBackend(res, jobId, backend) {
411
+ const backendUrl = `${backend.baseUrl}/render/${jobId}`;
409
412
  const response = await ffsFetch(backendUrl, {
410
- headers: ctx.renderBackendApiKey ? { Authorization: `Bearer ${ctx.renderBackendApiKey}` } : void 0
413
+ headers: backend.apiKey ? { Authorization: `Bearer ${backend.apiKey}` } : void 0
411
414
  });
412
415
  if (!response.ok) {
413
416
  res.status(response.status).json({ error: "Backend render failed" });
@@ -464,12 +467,12 @@ async function proxyRenderFromBackend(res, jobId, ctx) {
464
467
  }
465
468
 
466
469
  // src/handlers/orchestrating.ts
467
- async function createWarmupAndRenderJob(req, res, ctx) {
470
+ async function createWarmupAndRenderJob(req, res, ctx, options) {
468
471
  try {
469
- const options = req.body;
472
+ const body = req.body;
470
473
  let rawEffieData;
471
- if (typeof options.effie === "string") {
472
- const response = await ffsFetch(options.effie);
474
+ if (typeof body.effie === "string") {
475
+ const response = await ffsFetch(body.effie);
473
476
  if (!response.ok) {
474
477
  throw new Error(
475
478
  `Failed to fetch Effie data: ${response.status} ${response.statusText}`
@@ -477,7 +480,7 @@ async function createWarmupAndRenderJob(req, res, ctx) {
477
480
  }
478
481
  rawEffieData = await response.json();
479
482
  } else {
480
- rawEffieData = options.effie;
483
+ rawEffieData = body.effie;
481
484
  }
482
485
  let effie;
483
486
  if (!ctx.skipValidation) {
@@ -502,8 +505,8 @@ async function createWarmupAndRenderJob(req, res, ctx) {
502
505
  effie = data;
503
506
  }
504
507
  const sources = extractEffieSourcesWithTypes(effie);
505
- const scale = options.scale ?? 1;
506
- const upload = options.upload;
508
+ const scale = body.scale ?? 1;
509
+ const upload = body.upload;
507
510
  const jobId = randomUUID2();
508
511
  const warmupJobId = randomUUID2();
509
512
  const renderJobId = randomUUID2();
@@ -514,17 +517,18 @@ async function createWarmupAndRenderJob(req, res, ctx) {
514
517
  upload,
515
518
  warmupJobId,
516
519
  renderJobId,
517
- createdAt: Date.now()
520
+ createdAt: Date.now(),
521
+ metadata: options?.metadata
518
522
  };
519
523
  await ctx.transientStore.putJson(
520
524
  storeKeys.warmupAndRenderJob(jobId),
521
525
  job,
522
- ctx.transientStore.jobMetadataTtlMs
526
+ ctx.transientStore.jobDataTtlMs
523
527
  );
524
528
  await ctx.transientStore.putJson(
525
529
  storeKeys.warmupJob(warmupJobId),
526
- { sources },
527
- ctx.transientStore.jobMetadataTtlMs
530
+ { sources, metadata: options?.metadata },
531
+ ctx.transientStore.jobDataTtlMs
528
532
  );
529
533
  await ctx.transientStore.putJson(
530
534
  storeKeys.renderJob(renderJobId),
@@ -532,9 +536,10 @@ async function createWarmupAndRenderJob(req, res, ctx) {
532
536
  effie,
533
537
  scale,
534
538
  upload,
535
- createdAt: Date.now()
539
+ createdAt: Date.now(),
540
+ metadata: options?.metadata
536
541
  },
537
- ctx.transientStore.jobMetadataTtlMs
542
+ ctx.transientStore.jobDataTtlMs
538
543
  );
539
544
  res.json({
540
545
  id: jobId,
@@ -549,13 +554,15 @@ async function streamWarmupAndRenderJob(req, res, ctx) {
549
554
  try {
550
555
  setupCORSHeaders(res);
551
556
  const jobId = req.params.id;
552
- const jobCacheKey = storeKeys.warmupAndRenderJob(jobId);
553
- const job = await ctx.transientStore.getJson(jobCacheKey);
554
- ctx.transientStore.delete(jobCacheKey);
557
+ const jobStoreKey = storeKeys.warmupAndRenderJob(jobId);
558
+ const job = await ctx.transientStore.getJson(jobStoreKey);
559
+ ctx.transientStore.delete(jobStoreKey);
555
560
  if (!job) {
556
561
  res.status(404).json({ error: "Job not found" });
557
562
  return;
558
563
  }
564
+ const warmupBackend = ctx.warmupBackendResolver ? ctx.warmupBackendResolver(job.sources, job.metadata) : null;
565
+ const renderBackend = ctx.renderBackendResolver ? ctx.renderBackendResolver(job.effie, job.metadata) : null;
559
566
  setupSSEResponse(res);
560
567
  const sendEvent = createSSEEventSender(res);
561
568
  let keepalivePhase = "warmup";
@@ -563,13 +570,13 @@ async function streamWarmupAndRenderJob(req, res, ctx) {
563
570
  sendEvent("keepalive", { phase: keepalivePhase });
564
571
  }, 25e3);
565
572
  try {
566
- if (ctx.warmupBackendBaseUrl) {
573
+ if (warmupBackend) {
567
574
  await proxyRemoteSSE(
568
- `${ctx.warmupBackendBaseUrl}/warmup/${job.warmupJobId}`,
575
+ `${warmupBackend.baseUrl}/warmup/${job.warmupJobId}`,
569
576
  sendEvent,
570
577
  "warmup:",
571
578
  res,
572
- ctx.warmupBackendApiKey ? { Authorization: `Bearer ${ctx.warmupBackendApiKey}` } : void 0
579
+ warmupBackend.apiKey ? { Authorization: `Bearer ${warmupBackend.apiKey}` } : void 0
573
580
  );
574
581
  } else {
575
582
  const warmupSender = prefixEventSender(sendEvent, "warmup:");
@@ -577,13 +584,13 @@ async function streamWarmupAndRenderJob(req, res, ctx) {
577
584
  warmupSender("complete", { status: "ready" });
578
585
  }
579
586
  keepalivePhase = "render";
580
- if (ctx.renderBackendBaseUrl) {
587
+ if (renderBackend) {
581
588
  await proxyRemoteSSE(
582
- `${ctx.renderBackendBaseUrl}/render/${job.renderJobId}`,
589
+ `${renderBackend.baseUrl}/render/${job.renderJobId}`,
583
590
  sendEvent,
584
591
  "render:",
585
592
  res,
586
- ctx.renderBackendApiKey ? { Authorization: `Bearer ${ctx.renderBackendApiKey}` } : void 0
593
+ renderBackend.apiKey ? { Authorization: `Bearer ${renderBackend.apiKey}` } : void 0
587
594
  );
588
595
  } else {
589
596
  const renderSender = prefixEventSender(sendEvent, "render:");
@@ -602,7 +609,7 @@ async function streamWarmupAndRenderJob(req, res, ctx) {
602
609
  sendEvent("complete", { status: "ready", videoUrl });
603
610
  }
604
611
  }
605
- if (job.upload && !ctx.renderBackendBaseUrl) {
612
+ if (job.upload && !renderBackend) {
606
613
  sendEvent("complete", { status: "done" });
607
614
  }
608
615
  } catch (error) {
@@ -707,7 +714,7 @@ function shouldSkipWarmup(source) {
707
714
  return source.type === "video" || source.type === "audio";
708
715
  }
709
716
  var inFlightFetches = /* @__PURE__ */ new Map();
710
- async function createWarmupJob(req, res, ctx) {
717
+ async function createWarmupJob(req, res, ctx, options) {
711
718
  try {
712
719
  const parseResult = parseEffieData(req.body, ctx.skipValidation);
713
720
  if ("error" in parseResult) {
@@ -716,10 +723,11 @@ async function createWarmupJob(req, res, ctx) {
716
723
  }
717
724
  const sources = extractEffieSourcesWithTypes2(parseResult.effie);
718
725
  const jobId = randomUUID3();
726
+ const job = { sources, metadata: options?.metadata };
719
727
  await ctx.transientStore.putJson(
720
728
  storeKeys.warmupJob(jobId),
721
- { sources },
722
- ctx.transientStore.jobMetadataTtlMs
729
+ job,
730
+ ctx.transientStore.jobDataTtlMs
723
731
  );
724
732
  res.json({
725
733
  id: jobId,
@@ -734,29 +742,32 @@ async function streamWarmupJob(req, res, ctx) {
734
742
  try {
735
743
  setupCORSHeaders(res);
736
744
  const jobId = req.params.id;
737
- if (ctx.warmupBackendBaseUrl) {
738
- setupSSEResponse(res);
739
- const sendEvent2 = createSSEEventSender(res);
740
- try {
741
- await proxyRemoteSSE(
742
- `${ctx.warmupBackendBaseUrl}/warmup/${jobId}`,
743
- sendEvent2,
744
- "",
745
- res,
746
- ctx.warmupBackendApiKey ? { Authorization: `Bearer ${ctx.warmupBackendApiKey}` } : void 0
747
- );
748
- } finally {
749
- res.end();
750
- }
751
- return;
752
- }
753
- const jobCacheKey = storeKeys.warmupJob(jobId);
754
- const job = await ctx.transientStore.getJson(jobCacheKey);
755
- ctx.transientStore.delete(jobCacheKey);
745
+ const jobStoreKey = storeKeys.warmupJob(jobId);
746
+ const job = await ctx.transientStore.getJson(jobStoreKey);
756
747
  if (!job) {
757
748
  res.status(404).json({ error: "Job not found" });
758
749
  return;
759
750
  }
751
+ if (ctx.warmupBackendResolver) {
752
+ const backend = ctx.warmupBackendResolver(job.sources, job.metadata);
753
+ if (backend) {
754
+ setupSSEResponse(res);
755
+ const sendEvent2 = createSSEEventSender(res);
756
+ try {
757
+ await proxyRemoteSSE(
758
+ `${backend.baseUrl}/warmup/${jobId}`,
759
+ sendEvent2,
760
+ "",
761
+ res,
762
+ backend.apiKey ? { Authorization: `Bearer ${backend.apiKey}` } : void 0
763
+ );
764
+ } finally {
765
+ res.end();
766
+ }
767
+ return;
768
+ }
769
+ }
770
+ ctx.transientStore.delete(jobStoreKey);
760
771
  setupSSEResponse(res);
761
772
  const sendEvent = createSSEEventSender(res);
762
773
  try {
@@ -945,4 +956,4 @@ export {
945
956
  streamWarmupJob,
946
957
  purgeCache
947
958
  };
948
- //# sourceMappingURL=chunk-ZERUSI5T.js.map
959
+ //# sourceMappingURL=chunk-WD7MF3GH.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/handlers/shared.ts","../src/proxy.ts","../src/handlers/caching.ts","../src/handlers/orchestrating.ts","../src/handlers/rendering.ts"],"sourcesContent":["import express from \"express\";\nimport type { TransientStore } from \"../storage\";\nimport { createTransientStore } from \"../storage\";\nimport { HttpProxy } from \"../proxy\";\nimport type {\n EffieData,\n EffieSources,\n EffieSourceWithType,\n} from \"@effing/effie\";\nimport { effieDataSchema } from \"@effing/effie\";\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 RenderJob = {\n effie: EffieData<EffieSources>;\n scale: number;\n upload?: UploadOptions;\n createdAt: number;\n metadata?: Record<string, unknown>;\n};\n\nexport type WarmupAndRenderJob = {\n effie: EffieData<EffieSources>;\n sources: EffieSourceWithType[];\n scale: number;\n upload?: UploadOptions;\n warmupJobId: string;\n renderJobId: string;\n createdAt: 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 SSEEventSender = (event: string, data: object) => void;\n\nexport type ParseEffieResult =\n | { effie: EffieData<EffieSources> }\n | { error: string; issues?: object[] };\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 ?? !options?.renderBackendResolver;\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 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 { error: \"Invalid effie data: missing segments\" };\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 createSSEEventSender(res: express.Response): SSEEventSender {\n return (event: string, data: object) => {\n res.write(`event: ${event}\\ndata: ${JSON.stringify(data)}\\n\\n`);\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 express from \"express\";\nimport { Readable, Transform } from \"stream\";\nimport { randomUUID } from \"crypto\";\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 { ServerContext, SSEEventSender, WarmupJob } from \"./shared\";\nimport {\n parseEffieData,\n setupCORSHeaders,\n setupSSEResponse,\n createSSEEventSender,\n} from \"./shared\";\nimport { proxyRemoteSSE } from \"./orchestrating\";\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?: { metadata?: Record<string, unknown> },\n): Promise<void> {\n try {\n const parseResult = parseEffieData(req.body, ctx.skipValidation);\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 await ctx.transientStore.putJson(\n storeKeys.warmupJob(jobId),\n job,\n ctx.transientStore.jobDataTtlMs,\n );\n\n res.json({\n id: jobId,\n url: `${ctx.baseUrl}/warmup/${jobId}`,\n });\n } catch (error) {\n console.error(\"Error creating warmup job:\", error);\n res.status(500).json({ error: \"Failed to create warmup job\" });\n }\n}\n\n/**\n * GET /warmup/:id - Stream warmup progress via SSE\n * Fetches and caches sources, emitting progress events\n */\nexport async function streamWarmupJob(\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 res.status(404).json({ error: \"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 = createSSEEventSender(res);\n try {\n await proxyRemoteSSE(\n `${backend.baseUrl}/warmup/${jobId}`,\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 = createSSEEventSender(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 res.status(500).json({ error: \"Warmup streaming failed\" });\n } else {\n res.end();\n }\n }\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): Promise<void> {\n try {\n const parseResult = parseEffieData(req.body, ctx.skipValidation);\n if (\"error\" in parseResult) {\n res.status(400).json(parseResult);\n return;\n }\n\n const sources = extractEffieSources(parseResult.effie);\n\n let purged = 0;\n for (const url of sources) {\n const ck = storeKeys.source(url);\n if (await ctx.transientStore.exists(ck)) {\n await ctx.transientStore.delete(ck);\n purged++;\n }\n }\n\n res.json({ purged, total: sources.length });\n } catch (error) {\n console.error(\"Error purging cache:\", error);\n res.status(500).json({ 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: SSEEventSender,\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: SSEEventSender,\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.sourceTtlMs,\n );\n}\n","import express from \"express\";\nimport { randomUUID } from \"crypto\";\nimport type { Response as UndiciResponse } from \"undici\";\nimport { storeKeys } from \"../storage\";\nimport { ffsFetch } from \"../fetch\";\nimport { extractEffieSourcesWithTypes, effieDataSchema } from \"@effing/effie\";\nimport type { EffieData, EffieSources } from \"@effing/effie\";\nimport type {\n ServerContext,\n SSEEventSender,\n WarmupAndRenderJob,\n RenderJob,\n UploadOptions,\n} from \"./shared\";\nimport {\n setupCORSHeaders,\n setupSSEResponse,\n createSSEEventSender,\n} from \"./shared\";\nimport { warmupSources } from \"./caching\";\nimport { renderAndUploadInternal } from \"./rendering\";\n\n/**\n * POST /warmup-and-render - Create a combined warmup and render job\n * Returns a job ID and URL for SSE streaming\n */\nexport async function createWarmupAndRenderJob(\n req: express.Request,\n res: express.Response,\n ctx: ServerContext,\n options?: { metadata?: Record<string, unknown> },\n): Promise<void> {\n try {\n // Parse request body\n const body = req.body as {\n effie: unknown;\n scale?: number;\n upload?: UploadOptions;\n };\n\n let rawEffieData: unknown;\n if (typeof body.effie === \"string\") {\n // Effie is a URL to fetch the EffieData from\n const response = await ffsFetch(body.effie);\n if (!response.ok) {\n throw new Error(\n `Failed to fetch Effie data: ${response.status} ${response.statusText}`,\n );\n }\n rawEffieData = await response.json();\n } else {\n rawEffieData = body.effie;\n }\n\n // Validate/parse effie data\n let effie: EffieData<EffieSources>;\n if (!ctx.skipValidation) {\n const result = effieDataSchema.safeParse(rawEffieData);\n if (!result.success) {\n res.status(400).json({\n error: \"Invalid effie data\",\n issues: result.error.issues.map((issue) => ({\n path: issue.path.join(\".\"),\n message: issue.message,\n })),\n });\n return;\n }\n effie = result.data;\n } else {\n const data = rawEffieData as EffieData<EffieSources>;\n if (!data?.segments) {\n res.status(400).json({ error: \"Invalid effie data: missing segments\" });\n return;\n }\n effie = data;\n }\n\n const sources = extractEffieSourcesWithTypes(effie);\n const scale = body.scale ?? 1;\n const upload = body.upload;\n\n // Create IDs for warmup and render sub-jobs\n const jobId = randomUUID();\n const warmupJobId = randomUUID();\n const renderJobId = randomUUID();\n\n // Store the combined job\n const job: WarmupAndRenderJob = {\n effie,\n sources,\n scale,\n upload,\n warmupJobId,\n renderJobId,\n createdAt: Date.now(),\n metadata: options?.metadata,\n };\n\n await ctx.transientStore.putJson(\n storeKeys.warmupAndRenderJob(jobId),\n job,\n ctx.transientStore.jobDataTtlMs,\n );\n\n // Also store sub-jobs for backend execution\n await ctx.transientStore.putJson(\n storeKeys.warmupJob(warmupJobId),\n { sources, metadata: options?.metadata },\n ctx.transientStore.jobDataTtlMs,\n );\n await ctx.transientStore.putJson(\n storeKeys.renderJob(renderJobId),\n {\n effie,\n scale,\n upload,\n createdAt: Date.now(),\n metadata: options?.metadata,\n } satisfies RenderJob,\n ctx.transientStore.jobDataTtlMs,\n );\n\n res.json({\n id: jobId,\n url: `${ctx.baseUrl}/warmup-and-render/${jobId}`,\n });\n } catch (error) {\n console.error(\"Error creating warmup-and-render job:\", error);\n res.status(500).json({ error: \"Failed to create warmup-and-render job\" });\n }\n}\n\n/**\n * GET /warmup-and-render/:id - Stream warmup and render progress via SSE\n * Orchestrates warmup (local or remote) followed by render (local or remote)\n */\nexport async function streamWarmupAndRenderJob(\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.warmupAndRenderJob(jobId);\n const job =\n await ctx.transientStore.getJson<WarmupAndRenderJob>(jobStoreKey);\n // Only allow the job to run once\n ctx.transientStore.delete(jobStoreKey);\n\n if (!job) {\n res.status(404).json({ error: \"Job not found\" });\n return;\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 setupSSEResponse(res);\n const sendEvent = createSSEEventSender(res);\n\n // Keepalive interval for long-running operations\n let keepalivePhase: \"warmup\" | \"render\" = \"warmup\";\n const keepalive = setInterval(() => {\n sendEvent(\"keepalive\", { phase: keepalivePhase });\n }, 25_000);\n\n try {\n // Phase 1: Warmup\n if (warmupBackend) {\n // Proxy warmup from remote backend\n await proxyRemoteSSE(\n `${warmupBackend.baseUrl}/warmup/${job.warmupJobId}`,\n sendEvent,\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(sendEvent, \"warmup:\");\n await warmupSources(job.sources, warmupSender, ctx);\n warmupSender(\"complete\", { status: \"ready\" });\n }\n\n // Phase 2: Render\n keepalivePhase = \"render\";\n\n if (renderBackend) {\n // Proxy render from remote backend\n await proxyRemoteSSE(\n `${renderBackend.baseUrl}/render/${job.renderJobId}`,\n sendEvent,\n \"render:\",\n res,\n renderBackend.apiKey\n ? { Authorization: `Bearer ${renderBackend.apiKey}` }\n : undefined,\n );\n } else {\n // Local render execution\n const renderSender = prefixEventSender(sendEvent, \"render:\");\n\n if (job.upload) {\n // Upload mode: render and upload, emit SSE events\n renderSender(\"started\", { status: \"rendering\" });\n const timings = await renderAndUploadInternal(\n job.effie,\n job.scale,\n job.upload,\n renderSender,\n ctx,\n );\n renderSender(\"complete\", { status: \"uploaded\", timings });\n } else {\n // Non-upload mode: return URL to existing render job\n const videoUrl = `${ctx.baseUrl}/render/${job.renderJobId}`;\n sendEvent(\"complete\", { status: \"ready\", videoUrl });\n }\n }\n\n // Final complete event (only for upload mode, non-upload already sent complete)\n if (job.upload && !renderBackend) {\n sendEvent(\"complete\", { status: \"done\" });\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 warmup-and-render streaming:\", error);\n if (!res.headersSent) {\n res.status(500).json({ error: \"Warmup-and-render streaming failed\" });\n } else {\n res.end();\n }\n }\n}\n\n/**\n * Create a prefixed event sender that adds a prefix to event names\n */\nexport function prefixEventSender(\n sendEvent: SSEEventSender,\n prefix: string,\n): SSEEventSender {\n return (event: string, data: object) => {\n sendEvent(`${prefix}${event}`, data);\n };\n}\n\n/**\n * Proxy SSE events from a remote backend, prefixing event names\n */\nexport async function proxyRemoteSSE(\n url: string,\n sendEvent: SSEEventSender,\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\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 let currentEvent = \"\";\n let currentData = \"\";\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 express from \"express\";\nimport { randomUUID } from \"crypto\";\nimport { storeKeys } from \"../storage\";\nimport { ffsFetch } from \"../fetch\";\nimport { effieDataSchema } from \"@effing/effie\";\nimport type { EffieData, EffieSources } from \"@effing/effie\";\nimport type {\n ServerContext,\n SSEEventSender,\n BackendConfig,\n RenderJob,\n UploadOptions,\n} from \"./shared\";\nimport {\n setupCORSHeaders,\n setupSSEResponse,\n createSSEEventSender,\n} from \"./shared\";\nimport { proxyBinaryStream } from \"./orchestrating\";\n\n/**\n * POST /render - Create a render job\n * Returns a job ID and URL for streaming the rendered video\n */\nexport async function createRenderJob(\n req: express.Request,\n res: express.Response,\n ctx: ServerContext,\n options?: { metadata?: Record<string, unknown> },\n): Promise<void> {\n try {\n // Wrapped format has `effie` property,\n // otherwise it's just raw EffieData (which doesn't have an `effie` property)\n const isWrapped = \"effie\" in req.body;\n\n let rawEffieData: unknown;\n let scale: number;\n let upload: UploadOptions | undefined;\n\n if (isWrapped) {\n // Wrapped format: { effie: EffieData | string, scale?, upload? }\n const options = req.body as {\n effie: unknown;\n scale?: number;\n upload?: UploadOptions;\n };\n\n if (typeof options.effie === \"string\") {\n // Effie is a string, so it's a URL to fetch the EffieData from\n const response = await ffsFetch(options.effie);\n if (!response.ok) {\n throw new Error(\n `Failed to fetch Effie data: ${response.status} ${response.statusText}`,\n );\n }\n rawEffieData = await response.json();\n } else {\n // Effie is an EffieData object\n rawEffieData = options.effie;\n }\n\n scale = options.scale ?? 1;\n upload = options.upload;\n } else {\n // Body is the EffieData, options in query params\n rawEffieData = req.body;\n scale = parseFloat(req.query.scale?.toString() || \"1\");\n }\n\n // Validate/parse effie data (validation can be disabled by setting FFS_SKIP_VALIDATION)\n let effie: EffieData<EffieSources>;\n if (!ctx.skipValidation) {\n const result = effieDataSchema.safeParse(rawEffieData);\n if (!result.success) {\n res.status(400).json({\n error: \"Invalid effie data\",\n issues: result.error.issues.map((issue) => ({\n path: issue.path.join(\".\"),\n message: issue.message,\n })),\n });\n return;\n }\n effie = result.data;\n } else {\n // Minimal validation when schema validation is disabled\n const data = rawEffieData as EffieData<EffieSources>;\n if (!data?.segments) {\n res.status(400).json({ error: \"Invalid effie data: missing segments\" });\n return;\n }\n effie = data;\n }\n\n // Create render job\n const jobId = randomUUID();\n const job: RenderJob = {\n effie,\n scale,\n upload,\n createdAt: Date.now(),\n metadata: options?.metadata,\n };\n\n await ctx.transientStore.putJson(\n storeKeys.renderJob(jobId),\n job,\n ctx.transientStore.jobDataTtlMs,\n );\n\n res.json({\n id: jobId,\n url: `${ctx.baseUrl}/render/${jobId}`,\n });\n } catch (error) {\n console.error(\"Error creating render job:\", error);\n res.status(500).json({ error: \"Failed to create render job\" });\n }\n}\n\n/**\n * GET /render/:id - Execute render job\n * Streams video directly (no upload) or SSE progress events (with upload)\n */\nexport async function streamRenderJob(\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.renderJob(jobId);\n const job = await ctx.transientStore.getJson<RenderJob>(jobStoreKey);\n\n if (!job) {\n res.status(404).json({ error: \"Job not found or expired\" });\n return;\n }\n\n // Proxy to render backend if resolver is configured\n if (ctx.renderBackendResolver) {\n const backend = ctx.renderBackendResolver(job.effie, job.metadata);\n if (backend) {\n await proxyRenderFromBackend(res, jobId, backend);\n return;\n }\n }\n\n // Render locally — only allow the render job to run once\n ctx.transientStore.delete(jobStoreKey);\n\n // Dispatch based on upload mode\n if (job.upload) {\n await streamRenderWithUpload(res, job, ctx);\n } else {\n await streamRenderDirect(res, job, ctx);\n }\n } catch (error) {\n console.error(\"Error in render:\", error);\n if (!res.headersSent) {\n res.status(500).json({ error: \"Rendering failed\" });\n } else {\n res.end();\n }\n }\n}\n\n/**\n * Stream video directly to the response (no upload)\n */\nexport async function streamRenderDirect(\n res: express.Response,\n job: RenderJob,\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 * Render and upload, streaming SSE progress events\n */\nexport async function streamRenderWithUpload(\n res: express.Response,\n job: RenderJob,\n ctx: ServerContext,\n): Promise<void> {\n setupSSEResponse(res);\n const sendEvent = createSSEEventSender(res);\n\n // Keepalive interval for long-running renders\n const keepalive = setInterval(() => {\n sendEvent(\"keepalive\", { status: \"rendering\" });\n }, 25_000);\n\n try {\n sendEvent(\"started\", { status: \"rendering\" });\n\n const timings = await renderAndUploadInternal(\n job.effie,\n job.scale,\n job.upload!,\n sendEvent,\n ctx,\n );\n\n sendEvent(\"complete\", { status: \"uploaded\", timings });\n } catch (error) {\n sendEvent(\"error\", { message: String(error) });\n } finally {\n clearInterval(keepalive);\n res.end();\n }\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: SSEEventSender,\n ctx: ServerContext,\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 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 const coverBuffer = Buffer.from(await coverFetchResponse.arrayBuffer());\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 // Render effie data to video\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 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 timings.renderTime = Date.now() - renderStartTime;\n\n // Update keepalive status for upload phase\n sendEvent(\"keepalive\", { status: \"uploading\" });\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 * Proxy render from backend based on Content-Type.\n * SSE (upload mode) uses proxyRemoteSSE, video stream uses proxyBinaryStream.\n */\nasync function proxyRenderFromBackend(\n res: express.Response,\n jobId: string,\n backend: BackendConfig,\n): Promise<void> {\n const backendUrl = `${backend.baseUrl}/render/${jobId}`;\n const response = await ffsFetch(backendUrl, {\n headers: backend.apiKey\n ? { Authorization: `Bearer ${backend.apiKey}` }\n : undefined,\n });\n\n if (!response.ok) {\n res.status(response.status).json({ error: \"Backend render failed\" });\n return;\n }\n\n const contentType = response.headers.get(\"content-type\") || \"\";\n\n if (contentType.includes(\"text/event-stream\")) {\n // Upload mode: proxy SSE events\n setupSSEResponse(res);\n const sendEvent = createSSEEventSender(res);\n\n const reader = response.body?.getReader();\n if (!reader) {\n sendEvent(\"error\", { message: \"No response body from backend\" });\n res.end();\n return;\n }\n\n const decoder = new TextDecoder();\n let buffer = \"\";\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 buffer += decoder.decode(value, { stream: true });\n\n const lines = buffer.split(\"\\n\");\n buffer = lines.pop() || \"\";\n\n let currentEvent = \"\";\n let currentData = \"\";\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 try {\n const data = JSON.parse(currentData);\n sendEvent(currentEvent, data);\n } catch {\n // Skip malformed JSON\n }\n currentEvent = \"\";\n currentData = \"\";\n }\n }\n }\n } finally {\n reader.releaseLock();\n res.end();\n }\n } else {\n // Non-upload mode: proxy binary video stream\n await proxyBinaryStream(response, res);\n }\n}\n"],"mappings":"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","names":["Readable","randomUUID","extractEffieSourcesWithTypes","randomUUID","effieDataSchema","effieDataSchema","options","effieDataSchema","effieDataSchema","randomUUID","extractEffieSourcesWithTypes","randomUUID","sendEvent","Readable"]}
@@ -1,4 +1,4 @@
1
- import { T as TransientStore, H as HttpProxy } from '../proxy-qTA69nOV.js';
1
+ import { T as TransientStore, H as HttpProxy } from '../proxy-CsZ5h2Ya.js';
2
2
  import { EffieSourceWithType, EffieData, EffieSources } from '@effing/effie';
3
3
  import express from 'express';
4
4
  import { Response } from 'undici';
@@ -8,14 +8,22 @@ type UploadOptions = {
8
8
  videoUrl: string;
9
9
  coverUrl?: string;
10
10
  };
11
+ type BackendConfig = {
12
+ baseUrl: string;
13
+ apiKey?: string;
14
+ };
15
+ type WarmupBackendResolver = (sources: EffieSourceWithType[], metadata?: Record<string, unknown>) => BackendConfig | null;
16
+ type RenderBackendResolver = (effie: EffieData<EffieSources>, metadata?: Record<string, unknown>) => BackendConfig | null;
11
17
  type WarmupJob = {
12
18
  sources: EffieSourceWithType[];
19
+ metadata?: Record<string, unknown>;
13
20
  };
14
21
  type RenderJob = {
15
22
  effie: EffieData<EffieSources>;
16
23
  scale: number;
17
24
  upload?: UploadOptions;
18
25
  createdAt: number;
26
+ metadata?: Record<string, unknown>;
19
27
  };
20
28
  type WarmupAndRenderJob = {
21
29
  effie: EffieData<EffieSources>;
@@ -25,6 +33,7 @@ type WarmupAndRenderJob = {
25
33
  warmupJobId: string;
26
34
  renderJobId: string;
27
35
  createdAt: number;
36
+ metadata?: Record<string, unknown>;
28
37
  };
29
38
  type ServerContext = {
30
39
  transientStore: TransientStore;
@@ -32,22 +41,26 @@ type ServerContext = {
32
41
  baseUrl: string;
33
42
  skipValidation: boolean;
34
43
  warmupConcurrency: number;
35
- warmupBackendBaseUrl?: string;
36
- renderBackendBaseUrl?: string;
37
- warmupBackendApiKey?: string;
38
- renderBackendApiKey?: string;
44
+ warmupBackendResolver?: WarmupBackendResolver;
45
+ renderBackendResolver?: RenderBackendResolver;
39
46
  };
40
47
  type SSEEventSender = (event: string, data: object) => void;
41
48
  /**
42
49
  * Create the server context with configuration from environment variables
43
50
  */
44
- declare function createServerContext(): Promise<ServerContext>;
51
+ declare function createServerContext(options?: {
52
+ warmupBackendResolver?: WarmupBackendResolver;
53
+ renderBackendResolver?: RenderBackendResolver;
54
+ httpProxy?: boolean;
55
+ }): Promise<ServerContext>;
45
56
 
46
57
  /**
47
58
  * POST /warmup - Create a warmup job
48
59
  * Stores the source list in cache and returns a job ID for SSE streaming
49
60
  */
50
- declare function createWarmupJob(req: express.Request, res: express.Response, ctx: ServerContext): Promise<void>;
61
+ declare function createWarmupJob(req: express.Request, res: express.Response, ctx: ServerContext, options?: {
62
+ metadata?: Record<string, unknown>;
63
+ }): Promise<void>;
51
64
  /**
52
65
  * GET /warmup/:id - Stream warmup progress via SSE
53
66
  * Fetches and caches sources, emitting progress events
@@ -62,7 +75,9 @@ declare function purgeCache(req: express.Request, res: express.Response, ctx: Se
62
75
  * POST /render - Create a render job
63
76
  * Returns a job ID and URL for streaming the rendered video
64
77
  */
65
- declare function createRenderJob(req: express.Request, res: express.Response, ctx: ServerContext): Promise<void>;
78
+ declare function createRenderJob(req: express.Request, res: express.Response, ctx: ServerContext, options?: {
79
+ metadata?: Record<string, unknown>;
80
+ }): Promise<void>;
66
81
  /**
67
82
  * GET /render/:id - Execute render job
68
83
  * Streams video directly (no upload) or SSE progress events (with upload)
@@ -73,7 +88,9 @@ declare function streamRenderJob(req: express.Request, res: express.Response, ct
73
88
  * POST /warmup-and-render - Create a combined warmup and render job
74
89
  * Returns a job ID and URL for SSE streaming
75
90
  */
76
- declare function createWarmupAndRenderJob(req: express.Request, res: express.Response, ctx: ServerContext): Promise<void>;
91
+ declare function createWarmupAndRenderJob(req: express.Request, res: express.Response, ctx: ServerContext, options?: {
92
+ metadata?: Record<string, unknown>;
93
+ }): Promise<void>;
77
94
  /**
78
95
  * GET /warmup-and-render/:id - Stream warmup and render progress via SSE
79
96
  * Orchestrates warmup (local or remote) followed by render (local or remote)
@@ -89,4 +106,4 @@ declare function proxyRemoteSSE(url: string, sendEvent: SSEEventSender, prefix:
89
106
  */
90
107
  declare function proxyBinaryStream(response: Response, res: express.Response): Promise<void>;
91
108
 
92
- export { type RenderJob, type SSEEventSender, type ServerContext, type UploadOptions, type WarmupAndRenderJob, type WarmupJob, createRenderJob, createServerContext, createWarmupAndRenderJob, createWarmupJob, proxyBinaryStream, proxyRemoteSSE, purgeCache, streamRenderJob, streamWarmupAndRenderJob, streamWarmupJob };
109
+ export { type BackendConfig, type RenderBackendResolver, type RenderJob, type SSEEventSender, type ServerContext, type UploadOptions, type WarmupAndRenderJob, type WarmupBackendResolver, type WarmupJob, createRenderJob, createServerContext, createWarmupAndRenderJob, createWarmupJob, proxyBinaryStream, proxyRemoteSSE, purgeCache, streamRenderJob, streamWarmupAndRenderJob, streamWarmupJob };
@@ -9,8 +9,8 @@ import {
9
9
  streamRenderJob,
10
10
  streamWarmupAndRenderJob,
11
11
  streamWarmupJob
12
- } from "../chunk-ZERUSI5T.js";
13
- import "../chunk-5SGOYTM2.js";
12
+ } from "../chunk-WD7MF3GH.js";
13
+ import "../chunk-4N2GLGC5.js";
14
14
  export {
15
15
  createRenderJob,
16
16
  createServerContext,
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Readable } from 'stream';
2
2
  import { EffieWebUrl, EffieData, EffieSources } from '@effing/effie';
3
- import { T as TransientStore, H as HttpProxy } from './proxy-qTA69nOV.js';
3
+ import { T as TransientStore, H as HttpProxy } from './proxy-CsZ5h2Ya.js';
4
4
 
5
5
  type EffieRendererOptions = {
6
6
  /**
package/dist/index.js CHANGED
@@ -2,8 +2,8 @@ import {
2
2
  EffieRenderer,
3
3
  FFmpegCommand,
4
4
  FFmpegRunner
5
- } from "./chunk-N3D6I2BD.js";
6
- import "./chunk-5SGOYTM2.js";
5
+ } from "./chunk-O7Z6DV2I.js";
6
+ import "./chunk-4N2GLGC5.js";
7
7
  export {
8
8
  EffieRenderer,
9
9
  FFmpegCommand,