@effing/ffs 0.7.3 → 0.9.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.
package/dist/server.js CHANGED
@@ -136,9 +136,27 @@ var HttpProxy = class {
136
136
 
137
137
  // src/handlers/shared.ts
138
138
  import { effieDataSchema } from "@effing/effie";
139
+
140
+ // src/handlers/errors.ts
141
+ var ErrorCode = {
142
+ UNAUTHORIZED: "UNAUTHORIZED",
143
+ INVALID_EFFIE: "INVALID_EFFIE",
144
+ NOT_FOUND: "NOT_FOUND",
145
+ BACKEND_FAILED: "BACKEND_FAILED",
146
+ INTERNAL_ERROR: "INTERNAL_ERROR",
147
+ FETCH_FAILED: "FETCH_FAILED"
148
+ };
149
+ function sendError(res, status, code, message, issues) {
150
+ if (res.headersSent) return;
151
+ const body = { error: message, code };
152
+ if (issues) body.issues = issues;
153
+ res.status(status).json(body);
154
+ }
155
+
156
+ // src/handlers/shared.ts
139
157
  async function createServerContext(options) {
140
158
  const port2 = process.env.FFS_PORT || process.env.PORT || 2e3;
141
- const enableHttpProxy = options?.httpProxy ?? !options?.renderBackendResolver;
159
+ const enableHttpProxy = options?.httpProxy ?? true;
142
160
  let httpProxy;
143
161
  if (enableHttpProxy) {
144
162
  httpProxy = new HttpProxy();
@@ -162,6 +180,7 @@ function parseEffieData(body, skipValidation) {
162
180
  if (!result.success) {
163
181
  return {
164
182
  error: "Invalid effie data",
183
+ code: ErrorCode.INVALID_EFFIE,
165
184
  issues: result.error.issues.map((issue) => ({
166
185
  path: issue.path.join("."),
167
186
  message: issue.message
@@ -172,7 +191,10 @@ function parseEffieData(body, skipValidation) {
172
191
  } else {
173
192
  const effie = rawEffieData;
174
193
  if (!effie?.segments) {
175
- return { error: "Invalid effie data: missing segments" };
194
+ return {
195
+ error: "Invalid effie data: missing segments",
196
+ code: ErrorCode.INVALID_EFFIE
197
+ };
176
198
  }
177
199
  return { effie };
178
200
  }
@@ -187,7 +209,7 @@ function setupSSEResponse(res) {
187
209
  res.setHeader("Connection", "keep-alive");
188
210
  res.flushHeaders();
189
211
  }
190
- function createSSEEventSender(res) {
212
+ function createEventSender(res) {
191
213
  return (event, data) => {
192
214
  res.write(`event: ${event}
193
215
  data: ${JSON.stringify(data)}
@@ -196,9 +218,9 @@ data: ${JSON.stringify(data)}
196
218
  };
197
219
  }
198
220
  function prefixEventSender(sendEvent, prefix) {
199
- return (event, data) => {
221
+ return ((event, data) => {
200
222
  sendEvent(`${prefix}${event}`, data);
201
- };
223
+ });
202
224
  }
203
225
  async function proxyRemoteSSE(url, sendEvent, prefix, res, headers) {
204
226
  const response = await ffsFetch(url, {
@@ -216,6 +238,8 @@ async function proxyRemoteSSE(url, sendEvent, prefix, res, headers) {
216
238
  }
217
239
  const decoder = new TextDecoder();
218
240
  let buffer = "";
241
+ let currentEvent = "";
242
+ let currentData = "";
219
243
  try {
220
244
  while (true) {
221
245
  const { done, value } = await reader.read();
@@ -227,8 +251,6 @@ async function proxyRemoteSSE(url, sendEvent, prefix, res, headers) {
227
251
  buffer += decoder.decode(value, { stream: true });
228
252
  const lines = buffer.split("\n");
229
253
  buffer = lines.pop() || "";
230
- let currentEvent = "";
231
- let currentData = "";
232
254
  for (const line of lines) {
233
255
  if (line.startsWith("event: ")) {
234
256
  currentEvent = line.slice(7);
@@ -307,7 +329,12 @@ async function createWarmupJob(req, res, ctx2, options) {
307
329
  });
308
330
  } catch (error) {
309
331
  console.error("Error creating warmup job:", error);
310
- res.status(500).json({ error: "Failed to create warmup job" });
332
+ sendError(
333
+ res,
334
+ 500,
335
+ ErrorCode.INTERNAL_ERROR,
336
+ "Failed to create warmup job"
337
+ );
311
338
  }
312
339
  }
313
340
  async function streamWarmupProgress(req, res, ctx2) {
@@ -317,14 +344,14 @@ async function streamWarmupProgress(req, res, ctx2) {
317
344
  const jobStoreKey = storeKeys.warmupJob(jobId);
318
345
  const job = await ctx2.transientStore.getJson(jobStoreKey);
319
346
  if (!job) {
320
- res.status(404).json({ error: "Job not found" });
347
+ sendError(res, 404, ErrorCode.NOT_FOUND, "Job not found");
321
348
  return;
322
349
  }
323
350
  if (ctx2.warmupBackendResolver) {
324
351
  const backend = ctx2.warmupBackendResolver(job.sources, job.metadata);
325
352
  if (backend) {
326
353
  setupSSEResponse(res);
327
- const sendEvent2 = createSSEEventSender(res);
354
+ const sendEvent2 = createEventSender(res);
328
355
  try {
329
356
  await proxyRemoteSSE(
330
357
  `${backend.baseUrl}/warmup/${jobId}/progress`,
@@ -341,7 +368,7 @@ async function streamWarmupProgress(req, res, ctx2) {
341
368
  }
342
369
  ctx2.transientStore.delete(jobStoreKey);
343
370
  setupSSEResponse(res);
344
- const sendEvent = createSSEEventSender(res);
371
+ const sendEvent = createEventSender(res);
345
372
  try {
346
373
  await warmupSources(job.sources, sendEvent, ctx2);
347
374
  sendEvent("complete", { status: "ready" });
@@ -353,7 +380,7 @@ async function streamWarmupProgress(req, res, ctx2) {
353
380
  } catch (error) {
354
381
  console.error("Error in warmup streaming:", error);
355
382
  if (!res.headersSent) {
356
- res.status(500).json({ error: "Warmup streaming failed" });
383
+ sendError(res, 500, ErrorCode.INTERNAL_ERROR, "Warmup streaming failed");
357
384
  } else {
358
385
  res.end();
359
386
  }
@@ -382,7 +409,7 @@ async function purgeCache(req, res, ctx2) {
382
409
  res.json(result);
383
410
  } catch (error) {
384
411
  console.error("Error purging cache:", error);
385
- res.status(500).json({ error: "Failed to purge cache" });
412
+ sendError(res, 500, ErrorCode.INTERNAL_ERROR, "Failed to purge cache");
386
413
  }
387
414
  }
388
415
  async function warmupSources(sources, sendEvent, ctx2) {
@@ -525,50 +552,51 @@ import "express";
525
552
  import { randomUUID as randomUUID2 } from "crypto";
526
553
  import {
527
554
  extractEffieSourcesWithTypes as extractEffieSourcesWithTypes2,
528
- extractEffieSources as extractEffieSources2,
529
- effieDataSchema as effieDataSchema2
555
+ extractEffieSources as extractEffieSources2
530
556
  } from "@effing/effie";
531
557
  async function createRenderJob(req, res, ctx2, options) {
532
558
  try {
533
559
  const body = req.body;
534
- let rawEffieData;
535
560
  if (typeof body.effie === "string") {
536
- const response = await ffsFetch(body.effie);
537
- if (!response.ok) {
538
- throw new Error(
539
- `Failed to fetch Effie data: ${response.status} ${response.statusText}`
561
+ let response;
562
+ try {
563
+ response = await ffsFetch(body.effie);
564
+ } catch (error) {
565
+ sendError(
566
+ res,
567
+ 502,
568
+ ErrorCode.FETCH_FAILED,
569
+ `Failed to fetch Effie data: ${error instanceof Error ? error.message : String(error)}`
540
570
  );
541
- }
542
- rawEffieData = await response.json();
543
- } else {
544
- rawEffieData = body.effie;
545
- }
546
- let effie;
547
- if (!ctx2.skipValidation) {
548
- const result = effieDataSchema2.safeParse(rawEffieData);
549
- if (!result.success) {
550
- res.status(400).json({
551
- error: "Invalid effie data",
552
- issues: result.error.issues.map((issue) => ({
553
- path: issue.path.join("."),
554
- message: issue.message
555
- }))
556
- });
557
571
  return;
558
572
  }
559
- effie = result.data;
560
- } else {
561
- const data = rawEffieData;
562
- if (!data?.segments) {
563
- res.status(400).json({ error: "Invalid effie data: missing segments" });
573
+ if (!response.ok) {
574
+ sendError(
575
+ res,
576
+ 502,
577
+ ErrorCode.FETCH_FAILED,
578
+ `Failed to fetch Effie data: ${response.status} ${response.statusText}`
579
+ );
564
580
  return;
565
581
  }
566
- effie = data;
582
+ body.effie = await response.json();
583
+ }
584
+ const parseResult = parseEffieData(body, ctx2.skipValidation);
585
+ if ("error" in parseResult) {
586
+ sendError(
587
+ res,
588
+ 400,
589
+ parseResult.code,
590
+ parseResult.error,
591
+ parseResult.issues
592
+ );
593
+ return;
567
594
  }
595
+ const effie = parseResult.effie;
568
596
  const sources = extractEffieSourcesWithTypes2(effie);
569
- const scale = body.scale ?? 1;
597
+ const scale = body.scale ?? (req.query?.scale ? parseFloat(req.query.scale) : void 0) ?? 1;
598
+ const purge = body.purge ?? (req.query?.purge === "true" ? true : void 0) ?? false;
570
599
  const upload = body.upload;
571
- const purge = body.purge;
572
600
  const jobId = randomUUID2();
573
601
  const warmupJobId = randomUUID2();
574
602
  const job = {
@@ -597,7 +625,12 @@ async function createRenderJob(req, res, ctx2, options) {
597
625
  });
598
626
  } catch (error) {
599
627
  console.error("Error creating render job:", error);
600
- res.status(500).json({ error: "Failed to create render job" });
628
+ sendError(
629
+ res,
630
+ 500,
631
+ ErrorCode.INTERNAL_ERROR,
632
+ "Failed to create render job"
633
+ );
601
634
  }
602
635
  }
603
636
  async function streamRenderProgress(req, res, ctx2) {
@@ -607,14 +640,15 @@ async function streamRenderProgress(req, res, ctx2) {
607
640
  const jobStoreKey = storeKeys.renderJob(jobId);
608
641
  const job = await ctx2.transientStore.getJson(jobStoreKey);
609
642
  if (!job) {
610
- res.status(404).json({ error: "Job not found" });
643
+ sendError(res, 404, ErrorCode.NOT_FOUND, "Job not found");
611
644
  return;
612
645
  }
613
646
  ctx2.transientStore.delete(jobStoreKey);
614
647
  const warmupBackend = ctx2.warmupBackendResolver ? ctx2.warmupBackendResolver(job.sources, job.metadata) : null;
615
648
  const renderBackend = ctx2.renderBackendResolver ? ctx2.renderBackendResolver(job.effie, job.metadata) : null;
616
649
  setupSSEResponse(res);
617
- const sendEvent = createSSEEventSender(res);
650
+ const sendEvent = createEventSender(res);
651
+ const rawSendEvent = createEventSender(res);
618
652
  let keepalivePhase = "warmup";
619
653
  const keepalive = setInterval(() => {
620
654
  sendEvent("keepalive", { phase: keepalivePhase });
@@ -631,13 +665,16 @@ async function streamRenderProgress(req, res, ctx2) {
631
665
  if (warmupBackend) {
632
666
  await proxyRemoteSSE(
633
667
  `${warmupBackend.baseUrl}/warmup/${job.warmupJobId}/progress`,
634
- sendEvent,
668
+ rawSendEvent,
635
669
  "warmup:",
636
670
  res,
637
671
  warmupBackend.apiKey ? { Authorization: `Bearer ${warmupBackend.apiKey}` } : void 0
638
672
  );
639
673
  } else {
640
- const warmupSender = prefixEventSender(sendEvent, "warmup:");
674
+ const warmupSender = prefixEventSender(
675
+ rawSendEvent,
676
+ "warmup:"
677
+ );
641
678
  await warmupSources(job.sources, warmupSender, ctx2);
642
679
  warmupSender("complete", { status: "ready" });
643
680
  }
@@ -646,7 +683,8 @@ async function streamRenderProgress(req, res, ctx2) {
646
683
  if (renderBackend) {
647
684
  const videoJob = {
648
685
  effie: job.effie,
649
- scale: job.scale
686
+ scale: job.scale,
687
+ metadata: job.metadata
650
688
  };
651
689
  await ctx2.transientStore.putJson(
652
690
  storeKeys.videoJob(jobId),
@@ -667,7 +705,10 @@ async function streamRenderProgress(req, res, ctx2) {
667
705
  job.upload,
668
706
  sendEvent
669
707
  );
670
- sendEvent("render:complete", timings);
708
+ sendEvent(
709
+ "render:complete",
710
+ timings
711
+ );
671
712
  } else {
672
713
  const timings = await renderAndUploadInternal(
673
714
  job.effie,
@@ -676,13 +717,17 @@ async function streamRenderProgress(req, res, ctx2) {
676
717
  sendEvent,
677
718
  ctx2
678
719
  );
679
- sendEvent("render:complete", timings);
720
+ sendEvent(
721
+ "render:complete",
722
+ timings
723
+ );
680
724
  }
681
725
  sendEvent("complete", { status: "done" });
682
726
  } else {
683
727
  const videoJob = {
684
728
  effie: job.effie,
685
- scale: job.scale
729
+ scale: job.scale,
730
+ metadata: job.metadata
686
731
  };
687
732
  await ctx2.transientStore.putJson(
688
733
  storeKeys.videoJob(jobId),
@@ -704,7 +749,12 @@ async function streamRenderProgress(req, res, ctx2) {
704
749
  } catch (error) {
705
750
  console.error("Error in render progress streaming:", error);
706
751
  if (!res.headersSent) {
707
- res.status(500).json({ error: "Render progress streaming failed" });
752
+ sendError(
753
+ res,
754
+ 500,
755
+ ErrorCode.INTERNAL_ERROR,
756
+ "Render progress streaming failed"
757
+ );
708
758
  } else {
709
759
  res.end();
710
760
  }
@@ -717,18 +767,26 @@ async function streamRenderVideo(req, res, ctx2) {
717
767
  const videoJobKey = storeKeys.videoJob(jobId);
718
768
  const videoJob = await ctx2.transientStore.getJson(videoJobKey);
719
769
  if (!videoJob) {
720
- res.status(404).json({ error: "Video not found or expired" });
770
+ sendError(res, 404, ErrorCode.NOT_FOUND, "Video not found or expired");
721
771
  return;
722
772
  }
723
773
  if (ctx2.renderBackendResolver) {
724
- const backend = ctx2.renderBackendResolver(videoJob.effie);
774
+ const backend = ctx2.renderBackendResolver(
775
+ videoJob.effie,
776
+ videoJob.metadata
777
+ );
725
778
  if (backend) {
726
779
  const backendUrl = `${backend.baseUrl}/render/${jobId}/video`;
727
780
  const response = await ffsFetch(backendUrl, {
728
781
  headers: backend.apiKey ? { Authorization: `Bearer ${backend.apiKey}` } : void 0
729
782
  });
730
783
  if (!response.ok) {
731
- res.status(response.status).json({ error: "Backend render failed" });
784
+ sendError(
785
+ res,
786
+ response.status,
787
+ ErrorCode.BACKEND_FAILED,
788
+ "Backend render failed"
789
+ );
732
790
  return;
733
791
  }
734
792
  await proxyBinaryStream(response, res);
@@ -740,14 +798,14 @@ async function streamRenderVideo(req, res, ctx2) {
740
798
  } catch (error) {
741
799
  console.error("Error streaming video:", error);
742
800
  if (!res.headersSent) {
743
- res.status(500).json({ error: "Video streaming failed" });
801
+ sendError(res, 500, ErrorCode.INTERNAL_ERROR, "Video streaming failed");
744
802
  } else {
745
803
  res.end();
746
804
  }
747
805
  }
748
806
  }
749
807
  async function streamRenderDirect(res, job, ctx2) {
750
- const { EffieRenderer } = await import("./render-OMXRUVX5.js");
808
+ const { EffieRenderer } = await import("./render-YKC5W4YT.js");
751
809
  const renderer = new EffieRenderer(job.effie, {
752
810
  transientStore: ctx2.transientStore,
753
811
  httpProxy: ctx2.httpProxy
@@ -821,26 +879,30 @@ async function uploadRenderedVideo(videoBuffer, effie, upload, sendEvent) {
821
879
  }
822
880
  async function renderAndUploadInternal(effie, scale, upload, sendEvent, ctx2) {
823
881
  const renderStartTime = Date.now();
824
- const { EffieRenderer } = await import("./render-OMXRUVX5.js");
882
+ const { EffieRenderer } = await import("./render-YKC5W4YT.js");
825
883
  const renderer = new EffieRenderer(effie, {
826
884
  transientStore: ctx2.transientStore,
827
885
  httpProxy: ctx2.httpProxy
828
886
  });
829
- const videoStream = await renderer.render(scale);
830
- const chunks = [];
831
- for await (const chunk of videoStream) {
832
- chunks.push(Buffer.from(chunk));
887
+ try {
888
+ const videoStream = await renderer.render(scale);
889
+ const chunks = [];
890
+ for await (const chunk of videoStream) {
891
+ chunks.push(Buffer.from(chunk));
892
+ }
893
+ const videoBuffer = Buffer.concat(chunks);
894
+ const renderTime = Date.now() - renderStartTime;
895
+ const timings = await uploadRenderedVideo(
896
+ videoBuffer,
897
+ effie,
898
+ upload,
899
+ sendEvent
900
+ );
901
+ timings.renderTime = renderTime;
902
+ return timings;
903
+ } finally {
904
+ renderer.close();
833
905
  }
834
- const videoBuffer = Buffer.concat(chunks);
835
- const renderTime = Date.now() - renderStartTime;
836
- const timings = await uploadRenderedVideo(
837
- videoBuffer,
838
- effie,
839
- upload,
840
- sendEvent
841
- );
842
- timings.renderTime = renderTime;
843
- return timings;
844
906
  }
845
907
 
846
908
  // src/server.ts
@@ -856,7 +918,7 @@ function validateAuth(req, res) {
856
918
  if (!apiKey) return true;
857
919
  const authHeader = req.headers.authorization;
858
920
  if (!authHeader || authHeader !== `Bearer ${apiKey}`) {
859
- res.status(401).json({ error: "Unauthorized" });
921
+ sendError(res, 401, ErrorCode.UNAUTHORIZED, "Unauthorized");
860
922
  return false;
861
923
  }
862
924
  return true;
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/server.ts"],"sourcesContent":["import express from \"express\";\nimport bodyParser from \"body-parser\";\nimport {\n createServerContext,\n createWarmupJob,\n streamWarmupProgress,\n purgeCache,\n createRenderJob,\n streamRenderProgress,\n streamRenderVideo,\n} from \"./handlers\";\n\nconst app: express.Express = express();\napp.disable(\"x-powered-by\");\napp.use(bodyParser.json({ limit: \"50mb\" })); // Support large JSON requests\n\nconst ctx = await createServerContext();\nif (ctx.httpProxy) {\n console.log(`FFS HTTP proxy listening on port ${ctx.httpProxy.port}`);\n}\n\nfunction validateAuth(req: express.Request, res: express.Response): boolean {\n const apiKey = process.env.FFS_API_KEY;\n if (!apiKey) return true; // No auth required if api key not set\n\n const authHeader = req.headers.authorization;\n if (!authHeader || authHeader !== `Bearer ${apiKey}`) {\n res.status(401).json({ error: \"Unauthorized\" });\n return false;\n }\n return true;\n}\n\n// Routes with auth (POST endpoints)\napp.post(\"/warmup\", (req, res) => {\n if (!validateAuth(req, res)) return;\n createWarmupJob(req, res, ctx);\n});\napp.post(\"/purge\", (req, res) => {\n if (!validateAuth(req, res)) return;\n purgeCache(req, res, ctx);\n});\napp.post(\"/render\", (req, res) => {\n if (!validateAuth(req, res)) return;\n createRenderJob(req, res, ctx);\n});\n\n// Routes without auth (GET endpoints use job ID as capability token)\napp.get(\"/warmup/:id/progress\", (req, res) =>\n streamWarmupProgress(req, res, ctx),\n);\napp.get(\"/render/:id/progress\", (req, res) =>\n streamRenderProgress(req, res, ctx),\n);\napp.get(\"/render/:id/video\", (req, res) => streamRenderVideo(req, res, ctx));\n\n// Server lifecycle\nconst port = process.env.FFS_PORT || process.env.PORT || 2000; // ffmpeg was conceived in the year 2000\nconst server = app.listen(port, () => {\n console.log(`FFS server listening on port ${port}`);\n});\n\nfunction shutdown() {\n console.log(\"Shutting down FFS server...\");\n ctx.httpProxy?.close();\n ctx.transientStore.close();\n server.close(() => {\n console.log(\"FFS server stopped\");\n process.exit(0);\n });\n}\n\nprocess.on(\"SIGTERM\", shutdown);\nprocess.on(\"SIGINT\", shutdown);\n\nexport { app };\n"],"mappings":";;;;;;;;;;;;AAAA,OAAO,aAAa;AACpB,OAAO,gBAAgB;AAWvB,IAAM,MAAuB,QAAQ;AACrC,IAAI,QAAQ,cAAc;AAC1B,IAAI,IAAI,WAAW,KAAK,EAAE,OAAO,OAAO,CAAC,CAAC;AAE1C,IAAM,MAAM,MAAM,oBAAoB;AACtC,IAAI,IAAI,WAAW;AACjB,UAAQ,IAAI,oCAAoC,IAAI,UAAU,IAAI,EAAE;AACtE;AAEA,SAAS,aAAa,KAAsB,KAAgC;AAC1E,QAAM,SAAS,QAAQ,IAAI;AAC3B,MAAI,CAAC,OAAQ,QAAO;AAEpB,QAAM,aAAa,IAAI,QAAQ;AAC/B,MAAI,CAAC,cAAc,eAAe,UAAU,MAAM,IAAI;AACpD,QAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,eAAe,CAAC;AAC9C,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAGA,IAAI,KAAK,WAAW,CAAC,KAAK,QAAQ;AAChC,MAAI,CAAC,aAAa,KAAK,GAAG,EAAG;AAC7B,kBAAgB,KAAK,KAAK,GAAG;AAC/B,CAAC;AACD,IAAI,KAAK,UAAU,CAAC,KAAK,QAAQ;AAC/B,MAAI,CAAC,aAAa,KAAK,GAAG,EAAG;AAC7B,aAAW,KAAK,KAAK,GAAG;AAC1B,CAAC;AACD,IAAI,KAAK,WAAW,CAAC,KAAK,QAAQ;AAChC,MAAI,CAAC,aAAa,KAAK,GAAG,EAAG;AAC7B,kBAAgB,KAAK,KAAK,GAAG;AAC/B,CAAC;AAGD,IAAI;AAAA,EAAI;AAAA,EAAwB,CAAC,KAAK,QACpC,qBAAqB,KAAK,KAAK,GAAG;AACpC;AACA,IAAI;AAAA,EAAI;AAAA,EAAwB,CAAC,KAAK,QACpC,qBAAqB,KAAK,KAAK,GAAG;AACpC;AACA,IAAI,IAAI,qBAAqB,CAAC,KAAK,QAAQ,kBAAkB,KAAK,KAAK,GAAG,CAAC;AAG3E,IAAM,OAAO,QAAQ,IAAI,YAAY,QAAQ,IAAI,QAAQ;AACzD,IAAM,SAAS,IAAI,OAAO,MAAM,MAAM;AACpC,UAAQ,IAAI,gCAAgC,IAAI,EAAE;AACpD,CAAC;AAED,SAAS,WAAW;AAClB,UAAQ,IAAI,6BAA6B;AACzC,MAAI,WAAW,MAAM;AACrB,MAAI,eAAe,MAAM;AACzB,SAAO,MAAM,MAAM;AACjB,YAAQ,IAAI,oBAAoB;AAChC,YAAQ,KAAK,CAAC;AAAA,EAChB,CAAC;AACH;AAEA,QAAQ,GAAG,WAAW,QAAQ;AAC9B,QAAQ,GAAG,UAAU,QAAQ;","names":[]}
1
+ {"version":3,"sources":["../src/server.ts"],"sourcesContent":["import express from \"express\";\nimport bodyParser from \"body-parser\";\nimport {\n createServerContext,\n createWarmupJob,\n streamWarmupProgress,\n purgeCache,\n createRenderJob,\n streamRenderProgress,\n streamRenderVideo,\n sendError,\n ErrorCode,\n} from \"./handlers\";\n\nconst app: express.Express = express();\napp.disable(\"x-powered-by\");\napp.use(bodyParser.json({ limit: \"50mb\" })); // Support large JSON requests\n\nconst ctx = await createServerContext();\nif (ctx.httpProxy) {\n console.log(`FFS HTTP proxy listening on port ${ctx.httpProxy.port}`);\n}\n\nfunction validateAuth(req: express.Request, res: express.Response): boolean {\n const apiKey = process.env.FFS_API_KEY;\n if (!apiKey) return true; // No auth required if api key not set\n\n const authHeader = req.headers.authorization;\n if (!authHeader || authHeader !== `Bearer ${apiKey}`) {\n sendError(res, 401, ErrorCode.UNAUTHORIZED, \"Unauthorized\");\n return false;\n }\n return true;\n}\n\n// Routes with auth (POST endpoints)\napp.post(\"/warmup\", (req, res) => {\n if (!validateAuth(req, res)) return;\n createWarmupJob(req, res, ctx);\n});\napp.post(\"/purge\", (req, res) => {\n if (!validateAuth(req, res)) return;\n purgeCache(req, res, ctx);\n});\napp.post(\"/render\", (req, res) => {\n if (!validateAuth(req, res)) return;\n createRenderJob(req, res, ctx);\n});\n\n// Routes without auth (GET endpoints use job ID as capability token)\napp.get(\"/warmup/:id/progress\", (req, res) =>\n streamWarmupProgress(req, res, ctx),\n);\napp.get(\"/render/:id/progress\", (req, res) =>\n streamRenderProgress(req, res, ctx),\n);\napp.get(\"/render/:id/video\", (req, res) => streamRenderVideo(req, res, ctx));\n\n// Server lifecycle\nconst port = process.env.FFS_PORT || process.env.PORT || 2000; // ffmpeg was conceived in the year 2000\nconst server = app.listen(port, () => {\n console.log(`FFS server listening on port ${port}`);\n});\n\nfunction shutdown() {\n console.log(\"Shutting down FFS server...\");\n ctx.httpProxy?.close();\n ctx.transientStore.close();\n server.close(() => {\n console.log(\"FFS server stopped\");\n process.exit(0);\n });\n}\n\nprocess.on(\"SIGTERM\", shutdown);\nprocess.on(\"SIGINT\", shutdown);\n\nexport { app };\n"],"mappings":";;;;;;;;;;;;;;AAAA,OAAO,aAAa;AACpB,OAAO,gBAAgB;AAavB,IAAM,MAAuB,QAAQ;AACrC,IAAI,QAAQ,cAAc;AAC1B,IAAI,IAAI,WAAW,KAAK,EAAE,OAAO,OAAO,CAAC,CAAC;AAE1C,IAAM,MAAM,MAAM,oBAAoB;AACtC,IAAI,IAAI,WAAW;AACjB,UAAQ,IAAI,oCAAoC,IAAI,UAAU,IAAI,EAAE;AACtE;AAEA,SAAS,aAAa,KAAsB,KAAgC;AAC1E,QAAM,SAAS,QAAQ,IAAI;AAC3B,MAAI,CAAC,OAAQ,QAAO;AAEpB,QAAM,aAAa,IAAI,QAAQ;AAC/B,MAAI,CAAC,cAAc,eAAe,UAAU,MAAM,IAAI;AACpD,cAAU,KAAK,KAAK,UAAU,cAAc,cAAc;AAC1D,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAGA,IAAI,KAAK,WAAW,CAAC,KAAK,QAAQ;AAChC,MAAI,CAAC,aAAa,KAAK,GAAG,EAAG;AAC7B,kBAAgB,KAAK,KAAK,GAAG;AAC/B,CAAC;AACD,IAAI,KAAK,UAAU,CAAC,KAAK,QAAQ;AAC/B,MAAI,CAAC,aAAa,KAAK,GAAG,EAAG;AAC7B,aAAW,KAAK,KAAK,GAAG;AAC1B,CAAC;AACD,IAAI,KAAK,WAAW,CAAC,KAAK,QAAQ;AAChC,MAAI,CAAC,aAAa,KAAK,GAAG,EAAG;AAC7B,kBAAgB,KAAK,KAAK,GAAG;AAC/B,CAAC;AAGD,IAAI;AAAA,EAAI;AAAA,EAAwB,CAAC,KAAK,QACpC,qBAAqB,KAAK,KAAK,GAAG;AACpC;AACA,IAAI;AAAA,EAAI;AAAA,EAAwB,CAAC,KAAK,QACpC,qBAAqB,KAAK,KAAK,GAAG;AACpC;AACA,IAAI,IAAI,qBAAqB,CAAC,KAAK,QAAQ,kBAAkB,KAAK,KAAK,GAAG,CAAC;AAG3E,IAAM,OAAO,QAAQ,IAAI,YAAY,QAAQ,IAAI,QAAQ;AACzD,IAAM,SAAS,IAAI,OAAO,MAAM,MAAM;AACpC,UAAQ,IAAI,gCAAgC,IAAI,EAAE;AACpD,CAAC;AAED,SAAS,WAAW;AAClB,UAAQ,IAAI,6BAA6B;AACzC,MAAI,WAAW,MAAM;AACrB,MAAI,eAAe,MAAM;AACzB,SAAO,MAAM,MAAM;AACjB,YAAQ,IAAI,oBAAoB;AAChC,YAAQ,KAAK,CAAC;AAAA,EAChB,CAAC;AACH;AAEA,QAAQ,GAAG,WAAW,QAAQ;AAC9B,QAAQ,GAAG,UAAU,QAAQ;","names":[]}
package/dist/sse.d.ts ADDED
@@ -0,0 +1,72 @@
1
+ type WarmupEventMap = {
2
+ start: {
3
+ total: number;
4
+ };
5
+ progress: {
6
+ url: string;
7
+ status: "skipped" | "hit" | "cached" | "error";
8
+ cached: number;
9
+ failed: number;
10
+ skipped: number;
11
+ total: number;
12
+ ms?: number;
13
+ error?: string;
14
+ reason?: string;
15
+ };
16
+ downloading: {
17
+ url: string;
18
+ status: "started" | "downloading";
19
+ bytesReceived: number;
20
+ };
21
+ keepalive: {
22
+ cached: number;
23
+ failed: number;
24
+ skipped: number;
25
+ total: number;
26
+ };
27
+ summary: {
28
+ cached: number;
29
+ failed: number;
30
+ skipped: number;
31
+ total: number;
32
+ };
33
+ complete: {
34
+ status: "ready";
35
+ };
36
+ error: {
37
+ message: string;
38
+ };
39
+ };
40
+ type RenderEventMap = {
41
+ keepalive: {
42
+ phase: "warmup" | "render";
43
+ } | {
44
+ status: "uploading";
45
+ };
46
+ "purge:complete": {
47
+ purged: number;
48
+ total: number;
49
+ };
50
+ "render:complete": {
51
+ renderTime?: number;
52
+ uploadTime: number;
53
+ uploadCoverTime?: number;
54
+ fetchCoverTime?: number;
55
+ };
56
+ complete: {
57
+ status: "done";
58
+ };
59
+ ready: {
60
+ videoUrl: string;
61
+ };
62
+ error: {
63
+ phase: "warmup" | "render";
64
+ message: string;
65
+ };
66
+ };
67
+ type TypedEventSender<TMap extends Record<string, unknown>> = <K extends keyof TMap & string>(event: K, data: TMap[K]) => void;
68
+ type WarmupEventSender = TypedEventSender<WarmupEventMap>;
69
+ type RenderEventSender = TypedEventSender<RenderEventMap>;
70
+ type EventSender = (event: string, data: object) => void;
71
+
72
+ export type { EventSender, RenderEventMap, RenderEventSender, TypedEventSender, WarmupEventMap, WarmupEventSender };
package/dist/sse.js ADDED
@@ -0,0 +1 @@
1
+ //# sourceMappingURL=sse.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effing/ffs",
3
- "version": "0.7.3",
3
+ "version": "0.9.0",
4
4
  "description": "FFmpeg-based effie rendering service",
5
5
  "type": "module",
6
6
  "exports": {
@@ -15,6 +15,10 @@
15
15
  "./handlers": {
16
16
  "types": "./dist/handlers/index.d.ts",
17
17
  "import": "./dist/handlers/index.js"
18
+ },
19
+ "./sse": {
20
+ "types": "./dist/sse.d.ts",
21
+ "import": "./dist/sse.js"
18
22
  }
19
23
  },
20
24
  "bin": {
@@ -32,10 +36,10 @@
32
36
  "tar-stream": "^3.1.7",
33
37
  "undici": "^7.3.0",
34
38
  "zod": "^3.25.76",
35
- "@effing/effie": "0.7.3"
39
+ "@effing/effie": "0.9.0"
36
40
  },
37
41
  "optionalDependencies": {
38
- "@effing/ffmpeg": "0.7.3"
42
+ "@effing/ffmpeg": "0.9.0"
39
43
  },
40
44
  "devDependencies": {
41
45
  "@types/body-parser": "^1.19.5",