@effing/ffs 0.1.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.
@@ -0,0 +1,439 @@
1
+ import {
2
+ EffieRenderer,
3
+ cacheKeys,
4
+ createCacheStorage,
5
+ ffsFetch
6
+ } from "./chunk-RNE6TKMF.js";
7
+
8
+ // src/handlers/shared.ts
9
+ import "express";
10
+ import { effieDataSchema } from "@effing/effie";
11
+ function createServerContext() {
12
+ const port = process.env.FFS_PORT || 2e3;
13
+ return {
14
+ cacheStorage: createCacheStorage(),
15
+ baseUrl: process.env.FFS_BASE_URL || `http://localhost:${port}`,
16
+ skipValidation: !!process.env.FFS_SKIP_VALIDATION && process.env.FFS_SKIP_VALIDATION !== "false",
17
+ cacheConcurrency: parseInt(process.env.FFS_CACHE_CONCURRENCY || "4", 10)
18
+ };
19
+ }
20
+ function parseEffieData(body, skipValidation) {
21
+ const isWrapped = typeof body === "object" && body !== null && "effie" in body;
22
+ const rawEffieData = isWrapped ? body.effie : body;
23
+ if (!skipValidation) {
24
+ const result = effieDataSchema.safeParse(rawEffieData);
25
+ if (!result.success) {
26
+ return {
27
+ error: "Invalid effie data",
28
+ issues: result.error.issues.map((issue) => ({
29
+ path: issue.path.join("."),
30
+ message: issue.message
31
+ }))
32
+ };
33
+ }
34
+ return { effie: result.data };
35
+ } else {
36
+ const effie = rawEffieData;
37
+ if (!effie?.segments) {
38
+ return { error: "Invalid effie data: missing segments" };
39
+ }
40
+ return { effie };
41
+ }
42
+ }
43
+ function setupCORSHeaders(res) {
44
+ res.setHeader("Access-Control-Allow-Origin", "*");
45
+ res.setHeader("Access-Control-Allow-Methods", "GET");
46
+ }
47
+ function setupSSEResponse(res) {
48
+ res.setHeader("Content-Type", "text/event-stream");
49
+ res.setHeader("Cache-Control", "no-cache");
50
+ res.setHeader("Connection", "keep-alive");
51
+ res.flushHeaders();
52
+ }
53
+ function createSSEEventSender(res) {
54
+ return (event, data) => {
55
+ res.write(`event: ${event}
56
+ data: ${JSON.stringify(data)}
57
+
58
+ `);
59
+ };
60
+ }
61
+
62
+ // src/handlers/caching.ts
63
+ import "express";
64
+ import { Readable, Transform } from "stream";
65
+ import { randomUUID } from "crypto";
66
+ import { extractEffieSources } from "@effing/effie";
67
+ var inFlightFetches = /* @__PURE__ */ new Map();
68
+ async function createWarmupJob(req, res, ctx) {
69
+ try {
70
+ const parseResult = parseEffieData(req.body, ctx.skipValidation);
71
+ if ("error" in parseResult) {
72
+ res.status(400).json(parseResult);
73
+ return;
74
+ }
75
+ const sources = extractEffieSources(parseResult.effie);
76
+ const jobId = randomUUID();
77
+ await ctx.cacheStorage.putJson(cacheKeys.warmupJob(jobId), { sources });
78
+ res.json({
79
+ id: jobId,
80
+ url: `${ctx.baseUrl}/warmup/${jobId}`
81
+ });
82
+ } catch (error) {
83
+ console.error("Error creating warmup job:", error);
84
+ res.status(500).json({ error: "Failed to create warmup job" });
85
+ }
86
+ }
87
+ async function streamWarmupJob(req, res, ctx) {
88
+ try {
89
+ setupCORSHeaders(res);
90
+ const jobId = req.params.id;
91
+ const jobCacheKey = cacheKeys.warmupJob(jobId);
92
+ const job = await ctx.cacheStorage.getJson(jobCacheKey);
93
+ ctx.cacheStorage.delete(jobCacheKey);
94
+ if (!job) {
95
+ res.status(404).json({ error: "Job not found" });
96
+ return;
97
+ }
98
+ setupSSEResponse(res);
99
+ const sendEvent = createSSEEventSender(res);
100
+ try {
101
+ await warmupSources(job.sources, sendEvent, ctx);
102
+ sendEvent("complete", { status: "ready" });
103
+ } catch (error) {
104
+ sendEvent("error", { message: String(error) });
105
+ } finally {
106
+ res.end();
107
+ }
108
+ } catch (error) {
109
+ console.error("Error in warmup streaming:", error);
110
+ if (!res.headersSent) {
111
+ res.status(500).json({ error: "Warmup streaming failed" });
112
+ } else {
113
+ res.end();
114
+ }
115
+ }
116
+ }
117
+ async function purgeCache(req, res, ctx) {
118
+ try {
119
+ const parseResult = parseEffieData(req.body, ctx.skipValidation);
120
+ if ("error" in parseResult) {
121
+ res.status(400).json(parseResult);
122
+ return;
123
+ }
124
+ const sources = extractEffieSources(parseResult.effie);
125
+ let purged = 0;
126
+ for (const url of sources) {
127
+ const ck = cacheKeys.source(url);
128
+ if (await ctx.cacheStorage.exists(ck)) {
129
+ await ctx.cacheStorage.delete(ck);
130
+ purged++;
131
+ }
132
+ }
133
+ res.json({ purged, total: sources.length });
134
+ } catch (error) {
135
+ console.error("Error purging cache:", error);
136
+ res.status(500).json({ error: "Failed to purge cache" });
137
+ }
138
+ }
139
+ async function warmupSources(sources, sendEvent, ctx) {
140
+ const total = sources.length;
141
+ const sourceCacheKeys = sources.map(cacheKeys.source);
142
+ sendEvent("start", { total });
143
+ const existsMap = await ctx.cacheStorage.existsMany(sourceCacheKeys);
144
+ let cached = 0;
145
+ let failed = 0;
146
+ for (let i = 0; i < sources.length; i++) {
147
+ if (existsMap.get(sourceCacheKeys[i])) {
148
+ cached++;
149
+ sendEvent("progress", {
150
+ url: sources[i],
151
+ status: "hit",
152
+ cached,
153
+ failed,
154
+ total
155
+ });
156
+ }
157
+ }
158
+ const uncached = sources.filter((_, i) => !existsMap.get(sourceCacheKeys[i]));
159
+ if (uncached.length === 0) {
160
+ sendEvent("summary", { cached, failed, total });
161
+ return;
162
+ }
163
+ const keepalive = setInterval(() => {
164
+ sendEvent("keepalive", { cached, failed, total });
165
+ }, 25e3);
166
+ const queue = [...uncached];
167
+ const workers = Array.from(
168
+ { length: Math.min(ctx.cacheConcurrency, queue.length) },
169
+ async () => {
170
+ while (queue.length > 0) {
171
+ const url = queue.shift();
172
+ const cacheKey = cacheKeys.source(url);
173
+ const startTime = Date.now();
174
+ try {
175
+ let fetchPromise = inFlightFetches.get(cacheKey);
176
+ if (!fetchPromise) {
177
+ fetchPromise = fetchAndCache(url, cacheKey, sendEvent, ctx);
178
+ inFlightFetches.set(cacheKey, fetchPromise);
179
+ }
180
+ await fetchPromise;
181
+ inFlightFetches.delete(cacheKey);
182
+ cached++;
183
+ sendEvent("progress", {
184
+ url,
185
+ status: "cached",
186
+ cached,
187
+ failed,
188
+ total,
189
+ ms: Date.now() - startTime
190
+ });
191
+ } catch (error) {
192
+ inFlightFetches.delete(cacheKey);
193
+ failed++;
194
+ sendEvent("progress", {
195
+ url,
196
+ status: "error",
197
+ error: String(error),
198
+ cached,
199
+ failed,
200
+ total,
201
+ ms: Date.now() - startTime
202
+ });
203
+ }
204
+ }
205
+ }
206
+ );
207
+ await Promise.all(workers);
208
+ clearInterval(keepalive);
209
+ sendEvent("summary", { cached, failed, total });
210
+ }
211
+ async function fetchAndCache(url, cacheKey, sendEvent, ctx) {
212
+ const response = await ffsFetch(url, {
213
+ headersTimeout: 10 * 60 * 1e3,
214
+ // 10 minutes
215
+ bodyTimeout: 20 * 60 * 1e3
216
+ // 20 minutes
217
+ });
218
+ if (!response.ok) {
219
+ throw new Error(`${response.status} ${response.statusText}`);
220
+ }
221
+ sendEvent("downloading", { url, status: "started", bytesReceived: 0 });
222
+ const sourceStream = Readable.fromWeb(
223
+ response.body
224
+ );
225
+ let totalBytes = 0;
226
+ let lastEventTime = Date.now();
227
+ const PROGRESS_INTERVAL = 1e4;
228
+ const progressStream = new Transform({
229
+ transform(chunk, _encoding, callback) {
230
+ totalBytes += chunk.length;
231
+ const now = Date.now();
232
+ if (now - lastEventTime >= PROGRESS_INTERVAL) {
233
+ sendEvent("downloading", {
234
+ url,
235
+ status: "downloading",
236
+ bytesReceived: totalBytes
237
+ });
238
+ lastEventTime = now;
239
+ }
240
+ callback(null, chunk);
241
+ }
242
+ });
243
+ const trackedStream = sourceStream.pipe(progressStream);
244
+ await ctx.cacheStorage.put(cacheKey, trackedStream);
245
+ }
246
+
247
+ // src/handlers/rendering.ts
248
+ import "express";
249
+ import { randomUUID as randomUUID2 } from "crypto";
250
+ import { effieDataSchema as effieDataSchema2 } from "@effing/effie";
251
+ async function createRenderJob(req, res, ctx) {
252
+ try {
253
+ const isWrapped = "effie" in req.body;
254
+ let rawEffieData;
255
+ let scale;
256
+ let upload;
257
+ if (isWrapped) {
258
+ const options = req.body;
259
+ if (typeof options.effie === "string") {
260
+ const response = await ffsFetch(options.effie);
261
+ if (!response.ok) {
262
+ throw new Error(
263
+ `Failed to fetch Effie data: ${response.status} ${response.statusText}`
264
+ );
265
+ }
266
+ rawEffieData = await response.json();
267
+ } else {
268
+ rawEffieData = options.effie;
269
+ }
270
+ scale = options.scale ?? 1;
271
+ upload = options.upload;
272
+ } else {
273
+ rawEffieData = req.body;
274
+ scale = parseFloat(req.query.scale?.toString() || "1");
275
+ }
276
+ let effie;
277
+ if (!ctx.skipValidation) {
278
+ const result = effieDataSchema2.safeParse(rawEffieData);
279
+ if (!result.success) {
280
+ res.status(400).json({
281
+ error: "Invalid effie data",
282
+ issues: result.error.issues.map((issue) => ({
283
+ path: issue.path.join("."),
284
+ message: issue.message
285
+ }))
286
+ });
287
+ return;
288
+ }
289
+ effie = result.data;
290
+ } else {
291
+ const data = rawEffieData;
292
+ if (!data?.segments) {
293
+ res.status(400).json({ error: "Invalid effie data: missing segments" });
294
+ return;
295
+ }
296
+ effie = data;
297
+ }
298
+ const jobId = randomUUID2();
299
+ const job = {
300
+ effie,
301
+ scale,
302
+ upload,
303
+ createdAt: Date.now()
304
+ };
305
+ await ctx.cacheStorage.putJson(cacheKeys.renderJob(jobId), job);
306
+ res.json({
307
+ id: jobId,
308
+ url: `${ctx.baseUrl}/render/${jobId}`
309
+ });
310
+ } catch (error) {
311
+ console.error("Error creating render job:", error);
312
+ res.status(500).json({ error: "Failed to create render job" });
313
+ }
314
+ }
315
+ async function streamRenderJob(req, res, ctx) {
316
+ try {
317
+ setupCORSHeaders(res);
318
+ const jobId = req.params.id;
319
+ const jobCacheKey = cacheKeys.renderJob(jobId);
320
+ const job = await ctx.cacheStorage.getJson(jobCacheKey);
321
+ ctx.cacheStorage.delete(jobCacheKey);
322
+ if (!job) {
323
+ res.status(404).json({ error: "Job not found or expired" });
324
+ return;
325
+ }
326
+ if (job.upload) {
327
+ await streamRenderWithUpload(res, job, ctx);
328
+ } else {
329
+ await streamRenderDirect(res, job, ctx);
330
+ }
331
+ } catch (error) {
332
+ console.error("Error in render:", error);
333
+ if (!res.headersSent) {
334
+ res.status(500).json({ error: "Rendering failed" });
335
+ } else {
336
+ res.end();
337
+ }
338
+ }
339
+ }
340
+ async function streamRenderDirect(res, job, ctx) {
341
+ const renderer = new EffieRenderer(job.effie, {
342
+ cacheStorage: ctx.cacheStorage
343
+ });
344
+ const videoStream = await renderer.render(job.scale);
345
+ res.on("close", () => {
346
+ videoStream.destroy();
347
+ renderer.close();
348
+ });
349
+ res.set("Content-Type", "video/mp4");
350
+ videoStream.pipe(res);
351
+ }
352
+ async function streamRenderWithUpload(res, job, ctx) {
353
+ setupSSEResponse(res);
354
+ const sendEvent = createSSEEventSender(res);
355
+ const keepalive = setInterval(() => {
356
+ sendEvent("keepalive", { status: "rendering" });
357
+ }, 25e3);
358
+ try {
359
+ sendEvent("started", { status: "rendering" });
360
+ const timings = await renderAndUploadInternal(
361
+ job.effie,
362
+ job.scale,
363
+ job.upload,
364
+ sendEvent,
365
+ ctx
366
+ );
367
+ sendEvent("complete", { status: "uploaded", timings });
368
+ } catch (error) {
369
+ sendEvent("error", { message: String(error) });
370
+ } finally {
371
+ clearInterval(keepalive);
372
+ res.end();
373
+ }
374
+ }
375
+ async function renderAndUploadInternal(effie, scale, upload, sendEvent, ctx) {
376
+ const timings = {};
377
+ if (upload.coverUrl) {
378
+ const fetchCoverStartTime = Date.now();
379
+ const coverFetchResponse = await ffsFetch(effie.cover);
380
+ if (!coverFetchResponse.ok) {
381
+ throw new Error(
382
+ `Failed to fetch cover image: ${coverFetchResponse.status} ${coverFetchResponse.statusText}`
383
+ );
384
+ }
385
+ const coverBuffer = Buffer.from(await coverFetchResponse.arrayBuffer());
386
+ timings.fetchCoverTime = Date.now() - fetchCoverStartTime;
387
+ const uploadCoverStartTime = Date.now();
388
+ const uploadCoverResponse = await ffsFetch(upload.coverUrl, {
389
+ method: "PUT",
390
+ body: coverBuffer,
391
+ headers: {
392
+ "Content-Type": "image/png",
393
+ "Content-Length": coverBuffer.length.toString()
394
+ }
395
+ });
396
+ if (!uploadCoverResponse.ok) {
397
+ throw new Error(
398
+ `Failed to upload cover: ${uploadCoverResponse.status} ${uploadCoverResponse.statusText}`
399
+ );
400
+ }
401
+ timings.uploadCoverTime = Date.now() - uploadCoverStartTime;
402
+ }
403
+ const renderStartTime = Date.now();
404
+ const renderer = new EffieRenderer(effie, { cacheStorage: ctx.cacheStorage });
405
+ const videoStream = await renderer.render(scale);
406
+ const chunks = [];
407
+ for await (const chunk of videoStream) {
408
+ chunks.push(Buffer.from(chunk));
409
+ }
410
+ const videoBuffer = Buffer.concat(chunks);
411
+ timings.renderTime = Date.now() - renderStartTime;
412
+ sendEvent("keepalive", { status: "uploading" });
413
+ const uploadStartTime = Date.now();
414
+ const uploadResponse = await ffsFetch(upload.videoUrl, {
415
+ method: "PUT",
416
+ body: videoBuffer,
417
+ headers: {
418
+ "Content-Type": "video/mp4",
419
+ "Content-Length": videoBuffer.length.toString()
420
+ }
421
+ });
422
+ if (!uploadResponse.ok) {
423
+ throw new Error(
424
+ `Failed to upload rendered video: ${uploadResponse.status} ${uploadResponse.statusText}`
425
+ );
426
+ }
427
+ timings.uploadTime = Date.now() - uploadStartTime;
428
+ return timings;
429
+ }
430
+
431
+ export {
432
+ createServerContext,
433
+ createWarmupJob,
434
+ streamWarmupJob,
435
+ purgeCache,
436
+ createRenderJob,
437
+ streamRenderJob
438
+ };
439
+ //# sourceMappingURL=chunk-LK5K4SQV.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/handlers/shared.ts","../src/handlers/caching.ts","../src/handlers/rendering.ts"],"sourcesContent":["import express from \"express\";\nimport type { CacheStorage } from \"../cache\";\nimport { createCacheStorage } from \"../cache\";\nimport type { EffieData, EffieSources } 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: string[];\n};\n\nexport type RenderJob = {\n effie: EffieData<EffieSources>;\n scale: number;\n upload?: UploadOptions;\n createdAt: number;\n};\n\nexport type ServerContext = {\n cacheStorage: CacheStorage;\n baseUrl: string;\n skipValidation: boolean;\n cacheConcurrency: number;\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 function createServerContext(): ServerContext {\n const port = process.env.FFS_PORT || 2000;\n return {\n cacheStorage: createCacheStorage(),\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 cacheConcurrency: parseInt(process.env.FFS_CACHE_CONCURRENCY || \"4\", 10),\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 express from \"express\";\nimport { Readable, Transform } from \"stream\";\nimport { randomUUID } from \"crypto\";\nimport { cacheKeys } from \"../cache\";\nimport { ffsFetch } from \"../fetch\";\nimport { extractEffieSources } from \"@effing/effie\";\nimport type { ServerContext, SSEEventSender, WarmupJob } from \"./shared\";\nimport {\n parseEffieData,\n setupCORSHeaders,\n setupSSEResponse,\n createSSEEventSender,\n} from \"./shared\";\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 = extractEffieSources(parseResult.effie);\n const jobId = randomUUID();\n\n // Store job in cache\n await ctx.cacheStorage.putJson(cacheKeys.warmupJob(jobId), { sources });\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 const jobCacheKey = cacheKeys.warmupJob(jobId);\n const job = await ctx.cacheStorage.getJson<WarmupJob>(jobCacheKey);\n // only allow the warmup job to run once\n ctx.cacheStorage.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 = cacheKeys.source(url);\n if (await ctx.cacheStorage.exists(ck)) {\n await ctx.cacheStorage.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 */\nexport async function warmupSources(\n sources: string[],\n sendEvent: SSEEventSender,\n ctx: ServerContext,\n): Promise<void> {\n const total = sources.length;\n const sourceCacheKeys = sources.map(cacheKeys.source);\n\n sendEvent(\"start\", { total });\n\n // Check what's already cached\n const existsMap = await ctx.cacheStorage.existsMany(sourceCacheKeys);\n\n let cached = 0;\n let failed = 0;\n\n // Report hits immediately\n for (let i = 0; i < sources.length; i++) {\n if (existsMap.get(sourceCacheKeys[i])) {\n cached++;\n sendEvent(\"progress\", {\n url: sources[i],\n status: \"hit\",\n cached,\n failed,\n total,\n });\n }\n }\n\n // Filter to uncached sources\n const uncached = sources.filter((_, i) => !existsMap.get(sourceCacheKeys[i]));\n\n if (uncached.length === 0) {\n sendEvent(\"summary\", { cached, failed, total });\n return;\n }\n\n // Keepalive interval for long-running fetches\n const keepalive = setInterval(() => {\n sendEvent(\"keepalive\", { cached, failed, 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.cacheConcurrency, queue.length) },\n async () => {\n while (queue.length > 0) {\n const url = queue.shift()!;\n const cacheKey = cacheKeys.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(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,\n status: \"cached\",\n cached,\n failed,\n total,\n ms: Date.now() - startTime,\n });\n } catch (error) {\n inFlightFetches.delete(cacheKey);\n failed++;\n sendEvent(\"progress\", {\n url,\n status: \"error\",\n error: String(error),\n cached,\n failed,\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, 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\n const trackedStream = sourceStream.pipe(progressStream);\n await ctx.cacheStorage.put(cacheKey, trackedStream);\n}\n","import express from \"express\";\nimport { randomUUID } from \"crypto\";\nimport { cacheKeys } from \"../cache\";\nimport { ffsFetch } from \"../fetch\";\nimport { EffieRenderer } from \"../render\";\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\";\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.cacheStorage.putJson(cacheKeys.renderJob(jobId), job);\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 const jobCacheKey = cacheKeys.renderJob(jobId);\n const job = await ctx.cacheStorage.getJson<RenderJob>(jobCacheKey);\n // only allow the render job to run once\n ctx.cacheStorage.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 renderer = new EffieRenderer(job.effie, {\n cacheStorage: ctx.cacheStorage,\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 renderer = new EffieRenderer(effie, { cacheStorage: ctx.cacheStorage });\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"],"mappings":"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","names":["randomUUID","effieDataSchema","effieDataSchema","randomUUID"]}