@effing/ffs 0.6.0 → 0.7.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 CHANGED
@@ -12,7 +12,7 @@ Takes an `EffieData` composition and renders it to an MP4 video using FFmpeg. Us
12
12
  npm install @effing/ffs
13
13
  ```
14
14
 
15
- FFmpeg is bundled via `ffmpeg-static` — no system installation required.
15
+ FFmpeg is bundled via `@effing/ffmpeg` — no system installation required.
16
16
 
17
17
  ## Quick Start
18
18
 
@@ -44,17 +44,21 @@ npx @effing/ffs
44
44
  FFS_PORT=8080 npx @effing/ffs
45
45
  ```
46
46
 
47
- Rendering is a two-step process with the HTTP server: first obtain a stream URL, then stream that URL to get the video.
47
+ Rendering is a two-step process: POST to create a job, then connect to the SSE progress stream to track warmup and rendering. The video URL is revealed in the `ready` event.
48
48
 
49
49
  ```bash
50
- # 1. Obtain a stream URL
50
+ # 1. Create a render job
51
51
  curl -X POST http://localhost:2000/render \
52
52
  -H "Content-Type: application/json" \
53
- -d @composition.json
54
- # Returns: { "id": "...", "url": "http://localhost:2000/render/123e4567-e89b-12d3-a456-426614174000" }
53
+ -d '{"effie": ...}'
54
+ # Returns: { "id": "...", "progressUrl": "http://localhost:2000/render/.../progress" }
55
55
 
56
- # 2. Stream the URL to get the video
57
- curl http://localhost:2000/render/123e4567-e89b-12d3-a456-426614174000 -o output.mp4
56
+ # 2. Connect to SSE progress stream (or use EventSource in browser)
57
+ curl http://localhost:2000/render/.../progress
58
+ # SSE events: warmup:start, warmup:progress, warmup:complete, ready (with videoUrl)
59
+
60
+ # 3. Fetch the video
61
+ curl http://localhost:2000/render/.../video -o output.mp4
58
62
  ```
59
63
 
60
64
  The server uses an internal HTTP proxy for video/audio URLs to ensure reliable DNS resolution in containerized environments (e.g., Alpine Linux). This is why you might see another server running on a random port.
@@ -73,25 +77,13 @@ The server uses an internal HTTP proxy for video/audio URLs to ensure reliable D
73
77
  | `FFS_TRANSIENT_STORE_ACCESS_KEY` | S3 access key ID |
74
78
  | `FFS_TRANSIENT_STORE_SECRET_KEY` | S3 secret access key |
75
79
  | `FFS_TRANSIENT_STORE_LOCAL_DIR` | Local storage directory (when not using S3) |
76
- | `FFS_SOURCE_CACHE_TTL_MS` | TTL for cached sources in ms (default: 60 min) |
77
- | `FFS_JOB_DATA_TTL_MS` | TTL for job data in ms (default: 8 hours) |
80
+ | `FFS_TRANSIENT_STORE_TTL_MS` | TTL for all transient data in ms (default: 60 min) |
78
81
  | `FFS_WARMUP_CONCURRENCY` | Concurrent source fetches during warmup (default: 4) |
79
82
 
80
83
  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.
81
84
 
82
85
  For S3 storage, the TTL is set as the `Expires` header on objects. Note that this is metadata only. To enable automatic deletion, configure [S3 lifecycle rules](https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-lifecycle-mgmt.html) on your bucket to delete expired objects.
83
86
 
84
- ## Concepts
85
-
86
- ### EffieRenderer
87
-
88
- The main class that orchestrates video rendering:
89
-
90
- 1. **Builds FFmpeg command** — Constructs complex filter graphs for overlays, transitions, effects
91
- 2. **Fetches sources** — Downloads images, animations, and audio from URLs
92
- 3. **Processes layers** — Applies motion, effects, and timing to each layer
93
- 4. **Outputs video** — Streams H.264/AAC MP4 to stdout or file
94
-
95
87
  ## API Overview
96
88
 
97
89
  ### EffieRenderer
@@ -141,20 +133,15 @@ When running as an HTTP server, FFS provides endpoints for rendering, cache warm
141
133
 
142
134
  ### `POST /render`
