@effing/ffs 0.5.0 → 0.6.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/README.md +36 -20
- package/dist/{chunk-5SGOYTM2.js → chunk-4N2GLGC5.js} +11 -11
- package/dist/chunk-4N2GLGC5.js.map +1 -0
- package/dist/{chunk-ZERUSI5T.js → chunk-7KHGAMSG.js} +85 -75
- package/dist/chunk-7KHGAMSG.js.map +1 -0
- package/dist/{chunk-N3D6I2BD.js → chunk-O7Z6DV2I.js} +2 -2
- package/dist/{chunk-QPZEAH3J.js → chunk-PERB3C4S.js} +10 -10
- package/dist/handlers/index.d.ts +27 -10
- package/dist/handlers/index.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -2
- package/dist/{proxy-qTA69nOV.d.ts → proxy-CsZ5h2Ya.d.ts} +3 -3
- package/dist/render-IKGZZOBP.js +8 -0
- package/dist/{render-VWBOR3Y2.js → render-MUKKTCF6.js} +1 -1
- package/dist/server.js +84 -74
- package/package.json +3 -3
- package/dist/chunk-5SGOYTM2.js.map +0 -1
- package/dist/chunk-ZERUSI5T.js.map +0 -1
- package/dist/render-NEDCS65O.js +0 -8
- /package/dist/{chunk-N3D6I2BD.js.map → chunk-O7Z6DV2I.js.map} +0 -0
- /package/dist/{render-NEDCS65O.js.map → render-IKGZZOBP.js.map} +0 -0
package/dist/server.js
CHANGED
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
createTransientStore,
|
|
4
4
|
ffsFetch,
|
|
5
5
|
storeKeys
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-PERB3C4S.js";
|
|
7
7
|
|
|
8
8
|
// src/server.ts
|
|
9
9
|
import express5 from "express";
|
|
@@ -136,11 +136,11 @@ var HttpProxy = class {
|
|
|
136
136
|
|
|
137
137
|
// src/handlers/shared.ts
|
|
138
138
|
import { effieDataSchema } from "@effing/effie";
|
|
139
|
-
async function createServerContext() {
|
|
139
|
+
async function createServerContext(options) {
|
|
140
140
|
const port2 = process.env.FFS_PORT || process.env.PORT || 2e3;
|
|
141
|
-
const
|
|
141
|
+
const enableHttpProxy = options?.httpProxy ?? !options?.renderBackendResolver;
|
|
142
142
|
let httpProxy;
|
|
143
|
-
if (
|
|
143
|
+
if (enableHttpProxy) {
|
|
144
144
|
httpProxy = new HttpProxy();
|
|
145
145
|
await httpProxy.start();
|
|
146
146
|
}
|
|
@@ -150,10 +150,8 @@ async function createServerContext() {
|
|
|
150
150
|
baseUrl: process.env.FFS_BASE_URL || `http://localhost:${port2}`,
|
|
151
151
|
skipValidation: !!process.env.FFS_SKIP_VALIDATION && process.env.FFS_SKIP_VALIDATION !== "false",
|
|
152
152
|
warmupConcurrency: parseInt(process.env.FFS_WARMUP_CONCURRENCY || "4", 10),
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
warmupBackendApiKey: process.env.FFS_WARMUP_BACKEND_API_KEY,
|
|
156
|
-
renderBackendApiKey: process.env.FFS_RENDER_BACKEND_API_KEY
|
|
153
|
+
warmupBackendResolver: options?.warmupBackendResolver,
|
|
154
|
+
renderBackendResolver: options?.renderBackendResolver
|
|
157
155
|
};
|
|
158
156
|
}
|
|
159
157
|
function parseEffieData(body, skipValidation) {
|
|
@@ -216,16 +214,16 @@ import { extractEffieSourcesWithTypes, effieDataSchema as effieDataSchema3 } fro
|
|
|
216
214
|
import "express";
|
|
217
215
|
import { randomUUID } from "crypto";
|
|
218
216
|
import { effieDataSchema as effieDataSchema2 } from "@effing/effie";
|
|
219
|
-
async function createRenderJob(req, res, ctx2) {
|
|
217
|
+
async function createRenderJob(req, res, ctx2, options) {
|
|
220
218
|
try {
|
|
221
219
|
const isWrapped = "effie" in req.body;
|
|
222
220
|
let rawEffieData;
|
|
223
221
|
let scale;
|
|
224
222
|
let upload;
|
|
225
223
|
if (isWrapped) {
|
|
226
|
-
const
|
|
227
|
-
if (typeof
|
|
228
|
-
const response = await ffsFetch(
|
|
224
|
+
const options2 = req.body;
|
|
225
|
+
if (typeof options2.effie === "string") {
|
|
226
|
+
const response = await ffsFetch(options2.effie);
|
|
229
227
|
if (!response.ok) {
|
|
230
228
|
throw new Error(
|
|
231
229
|
`Failed to fetch Effie data: ${response.status} ${response.statusText}`
|
|
@@ -233,10 +231,10 @@ async function createRenderJob(req, res, ctx2) {
|
|
|
233
231
|
}
|
|
234
232
|
rawEffieData = await response.json();
|
|
235
233
|
} else {
|
|
236
|
-
rawEffieData =
|
|
234
|
+
rawEffieData = options2.effie;
|
|
237
235
|
}
|
|
238
|
-
scale =
|
|
239
|
-
upload =
|
|
236
|
+
scale = options2.scale ?? 1;
|
|
237
|
+
upload = options2.upload;
|
|
240
238
|
} else {
|
|
241
239
|
rawEffieData = req.body;
|
|
242
240
|
scale = parseFloat(req.query.scale?.toString() || "1");
|
|
@@ -268,12 +266,13 @@ async function createRenderJob(req, res, ctx2) {
|
|
|
268
266
|
effie,
|
|
269
267
|
scale,
|
|
270
268
|
upload,
|
|
271
|
-
createdAt: Date.now()
|
|
269
|
+
createdAt: Date.now(),
|
|
270
|
+
metadata: options?.metadata
|
|
272
271
|
};
|
|
273
272
|
await ctx2.transientStore.putJson(
|
|
274
273
|
storeKeys.renderJob(jobId),
|
|
275
274
|
job,
|
|
276
|
-
ctx2.transientStore.
|
|
275
|
+
ctx2.transientStore.jobDataTtlMs
|
|
277
276
|
);
|
|
278
277
|
res.json({
|
|
279
278
|
id: jobId,
|
|
@@ -288,17 +287,20 @@ async function streamRenderJob(req, res, ctx2) {
|
|
|
288
287
|
try {
|
|
289
288
|
setupCORSHeaders(res);
|
|
290
289
|
const jobId = req.params.id;
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
return;
|
|
294
|
-
}
|
|
295
|
-
const jobCacheKey = storeKeys.renderJob(jobId);
|
|
296
|
-
const job = await ctx2.transientStore.getJson(jobCacheKey);
|
|
297
|
-
ctx2.transientStore.delete(jobCacheKey);
|
|
290
|
+
const jobStoreKey = storeKeys.renderJob(jobId);
|
|
291
|
+
const job = await ctx2.transientStore.getJson(jobStoreKey);
|
|
298
292
|
if (!job) {
|
|
299
293
|
res.status(404).json({ error: "Job not found or expired" });
|
|
300
294
|
return;
|
|
301
295
|
}
|
|
296
|
+
if (ctx2.renderBackendResolver) {
|
|
297
|
+
const backend = ctx2.renderBackendResolver(job.effie, job.metadata);
|
|
298
|
+
if (backend) {
|
|
299
|
+
await proxyRenderFromBackend(res, jobId, backend);
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
ctx2.transientStore.delete(jobStoreKey);
|
|
302
304
|
if (job.upload) {
|
|
303
305
|
await streamRenderWithUpload(res, job, ctx2);
|
|
304
306
|
} else {
|
|
@@ -314,7 +316,7 @@ async function streamRenderJob(req, res, ctx2) {
|
|
|
314
316
|
}
|
|
315
317
|
}
|
|
316
318
|
async function streamRenderDirect(res, job, ctx2) {
|
|
317
|
-
const { EffieRenderer } = await import("./render-
|
|
319
|
+
const { EffieRenderer } = await import("./render-MUKKTCF6.js");
|
|
318
320
|
const renderer = new EffieRenderer(job.effie, {
|
|
319
321
|
transientStore: ctx2.transientStore,
|
|
320
322
|
httpProxy: ctx2.httpProxy
|
|
@@ -379,7 +381,7 @@ async function renderAndUploadInternal(effie, scale, upload, sendEvent, ctx2) {
|
|
|
379
381
|
timings.uploadCoverTime = Date.now() - uploadCoverStartTime;
|
|
380
382
|
}
|
|
381
383
|
const renderStartTime = Date.now();
|
|
382
|
-
const { EffieRenderer } = await import("./render-
|
|
384
|
+
const { EffieRenderer } = await import("./render-MUKKTCF6.js");
|
|
383
385
|
const renderer = new EffieRenderer(effie, {
|
|
384
386
|
transientStore: ctx2.transientStore,
|
|
385
387
|
httpProxy: ctx2.httpProxy
|
|
@@ -409,10 +411,10 @@ async function renderAndUploadInternal(effie, scale, upload, sendEvent, ctx2) {
|
|
|
409
411
|
timings.uploadTime = Date.now() - uploadStartTime;
|
|
410
412
|
return timings;
|
|
411
413
|
}
|
|
412
|
-
async function proxyRenderFromBackend(res, jobId,
|
|
413
|
-
const backendUrl = `${
|
|
414
|
+
async function proxyRenderFromBackend(res, jobId, backend) {
|
|
415
|
+
const backendUrl = `${backend.baseUrl}/render/${jobId}`;
|
|
414
416
|
const response = await ffsFetch(backendUrl, {
|
|
415
|
-
headers:
|
|
417
|
+
headers: backend.apiKey ? { Authorization: `Bearer ${backend.apiKey}` } : void 0
|
|
416
418
|
});
|
|
417
419
|
if (!response.ok) {
|
|
418
420
|
res.status(response.status).json({ error: "Backend render failed" });
|
|
@@ -469,12 +471,12 @@ async function proxyRenderFromBackend(res, jobId, ctx2) {
|
|
|
469
471
|
}
|
|
470
472
|
|
|
471
473
|
// src/handlers/orchestrating.ts
|
|
472
|
-
async function createWarmupAndRenderJob(req, res, ctx2) {
|
|
474
|
+
async function createWarmupAndRenderJob(req, res, ctx2, options) {
|
|
473
475
|
try {
|
|
474
|
-
const
|
|
476
|
+
const body = req.body;
|
|
475
477
|
let rawEffieData;
|
|
476
|
-
if (typeof
|
|
477
|
-
const response = await ffsFetch(
|
|
478
|
+
if (typeof body.effie === "string") {
|
|
479
|
+
const response = await ffsFetch(body.effie);
|
|
478
480
|
if (!response.ok) {
|
|
479
481
|
throw new Error(
|
|
480
482
|
`Failed to fetch Effie data: ${response.status} ${response.statusText}`
|
|
@@ -482,7 +484,7 @@ async function createWarmupAndRenderJob(req, res, ctx2) {
|
|
|
482
484
|
}
|
|
483
485
|
rawEffieData = await response.json();
|
|
484
486
|
} else {
|
|
485
|
-
rawEffieData =
|
|
487
|
+
rawEffieData = body.effie;
|
|
486
488
|
}
|
|
487
489
|
let effie;
|
|
488
490
|
if (!ctx2.skipValidation) {
|
|
@@ -507,8 +509,8 @@ async function createWarmupAndRenderJob(req, res, ctx2) {
|
|
|
507
509
|
effie = data;
|
|
508
510
|
}
|
|
509
511
|
const sources = extractEffieSourcesWithTypes(effie);
|
|
510
|
-
const scale =
|
|
511
|
-
const upload =
|
|
512
|
+
const scale = body.scale ?? 1;
|
|
513
|
+
const upload = body.upload;
|
|
512
514
|
const jobId = randomUUID2();
|
|
513
515
|
const warmupJobId = randomUUID2();
|
|
514
516
|
const renderJobId = randomUUID2();
|
|
@@ -519,17 +521,18 @@ async function createWarmupAndRenderJob(req, res, ctx2) {
|
|
|
519
521
|
upload,
|
|
520
522
|
warmupJobId,
|
|
521
523
|
renderJobId,
|
|
522
|
-
createdAt: Date.now()
|
|
524
|
+
createdAt: Date.now(),
|
|
525
|
+
metadata: options?.metadata
|
|
523
526
|
};
|
|
524
527
|
await ctx2.transientStore.putJson(
|
|
525
528
|
storeKeys.warmupAndRenderJob(jobId),
|
|
526
529
|
job,
|
|
527
|
-
ctx2.transientStore.
|
|
530
|
+
ctx2.transientStore.jobDataTtlMs
|
|
528
531
|
);
|
|
529
532
|
await ctx2.transientStore.putJson(
|
|
530
533
|
storeKeys.warmupJob(warmupJobId),
|
|
531
|
-
{ sources },
|
|
532
|
-
ctx2.transientStore.
|
|
534
|
+
{ sources, metadata: options?.metadata },
|
|
535
|
+
ctx2.transientStore.jobDataTtlMs
|
|
533
536
|
);
|
|
534
537
|
await ctx2.transientStore.putJson(
|
|
535
538
|
storeKeys.renderJob(renderJobId),
|
|
@@ -537,9 +540,10 @@ async function createWarmupAndRenderJob(req, res, ctx2) {
|
|
|
537
540
|
effie,
|
|
538
541
|
scale,
|
|
539
542
|
upload,
|
|
540
|
-
createdAt: Date.now()
|
|
543
|
+
createdAt: Date.now(),
|
|
544
|
+
metadata: options?.metadata
|
|
541
545
|
},
|
|
542
|
-
ctx2.transientStore.
|
|
546
|
+
ctx2.transientStore.jobDataTtlMs
|
|
543
547
|
);
|
|
544
548
|
res.json({
|
|
545
549
|
id: jobId,
|
|
@@ -554,13 +558,15 @@ async function streamWarmupAndRenderJob(req, res, ctx2) {
|
|
|
554
558
|
try {
|
|
555
559
|
setupCORSHeaders(res);
|
|
556
560
|
const jobId = req.params.id;
|
|
557
|
-
const
|
|
558
|
-
const job = await ctx2.transientStore.getJson(
|
|
559
|
-
ctx2.transientStore.delete(
|
|
561
|
+
const jobStoreKey = storeKeys.warmupAndRenderJob(jobId);
|
|
562
|
+
const job = await ctx2.transientStore.getJson(jobStoreKey);
|
|
563
|
+
ctx2.transientStore.delete(jobStoreKey);
|
|
560
564
|
if (!job) {
|
|
561
565
|
res.status(404).json({ error: "Job not found" });
|
|
562
566
|
return;
|
|
563
567
|
}
|
|
568
|
+
const warmupBackend = ctx2.warmupBackendResolver ? ctx2.warmupBackendResolver(job.sources, job.metadata) : null;
|
|
569
|
+
const renderBackend = ctx2.renderBackendResolver ? ctx2.renderBackendResolver(job.effie, job.metadata) : null;
|
|
564
570
|
setupSSEResponse(res);
|
|
565
571
|
const sendEvent = createSSEEventSender(res);
|
|
566
572
|
let keepalivePhase = "warmup";
|
|
@@ -568,13 +574,13 @@ async function streamWarmupAndRenderJob(req, res, ctx2) {
|
|
|
568
574
|
sendEvent("keepalive", { phase: keepalivePhase });
|
|
569
575
|
}, 25e3);
|
|
570
576
|
try {
|
|
571
|
-
if (
|
|
577
|
+
if (warmupBackend) {
|
|
572
578
|
await proxyRemoteSSE(
|
|
573
|
-
`${
|
|
579
|
+
`${warmupBackend.baseUrl}/warmup/${job.warmupJobId}`,
|
|
574
580
|
sendEvent,
|
|
575
581
|
"warmup:",
|
|
576
582
|
res,
|
|
577
|
-
|
|
583
|
+
warmupBackend.apiKey ? { Authorization: `Bearer ${warmupBackend.apiKey}` } : void 0
|
|
578
584
|
);
|
|
579
585
|
} else {
|
|
580
586
|
const warmupSender = prefixEventSender(sendEvent, "warmup:");
|
|
@@ -582,13 +588,13 @@ async function streamWarmupAndRenderJob(req, res, ctx2) {
|
|
|
582
588
|
warmupSender("complete", { status: "ready" });
|
|
583
589
|
}
|
|
584
590
|
keepalivePhase = "render";
|
|
585
|
-
if (
|
|
591
|
+
if (renderBackend) {
|
|
586
592
|
await proxyRemoteSSE(
|
|
587
|
-
`${
|
|
593
|
+
`${renderBackend.baseUrl}/render/${job.renderJobId}`,
|
|
588
594
|
sendEvent,
|
|
589
595
|
"render:",
|
|
590
596
|
res,
|
|
591
|
-
|
|
597
|
+
renderBackend.apiKey ? { Authorization: `Bearer ${renderBackend.apiKey}` } : void 0
|
|
592
598
|
);
|
|
593
599
|
} else {
|
|
594
600
|
const renderSender = prefixEventSender(sendEvent, "render:");
|
|
@@ -607,7 +613,7 @@ async function streamWarmupAndRenderJob(req, res, ctx2) {
|
|
|
607
613
|
sendEvent("complete", { status: "ready", videoUrl });
|
|
608
614
|
}
|
|
609
615
|
}
|
|
610
|
-
if (job.upload && !
|
|
616
|
+
if (job.upload && !renderBackend) {
|
|
611
617
|
sendEvent("complete", { status: "done" });
|
|
612
618
|
}
|
|
613
619
|
} catch (error) {
|
|
@@ -712,7 +718,7 @@ function shouldSkipWarmup(source) {
|
|
|
712
718
|
return source.type === "video" || source.type === "audio";
|
|
713
719
|
}
|
|
714
720
|
var inFlightFetches = /* @__PURE__ */ new Map();
|
|
715
|
-
async function createWarmupJob(req, res, ctx2) {
|
|
721
|
+
async function createWarmupJob(req, res, ctx2, options) {
|
|
716
722
|
try {
|
|
717
723
|
const parseResult = parseEffieData(req.body, ctx2.skipValidation);
|
|
718
724
|
if ("error" in parseResult) {
|
|
@@ -721,10 +727,11 @@ async function createWarmupJob(req, res, ctx2) {
|
|
|
721
727
|
}
|
|
722
728
|
const sources = extractEffieSourcesWithTypes2(parseResult.effie);
|
|
723
729
|
const jobId = randomUUID3();
|
|
730
|
+
const job = { sources, metadata: options?.metadata };
|
|
724
731
|
await ctx2.transientStore.putJson(
|
|
725
732
|
storeKeys.warmupJob(jobId),
|
|
726
|
-
|
|
727
|
-
ctx2.transientStore.
|
|
733
|
+
job,
|
|
734
|
+
ctx2.transientStore.jobDataTtlMs
|
|
728
735
|
);
|
|
729
736
|
res.json({
|
|
730
737
|
id: jobId,
|
|
@@ -739,29 +746,32 @@ async function streamWarmupJob(req, res, ctx2) {
|
|
|
739
746
|
try {
|
|
740
747
|
setupCORSHeaders(res);
|
|
741
748
|
const jobId = req.params.id;
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
const sendEvent2 = createSSEEventSender(res);
|
|
745
|
-
try {
|
|
746
|
-
await proxyRemoteSSE(
|
|
747
|
-
`${ctx2.warmupBackendBaseUrl}/warmup/${jobId}`,
|
|
748
|
-
sendEvent2,
|
|
749
|
-
"",
|
|
750
|
-
res,
|
|
751
|
-
ctx2.warmupBackendApiKey ? { Authorization: `Bearer ${ctx2.warmupBackendApiKey}` } : void 0
|
|
752
|
-
);
|
|
753
|
-
} finally {
|
|
754
|
-
res.end();
|
|
755
|
-
}
|
|
756
|
-
return;
|
|
757
|
-
}
|
|
758
|
-
const jobCacheKey = storeKeys.warmupJob(jobId);
|
|
759
|
-
const job = await ctx2.transientStore.getJson(jobCacheKey);
|
|
760
|
-
ctx2.transientStore.delete(jobCacheKey);
|
|
749
|
+
const jobStoreKey = storeKeys.warmupJob(jobId);
|
|
750
|
+
const job = await ctx2.transientStore.getJson(jobStoreKey);
|
|
761
751
|
if (!job) {
|
|
762
752
|
res.status(404).json({ error: "Job not found" });
|
|
763
753
|
return;
|
|
764
754
|
}
|
|
755
|
+
if (ctx2.warmupBackendResolver) {
|
|
756
|
+
const backend = ctx2.warmupBackendResolver(job.sources, job.metadata);
|
|
757
|
+
if (backend) {
|
|
758
|
+
setupSSEResponse(res);
|
|
759
|
+
const sendEvent2 = createSSEEventSender(res);
|
|
760
|
+
try {
|
|
761
|
+
await proxyRemoteSSE(
|
|
762
|
+
`${backend.baseUrl}/warmup/${jobId}`,
|
|
763
|
+
sendEvent2,
|
|
764
|
+
"",
|
|
765
|
+
res,
|
|
766
|
+
backend.apiKey ? { Authorization: `Bearer ${backend.apiKey}` } : void 0
|
|
767
|
+
);
|
|
768
|
+
} finally {
|
|
769
|
+
res.end();
|
|
770
|
+
}
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
ctx2.transientStore.delete(jobStoreKey);
|
|
765
775
|
setupSSEResponse(res);
|
|
766
776
|
const sendEvent = createSSEEventSender(res);
|
|
767
777
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effing/ffs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "FFmpeg-based effie rendering service",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -32,10 +32,10 @@
|
|
|
32
32
|
"tar-stream": "^3.1.7",
|
|
33
33
|
"undici": "^7.3.0",
|
|
34
34
|
"zod": "^3.25.76",
|
|
35
|
-
"@effing/effie": "0.
|
|
35
|
+
"@effing/effie": "0.6.0"
|
|
36
36
|
},
|
|
37
37
|
"optionalDependencies": {
|
|
38
|
-
"@effing/ffmpeg": "0.
|
|
38
|
+
"@effing/ffmpeg": "0.6.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@types/body-parser": "^1.19.5",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/fetch.ts","../src/storage.ts"],"sourcesContent":["import { fetch, Agent, type Response, type BodyInit } from \"undici\";\n\n/**\n * Options for ffsFetch function\n */\nexport type FfsFetchOptions = {\n /** HTTP method */\n method?: \"GET\" | \"POST\" | \"PUT\" | \"DELETE\" | \"PATCH\" | \"HEAD\" | \"OPTIONS\";\n /** Request body */\n body?: BodyInit;\n /** Headers to send (merged with default User-Agent) */\n headers?: Record<string, string>;\n /** Timeout for receiving response headers in ms. @default 300000 (5 min) */\n headersTimeout?: number;\n /** Timeout between body data chunks in ms. 0 = no timeout. @default 300000 (5 min) */\n bodyTimeout?: number;\n};\n\n/**\n * Fetch with default User-Agent and configurable timeouts.\n *\n * @example\n * // Simple GET\n * const response = await ffsFetch(\"https://example.com/data.json\");\n *\n * @example\n * // Large file with infinite body timeout\n * const response = await ffsFetch(\"https://example.com/video.mp4\", {\n * bodyTimeout: 0,\n * });\n *\n * @example\n * // PUT upload\n * const response = await ffsFetch(\"https://s3.example.com/video.mp4\", {\n * method: \"PUT\",\n * body: videoBuffer,\n * bodyTimeout: 0,\n * headers: { \"Content-Type\": \"video/mp4\" },\n * });\n */\nexport async function ffsFetch(\n url: string,\n options?: FfsFetchOptions,\n): Promise<Response> {\n const {\n method,\n body,\n headers,\n headersTimeout = 300000, // 5 minutes\n bodyTimeout = 300000, // 5 minutes\n } = options ?? {};\n\n const agent = new Agent({ headersTimeout, bodyTimeout });\n\n return fetch(url, {\n method,\n body,\n headers: { \"User-Agent\": \"FFS (+https://effing.dev/ffs)\", ...headers },\n dispatcher: agent,\n });\n}\n","import {\n S3Client,\n PutObjectCommand,\n GetObjectCommand,\n HeadObjectCommand,\n DeleteObjectCommand,\n} from \"@aws-sdk/client-s3\";\nimport { Upload } from \"@aws-sdk/lib-storage\";\nimport fs from \"fs/promises\";\nimport { createReadStream, createWriteStream, existsSync } from \"fs\";\nimport { pipeline } from \"stream/promises\";\nimport path from \"path\";\nimport os from \"os\";\nimport crypto from \"crypto\";\nimport type { Readable } from \"stream\";\n\n/** Default TTL for sources: 60 minutes */\nconst DEFAULT_SOURCE_TTL_MS = 60 * 60 * 1000;\n/** Default TTL for job metadata: 8 hours */\nconst DEFAULT_JOB_METADATA_TTL_MS = 8 * 60 * 60 * 1000;\n\n/**\n * Transient store interface for caching sources and storing ephemeral job metadata.\n */\nexport interface TransientStore {\n /** TTL for cached sources in milliseconds */\n readonly sourceTtlMs: number;\n /** TTL for job metadata in milliseconds */\n readonly jobMetadataTtlMs: number;\n /** Store a stream with the given key and optional TTL override */\n put(key: string, stream: Readable, ttlMs?: number): Promise<void>;\n /** Get a stream for the given key, or null if not found */\n getStream(key: string): Promise<Readable | null>;\n /** Check if a key exists */\n exists(key: string): Promise<boolean>;\n /** Check if multiple keys exist (batch operation) */\n existsMany(keys: string[]): Promise<Map<string, boolean>>;\n /** Delete a key */\n delete(key: string): Promise<void>;\n /** Store JSON data with optional TTL override */\n putJson(key: string, data: object, ttlMs?: number): Promise<void>;\n /** Get JSON data, or null if not found */\n getJson<T>(key: string): Promise<T | null>;\n /** Close and cleanup resources */\n close(): void;\n}\n\n/**\n * S3-compatible transient store implementation\n */\nexport class S3TransientStore implements TransientStore {\n private client: S3Client;\n private bucket: string;\n private prefix: string;\n public readonly sourceTtlMs: number;\n public readonly jobMetadataTtlMs: number;\n\n constructor(options: {\n endpoint?: string;\n region?: string;\n bucket: string;\n prefix?: string;\n accessKeyId?: string;\n secretAccessKey?: string;\n sourceTtlMs?: number;\n jobMetadataTtlMs?: number;\n }) {\n this.client = new S3Client({\n endpoint: options.endpoint,\n region: options.region ?? \"auto\",\n credentials: options.accessKeyId\n ? {\n accessKeyId: options.accessKeyId,\n secretAccessKey: options.secretAccessKey!,\n }\n : undefined,\n forcePathStyle: !!options.endpoint,\n });\n this.bucket = options.bucket;\n this.prefix = options.prefix ?? \"\";\n this.sourceTtlMs = options.sourceTtlMs ?? DEFAULT_SOURCE_TTL_MS;\n this.jobMetadataTtlMs =\n options.jobMetadataTtlMs ?? DEFAULT_JOB_METADATA_TTL_MS;\n }\n\n private getExpires(ttlMs: number): Date {\n return new Date(Date.now() + ttlMs);\n }\n\n private getFullKey(key: string): string {\n return `${this.prefix}${key}`;\n }\n\n async put(key: string, stream: Readable, ttlMs?: number): Promise<void> {\n const upload = new Upload({\n client: this.client,\n params: {\n Bucket: this.bucket,\n Key: this.getFullKey(key),\n Body: stream,\n Expires: this.getExpires(ttlMs ?? this.sourceTtlMs),\n },\n });\n await upload.done();\n }\n\n async getStream(key: string): Promise<Readable | null> {\n try {\n const response = await this.client.send(\n new GetObjectCommand({\n Bucket: this.bucket,\n Key: this.getFullKey(key),\n }),\n );\n return response.Body as Readable;\n } catch (err: unknown) {\n const error = err as {\n name?: string;\n $metadata?: { httpStatusCode?: number };\n };\n if (\n error.name === \"NoSuchKey\" ||\n error.$metadata?.httpStatusCode === 404\n ) {\n return null;\n }\n throw err;\n }\n }\n\n async exists(key: string): Promise<boolean> {\n try {\n await this.client.send(\n new HeadObjectCommand({\n Bucket: this.bucket,\n Key: this.getFullKey(key),\n }),\n );\n return true;\n } catch (err: unknown) {\n const error = err as {\n name?: string;\n $metadata?: { httpStatusCode?: number };\n };\n if (\n error.name === \"NotFound\" ||\n error.$metadata?.httpStatusCode === 404\n ) {\n return false;\n }\n throw err;\n }\n }\n\n async existsMany(keys: string[]): Promise<Map<string, boolean>> {\n const results = await Promise.all(\n keys.map(async (key) => [key, await this.exists(key)] as const),\n );\n return new Map(results);\n }\n\n async delete(key: string): Promise<void> {\n try {\n await this.client.send(\n new DeleteObjectCommand({\n Bucket: this.bucket,\n Key: this.getFullKey(key),\n }),\n );\n } catch (err: unknown) {\n const error = err as {\n name?: string;\n $metadata?: { httpStatusCode?: number };\n };\n if (\n error.name === \"NoSuchKey\" ||\n error.$metadata?.httpStatusCode === 404\n ) {\n return;\n }\n throw err;\n }\n }\n\n async putJson(key: string, data: object, ttlMs?: number): Promise<void> {\n await this.client.send(\n new PutObjectCommand({\n Bucket: this.bucket,\n Key: this.getFullKey(key),\n Body: JSON.stringify(data),\n ContentType: \"application/json\",\n Expires: this.getExpires(ttlMs ?? this.jobMetadataTtlMs),\n }),\n );\n }\n\n async getJson<T>(key: string): Promise<T | null> {\n try {\n const response = await this.client.send(\n new GetObjectCommand({\n Bucket: this.bucket,\n Key: this.getFullKey(key),\n }),\n );\n const body = await response.Body?.transformToString();\n if (!body) return null;\n return JSON.parse(body) as T;\n } catch (err: unknown) {\n const error = err as {\n name?: string;\n $metadata?: { httpStatusCode?: number };\n };\n if (\n error.name === \"NoSuchKey\" ||\n error.$metadata?.httpStatusCode === 404\n ) {\n return null;\n }\n throw err;\n }\n }\n\n close(): void {\n // nothing to do here\n }\n}\n\n/**\n * Local filesystem transient store implementation\n */\nexport class LocalTransientStore implements TransientStore {\n private baseDir: string;\n private initialized = false;\n private cleanupInterval?: ReturnType<typeof setInterval>;\n public readonly sourceTtlMs: number;\n public readonly jobMetadataTtlMs: number;\n /** For cleanup, use the longer of the two TTLs */\n private maxTtlMs: number;\n\n constructor(options?: {\n baseDir?: string;\n sourceTtlMs?: number;\n jobMetadataTtlMs?: number;\n }) {\n this.baseDir = options?.baseDir ?? path.join(os.tmpdir(), \"ffs-transient\");\n this.sourceTtlMs = options?.sourceTtlMs ?? DEFAULT_SOURCE_TTL_MS;\n this.jobMetadataTtlMs =\n options?.jobMetadataTtlMs ?? DEFAULT_JOB_METADATA_TTL_MS;\n this.maxTtlMs = Math.max(this.sourceTtlMs, this.jobMetadataTtlMs);\n\n // Cleanup expired files every 5 minutes\n this.cleanupInterval = setInterval(() => {\n this.cleanupExpired().catch(console.error);\n }, 300_000);\n }\n\n /**\n * Remove files older than max TTL\n */\n public async cleanupExpired(): Promise<void> {\n if (!this.initialized) return;\n\n const now = Date.now();\n await this.cleanupDir(this.baseDir, now);\n }\n\n private async cleanupDir(dir: string, now: number): Promise<void> {\n let entries;\n try {\n entries = await fs.readdir(dir, { withFileTypes: true });\n } catch {\n return; // Directory doesn't exist or can't be read\n }\n\n for (const entry of entries) {\n const fullPath = path.join(dir, entry.name);\n\n if (entry.isDirectory()) {\n await this.cleanupDir(fullPath, now);\n // Remove empty directories\n try {\n await fs.rmdir(fullPath);\n } catch {\n // Directory not empty or other error, ignore\n }\n } else if (entry.isFile()) {\n try {\n const stat = await fs.stat(fullPath);\n if (now - stat.mtimeMs > this.maxTtlMs) {\n await fs.rm(fullPath, { force: true });\n }\n } catch {\n // File may have been deleted, ignore\n }\n }\n }\n }\n\n private async ensureDir(filePath: string): Promise<void> {\n await fs.mkdir(path.dirname(filePath), { recursive: true });\n this.initialized = true;\n }\n\n private filePath(key: string): string {\n return path.join(this.baseDir, key);\n }\n\n private tmpPathFor(finalPath: string): string {\n const rand = crypto.randomBytes(8).toString(\"hex\");\n // Keep tmp file in the same directory so rename stays atomic on POSIX filesystems.\n return `${finalPath}.tmp-${process.pid}-${rand}`;\n }\n\n async put(key: string, stream: Readable, _ttlMs?: number): Promise<void> {\n // Note: TTL is not used for local storage; cleanup uses file mtime\n const fp = this.filePath(key);\n await this.ensureDir(fp);\n\n // Write to temp file, then rename for atomicity (no partial reads).\n const tmpPath = this.tmpPathFor(fp);\n try {\n const writeStream = createWriteStream(tmpPath);\n await pipeline(stream, writeStream);\n await fs.rename(tmpPath, fp);\n } catch (err) {\n await fs.rm(tmpPath, { force: true }).catch(() => {});\n throw err;\n }\n }\n\n async getStream(key: string): Promise<Readable | null> {\n const fp = this.filePath(key);\n if (!existsSync(fp)) return null;\n return createReadStream(fp);\n }\n\n async exists(key: string): Promise<boolean> {\n try {\n await fs.access(this.filePath(key));\n return true;\n } catch {\n return false;\n }\n }\n\n async existsMany(keys: string[]): Promise<Map<string, boolean>> {\n const results = await Promise.all(\n keys.map(async (key) => [key, await this.exists(key)] as const),\n );\n return new Map(results);\n }\n\n async delete(key: string): Promise<void> {\n await fs.rm(this.filePath(key), { force: true });\n }\n\n async putJson(key: string, data: object, _ttlMs?: number): Promise<void> {\n // Note: TTL is not used for local storage; cleanup uses file mtime\n const fp = this.filePath(key);\n await this.ensureDir(fp);\n\n // Write to temp file, then rename for atomicity (no partial reads).\n const tmpPath = this.tmpPathFor(fp);\n try {\n await fs.writeFile(tmpPath, JSON.stringify(data));\n await fs.rename(tmpPath, fp);\n } catch (err) {\n await fs.rm(tmpPath, { force: true }).catch(() => {});\n throw err;\n }\n }\n\n async getJson<T>(key: string): Promise<T | null> {\n try {\n const content = await fs.readFile(this.filePath(key), \"utf-8\");\n return JSON.parse(content) as T;\n } catch {\n return null;\n }\n }\n\n close(): void {\n // Stop the cleanup interval\n if (this.cleanupInterval) {\n clearInterval(this.cleanupInterval);\n this.cleanupInterval = undefined;\n }\n }\n}\n\n/**\n * Create a transient store instance based on environment variables.\n * Uses S3 if FFS_TRANSIENT_STORE_BUCKET is set, otherwise uses local filesystem.\n */\nexport function createTransientStore(): TransientStore {\n // Parse TTLs from env\n const sourceTtlMs = process.env.FFS_SOURCE_CACHE_TTL_MS\n ? parseInt(process.env.FFS_SOURCE_CACHE_TTL_MS, 10)\n : DEFAULT_SOURCE_TTL_MS;\n const jobMetadataTtlMs = process.env.FFS_JOB_METADATA_TTL_MS\n ? parseInt(process.env.FFS_JOB_METADATA_TTL_MS, 10)\n : DEFAULT_JOB_METADATA_TTL_MS;\n\n if (process.env.FFS_TRANSIENT_STORE_BUCKET) {\n return new S3TransientStore({\n endpoint: process.env.FFS_TRANSIENT_STORE_ENDPOINT,\n region: process.env.FFS_TRANSIENT_STORE_REGION ?? \"auto\",\n bucket: process.env.FFS_TRANSIENT_STORE_BUCKET,\n prefix: process.env.FFS_TRANSIENT_STORE_PREFIX,\n accessKeyId: process.env.FFS_TRANSIENT_STORE_ACCESS_KEY,\n secretAccessKey: process.env.FFS_TRANSIENT_STORE_SECRET_KEY,\n sourceTtlMs,\n jobMetadataTtlMs,\n });\n }\n\n return new LocalTransientStore({\n baseDir: process.env.FFS_TRANSIENT_STORE_LOCAL_DIR,\n sourceTtlMs,\n jobMetadataTtlMs,\n });\n}\n\nexport function hashUrl(url: string): string {\n return crypto.createHash(\"sha256\").update(url).digest(\"hex\").slice(0, 16);\n}\n\nexport type SourceStoreKey = `sources/${string}`;\nexport type WarmupJobStoreKey = `jobs/warmup/${string}.json`;\nexport type RenderJobStoreKey = `jobs/render/${string}.json`;\nexport type WarmupAndRenderJobStoreKey =\n `jobs/warmup-and-render/${string}.json`;\n\n/**\n * Build the store key for a source URL (hashing is handled internally).\n */\nexport function sourceStoreKey(url: string): SourceStoreKey {\n return `sources/${hashUrl(url)}`;\n}\n\nexport function warmupJobStoreKey(jobId: string): WarmupJobStoreKey {\n return `jobs/warmup/${jobId}.json`;\n}\n\nexport function renderJobStoreKey(jobId: string): RenderJobStoreKey {\n return `jobs/render/${jobId}.json`;\n}\n\nexport function warmupAndRenderJobStoreKey(\n jobId: string,\n): WarmupAndRenderJobStoreKey {\n return `jobs/warmup-and-render/${jobId}.json`;\n}\n\n/**\n * Centralized store key builders for known namespaces.\n * Prefer using these helpers over manual string interpolation.\n */\nexport const storeKeys = {\n source: sourceStoreKey,\n warmupJob: warmupJobStoreKey,\n renderJob: renderJobStoreKey,\n warmupAndRenderJob: warmupAndRenderJobStoreKey,\n} as const;\n"],"mappings":"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","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
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 WarmupJob = {\n sources: EffieSourceWithType[];\n};\n\nexport type RenderJob = {\n effie: EffieData<EffieSources>;\n scale: number;\n upload?: UploadOptions;\n createdAt: number;\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};\n\nexport type ServerContext = {\n transientStore: TransientStore;\n httpProxy?: HttpProxy;\n baseUrl: string;\n skipValidation: boolean;\n warmupConcurrency: number;\n warmupBackendBaseUrl?: string;\n renderBackendBaseUrl?: string;\n warmupBackendApiKey?: string;\n renderBackendApiKey?: string;\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(): Promise<ServerContext> {\n const port = process.env.FFS_PORT || process.env.PORT || 2000;\n const renderBackendBaseUrl = process.env.FFS_RENDER_BACKEND_BASE_URL;\n let httpProxy: HttpProxy | undefined;\n if (!renderBackendBaseUrl) {\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 warmupBackendBaseUrl: process.env.FFS_WARMUP_BACKEND_BASE_URL,\n renderBackendBaseUrl: process.env.FFS_RENDER_BACKEND_BASE_URL,\n warmupBackendApiKey: process.env.FFS_WARMUP_BACKEND_API_KEY,\n renderBackendApiKey: process.env.FFS_RENDER_BACKEND_API_KEY,\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): 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 // Store job in cache with job metadata TTL\n await ctx.transientStore.putJson(\n storeKeys.warmupJob(jobId),\n { sources },\n ctx.transientStore.jobMetadataTtlMs,\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 // Proxy to warmup backend if configured\n if (ctx.warmupBackendBaseUrl) {\n setupSSEResponse(res);\n const sendEvent = createSSEEventSender(res);\n try {\n await proxyRemoteSSE(\n `${ctx.warmupBackendBaseUrl}/warmup/${jobId}`,\n sendEvent,\n \"\",\n res,\n ctx.warmupBackendApiKey\n ? { Authorization: `Bearer ${ctx.warmupBackendApiKey}` }\n : undefined,\n );\n } finally {\n res.end();\n }\n return;\n }\n\n const jobCacheKey = storeKeys.warmupJob(jobId);\n const job = await ctx.transientStore.getJson<WarmupJob>(jobCacheKey);\n // only allow the warmup job to run once\n ctx.transientStore.delete(jobCacheKey);\n\n if (!job) {\n res.status(404).json({ error: \"Job not found\" });\n return;\n }\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): Promise<void> {\n try {\n // Parse request body\n const options = req.body as {\n effie: unknown;\n scale?: number;\n upload?: UploadOptions;\n };\n\n let rawEffieData: unknown;\n if (typeof options.effie === \"string\") {\n // Effie is 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 rawEffieData = options.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 = options.scale ?? 1;\n const upload = options.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 };\n\n await ctx.transientStore.putJson(\n storeKeys.warmupAndRenderJob(jobId),\n job,\n ctx.transientStore.jobMetadataTtlMs,\n );\n\n // Also store sub-jobs for backend execution\n await ctx.transientStore.putJson(\n storeKeys.warmupJob(warmupJobId),\n { sources },\n ctx.transientStore.jobMetadataTtlMs,\n );\n await ctx.transientStore.putJson(\n storeKeys.renderJob(renderJobId),\n {\n effie,\n scale,\n upload,\n createdAt: Date.now(),\n } satisfies RenderJob,\n ctx.transientStore.jobMetadataTtlMs,\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 jobCacheKey = storeKeys.warmupAndRenderJob(jobId);\n const job =\n await ctx.transientStore.getJson<WarmupAndRenderJob>(jobCacheKey);\n // Only allow the job to run once\n ctx.transientStore.delete(jobCacheKey);\n\n if (!job) {\n res.status(404).json({ error: \"Job not found\" });\n return;\n }\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 (ctx.warmupBackendBaseUrl) {\n // Proxy warmup from remote backend\n await proxyRemoteSSE(\n `${ctx.warmupBackendBaseUrl}/warmup/${job.warmupJobId}`,\n sendEvent,\n \"warmup:\",\n res,\n ctx.warmupBackendApiKey\n ? { Authorization: `Bearer ${ctx.warmupBackendApiKey}` }\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 (ctx.renderBackendBaseUrl) {\n // Proxy render from remote backend\n await proxyRemoteSSE(\n `${ctx.renderBackendBaseUrl}/render/${job.renderJobId}`,\n sendEvent,\n \"render:\",\n res,\n ctx.renderBackendApiKey\n ? { Authorization: `Bearer ${ctx.renderBackendApiKey}` }\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 && !ctx.renderBackendBaseUrl) {\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 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): 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 };\n\n await ctx.transientStore.putJson(\n storeKeys.renderJob(jobId),\n job,\n ctx.transientStore.jobMetadataTtlMs,\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 // Proxy to render backend if configured\n if (ctx.renderBackendBaseUrl) {\n await proxyRenderFromBackend(res, jobId, ctx);\n return;\n }\n\n const jobCacheKey = storeKeys.renderJob(jobId);\n const job = await ctx.transientStore.getJson<RenderJob>(jobCacheKey);\n // only allow the render job to run once\n ctx.transientStore.delete(jobCacheKey);\n\n if (!job) {\n res.status(404).json({ error: \"Job not found or expired\" });\n return;\n }\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 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 ctx: ServerContext,\n): Promise<void> {\n const backendUrl = `${ctx.renderBackendBaseUrl}/render/${jobId}`;\n const response = await ffsFetch(backendUrl, {\n headers: ctx.renderBackendApiKey\n ? { Authorization: `Bearer ${ctx.renderBackendApiKey}` }\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","effieDataSchema","effieDataSchema","randomUUID","extractEffieSourcesWithTypes","randomUUID","sendEvent","Readable"]}
|
package/dist/render-NEDCS65O.js
DELETED
|
File without changes
|
|
File without changes
|