@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/README.md +35 -2
- package/dist/{chunk-46RJNEOH.js → chunk-G46KSSSR.js} +139 -75
- package/dist/chunk-G46KSSSR.js.map +1 -0
- package/dist/{chunk-2CCHBAXN.js → chunk-O3UH3WAZ.js} +4 -1
- package/dist/{chunk-2CCHBAXN.js.map → chunk-O3UH3WAZ.js.map} +1 -1
- package/dist/handlers/index.d.ts +26 -3
- package/dist/handlers/index.js +5 -1
- package/dist/index.js +1 -1
- package/dist/{render-S3MCYNKR.js → render-DMHG3YJU.js} +2 -2
- package/dist/{render-OMXRUVX5.js → render-YKC5W4YT.js} +3 -0
- package/dist/server.js +137 -75
- package/dist/server.js.map +1 -1
- package/dist/sse.d.ts +72 -0
- package/dist/sse.js +1 -0
- package/dist/sse.js.map +1 -0
- package/package.json +7 -3
- package/dist/chunk-46RJNEOH.js.map +0 -1
- /package/dist/{render-S3MCYNKR.js.map → render-DMHG3YJU.js.map} +0 -0
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 ??
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
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
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
|
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-
|
|
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-
|
|
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
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
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
|
|
921
|
+
sendError(res, 401, ErrorCode.UNAUTHORIZED, "Unauthorized");
|
|
860
922
|
return false;
|
|
861
923
|
}
|
|
862
924
|
return true;
|
package/dist/server.js.map
CHANGED
|
@@ -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
|
|
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
|
package/dist/sse.js.map
ADDED
|
@@ -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.
|
|
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.
|
|
39
|
+
"@effing/effie": "0.9.0"
|
|
36
40
|
},
|
|
37
41
|
"optionalDependencies": {
|
|
38
|
-
"@effing/ffmpeg": "0.
|
|
42
|
+
"@effing/ffmpeg": "0.9.0"
|
|
39
43
|
},
|
|
40
44
|
"devDependencies": {
|
|
41
45
|
"@types/body-parser": "^1.19.5",
|