143
135
 
144
- Creates a render job and returns a stream URL to execute it. Supports two request formats:
145
-
146
- **Raw EffieData**: Body is raw EffieData, options in query params.
136
+ Creates a render job that includes warmup and render phases. Supports optional cache purging.
147
137
 
148
- | Query Param | Effect |
149
- | ----------- | ------------------------- |
150
- | `scale` | Scale factor (default: 1) |
151
-
152
- **Wrapped format**: Body contains `effie` plus options.
138
+ **Request:**
153
139
 
154
140
  ```typescript
155
141
  type RenderOptions = {
156
142
  effie: EffieData | string; // EffieData object or URL to fetch from
157
143
  scale?: number; // Scale factor (default: 1)
144
+ purge?: boolean; // Purge cached sources before warmup
158
145
  upload?: {
159
146
  videoUrl: string; // Pre-signed URL to upload rendered video
160
147
  coverUrl?: string; // Pre-signed URL to upload cover image
@@ -162,70 +149,84 @@ type RenderOptions = {
162
149
  };
163
150
  ```
164
151
 
165
- | Option | Effect |
166
- | -------------- | ---------------------------------------------- |
167
- | `effie` as URL | Fetches EffieData from the URL before storing |
168
- | `upload` | GET will upload and stream SSE progress events |
169
-
170
152
  **Response:**
171
153
 
172
154
  ```json
173
155
  {
174
156
  "id": "550e8400-e29b-41d4-a716-446655440000",
175
- "url": "http://localhost:2000/render/550e8400-e29b-41d4-a716-446655440000"
157
+ "progressUrl": "http://localhost:2000/render/550e8400-e29b-41d4-a716-446655440000/progress"
176
158
  }
177
159
  ```
178
160
 
179
- ### `GET /render/:id`
161
+ ### `GET /render/:id/progress`
180
162
 
181
- Executes the render job. Behavior depends on whether `upload` was specified:
163
+ Streams warmup and render progress via SSE. All warmup events are prefixed with `warmup:`, render events with `render:`.
182
164
 
183
- **Without upload** — Streams the MP4 video directly:
165
+ **Events:**
184
166
 
185
- ```bash
186
- curl http://localhost:2000/render/550e8400-... -o output.mp4
187
- ```
167
+ | Event | Phase | Data |
168
+ | -------------------- | ------ | ----------------------------------------------------------------- |
169
+ | `purge:complete` | purge | `{ "purged": 3, "total": 5 }` |
170
+ | `warmup:start` | warmup | `{ "total": 5 }` |
171
+ | `warmup:progress` | warmup | `{ "url": "...", "status": "hit"\|"cached"\|"error", ... }` |
172
+ | `warmup:downloading` | warmup | `{ "url": "...", "status": "downloading", "bytesReceived": ... }` |
173
+ | `warmup:summary` | warmup | `{ "cached": 5, "failed": 0, "skipped": 0, "total": 5 }` |
174
+ | `warmup:complete` | warmup | `{ "status": "ready" }` |
175
+ | `render:started` | render | `{ "status": "rendering" }` |
176
+ | `keepalive` | both | `{ "phase": "warmup" }` or `{ "phase": "render" }` |
177
+ | `render:complete` | render | `{ "status": "uploaded", "timings": {...} }` (upload mode) |
178
+ | `ready` | - | `{ "videoUrl": "..." }` (non-upload mode) |
179
+ | `complete` | - | `{ "status": "done" }` (upload mode) |
180
+ | `error` | any | `{ "phase": "warmup"\|"render", "message": "..." }` |
188
181
 
189
- **With upload** — Streams progress via Server-Sent Events (SSE) while uploading:
182
+ **Without upload** — The `ready` event provides a `videoUrl` pointing to `/render/:id/video`. The actual rendering happens when you fetch that URL:
190
183
 
