@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/README.md
CHANGED
|
@@ -74,12 +74,8 @@ The server uses an internal HTTP proxy for video/audio URLs to ensure reliable D
|
|
|
74
74
|
| `FFS_TRANSIENT_STORE_SECRET_KEY` | S3 secret access key |
|
|
75
75
|
| `FFS_TRANSIENT_STORE_LOCAL_DIR` | Local storage directory (when not using S3) |
|
|
76
76
|
| `FFS_SOURCE_CACHE_TTL_MS` | TTL for cached sources in ms (default: 60 min) |
|
|
77
|
-
| `
|
|
77
|
+
| `FFS_JOB_DATA_TTL_MS` | TTL for job data in ms (default: 8 hours) |
|
|
78
78
|
| `FFS_WARMUP_CONCURRENCY` | Concurrent source fetches during warmup (default: 4) |
|
|
79
|
-
| `FFS_WARMUP_BACKEND_BASE_URL` | Separate backend for warmup (see Backend Separation) |
|
|
80
|
-
| `FFS_RENDER_BACKEND_BASE_URL` | Separate backend for render (see Backend Separation) |
|
|
81
|
-
| `FFS_WARMUP_BACKEND_API_KEY` | API key for authenticating to the warmup backend |
|
|
82
|
-
| `FFS_RENDER_BACKEND_API_KEY` | API key for authenticating to the render backend |
|
|
83
79
|
|
|
84
80
|
When `FFS_TRANSIENT_STORE_BUCKET` is not set, FFS uses the local filesystem for storage (default: system temp directory). Local files are automatically cleaned up after the TTL expires.
|
|
85
81
|
|
|
@@ -343,27 +339,47 @@ events.addEventListener("render:complete", (e) => {
|
|
|
343
339
|
|
|
344
340
|
## Backend Separation
|
|
345
341
|
|
|
346
|
-
FFS supports running warmup and render on separate backends
|
|
342
|
+
FFS supports running warmup and render on separate backends via resolver callbacks.
|
|
343
|
+
When backends are configured, the transient storage must be shared between services (e.g., using S3).
|
|
347
344
|
|
|
348
|
-
|
|
345
|
+
### Setup
|
|
349
346
|
|
|
350
|
-
|
|
351
|
-
- `FFS_RENDER_BACKEND_BASE_URL` — Base URL for render backend (e.g., `https://render.your.app`)
|
|
347
|
+
Pass resolvers to `createServerContext`:
|
|
352
348
|
|
|
353
|
-
|
|
349
|
+
```typescript
|
|
350
|
+
import { createServerContext } from "@effing/ffs/handlers";
|
|
351
|
+
import type {
|
|
352
|
+
RenderBackendResolver,
|
|
353
|
+
WarmupBackendResolver,
|
|
354
|
+
} from "@effing/ffs/handlers";
|
|
355
|
+
|
|
356
|
+
const renderBackendResolver: RenderBackendResolver = (effie, metadata) => ({
|
|
357
|
+
baseUrl: "https://render.your.app",
|
|
358
|
+
apiKey: "secret",
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
const warmupBackendResolver: WarmupBackendResolver = (sources, metadata) => ({
|
|
362
|
+
baseUrl: "https://warmup.your.app",
|
|
363
|
+
apiKey: "secret",
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
const ctx = await createServerContext({
|
|
367
|
+
renderBackendResolver,
|
|
368
|
+
warmupBackendResolver,
|
|
369
|
+
});
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
The render resolver receives the effie data; the warmup resolver receives the source list.
|
|
373
|
+
Both receive optional metadata (passed via handler options). Return `null` to handle locally.
|
|
354
374
|
|
|
355
|
-
|
|
356
|
-
| ---------------------------- | ---------------------------------------------------- |
|
|
357
|
-
| `POST /warmup` | Returns URL pointing to local server (orchestrator) |
|
|
358
|
-
| `GET /warmup/:id` | Proxies SSE from warmup backend |
|
|
359
|
-
| `POST /render` | Returns URL pointing to local server (orchestrator) |
|
|
360
|
-
| `GET /render/:id` | Proxies from render backend (SSE or video stream) |
|
|
361
|
-
| `POST /warmup-and-render` | Returns URL pointing to local server (orchestrator) |
|
|
362
|
-
| `GET /warmup-and-render/:id` | Proxies SSE from warmup backend, then render backend |
|
|
375
|
+
### Job metadata
|
|
363
376
|
|
|
364
|
-
|
|
377
|
+
Pass server-side metadata to be stored with the job and forwarded to the resolver:
|
|
365
378
|
|
|
366
|
-
|
|
379
|
+
```typescript
|
|
380
|
+
createRenderJob(req, res, ctx, { metadata: { tenantId: "abc" } });
|
|
381
|
+
createWarmupJob(req, res, ctx, { metadata: { tenantId: "abc" } });
|
|
382
|
+
```
|
|
367
383
|
|
|
368
384
|
## Examples
|
|
369
385
|
|
|
@@ -35,13 +35,13 @@ import path from "path";
|
|
|
35
35
|
import os from "os";
|
|
36
36
|
import crypto from "crypto";
|
|
37
37
|
var DEFAULT_SOURCE_TTL_MS = 60 * 60 * 1e3;
|
|
38
|
-
var
|
|
38
|
+
var DEFAULT_JOB_DATA_TTL_MS = 8 * 60 * 60 * 1e3;
|
|
39
39
|
var S3TransientStore = class {
|
|
40
40
|
client;
|
|
41
41
|
bucket;
|
|
42
42
|
prefix;
|
|
43
43
|
sourceTtlMs;
|
|
44
|
-
|
|
44
|
+
jobDataTtlMs;
|
|
45
45
|
constructor(options) {
|
|
46
46
|
this.client = new S3Client({
|
|
47
47
|
endpoint: options.endpoint,
|
|
@@ -55,7 +55,7 @@ var S3TransientStore = class {
|
|
|
55
55
|
this.bucket = options.bucket;
|
|
56
56
|
this.prefix = options.prefix ?? "";
|
|
57
57
|
this.sourceTtlMs = options.sourceTtlMs ?? DEFAULT_SOURCE_TTL_MS;
|
|
58
|
-
this.
|
|
58
|
+
this.jobDataTtlMs = options.jobDataTtlMs ?? DEFAULT_JOB_DATA_TTL_MS;
|
|
59
59
|
}
|
|
60
60
|
getExpires(ttlMs) {
|
|
61
61
|
return new Date(Date.now() + ttlMs);
|
|
@@ -138,7 +138,7 @@ var S3TransientStore = class {
|
|
|
138
138
|
Key: this.getFullKey(key),
|
|
139
139
|
Body: JSON.stringify(data),
|
|
140
140
|
ContentType: "application/json",
|
|
141
|
-
Expires: this.getExpires(ttlMs ?? this.
|
|
141
|
+
Expires: this.getExpires(ttlMs ?? this.jobDataTtlMs)
|
|
142
142
|
})
|
|
143
143
|
);
|
|
144
144
|
}
|
|
@@ -169,14 +169,14 @@ var LocalTransientStore = class {
|
|
|
169
169
|
initialized = false;
|
|
170
170
|
cleanupInterval;
|
|
171
171
|
sourceTtlMs;
|
|
172
|
-
|
|
172
|
+
jobDataTtlMs;
|
|
173
173
|
/** For cleanup, use the longer of the two TTLs */
|
|
174
174
|
maxTtlMs;
|
|
175
175
|
constructor(options) {
|
|
176
176
|
this.baseDir = options?.baseDir ?? path.join(os.tmpdir(), "ffs-transient");
|
|
177
177
|
this.sourceTtlMs = options?.sourceTtlMs ?? DEFAULT_SOURCE_TTL_MS;
|
|
178
|
-
this.
|
|
179
|
-
this.maxTtlMs = Math.max(this.sourceTtlMs, this.
|
|
178
|
+
this.jobDataTtlMs = options?.jobDataTtlMs ?? DEFAULT_JOB_DATA_TTL_MS;
|
|
179
|
+
this.maxTtlMs = Math.max(this.sourceTtlMs, this.jobDataTtlMs);
|
|
180
180
|
this.cleanupInterval = setInterval(() => {
|
|
181
181
|
this.cleanupExpired().catch(console.error);
|
|
182
182
|
}, 3e5);
|
|
@@ -292,7 +292,7 @@ var LocalTransientStore = class {
|
|
|
292
292
|
};
|
|
293
293
|
function createTransientStore() {
|
|
294
294
|
const sourceTtlMs = process.env.FFS_SOURCE_CACHE_TTL_MS ? parseInt(process.env.FFS_SOURCE_CACHE_TTL_MS, 10) : DEFAULT_SOURCE_TTL_MS;
|
|
295
|
-
const
|
|
295
|
+
const jobDataTtlMs = process.env.FFS_JOB_DATA_TTL_MS ? parseInt(process.env.FFS_JOB_DATA_TTL_MS, 10) : DEFAULT_JOB_DATA_TTL_MS;
|
|
296
296
|
if (process.env.FFS_TRANSIENT_STORE_BUCKET) {
|
|
297
297
|
return new S3TransientStore({
|
|
298
298
|
endpoint: process.env.FFS_TRANSIENT_STORE_ENDPOINT,
|
|
@@ -302,13 +302,13 @@ function createTransientStore() {
|
|
|
302
302
|
accessKeyId: process.env.FFS_TRANSIENT_STORE_ACCESS_KEY,
|
|
303
303
|
secretAccessKey: process.env.FFS_TRANSIENT_STORE_SECRET_KEY,
|
|
304
304
|
sourceTtlMs,
|
|
305
|
-
|
|
305
|
+
jobDataTtlMs
|
|
306
306
|
});
|
|
307
307
|
}
|
|
308
308
|
return new LocalTransientStore({
|
|
309
309
|
baseDir: process.env.FFS_TRANSIENT_STORE_LOCAL_DIR,
|
|
310
310
|
sourceTtlMs,
|
|
311
|
-
|
|
311
|
+
jobDataTtlMs
|
|
312
312
|
});
|
|
313
313
|
}
|
|
314
314
|
function hashUrl(url) {
|
|
@@ -338,4 +338,4 @@ export {
|
|
|
338
338
|
createTransientStore,
|
|
339
339
|
storeKeys
|
|
340
340
|
};
|
|
341
|
-
//# sourceMappingURL=chunk-
|
|
341
|
+
//# sourceMappingURL=chunk-4N2GLGC5.js.map
|
|
@@ -0,0 +1 @@
|
|
|
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 data: 8 hours */\nconst DEFAULT_JOB_DATA_TTL_MS = 8 * 60 * 60 * 1000;\n\n/**\n * Transient store interface for caching sources and storing ephemeral job data.\n */\nexport interface TransientStore {\n /** TTL for cached sources in milliseconds */\n readonly sourceTtlMs: number;\n /** TTL for job data in milliseconds */\n readonly jobDataTtlMs: 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 jobDataTtlMs: 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 jobDataTtlMs?: 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.jobDataTtlMs = options.jobDataTtlMs ?? DEFAULT_JOB_DATA_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.jobDataTtlMs),\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 jobDataTtlMs: 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 jobDataTtlMs?: number;\n }) {\n this.baseDir = options?.baseDir ?? path.join(os.tmpdir(), \"ffs-transient\");\n this.sourceTtlMs = options?.sourceTtlMs ?? DEFAULT_SOURCE_TTL_MS;\n this.jobDataTtlMs = options?.jobDataTtlMs ?? DEFAULT_JOB_DATA_TTL_MS;\n this.maxTtlMs = Math.max(this.sourceTtlMs, this.jobDataTtlMs);\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 jobDataTtlMs = process.env.FFS_JOB_DATA_TTL_MS\n ? parseInt(process.env.FFS_JOB_DATA_TTL_MS, 10)\n : DEFAULT_JOB_DATA_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 jobDataTtlMs,\n });\n }\n\n return new LocalTransientStore({\n baseDir: process.env.FFS_TRANSIENT_STORE_LOCAL_DIR,\n sourceTtlMs,\n jobDataTtlMs,\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":[]}
|
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
createTransientStore,
|
|
3
3
|
ffsFetch,
|
|
4
4
|
storeKeys
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-4N2GLGC5.js";
|
|
6
6
|
|
|
7
7
|
// src/handlers/shared.ts
|
|
8
8
|
import "express";
|
|
@@ -131,11 +131,11 @@ var HttpProxy = class {
|
|
|
131
131
|
|
|
132
132
|
// src/handlers/shared.ts
|
|
133
133
|
import { effieDataSchema } from "@effing/effie";
|
|
134
|
-
async function createServerContext() {
|
|
134
|
+
async function createServerContext(options) {
|
|
135
135
|
const port = process.env.FFS_PORT || process.env.PORT || 2e3;
|
|
136
|
-
const
|
|
136
|
+
const enableHttpProxy = options?.httpProxy ?? !options?.renderBackendResolver;
|
|
137
137
|
let httpProxy;
|
|
138
|
-
if (
|
|
138
|
+
if (enableHttpProxy) {
|
|
139
139
|
httpProxy = new HttpProxy();
|
|
140
140
|
await httpProxy.start();
|
|
141
141
|
}
|
|
@@ -145,10 +145,8 @@ async function createServerContext() {
|
|
|
145
145
|
baseUrl: process.env.FFS_BASE_URL || `http://localhost:${port}`,
|
|
146
146
|
skipValidation: !!process.env.FFS_SKIP_VALIDATION && process.env.FFS_SKIP_VALIDATION !== "false",
|
|
147
147
|
warmupConcurrency: parseInt(process.env.FFS_WARMUP_CONCURRENCY || "4", 10),
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
warmupBackendApiKey: process.env.FFS_WARMUP_BACKEND_API_KEY,
|
|
151
|
-
renderBackendApiKey: process.env.FFS_RENDER_BACKEND_API_KEY
|
|
148
|
+
warmupBackendResolver: options?.warmupBackendResolver,
|
|
149
|
+
renderBackendResolver: options?.renderBackendResolver
|
|
152
150
|
};
|
|
153
151
|
}
|
|
154
152
|
function parseEffieData(body, skipValidation) {
|
|
@@ -211,16 +209,16 @@ import { extractEffieSourcesWithTypes, effieDataSchema as effieDataSchema3 } fro
|
|
|
211
209
|
import "express";
|
|
212
210
|
import { randomUUID } from "crypto";
|
|
213
211
|
import { effieDataSchema as effieDataSchema2 } from "@effing/effie";
|
|
214
|
-
async function createRenderJob(req, res, ctx) {
|
|
212
|
+
async function createRenderJob(req, res, ctx, options) {
|
|
215
213
|
try {
|
|
216
214
|
const isWrapped = "effie" in req.body;
|
|
217
215
|
let rawEffieData;
|
|
218
216
|
let scale;
|
|
219
217
|
let upload;
|
|
220
218
|
if (isWrapped) {
|
|
221
|
-
const
|
|
222
|
-
if (typeof
|
|
223
|
-
const response = await ffsFetch(
|
|
219
|
+
const options2 = req.body;
|
|
220
|
+
if (typeof options2.effie === "string") {
|
|
221
|
+
const response = await ffsFetch(options2.effie);
|
|
224
222
|
if (!response.ok) {
|
|
225
223
|
throw new Error(
|
|
226
224
|
`Failed to fetch Effie data: ${response.status} ${response.statusText}`
|
|
@@ -228,10 +226,10 @@ async function createRenderJob(req, res, ctx) {
|
|
|
228
226
|
}
|
|
229
227
|
rawEffieData = await response.json();
|
|
230
228
|
} else {
|
|
231
|
-
rawEffieData =
|
|
229
|
+
rawEffieData = options2.effie;
|
|
232
230
|
}
|
|
233
|
-
scale =
|
|
234
|
-
upload =
|
|
231
|
+
scale = options2.scale ?? 1;
|
|
232
|
+
upload = options2.upload;
|
|
235
233
|
} else {
|
|
236
234
|
rawEffieData = req.body;
|
|
237
235
|
scale = parseFloat(req.query.scale?.toString() || "1");
|
|
@@ -263,12 +261,13 @@ async function createRenderJob(req, res, ctx) {
|
|
|
263
261
|
effie,
|
|
264
262
|
scale,
|
|
265
263
|
upload,
|
|
266
|
-
createdAt: Date.now()
|
|
264
|
+
createdAt: Date.now(),
|
|
265
|
+
metadata: options?.metadata
|
|
267
266
|
};
|
|
268
267
|
await ctx.transientStore.putJson(
|
|
269
268
|
storeKeys.renderJob(jobId),
|
|
270
269
|
job,
|
|
271
|
-
ctx.transientStore.
|
|
270
|
+
ctx.transientStore.jobDataTtlMs
|
|
272
271
|
);
|
|
273
272
|
res.json({
|
|
274
273
|
id: jobId,
|
|
@@ -283,17 +282,20 @@ async function streamRenderJob(req, res, ctx) {
|
|
|
283
282
|
try {
|
|
284
283
|
setupCORSHeaders(res);
|
|
285
284
|
const jobId = req.params.id;
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
return;
|
|
289
|
-
}
|
|
290
|
-
const jobCacheKey = storeKeys.renderJob(jobId);
|
|
291
|
-
const job = await ctx.transientStore.getJson(jobCacheKey);
|
|
292
|
-
ctx.transientStore.delete(jobCacheKey);
|
|
285
|
+
const jobStoreKey = storeKeys.renderJob(jobId);
|
|
286
|
+
const job = await ctx.transientStore.getJson(jobStoreKey);
|
|
293
287
|
if (!job) {
|
|
294
288
|
res.status(404).json({ error: "Job not found or expired" });
|
|
295
289
|
return;
|
|
296
290
|
}
|
|
291
|
+
if (ctx.renderBackendResolver) {
|
|
292
|
+
const backend = ctx.renderBackendResolver(job.effie, job.metadata);
|
|
293
|
+
if (backend) {
|
|
294
|
+
await proxyRenderFromBackend(res, jobId, backend);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
ctx.transientStore.delete(jobStoreKey);
|
|
297
299
|
if (job.upload) {
|
|
298
300
|
await streamRenderWithUpload(res, job, ctx);
|
|
299
301
|
} else {
|
|
@@ -309,7 +311,7 @@ async function streamRenderJob(req, res, ctx) {
|
|
|
309
311
|
}
|
|
310
312
|
}
|
|
311
313
|
async function streamRenderDirect(res, job, ctx) {
|
|
312
|
-
const { EffieRenderer } = await import("./render-
|
|
314
|
+
const { EffieRenderer } = await import("./render-IKGZZOBP.js");
|
|
313
315
|
const renderer = new EffieRenderer(job.effie, {
|
|
314
316
|
transientStore: ctx.transientStore,
|
|
315
317
|
httpProxy: ctx.httpProxy
|
|
@@ -374,7 +376,7 @@ async function renderAndUploadInternal(effie, scale, upload, sendEvent, ctx) {
|
|
|
374
376
|
timings.uploadCoverTime = Date.now() - uploadCoverStartTime;
|
|
375
377
|
}
|
|
376
378
|
const renderStartTime = Date.now();
|
|
377
|
-
const { EffieRenderer } = await import("./render-
|
|
379
|
+
const { EffieRenderer } = await import("./render-IKGZZOBP.js");
|
|
378
380
|
const renderer = new EffieRenderer(effie, {
|
|
379
381
|
transientStore: ctx.transientStore,
|
|
380
382
|
httpProxy: ctx.httpProxy
|
|
@@ -404,10 +406,10 @@ async function renderAndUploadInternal(effie, scale, upload, sendEvent, ctx) {
|
|
|
404
406
|
timings.uploadTime = Date.now() - uploadStartTime;
|
|
405
407
|
return timings;
|
|
406
408
|
}
|
|
407
|
-
async function proxyRenderFromBackend(res, jobId,
|
|
408
|
-
const backendUrl = `${
|
|
409
|
+
async function proxyRenderFromBackend(res, jobId, backend) {
|
|
410
|
+
const backendUrl = `${backend.baseUrl}/render/${jobId}`;
|
|
409
411
|
const response = await ffsFetch(backendUrl, {
|
|
410
|
-
headers:
|
|
412
|
+
headers: backend.apiKey ? { Authorization: `Bearer ${backend.apiKey}` } : void 0
|
|
411
413
|
});
|
|
412
414
|
if (!response.ok) {
|
|
413
415
|
res.status(response.status).json({ error: "Backend render failed" });
|
|
@@ -464,12 +466,12 @@ async function proxyRenderFromBackend(res, jobId, ctx) {
|
|
|
464
466
|
}
|
|
465
467
|
|
|
466
468
|
// src/handlers/orchestrating.ts
|
|
467
|
-
async function createWarmupAndRenderJob(req, res, ctx) {
|
|
469
|
+
async function createWarmupAndRenderJob(req, res, ctx, options) {
|
|
468
470
|
try {
|
|
469
|
-
const
|
|
471
|
+
const body = req.body;
|
|
470
472
|
let rawEffieData;
|
|
471
|
-
if (typeof
|
|
472
|
-
const response = await ffsFetch(
|
|
473
|
+
if (typeof body.effie === "string") {
|
|
474
|
+
const response = await ffsFetch(body.effie);
|
|
473
475
|
if (!response.ok) {
|
|
474
476
|
throw new Error(
|
|
475
477
|
`Failed to fetch Effie data: ${response.status} ${response.statusText}`
|
|
@@ -477,7 +479,7 @@ async function createWarmupAndRenderJob(req, res, ctx) {
|
|
|
477
479
|
}
|
|
478
480
|
rawEffieData = await response.json();
|
|
479
481
|
} else {
|
|
480
|
-
rawEffieData =
|
|
482
|
+
rawEffieData = body.effie;
|
|
481
483
|
}
|
|
482
484
|
let effie;
|
|
483
485
|
if (!ctx.skipValidation) {
|
|
@@ -502,8 +504,8 @@ async function createWarmupAndRenderJob(req, res, ctx) {
|
|
|
502
504
|
effie = data;
|
|
503
505
|
}
|
|
504
506
|
const sources = extractEffieSourcesWithTypes(effie);
|
|
505
|
-
const scale =
|
|
506
|
-
const upload =
|
|
507
|
+
const scale = body.scale ?? 1;
|
|
508
|
+
const upload = body.upload;
|
|
507
509
|
const jobId = randomUUID2();
|
|
508
510
|
const warmupJobId = randomUUID2();
|
|
509
511
|
const renderJobId = randomUUID2();
|
|
@@ -514,17 +516,18 @@ async function createWarmupAndRenderJob(req, res, ctx) {
|
|
|
514
516
|
upload,
|
|
515
517
|
warmupJobId,
|
|
516
518
|
renderJobId,
|
|
517
|
-
createdAt: Date.now()
|
|
519
|
+
createdAt: Date.now(),
|
|
520
|
+
metadata: options?.metadata
|
|
518
521
|
};
|
|
519
522
|
await ctx.transientStore.putJson(
|
|
520
523
|
storeKeys.warmupAndRenderJob(jobId),
|
|
521
524
|
job,
|
|
522
|
-
ctx.transientStore.
|
|
525
|
+
ctx.transientStore.jobDataTtlMs
|
|
523
526
|
);
|
|
524
527
|
await ctx.transientStore.putJson(
|
|
525
528
|
storeKeys.warmupJob(warmupJobId),
|
|
526
|
-
{ sources },
|
|
527
|
-
ctx.transientStore.
|
|
529
|
+
{ sources, metadata: options?.metadata },
|
|
530
|
+
ctx.transientStore.jobDataTtlMs
|
|
528
531
|
);
|
|
529
532
|
await ctx.transientStore.putJson(
|
|
530
533
|
storeKeys.renderJob(renderJobId),
|
|
@@ -532,9 +535,10 @@ async function createWarmupAndRenderJob(req, res, ctx) {
|
|
|
532
535
|
effie,
|
|
533
536
|
scale,
|
|
534
537
|
upload,
|
|
535
|
-
createdAt: Date.now()
|
|
538
|
+
createdAt: Date.now(),
|
|
539
|
+
metadata: options?.metadata
|
|
536
540
|
},
|
|
537
|
-
ctx.transientStore.
|
|
541
|
+
ctx.transientStore.jobDataTtlMs
|
|
538
542
|
);
|
|
539
543
|
res.json({
|
|
540
544
|
id: jobId,
|
|
@@ -549,13 +553,15 @@ async function streamWarmupAndRenderJob(req, res, ctx) {
|
|
|
549
553
|
try {
|
|
550
554
|
setupCORSHeaders(res);
|
|
551
555
|
const jobId = req.params.id;
|
|
552
|
-
const
|
|
553
|
-
const job = await ctx.transientStore.getJson(
|
|
554
|
-
ctx.transientStore.delete(
|
|
556
|
+
const jobStoreKey = storeKeys.warmupAndRenderJob(jobId);
|
|
557
|
+
const job = await ctx.transientStore.getJson(jobStoreKey);
|
|
558
|
+
ctx.transientStore.delete(jobStoreKey);
|
|
555
559
|
if (!job) {
|
|
556
560
|
res.status(404).json({ error: "Job not found" });
|
|
557
561
|
return;
|
|
558
562
|
}
|
|
563
|
+
const warmupBackend = ctx.warmupBackendResolver ? ctx.warmupBackendResolver(job.sources, job.metadata) : null;
|
|
564
|
+
const renderBackend = ctx.renderBackendResolver ? ctx.renderBackendResolver(job.effie, job.metadata) : null;
|
|
559
565
|
setupSSEResponse(res);
|
|
560
566
|
const sendEvent = createSSEEventSender(res);
|
|
561
567
|
let keepalivePhase = "warmup";
|
|
@@ -563,13 +569,13 @@ async function streamWarmupAndRenderJob(req, res, ctx) {
|
|
|
563
569
|
sendEvent("keepalive", { phase: keepalivePhase });
|
|
564
570
|
}, 25e3);
|
|
565
571
|
try {
|
|
566
|
-
if (
|
|
572
|
+
if (warmupBackend) {
|
|
567
573
|
await proxyRemoteSSE(
|
|
568
|
-
`${
|
|
574
|
+
`${warmupBackend.baseUrl}/warmup/${job.warmupJobId}`,
|
|
569
575
|
sendEvent,
|
|
570
576
|
"warmup:",
|
|
571
577
|
res,
|
|
572
|
-
|
|
578
|
+
warmupBackend.apiKey ? { Authorization: `Bearer ${warmupBackend.apiKey}` } : void 0
|
|
573
579
|
);
|
|
574
580
|
} else {
|
|
575
581
|
const warmupSender = prefixEventSender(sendEvent, "warmup:");
|
|
@@ -577,13 +583,13 @@ async function streamWarmupAndRenderJob(req, res, ctx) {
|
|
|
577
583
|
warmupSender("complete", { status: "ready" });
|
|
578
584
|
}
|
|
579
585
|
keepalivePhase = "render";
|
|
580
|
-
if (
|
|
586
|
+
if (renderBackend) {
|
|
581
587
|
await proxyRemoteSSE(
|
|
582
|
-
`${
|
|
588
|
+
`${renderBackend.baseUrl}/render/${job.renderJobId}`,
|
|
583
589
|
sendEvent,
|
|
584
590
|
"render:",
|
|
585
591
|
res,
|
|
586
|
-
|
|
592
|
+
renderBackend.apiKey ? { Authorization: `Bearer ${renderBackend.apiKey}` } : void 0
|
|
587
593
|
);
|
|
588
594
|
} else {
|
|
589
595
|
const renderSender = prefixEventSender(sendEvent, "render:");
|
|
@@ -602,7 +608,7 @@ async function streamWarmupAndRenderJob(req, res, ctx) {
|
|
|
602
608
|
sendEvent("complete", { status: "ready", videoUrl });
|
|
603
609
|
}
|
|
604
610
|
}
|
|
605
|
-
if (job.upload && !
|
|
611
|
+
if (job.upload && !renderBackend) {
|
|
606
612
|
sendEvent("complete", { status: "done" });
|
|
607
613
|
}
|
|
608
614
|
} catch (error) {
|
|
@@ -707,7 +713,7 @@ function shouldSkipWarmup(source) {
|
|
|
707
713
|
return source.type === "video" || source.type === "audio";
|
|
708
714
|
}
|
|
709
715
|
var inFlightFetches = /* @__PURE__ */ new Map();
|
|
710
|
-
async function createWarmupJob(req, res, ctx) {
|
|
716
|
+
async function createWarmupJob(req, res, ctx, options) {
|
|
711
717
|
try {
|
|
712
718
|
const parseResult = parseEffieData(req.body, ctx.skipValidation);
|
|
713
719
|
if ("error" in parseResult) {
|
|
@@ -716,10 +722,11 @@ async function createWarmupJob(req, res, ctx) {
|
|
|
716
722
|
}
|
|
717
723
|
const sources = extractEffieSourcesWithTypes2(parseResult.effie);
|
|
718
724
|
const jobId = randomUUID3();
|
|
725
|
+
const job = { sources, metadata: options?.metadata };
|
|
719
726
|
await ctx.transientStore.putJson(
|
|
720
727
|
storeKeys.warmupJob(jobId),
|
|
721
|
-
|
|
722
|
-
ctx.transientStore.
|
|
728
|
+
job,
|
|
729
|
+
ctx.transientStore.jobDataTtlMs
|
|
723
730
|
);
|
|
724
731
|
res.json({
|
|
725
732
|
id: jobId,
|
|
@@ -734,29 +741,32 @@ async function streamWarmupJob(req, res, ctx) {
|
|
|
734
741
|
try {
|
|
735
742
|
setupCORSHeaders(res);
|
|
736
743
|
const jobId = req.params.id;
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
const sendEvent2 = createSSEEventSender(res);
|
|
740
|
-
try {
|
|
741
|
-
await proxyRemoteSSE(
|
|
742
|
-
`${ctx.warmupBackendBaseUrl}/warmup/${jobId}`,
|
|
743
|
-
sendEvent2,
|
|
744
|
-
"",
|
|
745
|
-
res,
|
|
746
|
-
ctx.warmupBackendApiKey ? { Authorization: `Bearer ${ctx.warmupBackendApiKey}` } : void 0
|
|
747
|
-
);
|
|
748
|
-
} finally {
|
|
749
|
-
res.end();
|
|
750
|
-
}
|
|
751
|
-
return;
|
|
752
|
-
}
|
|
753
|
-
const jobCacheKey = storeKeys.warmupJob(jobId);
|
|
754
|
-
const job = await ctx.transientStore.getJson(jobCacheKey);
|
|
755
|
-
ctx.transientStore.delete(jobCacheKey);
|
|
744
|
+
const jobStoreKey = storeKeys.warmupJob(jobId);
|
|
745
|
+
const job = await ctx.transientStore.getJson(jobStoreKey);
|
|
756
746
|
if (!job) {
|
|
757
747
|
res.status(404).json({ error: "Job not found" });
|
|
758
748
|
return;
|
|
759
749
|
}
|
|
750
|
+
if (ctx.warmupBackendResolver) {
|
|
751
|
+
const backend = ctx.warmupBackendResolver(job.sources, job.metadata);
|
|
752
|
+
if (backend) {
|
|
753
|
+
setupSSEResponse(res);
|
|
754
|
+
const sendEvent2 = createSSEEventSender(res);
|
|
755
|
+
try {
|
|
756
|
+
await proxyRemoteSSE(
|
|
757
|
+
`${backend.baseUrl}/warmup/${jobId}`,
|
|
758
|
+
sendEvent2,
|
|
759
|
+
"",
|
|
760
|
+
res,
|
|
761
|
+
backend.apiKey ? { Authorization: `Bearer ${backend.apiKey}` } : void 0
|
|
762
|
+
);
|
|
763
|
+
} finally {
|
|
764
|
+
res.end();
|
|
765
|
+
}
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
ctx.transientStore.delete(jobStoreKey);
|
|
760
770
|
setupSSEResponse(res);
|
|
761
771
|
const sendEvent = createSSEEventSender(res);
|
|
762
772
|
try {
|
|
@@ -945,4 +955,4 @@ export {
|
|
|
945
955
|
streamWarmupJob,
|
|
946
956
|
purgeCache
|
|
947
957
|
};
|
|
948
|
-
//# sourceMappingURL=chunk-
|
|
958
|
+
//# sourceMappingURL=chunk-7KHGAMSG.js.map
|