@effing/create 0.13.1 → 0.14.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/package.json +1 -1
- package/template/app/root.tsx +0 -7
- package/template/app/routes/pff.$effieId.tsx +131 -110
- package/template/package.json +8 -8
- package/template/public/sw.js +0 -48
package/package.json
CHANGED
package/template/app/root.tsx
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { useEffect } from "react";
|
|
2
1
|
import type { MetaFunction } from "react-router";
|
|
3
2
|
import { Links, Meta, Outlet, Scripts, ScrollRestoration } from "react-router";
|
|
4
3
|
|
|
@@ -8,12 +7,6 @@ export const meta: MetaFunction = () => [
|
|
|
8
7
|
];
|
|
9
8
|
|
|
10
9
|
export default function App() {
|
|
11
|
-
useEffect(() => {
|
|
12
|
-
if ("serviceWorker" in navigator) {
|
|
13
|
-
navigator.serviceWorker.register("/sw.js");
|
|
14
|
-
}
|
|
15
|
-
}, []);
|
|
16
|
-
|
|
17
10
|
return (
|
|
18
11
|
<html lang="en">
|
|
19
12
|
<head>
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
useNavigation,
|
|
6
6
|
type ShouldRevalidateFunctionArgs,
|
|
7
7
|
} from "react-router";
|
|
8
|
-
import { useEffect, useState } from "react";
|
|
8
|
+
import { useEffect, useReducer, useState } from "react";
|
|
9
9
|
import invariant from "tiny-invariant";
|
|
10
10
|
import { serialize } from "@effing/serde";
|
|
11
11
|
import {
|
|
@@ -40,39 +40,60 @@ const RENDER_SCALES = [
|
|
|
40
40
|
|
|
41
41
|
// ============ Types ============
|
|
42
42
|
|
|
43
|
-
type
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
type
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
43
|
+
type ActionResult =
|
|
44
|
+
| {
|
|
45
|
+
intent: "render";
|
|
46
|
+
success: true;
|
|
47
|
+
progressUrl: string;
|
|
48
|
+
renderScale: number;
|
|
49
|
+
}
|
|
50
|
+
| {
|
|
51
|
+
intent: "render";
|
|
52
|
+
success: false;
|
|
53
|
+
error: string;
|
|
54
|
+
issues?: EffieValidationIssue[];
|
|
55
|
+
}
|
|
56
|
+
| { intent: "reload"; success: true; purged: number; total: number }
|
|
57
|
+
| { intent: "reload"; success: false; error: string };
|
|
58
|
+
|
|
59
|
+
type RenderState =
|
|
60
|
+
| { step: "idle" }
|
|
61
|
+
| { step: "started"; startedAt: number }
|
|
62
|
+
| { step: "ready"; startedAt: number; videoUrl: string; scale: number }
|
|
63
|
+
| {
|
|
64
|
+
step: "done";
|
|
65
|
+
startedAt: number;
|
|
66
|
+
videoUrl: string;
|
|
67
|
+
scale: number;
|
|
68
|
+
playbackAt: number;
|
|
69
|
+
};
|
|
64
70
|
|
|
65
|
-
|
|
66
|
-
|
|
71
|
+
type RenderAction =
|
|
72
|
+
| { type: "start" }
|
|
73
|
+
| { type: "ready"; videoUrl: string; scale: number }
|
|
74
|
+
| { type: "play" }
|
|
75
|
+
| { type: "error" };
|
|
76
|
+
|
|
77
|
+
function renderReducer(state: RenderState, action: RenderAction): RenderState {
|
|
78
|
+
switch (action.type) {
|
|
79
|
+
case "start":
|
|
80
|
+
return { step: "started", startedAt: Date.now() };
|
|
81
|
+
case "ready":
|
|
82
|
+
if (state.step !== "started") return state;
|
|
83
|
+
return {
|
|
84
|
+
step: "ready",
|
|
85
|
+
startedAt: state.startedAt,
|
|
86
|
+
videoUrl: action.videoUrl,
|
|
87
|
+
scale: action.scale,
|
|
88
|
+
};
|
|
89
|
+
case "play":
|
|
90
|
+
if (state.step !== "ready") return state;
|
|
91
|
+
return { ...state, step: "done", playbackAt: Date.now() };
|
|
92
|
+
case "error":
|
|
93
|
+
return { step: "idle" };
|
|
94
|
+
}
|
|
67
95
|
}
|
|
68
96
|
|
|
69
|
-
type RenderState = {
|
|
70
|
-
videoUrl: string | null;
|
|
71
|
-
startTime: number | null;
|
|
72
|
-
playbackTime: number | null;
|
|
73
|
-
scale: number | null;
|
|
74
|
-
};
|
|
75
|
-
|
|
76
97
|
// ============ Loader ============
|
|
77
98
|
|
|
78
99
|
export async function loader({ request, params }: Route.LoaderArgs) {
|
|
@@ -80,9 +101,11 @@ export async function loader({ request, params }: Route.LoaderArgs) {
|
|
|
80
101
|
const width = parseInt(requestUrl.searchParams.get("w") || "1080", 10);
|
|
81
102
|
const height = parseInt(requestUrl.searchParams.get("h") || "1080", 10);
|
|
82
103
|
|
|
83
|
-
const {
|
|
84
|
-
|
|
85
|
-
|
|
104
|
+
const {
|
|
105
|
+
previewProps,
|
|
106
|
+
renderer: generateEffie,
|
|
107
|
+
propsSchema,
|
|
108
|
+
} = await getEffie(params.effieId);
|
|
86
109
|
|
|
87
110
|
if (propsSchema) {
|
|
88
111
|
invariant(
|
|
@@ -96,10 +119,10 @@ export async function loader({ request, params }: Route.LoaderArgs) {
|
|
|
96
119
|
process.env.SECRET_KEY!,
|
|
97
120
|
);
|
|
98
121
|
|
|
99
|
-
const effie = await
|
|
122
|
+
const effie = await generateEffie({ props: previewProps, width, height });
|
|
100
123
|
|
|
101
124
|
// Create warmup job if FFS is configured
|
|
102
|
-
let
|
|
125
|
+
let warmupUrl: string | null = null;
|
|
103
126
|
if (process.env.FFS_BASE_URL && process.env.FFS_API_KEY) {
|
|
104
127
|
try {
|
|
105
128
|
const warmupResponse = await fetch(`${process.env.FFS_BASE_URL}/warmup`, {
|
|
@@ -113,7 +136,7 @@ export async function loader({ request, params }: Route.LoaderArgs) {
|
|
|
113
136
|
|
|
114
137
|
if (warmupResponse.ok) {
|
|
115
138
|
const warmupData = await warmupResponse.json();
|
|
116
|
-
|
|
139
|
+
warmupUrl = warmupData.progressUrl;
|
|
117
140
|
}
|
|
118
141
|
} catch {
|
|
119
142
|
// Warmup is best-effort, don't fail the page load
|
|
@@ -126,7 +149,7 @@ export async function loader({ request, params }: Route.LoaderArgs) {
|
|
|
126
149
|
height,
|
|
127
150
|
effie,
|
|
128
151
|
jsonUrl: `/ff/${urlSegment}?w=${width}&h=${height}`,
|
|
129
|
-
|
|
152
|
+
warmupUrl,
|
|
130
153
|
};
|
|
131
154
|
}
|
|
132
155
|
|
|
@@ -145,9 +168,9 @@ export function shouldRevalidate({
|
|
|
145
168
|
|
|
146
169
|
// ============ Action ============
|
|
147
170
|
|
|
148
|
-
async function handleReload(effieJson: string): Promise<
|
|
171
|
+
async function handleReload(effieJson: string): Promise<ActionResult> {
|
|
149
172
|
if (!process.env.FFS_BASE_URL || !process.env.FFS_API_KEY) {
|
|
150
|
-
return {
|
|
173
|
+
return { intent: "reload", success: false, error: "FFS not configured" };
|
|
151
174
|
}
|
|
152
175
|
|
|
153
176
|
try {
|
|
@@ -161,18 +184,24 @@ async function handleReload(effieJson: string): Promise<ReloadResult> {
|
|
|
161
184
|
});
|
|
162
185
|
|
|
163
186
|
if (!purgeResponse.ok) {
|
|
164
|
-
return {
|
|
187
|
+
return {
|
|
188
|
+
intent: "reload",
|
|
189
|
+
success: false,
|
|
190
|
+
error: "Failed to purge cache",
|
|
191
|
+
};
|
|
165
192
|
}
|
|
166
193
|
|
|
167
194
|
const purgeData = await purgeResponse.json();
|
|
168
195
|
return {
|
|
169
|
-
|
|
196
|
+
intent: "reload",
|
|
197
|
+
success: true,
|
|
170
198
|
purged: purgeData.purged,
|
|
171
199
|
total: purgeData.total,
|
|
172
200
|
};
|
|
173
201
|
} catch (err) {
|
|
174
202
|
return {
|
|
175
|
-
|
|
203
|
+
intent: "reload",
|
|
204
|
+
success: false,
|
|
176
205
|
error: err instanceof Error ? err.message : "Unknown error",
|
|
177
206
|
};
|
|
178
207
|
}
|
|
@@ -181,9 +210,9 @@ async function handleReload(effieJson: string): Promise<ReloadResult> {
|
|
|
181
210
|
async function handleRender(
|
|
182
211
|
effieJson: string,
|
|
183
212
|
scale: number,
|
|
184
|
-
): Promise<
|
|
213
|
+
): Promise<ActionResult> {
|
|
185
214
|
if (!process.env.FFS_BASE_URL || !process.env.FFS_API_KEY) {
|
|
186
|
-
return {
|
|
215
|
+
return { intent: "render", success: false, error: "FFS not configured" };
|
|
187
216
|
}
|
|
188
217
|
|
|
189
218
|
try {
|
|
@@ -203,20 +232,26 @@ async function handleRender(
|
|
|
203
232
|
try {
|
|
204
233
|
const errorBody = await createResponse.json();
|
|
205
234
|
return {
|
|
206
|
-
|
|
235
|
+
intent: "render",
|
|
236
|
+
success: false,
|
|
207
237
|
error: errorBody.error || createResponse.statusText,
|
|
208
238
|
issues: parseEffieValidationIssues(errorBody.issues),
|
|
209
239
|
};
|
|
210
240
|
} catch {
|
|
211
|
-
return {
|
|
241
|
+
return {
|
|
242
|
+
intent: "render",
|
|
243
|
+
success: false,
|
|
244
|
+
error: createResponse.statusText,
|
|
245
|
+
};
|
|
212
246
|
}
|
|
213
247
|
}
|
|
214
248
|
|
|
215
249
|
const { progressUrl } = await createResponse.json();
|
|
216
|
-
return {
|
|
250
|
+
return { intent: "render", success: true, renderScale: scale, progressUrl };
|
|
217
251
|
} catch (err) {
|
|
218
252
|
return {
|
|
219
|
-
|
|
253
|
+
intent: "render",
|
|
254
|
+
success: false,
|
|
220
255
|
error: err instanceof Error ? err.message : "Unknown error",
|
|
221
256
|
};
|
|
222
257
|
}
|
|
@@ -230,7 +265,7 @@ export async function action({
|
|
|
230
265
|
const effieJson = formData.get("effie")?.toString();
|
|
231
266
|
|
|
232
267
|
if (!effieJson) {
|
|
233
|
-
return {
|
|
268
|
+
return { intent: "render", success: false, error: "Missing effie data" };
|
|
234
269
|
}
|
|
235
270
|
|
|
236
271
|
if (intent === "reload") {
|
|
@@ -244,39 +279,35 @@ export async function action({
|
|
|
244
279
|
// ============ Component ============
|
|
245
280
|
|
|
246
281
|
export default function EffiePreviewPage() {
|
|
247
|
-
const { effie, jsonUrl, effieId, width, height,
|
|
282
|
+
const { effie, jsonUrl, effieId, width, height, warmupUrl } =
|
|
248
283
|
useLoaderData<typeof loader>();
|
|
249
284
|
const actionData = useActionData<typeof action>();
|
|
250
285
|
const navigation = useNavigation();
|
|
251
286
|
|
|
252
|
-
const [render,
|
|
253
|
-
videoUrl: null,
|
|
254
|
-
startTime: null,
|
|
255
|
-
playbackTime: null,
|
|
256
|
-
scale: null,
|
|
257
|
-
});
|
|
287
|
+
const [render, dispatch] = useReducer(renderReducer, { step: "idle" });
|
|
258
288
|
const [elapsedToPlay, setElapsedToPlay] = useState<number | null>(null);
|
|
259
289
|
|
|
260
290
|
// Update elapsed time while rendering is in progress
|
|
261
291
|
useEffect(() => {
|
|
262
|
-
if (render.
|
|
292
|
+
if (render.step === "idle") {
|
|
263
293
|
setElapsedToPlay(null);
|
|
264
294
|
return;
|
|
265
295
|
}
|
|
266
|
-
if (render.
|
|
267
|
-
setElapsedToPlay((render.
|
|
296
|
+
if (render.step === "done") {
|
|
297
|
+
setElapsedToPlay((render.playbackAt - render.startedAt) / 1000);
|
|
268
298
|
return;
|
|
269
299
|
}
|
|
270
300
|
const update = () =>
|
|
271
|
-
setElapsedToPlay((Date.now() - render.
|
|
301
|
+
setElapsedToPlay((Date.now() - render.startedAt) / 1000);
|
|
272
302
|
update();
|
|
273
303
|
const interval = setInterval(update, 100);
|
|
274
304
|
return () => clearInterval(interval);
|
|
275
|
-
}, [render
|
|
305
|
+
}, [render]);
|
|
276
306
|
|
|
277
307
|
// Connect to SSE progress when render action completes
|
|
278
308
|
useEffect(() => {
|
|
279
|
-
if (!actionData || !
|
|
309
|
+
if (!actionData || actionData.intent !== "render" || !actionData.success)
|
|
310
|
+
return;
|
|
280
311
|
|
|
281
312
|
const { progressUrl, renderScale } = actionData;
|
|
282
313
|
const eventSource = new EventSource(progressUrl);
|
|
@@ -284,11 +315,7 @@ export default function EffiePreviewPage() {
|
|
|
284
315
|
eventSource.addEventListener("ready", (e) => {
|
|
285
316
|
try {
|
|
286
317
|
const { videoUrl } = JSON.parse(e.data);
|
|
287
|
-
|
|
288
|
-
...prev,
|
|
289
|
-
videoUrl,
|
|
290
|
-
scale: renderScale,
|
|
291
|
-
}));
|
|
318
|
+
dispatch({ type: "ready", videoUrl, scale: renderScale });
|
|
292
319
|
} catch {
|
|
293
320
|
// Ignore parse errors
|
|
294
321
|
}
|
|
@@ -296,6 +323,7 @@ export default function EffiePreviewPage() {
|
|
|
296
323
|
});
|
|
297
324
|
|
|
298
325
|
eventSource.addEventListener("error", () => {
|
|
326
|
+
dispatch({ type: "error" });
|
|
299
327
|
eventSource.close();
|
|
300
328
|
});
|
|
301
329
|
|
|
@@ -304,7 +332,7 @@ export default function EffiePreviewPage() {
|
|
|
304
332
|
};
|
|
305
333
|
}, [actionData]);
|
|
306
334
|
|
|
307
|
-
const warmup = useEffieWarmup(
|
|
335
|
+
const warmup = useEffieWarmup(warmupUrl);
|
|
308
336
|
const resolveSource = createEffieSourceResolver(effie.sources);
|
|
309
337
|
|
|
310
338
|
// Compute scaled resolution for preview (540px height for cover)
|
|
@@ -325,7 +353,7 @@ export default function EffiePreviewPage() {
|
|
|
325
353
|
const isReloading =
|
|
326
354
|
navigation.state === "submitting" &&
|
|
327
355
|
navigation.formData?.get("intent") === "reload";
|
|
328
|
-
const
|
|
356
|
+
const isReloadProhibited = isLoading || isReloading || warmup.isWarming;
|
|
329
357
|
|
|
330
358
|
const warmupElapsed =
|
|
331
359
|
warmup.state.startTime && warmup.state.status !== "idle"
|
|
@@ -348,21 +376,14 @@ export default function EffiePreviewPage() {
|
|
|
348
376
|
const showProgressBar = warmup.state.status === "warming";
|
|
349
377
|
|
|
350
378
|
const handleVideoPlay = () => {
|
|
351
|
-
|
|
352
|
-
setRender((prev) => ({ ...prev, playbackTime: Date.now() }));
|
|
353
|
-
}
|
|
379
|
+
dispatch({ type: "play" });
|
|
354
380
|
};
|
|
355
381
|
|
|
356
382
|
const handleRenderSubmit = () => {
|
|
357
|
-
|
|
358
|
-
videoUrl: null,
|
|
359
|
-
startTime: Date.now(),
|
|
360
|
-
playbackTime: null,
|
|
361
|
-
scale: null,
|
|
362
|
-
});
|
|
383
|
+
dispatch({ type: "start" });
|
|
363
384
|
};
|
|
364
385
|
|
|
365
|
-
const
|
|
386
|
+
const formatSourceUrl = (url: string, maxLen = 70) => {
|
|
366
387
|
if (url.length <= maxLen) return url;
|
|
367
388
|
const keepStart = Math.max(20, Math.floor(maxLen * 0.6));
|
|
368
389
|
const keepEnd = Math.max(10, maxLen - keepStart - 5);
|
|
@@ -438,7 +459,7 @@ export default function EffiePreviewPage() {
|
|
|
438
459
|
<EffieCoverPreview
|
|
439
460
|
cover={effie.cover}
|
|
440
461
|
resolution={coverResolution}
|
|
441
|
-
video={render.videoUrl}
|
|
462
|
+
video={"videoUrl" in render ? render.videoUrl : null}
|
|
442
463
|
onPlay={handleVideoPlay}
|
|
443
464
|
style={{
|
|
444
465
|
border: "1px solid black",
|
|
@@ -490,32 +511,6 @@ export default function EffiePreviewPage() {
|
|
|
490
511
|
alignItems: "flex-start",
|
|
491
512
|
}}
|
|
492
513
|
>
|
|
493
|
-
<Form method="post">
|
|
494
|
-
<input
|
|
495
|
-
type="hidden"
|
|
496
|
-
name="effie"
|
|
497
|
-
value={JSON.stringify(effie)}
|
|
498
|
-
/>
|
|
499
|
-
<button
|
|
500
|
-
type="submit"
|
|
501
|
-
name="intent"
|
|
502
|
-
value="reload"
|
|
503
|
-
disabled={isReloadPending}
|
|
504
|
-
style={{
|
|
505
|
-
padding: "0.4rem 0.75rem",
|
|
506
|
-
backgroundColor: "#fff",
|
|
507
|
-
color: "#222",
|
|
508
|
-
border: "1px solid black",
|
|
509
|
-
borderRadius: 4,
|
|
510
|
-
fontSize: "14px",
|
|
511
|
-
cursor: isReloadPending ? "wait" : "pointer",
|
|
512
|
-
opacity: isReloadPending ? 0.6 : 1,
|
|
513
|
-
}}
|
|
514
|
-
>
|
|
515
|
-
{isReloadPending ? "Reloading sources..." : "Reload sources"}
|
|
516
|
-
</button>
|
|
517
|
-
</Form>
|
|
518
|
-
|
|
519
514
|
<Form
|
|
520
515
|
method="post"
|
|
521
516
|
style={{ display: "flex", gap: "0.5rem", alignItems: "center" }}
|
|
@@ -563,10 +558,36 @@ export default function EffiePreviewPage() {
|
|
|
563
558
|
))}
|
|
564
559
|
</select>
|
|
565
560
|
</Form>
|
|
561
|
+
|
|
562
|
+
<Form method="post">
|
|
563
|
+
<input
|
|
564
|
+
type="hidden"
|
|
565
|
+
name="effie"
|
|
566
|
+
value={JSON.stringify(effie)}
|
|
567
|
+
/>
|
|
568
|
+
<button
|
|
569
|
+
type="submit"
|
|
570
|
+
name="intent"
|
|
571
|
+
value="reload"
|
|
572
|
+
disabled={isReloadProhibited}
|
|
573
|
+
style={{
|
|
574
|
+
padding: "0.4rem 0.75rem",
|
|
575
|
+
backgroundColor: "#fff",
|
|
576
|
+
color: "#222",
|
|
577
|
+
border: "1px solid black",
|
|
578
|
+
borderRadius: 4,
|
|
579
|
+
fontSize: "14px",
|
|
580
|
+
cursor: isReloadProhibited ? "wait" : "pointer",
|
|
581
|
+
opacity: isReloadProhibited ? 0.6 : 1,
|
|
582
|
+
}}
|
|
583
|
+
>
|
|
584
|
+
{isReloadProhibited ? "Loading sources..." : "Reload sources"}
|
|
585
|
+
</button>
|
|
586
|
+
</Form>
|
|
566
587
|
</div>
|
|
567
588
|
|
|
568
589
|
{/* Render Error */}
|
|
569
|
-
{actionData &&
|
|
590
|
+
{actionData?.intent === "render" && !actionData.success && (
|
|
570
591
|
<EffieValidationErrors
|
|
571
592
|
error={actionData.error}
|
|
572
593
|
issues={actionData.issues}
|
|
@@ -574,7 +595,7 @@ export default function EffiePreviewPage() {
|
|
|
574
595
|
)}
|
|
575
596
|
|
|
576
597
|
{/* Render Success */}
|
|
577
|
-
{render.
|
|
598
|
+
{render.step === "done" && elapsedToPlay !== null && (
|
|
578
599
|
<div style={{ color: "#4CAE4C" }}>
|
|
579
600
|
Started playing after {elapsedToPlay.toFixed(1)}s (at{" "}
|
|
580
601
|
{Math.round(render.scale * 100)}%)
|
|
@@ -595,7 +616,7 @@ export default function EffiePreviewPage() {
|
|
|
595
616
|
{warmupDownloadingItems.map((d, i) => (
|
|
596
617
|
<span key={d.url}>
|
|
597
618
|
{i > 0 ? ", " : ""}
|
|
598
|
-
<span title={d.url}>{
|
|
619
|
+
<span title={d.url}>{formatSourceUrl(d.url)}</span>
|
|
599
620
|
</span>
|
|
600
621
|
))}
|
|
601
622
|
</div>
|
package/template/package.json
CHANGED
|
@@ -11,15 +11,15 @@
|
|
|
11
11
|
"typecheck": "react-router typegen && tsc"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@effing/annie": "^0.
|
|
15
|
-
"@effing/annie-player": "^0.
|
|
16
|
-
"@effing/effie": "^0.
|
|
17
|
-
"@effing/effie-preview": "^0.
|
|
18
|
-
"@effing/ffs": "^0.
|
|
19
|
-
"@effing/satori": "^0.
|
|
14
|
+
"@effing/annie": "^0.14.0",
|
|
15
|
+
"@effing/annie-player": "^0.14.0",
|
|
16
|
+
"@effing/effie": "^0.14.0",
|
|
17
|
+
"@effing/effie-preview": "^0.14.0",
|
|
18
|
+
"@effing/ffs": "^0.14.0",
|
|
19
|
+
"@effing/satori": "^0.14.0",
|
|
20
20
|
"@resvg/resvg-js": "^2.6.2",
|
|
21
|
-
"@effing/serde": "^0.
|
|
22
|
-
"@effing/tween": "^0.
|
|
21
|
+
"@effing/serde": "^0.14.0",
|
|
22
|
+
"@effing/tween": "^0.14.0",
|
|
23
23
|
"@react-router/node": "^7.0.0",
|
|
24
24
|
"@react-router/serve": "^7.0.0",
|
|
25
25
|
"cross-env": "^7.0.3",
|
package/template/public/sw.js
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
/* global clients */
|
|
2
|
-
|
|
3
|
-
// FFS render URLs (GET /render/{uuid}) are one-time-consumption — the server
|
|
4
|
-
// deletes the job after streaming the response. However, browsers might make
|
|
5
|
-
// follow-up requests to the same URL (e.g. to re-read the moov atom for seeking),
|
|
6
|
-
// which return 404 and cause the video to break. This service worker caches the
|
|
7
|
-
// response so those subsequent requests are served from cache.
|
|
8
|
-
|
|
9
|
-
const CACHE_NAME = "ffs-render-cache";
|
|
10
|
-
const RENDER_URL_PATTERN = /\/render\/[0-9a-f-]{36}\/video$/;
|
|
11
|
-
const MAX_ENTRIES = 5;
|
|
12
|
-
|
|
13
|
-
/** @type {string[]} URLs in insertion order — reset on activate alongside the cache. */
|
|
14
|
-
const cacheOrder = [];
|
|
15
|
-
|
|
16
|
-
self.addEventListener("activate", (event) => {
|
|
17
|
-
cacheOrder.length = 0;
|
|
18
|
-
event.waitUntil(caches.delete(CACHE_NAME).then(() => clients.claim()));
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
self.addEventListener("fetch", (event) => {
|
|
22
|
-
if (!RENDER_URL_PATTERN.test(new URL(event.request.url).pathname)) return;
|
|
23
|
-
|
|
24
|
-
event.respondWith(
|
|
25
|
-
caches.open(CACHE_NAME).then(async (cache) => {
|
|
26
|
-
const cached = await cache.match(event.request);
|
|
27
|
-
if (cached) return cached;
|
|
28
|
-
|
|
29
|
-
const response = await fetch(event.request);
|
|
30
|
-
if (response.ok) {
|
|
31
|
-
await cache.put(event.request, response.clone());
|
|
32
|
-
cacheOrder.push(event.request.url);
|
|
33
|
-
|
|
34
|
-
// Prune oldest entries in the background — don't block the response.
|
|
35
|
-
if (cacheOrder.length > MAX_ENTRIES) {
|
|
36
|
-
const toDelete = cacheOrder.splice(
|
|
37
|
-
0,
|
|
38
|
-
cacheOrder.length - MAX_ENTRIES,
|
|
39
|
-
);
|
|
40
|
-
event.waitUntil(
|
|
41
|
-
Promise.all(toDelete.map((url) => cache.delete(url))),
|
|
42
|
-
);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
return response;
|
|
46
|
-
}),
|
|
47
|
-
);
|
|
48
|
-
});
|