191
- | Event | Data |
192
- | ----------- | ---------------------------------------------------------- |
193
- | `started` | `{ "status": "rendering" }` |
194
- | `keepalive` | `{ "status": "rendering" }` or `{ "status": "uploading" }` |
195
- | `complete` | `{ "status": "uploaded", "timings": {...} }` |
196
- | `error` | `{ "message": "..." }` |
184
+ ```typescript
185
+ const events = new EventSource(progressUrl);
186
+ events.addEventListener("ready", (e) => {
187
+ const { videoUrl } = JSON.parse(e.data);
188
+ // Fetch videoUrl to stream the rendered video
189
+ events.close();
190
+ });
191
+ ```
197
192
 
198
- **Example:**
193
+ **With upload** — Uploads directly and streams progress:
199
194
 
200
195
  ```typescript
201
- // Create render job
202
- const { url } = await fetch("/render", {
203
- method: "POST",
204
- headers: { "Content-Type": "application/json" },
205
- body: JSON.stringify({ effie: effieData, scale: 0.5 }),
206
- }).then((r) => r.json());
196
+ const events = new EventSource(progressUrl);
197
+ events.addEventListener("render:complete", (e) => {
198
+ const { timings } = JSON.parse(e.data);
199
+ console.log("Uploaded!", timings);
200
+ });
201
+ events.addEventListener("complete", () => {
202
+ events.close();
203
+ });
204
+ ```
207
205
 
208
- // Stream video directly
209
- const videoResponse = await fetch(url);
210
- const videoBlob = await videoResponse.blob();
206
+ ### `GET /render/:id/video`
207
+
208
+ Streams the rendered MP4 video (non-upload mode only). Returns 404 until the warmup phase completes and the video sub-job is created.
209
+
210
+ ```bash
211
+ curl http://localhost:2000/render/550e8400-.../video -o output.mp4
211
212
  ```
212
213
 
213
214
  ### `POST /warmup`
214
215
 
215
- Creates a warmup job for pre-fetching and caching the sources from an Effie composition. Use this to avoid render timeouts when sources (especially annies) take a long time to generate.
216
+ Creates a standalone warmup job for pre-fetching and caching the sources from an Effie composition.
216
217
 
217
- **Request:** Same format as `/render` (raw or wrapped EffieData with `effie` field).
218
+ **Request:** Same format as `/render` (wrapped EffieData with `effie` field).
218
219
 
219
220
  **Response:**
220
221
 
221
222
  ```json
222
223
  {
223
224
  "id": "550e8400-e29b-41d4-a716-446655440000",
224
- "url": "http://localhost:2000/warmup/550e8400-e29b-41d4-a716-446655440000"
225
+ "progressUrl": "http://localhost:2000/warmup/550e8400-e29b-41d4-a716-446655440000/progress"
225
226
  }
226
227
  ```
227
228
 
228
- ### `GET /warmup/:id`
229
+ ### `GET /warmup/:id/progress`
229
230
 
230
231
  Runs the cache warmup job and streams the progress via Server-Sent Events (SSE). Connect with `EventSource` for real-time updates.
231
232
 
@@ -244,14 +245,14 @@ Runs the cache warmup job and streams the progress via Server-Sent Events (SSE).
244
245
 
245
246
  ```typescript
246
247
  // Create warmup job
247
- const { url } = await fetch("/warmup", {
248
+ const { progressUrl } = await fetch("/warmup", {
248
249
  method: "POST",
249
250
  headers: { "Content-Type": "application/json" },
250
251
  body: JSON.stringify({ effie: effieData }),
251
252
  }).then((r) => r.json());
252
253
 
253
- // Stream progress (url is a full URL)
254
- const events = new EventSource(url);
254
+ // Stream progress
255
+ const events = new EventSource(progressUrl);
255
256
  events.addEventListener("complete", () => {
256
257
  events.close();
257
258
  // Now safe to call /render
@@ -262,7 +263,7 @@ events.addEventListener("complete", () => {
262
263
 
263
264
  Purges cached sources for a given Effie composition.
264
265
 
265
- **Request:** Same format as `/render` (raw or wrapped EffieData with `effie` field).
266
+ **Request:** Same format as `/render` (wrapped EffieData with `effie` field).
266
267
 
267
268
  **Response:**
268
269
 
@@ -270,73 +271,6 @@ Purges cached sources for a given Effie composition.
270
271
  { "purged": 3, "total": 5 }
271
272
  ```
272
273
 
273
- ### `POST /warmup-and-render`
274
-
275
- Creates a combined warmup and render job that runs both phases in a single SSE stream. This is useful when you want to warmup sources and render in one request.
276
-
277
- **Request:**
278
-
279
- ```typescript
280
- type WarmupAndRenderOptions = {
281
- effie: EffieData | string; // EffieData object or URL to fetch from
282
- scale?: number; // Scale factor (default: 1)
283
- upload?: {
284
- videoUrl: string; // Pre-signed URL to upload rendered video
285
- coverUrl?: string; // Pre-signed URL to upload cover image
286
- };
287
- };
288
- ```
289
-
290
- **Response:**
291
-
292
- ```json
293
- {
294
- "id": "550e8400-e29b-41d4-a716-446655440000",
295
- "url": "http://localhost:2000/warmup-and-render/550e8400-e29b-41d4-a716-446655440000"
296
- }
297
- ```
298
-
299
- ### `GET /warmup-and-render/:id`
300
-
301
- Executes the combined warmup and render job, streaming progress via SSE. All events are prefixed with `warmup:` or `render:` to indicate the phase.
302
-
303
- **Events:**
304
-
305
- | Event | Phase | Data |
306
- | -------------------- | ------ | ----------------------------------------------------------------- |
307
- | `warmup:start` | warmup | `{ "total": 5 }` |
308
- | `warmup:progress` | warmup | `{ "url": "...", "status": "hit"\|"cached"\|"error", ... }` |
309
- | `warmup:downloading` | warmup | `{ "url": "...", "status": "downloading", "bytesReceived": ... }` |
310
- | `warmup:summary` | warmup | `{ "cached": 5, "failed": 0, "skipped": 0, "total": 5 }` |
311
- | `warmup:complete` | warmup | `{ "status": "ready" }` |
312
- | `render:started` | render | `{ "status": "rendering" }` |
313
- | `keepalive` | both | `{ "phase": "warmup" }` or `{ "phase": "render" }` |
314
- | `render:complete` | render | `{ "status": "uploaded", "timings": {...} }` (upload mode) |
315
- | `complete` | - | `{ "status": "ready", "videoUrl": "..." }` (non-upload mode) |
316
- | `error` | any | `{ "phase": "warmup"\|"render", "message": "..." }` |
317
-
318
- **Without upload** — Returns a `videoUrl` pointing to `/render/:id` for streaming:
319
-
320
- ```typescript
321
- const events = new EventSource(url);
322
- events.addEventListener("complete", (e) => {
323
- const { videoUrl } = JSON.parse(e.data);
324
- // Fetch videoUrl to stream the rendered video
325
- events.close();
326
- });
327
- ```
328
-
329
- **With upload** — Uploads directly and streams progress:
330
-
331
- ```typescript
332
- const events = new EventSource(url);
333
- events.addEventListener("render:complete", (e) => {
334
- const { timings } = JSON.parse(e.data);
335
- console.log("Uploaded!", timings);
336
- events.close();
337
- });
338
- ```
339
-
340
274
  ## Backend Separation
341
275
 
342
276
  FFS supports running warmup and render on separate backends via resolver callbacks.
@@ -369,8 +303,9 @@ const ctx = await createServerContext({
369
303
  });
370
304
  ```
371
305
 
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.
306
+ The `warmupBackendResolver` determines where warmup _work_ happens — used by `/warmup/:id/progress` and the warmup phase within `/render/:id/progress`. The `renderBackendResolver` determines where video rendering _work_ happens — used by `/render/:id/video` and the render+upload phase in upload mode.
307
+
308
+ Both resolvers receive optional metadata (passed via handler options). Return `null` to handle locally.
374
309
 
375
310
  ### Job metadata
376
311
 
@@ -429,33 +364,36 @@ const finalStream = await joinRenderer.render();
429
364
 
430
365
  ```typescript
431
366
  // Create render job
432
- const { url } = await fetch("http://localhost:2000/render", {
367
+ const { progressUrl } = await fetch("http://localhost:2000/render", {
433
368
  method: "POST",
434
369
  headers: { "Content-Type": "application/json" },
435
370
  body: JSON.stringify({ effie: effieData, scale: 0.5 }),
436
371
  }).then((r) => r.json());
437
372
 
438
- // Stream the video
439
- const video = await fetch(url).then((r) => r.blob());
373
+ // Connect to SSE progress
374
+ const events = new EventSource(progressUrl);
375
+ events.addEventListener("ready", (e) => {
376
+ const { videoUrl } = JSON.parse(e.data);
377
+ // Fetch the video (rendering happens on-demand)
378
+ const video = await fetch(videoUrl).then((r) => r.blob());
379
+ events.close();
380
+ });
440
381
  ```
441
382
 
442
- **Fetch from URL and render:**
383
+ **Render with cache purge:**
443
384
 
444
385
  ```typescript
445
- const { url } = await fetch("http://localhost:2000/render", {
386
+ const { progressUrl } = await fetch("http://localhost:2000/render", {
446
387
  method: "POST",
447
388
  headers: { "Content-Type": "application/json" },
448
- body: JSON.stringify({
449
- effie: "https://example.com/composition.json",
450
- scale: 0.5,
451
- }),
389
+ body: JSON.stringify({ effie: effieData, scale: 0.5, purge: true }),
452
390
  }).then((r) => r.json());
453
391
  ```
454
392
 
455
393
  **Render and upload to S3 (SSE progress):**
456
394
 
457
395
  ```typescript
458
- const { url } = await fetch("http://localhost:2000/render", {
396
+ const { progressUrl } = await fetch("http://localhost:2000/render", {
459
397
  method: "POST",
460
398
  headers: { "Content-Type": "application/json" },
461
399
  body: JSON.stringify({
@@ -468,38 +406,9 @@ const { url } = await fetch("http://localhost:2000/render", {
468
406
  }).then((r) => r.json());
469
407
 
470
408
  // Connect to SSE for progress
471
- const events = new EventSource(url);
472
- events.addEventListener("complete", (e) => {
473
- const { timings } = JSON.parse(e.data);
474
- console.log("Uploaded!", timings);
475
- events.close();
476
- });
477
- ```
478
-
479
- **Warmup and render in one stream:**
480
-
481
- ```typescript
482
- const { url } = await fetch("http://localhost:2000/warmup-and-render", {
483
- method: "POST",
484
- headers: { "Content-Type": "application/json" },
485
- body: JSON.stringify({ effie: effieData, scale: 0.5 }),
486
- }).then((r) => r.json());
487
-
488
- // Connect to SSE for combined progress
489
- const events = new EventSource(url);
490
-
491
- events.addEventListener("warmup:progress", (e) => {
492
- const { url, status, cached, total } = JSON.parse(e.data);
493
- console.log(`Warmup: ${cached}/${total} - ${url} ${status}`);
494
- });
495
-
496
- events.addEventListener("render:started", () => {
497
- console.log("Rendering started...");
498
- });
499
-
500
- events.addEventListener("complete", (e) => {
501
- const { videoUrl } = JSON.parse(e.data);
502
- console.log("Ready! Video at:", videoUrl);
409
+ const events = new EventSource(progressUrl);
410
+ events.addEventListener("complete", () => {
411
+ console.log("Done!");
503
412
  events.close();
504
413
  });
505
414
  ```
@@ -15,14 +15,12 @@ import { pipeline } from "stream/promises";
15
15
  import path from "path";
16
16
  import os from "os";
17
17
  import crypto from "crypto";
18
- var DEFAULT_SOURCE_TTL_MS = 60 * 60 * 1e3;
19
- var DEFAULT_JOB_DATA_TTL_MS = 8 * 60 * 60 * 1e3;
18
+ var DEFAULT_TTL_MS = 60 * 60 * 1e3;
20
19
  var S3TransientStore = class {
21
20
  client;
22
21
  bucket;
23
22
  prefix;
24
- sourceTtlMs;
25
- jobDataTtlMs;
23
+ ttlMs;
26
24
  constructor(options) {
27
25
  this.client = new S3Client({
28
26
  endpoint: options.endpoint,
@@ -35,8 +33,7 @@ var S3TransientStore = class {
35
33
  });
36
34
  this.bucket = options.bucket;
37
35
  this.prefix = options.prefix ?? "";
38
- this.sourceTtlMs = options.sourceTtlMs ?? DEFAULT_SOURCE_TTL_MS;
39
- this.jobDataTtlMs = options.jobDataTtlMs ?? DEFAULT_JOB_DATA_TTL_MS;
36
+ this.ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
40
37
  }
41
38
  getExpires(ttlMs) {
42
39
  return new Date(Date.now() + ttlMs);
@@ -51,7 +48,7 @@ var S3TransientStore = class {
51
48
  Bucket: this.bucket,
52
49
  Key: this.getFullKey(key),
53
50
  Body: stream,
54
- Expires: this.getExpires(ttlMs ?? this.sourceTtlMs)
51
+ Expires: this.getExpires(ttlMs ?? this.ttlMs)
55
52
  }
56
53
  });
57
54
  await upload.done();
@@ -119,7 +116,7 @@ var S3TransientStore = class {
119
116
  Key: this.getFullKey(key),
120
117
  Body: JSON.stringify(data),
121
118
  ContentType: "application/json",
122
- Expires: this.getExpires(ttlMs ?? this.jobDataTtlMs)
119
+ Expires: this.getExpires(ttlMs ?? this.ttlMs)
123
120
  })
124
121
  );
125
122
  }
@@ -149,15 +146,10 @@ var LocalTransientStore = class {
149
146
  baseDir;
150
147
  initialized = false;
151
148
  cleanupInterval;
152
- sourceTtlMs;
153
- jobDataTtlMs;
154
- /** For cleanup, use the longer of the two TTLs */
155
- maxTtlMs;
149
+ ttlMs;
156
150
  constructor(options) {
157
151
  this.baseDir = options?.baseDir ?? path.join(os.tmpdir(), "ffs-transient");
158
- this.sourceTtlMs = options?.sourceTtlMs ?? DEFAULT_SOURCE_TTL_MS;
159
- this.jobDataTtlMs = options?.jobDataTtlMs ?? DEFAULT_JOB_DATA_TTL_MS;
160
- this.maxTtlMs = Math.max(this.sourceTtlMs, this.jobDataTtlMs);
152
+ this.ttlMs = options?.ttlMs ?? DEFAULT_TTL_MS;
161
153
  this.cleanupInterval = setInterval(() => {
162
154
  this.cleanupExpired().catch(console.error);
163
155
  }, 3e5);
@@ -188,7 +180,7 @@ var LocalTransientStore = class {
188
180
  } else if (entry.isFile()) {
189
181
  try {
190
182
  const stat = await fs.stat(fullPath);
191
- if (now - stat.mtimeMs > this.maxTtlMs) {
183
+ if (now - stat.mtimeMs > this.ttlMs) {
192
184
  await fs.rm(fullPath, { force: true });
193
185
  }
194
186
  } catch {
@@ -272,8 +264,7 @@ var LocalTransientStore = class {
272
264
  }
273
265
  };
274
266
  function createTransientStore() {
275
- const sourceTtlMs = process.env.FFS_SOURCE_CACHE_TTL_MS ? parseInt(process.env.FFS_SOURCE_CACHE_TTL_MS, 10) : DEFAULT_SOURCE_TTL_MS;
276
- const jobDataTtlMs = process.env.FFS_JOB_DATA_TTL_MS ? parseInt(process.env.FFS_JOB_DATA_TTL_MS, 10) : DEFAULT_JOB_DATA_TTL_MS;
267
+ const ttlMs = process.env.FFS_TRANSIENT_STORE_TTL_MS ? parseInt(process.env.FFS_TRANSIENT_STORE_TTL_MS, 10) : DEFAULT_TTL_MS;
277
268
  if (process.env.FFS_TRANSIENT_STORE_BUCKET) {
278
269
  return new S3TransientStore({
279
270
  endpoint: process.env.FFS_TRANSIENT_STORE_ENDPOINT,
@@ -282,14 +273,12 @@ function createTransientStore() {
282
273
  prefix: process.env.FFS_TRANSIENT_STORE_PREFIX,
283
274
  accessKeyId: process.env.FFS_TRANSIENT_STORE_ACCESS_KEY,
284
275
  secretAccessKey: process.env.FFS_TRANSIENT_STORE_SECRET_KEY,
285
- sourceTtlMs,
286
- jobDataTtlMs
276
+ ttlMs
287
277
  });
288
278
  }
289
279
  return new LocalTransientStore({
290
280
  baseDir: process.env.FFS_TRANSIENT_STORE_LOCAL_DIR,
291
- sourceTtlMs,
292
- jobDataTtlMs
281
+ ttlMs
293
282
  });
294
283
  }
295
284
  function hashUrl(url) {
@@ -304,14 +293,14 @@ function warmupJobStoreKey(jobId) {
304
293
  function renderJobStoreKey(jobId) {
305
294
  return `jobs/render/${jobId}.json`;
306
295
  }
307
- function warmupAndRenderJobStoreKey(jobId) {
308
- return `jobs/warmup-and-render/${jobId}.json`;
296
+ function videoJobStoreKey(jobId) {
297
+ return `jobs/video/${jobId}.json`;
309
298
  }
310
299
  var storeKeys = {
311
300
  source: sourceStoreKey,
312
301
  warmupJob: warmupJobStoreKey,
313
302
  renderJob: renderJobStoreKey,
314
- warmupAndRenderJob: warmupAndRenderJobStoreKey
303
+ videoJob: videoJobStoreKey
315
304
  };
316
305
 
317
306
  // src/fetch.ts
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  ffsFetch,
3
3
  storeKeys
4
- } from "./chunk-4N2GLGC5.js";
4
+ } from "./chunk-UN34ESVZ.js";
5
5
 
6
6
  // src/render.ts
7
7
  import { Readable } from "stream";
@@ -936,4 +936,4 @@ export {
936
936
  FFmpegRunner,
937
937
  EffieRenderer
938
938
  };
939
- //# sourceMappingURL=chunk-O7Z6DV2I.js.map
939
+ //# sourceMappingURL=chunk-CSKH34HX.js.map
@@ -34,14 +34,12 @@ import { pipeline } from "stream/promises";
34
34
  import path from "path";
35
35
  import os from "os";
36
36
  import crypto from "crypto";
37
- var DEFAULT_SOURCE_TTL_MS = 60 * 60 * 1e3;
38
- var DEFAULT_JOB_DATA_TTL_MS = 8 * 60 * 60 * 1e3;
37
+ var DEFAULT_TTL_MS = 60 * 60 * 1e3;
39
38
  var S3TransientStore = class {
40
39
  client;
41
40
  bucket;
42
41
  prefix;
43
- sourceTtlMs;
44
- jobDataTtlMs;
42
+ ttlMs;
45
43
  constructor(options) {
46
44
  this.client = new S3Client({
47
45
  endpoint: options.endpoint,
@@ -54,8 +52,7 @@ var S3TransientStore = class {
54
52
  });
55
53
  this.bucket = options.bucket;
56
54
  this.prefix = options.prefix ?? "";
57
- this.sourceTtlMs = options.sourceTtlMs ?? DEFAULT_SOURCE_TTL_MS;
58
- this.jobDataTtlMs = options.jobDataTtlMs ?? DEFAULT_JOB_DATA_TTL_MS;
55
+ this.ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
59
56
  }
60
57
  getExpires(ttlMs) {
61
58
  return new Date(Date.now() + ttlMs);
@@ -70,7 +67,7 @@ var S3TransientStore = class {
70
67
  Bucket: this.bucket,
71
68
  Key: this.getFullKey(key),
72
69
  Body: stream,
73
- Expires: this.getExpires(ttlMs ?? this.sourceTtlMs)
70
+ Expires: this.getExpires(ttlMs ?? this.ttlMs)
74
71
  }
75
72
  });
76
73
  await upload.done();
@@ -138,7 +135,7 @@ var S3TransientStore = class {
138
135
  Key: this.getFullKey(key),
139
136
  Body: JSON.stringify(data),
140
137
  ContentType: "application/json",
141
- Expires: this.getExpires(ttlMs ?? this.jobDataTtlMs)
138
+ Expires: this.getExpires(ttlMs ?? this.ttlMs)
142
139
  })
143
140
  );
144
141
  }
@@ -168,15 +165,10 @@ var LocalTransientStore = class {
168
165
  baseDir;
169
166
  initialized = false;
170
167
  cleanupInterval;
171
- sourceTtlMs;
172
- jobDataTtlMs;
173
- /** For cleanup, use the longer of the two TTLs */
174
- maxTtlMs;
168
+ ttlMs;
175
169
  constructor(options) {
176
170
  this.baseDir = options?.baseDir ?? path.join(os.tmpdir(), "ffs-transient");
177
- this.sourceTtlMs = options?.sourceTtlMs ?? DEFAULT_SOURCE_TTL_MS;
178
- this.jobDataTtlMs = options?.jobDataTtlMs ?? DEFAULT_JOB_DATA_TTL_MS;
179
- this.maxTtlMs = Math.max(this.sourceTtlMs, this.jobDataTtlMs);
171
+ this.ttlMs = options?.ttlMs ?? DEFAULT_TTL_MS;
180
172
  this.cleanupInterval = setInterval(() => {
181
173
  this.cleanupExpired().catch(console.error);
182
174
  }, 3e5);
@@ -207,7 +199,7 @@ var LocalTransientStore = class {
207
199
  } else if (entry.isFile()) {
208
200
  try {
209
201
  const stat = await fs.stat(fullPath);
210
- if (now - stat.mtimeMs > this.maxTtlMs) {
202
+ if (now - stat.mtimeMs > this.ttlMs) {
211
203
  await fs.rm(fullPath, { force: true });
212
204
  }
213
205
  } catch {
@@ -291,8 +283,7 @@ var LocalTransientStore = class {
291
283
  }
292
284
  };
293
285
  function createTransientStore() {
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 jobDataTtlMs = process.env.FFS_JOB_DATA_TTL_MS ? parseInt(process.env.FFS_JOB_DATA_TTL_MS, 10) : DEFAULT_JOB_DATA_TTL_MS;
286
+ const ttlMs = process.env.FFS_TRANSIENT_STORE_TTL_MS ? parseInt(process.env.FFS_TRANSIENT_STORE_TTL_MS, 10) : DEFAULT_TTL_MS;
296
287
  if (process.env.FFS_TRANSIENT_STORE_BUCKET) {
297
288
  return new S3TransientStore({
298
289
  endpoint: process.env.FFS_TRANSIENT_STORE_ENDPOINT,
@@ -301,14 +292,12 @@ function createTransientStore() {
301
292
  prefix: process.env.FFS_TRANSIENT_STORE_PREFIX,
302
293
  accessKeyId: process.env.FFS_TRANSIENT_STORE_ACCESS_KEY,
303
294
  secretAccessKey: process.env.FFS_TRANSIENT_STORE_SECRET_KEY,
304
- sourceTtlMs,
305
- jobDataTtlMs
295
+ ttlMs
306
296
  });
307
297
  }
308
298
  return new LocalTransientStore({
309
299
  baseDir: process.env.FFS_TRANSIENT_STORE_LOCAL_DIR,
310
- sourceTtlMs,
311
- jobDataTtlMs
300
+ ttlMs
312
301
  });
313
302
  }
314
303
  function hashUrl(url) {
@@ -323,14 +312,14 @@ function warmupJobStoreKey(jobId) {
323
312
  function renderJobStoreKey(jobId) {
324
313
  return `jobs/render/${jobId}.json`;
325
314
  }
326
- function warmupAndRenderJobStoreKey(jobId) {
327
- return `jobs/warmup-and-render/${jobId}.json`;
315
+ function videoJobStoreKey(jobId) {
316
+ return `jobs/video/${jobId}.json`;
328
317
  }
329
318
  var storeKeys = {
330
319
  source: sourceStoreKey,
331
320
  warmupJob: warmupJobStoreKey,
332
321
  renderJob: renderJobStoreKey,
333
- warmupAndRenderJob: warmupAndRenderJobStoreKey
322
+ videoJob: videoJobStoreKey
334
323
  };
335
324
 
336
325
  export {
@@ -338,4 +327,4 @@ export {
338
327
  createTransientStore,
339
328
  storeKeys
340
329
  };
341
- //# sourceMappingURL=chunk-4N2GLGC5.js.map
330
+ //# sourceMappingURL=chunk-UN34ESVZ.js.map