@officexapp/vidfarm-devcli 0.21.36 → 0.21.37
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.
|
@@ -235,6 +235,24 @@ function serveAsset(res, pathname) {
|
|
|
235
235
|
return true;
|
|
236
236
|
}
|
|
237
237
|
// ── reverse proxy to the cloud host ─────────────────────────────────────────
|
|
238
|
+
/** How long to wait for upstream RESPONSE HEADERS before giving up (504). */
|
|
239
|
+
const PROXY_HEADERS_TIMEOUT_MS = 30_000;
|
|
240
|
+
function errorText(error) {
|
|
241
|
+
return error instanceof Error ? error.message : String(error);
|
|
242
|
+
}
|
|
243
|
+
/** Stream a finished render file. Same rule as the proxy pipe: a disk read
|
|
244
|
+
* error (file deleted mid-download, bad sector) must fail THIS response, not
|
|
245
|
+
* raise an uncaught 'error' that takes the whole serve process down. */
|
|
246
|
+
function pipeFile(res, filePath) {
|
|
247
|
+
const stream = createReadStream(filePath);
|
|
248
|
+
stream.on("error", (error) => {
|
|
249
|
+
console.warn(`[vidfarm] serve: could not stream ${filePath} (${errorText(error)})`);
|
|
250
|
+
res.destroy();
|
|
251
|
+
});
|
|
252
|
+
res.on("close", () => stream.destroy());
|
|
253
|
+
res.on("error", () => stream.destroy());
|
|
254
|
+
stream.pipe(res);
|
|
255
|
+
}
|
|
238
256
|
// Hop-by-hop headers are stripped so we forward a clean request/response.
|
|
239
257
|
const HOP_BY_HOP = new Set([
|
|
240
258
|
"connection", "keep-alive", "proxy-authenticate", "proxy-authorization",
|
|
@@ -267,15 +285,40 @@ async function proxyToCloud(req, res, host, pathWithSearch, auth, bodyOverride)
|
|
|
267
285
|
const method = req.method ?? "GET";
|
|
268
286
|
const body = method === "GET" || method === "HEAD" ? undefined : (bodyOverride ?? await readBody(req));
|
|
269
287
|
let upstream;
|
|
288
|
+
// Bound the wait for RESPONSE HEADERS only. A cloud route that never answers
|
|
289
|
+
// used to hang the browser forever with no error — the page shell paints from
|
|
290
|
+
// disk, then a data call spins for good. The BODY stays unbounded on purpose:
|
|
291
|
+
// proxied SSE (/api/v1/editor-chat) and media downloads are long-lived by
|
|
292
|
+
// design, so a body deadline would cut healthy streams.
|
|
293
|
+
const headersAbort = new AbortController();
|
|
294
|
+
const headersTimer = setTimeout(() => headersAbort.abort(), PROXY_HEADERS_TIMEOUT_MS);
|
|
270
295
|
try {
|
|
271
|
-
upstream = await fetch(target, {
|
|
296
|
+
upstream = await fetch(target, {
|
|
297
|
+
method,
|
|
298
|
+
headers,
|
|
299
|
+
body: body,
|
|
300
|
+
redirect: "manual",
|
|
301
|
+
signal: headersAbort.signal
|
|
302
|
+
});
|
|
272
303
|
}
|
|
273
304
|
catch (error) {
|
|
274
|
-
|
|
305
|
+
const timedOut = headersAbort.signal.aborted;
|
|
306
|
+
res.statusCode = timedOut ? 504 : 502;
|
|
275
307
|
res.setHeader("content-type", "application/json");
|
|
276
|
-
res.end(JSON.stringify({
|
|
308
|
+
res.end(JSON.stringify({
|
|
309
|
+
error: timedOut ? "upstream_timeout" : "upstream_unreachable",
|
|
310
|
+
detail: timedOut
|
|
311
|
+
? `${host} sent no response headers within ${PROXY_HEADERS_TIMEOUT_MS / 1000}s`
|
|
312
|
+
: error instanceof Error ? error.message : String(error),
|
|
313
|
+
host
|
|
314
|
+
}));
|
|
277
315
|
return;
|
|
278
316
|
}
|
|
317
|
+
finally {
|
|
318
|
+
// Headers are in (or the request failed) — from here the body streams for
|
|
319
|
+
// as long as it needs to.
|
|
320
|
+
clearTimeout(headersTimer);
|
|
321
|
+
}
|
|
279
322
|
res.statusCode = upstream.status;
|
|
280
323
|
upstream.headers.forEach((value, key) => {
|
|
281
324
|
if (HOP_BY_HOP.has(key.toLowerCase()))
|
|
@@ -287,7 +330,30 @@ async function proxyToCloud(req, res, host, pathWithSearch, auth, bodyOverride)
|
|
|
287
330
|
res.setHeader("x-vidfarm-upstream", host);
|
|
288
331
|
if (upstream.body) {
|
|
289
332
|
// Stream the response (covers SSE from /api/v1/editor-chat too).
|
|
290
|
-
|
|
333
|
+
//
|
|
334
|
+
// An upstream body can break AFTER the headers arrive: a CloudFront/Lambda
|
|
335
|
+
// connection reset, or undici's own body timeout on a proxied stream that
|
|
336
|
+
// idles. That surfaces as an 'error' on this stream — and an UNHANDLED
|
|
337
|
+
// 'error' is an uncaught exception, which killed the entire serve process.
|
|
338
|
+
// The visible symptom was the reported one: the editor page paints, the
|
|
339
|
+
// server dies mid-load, and every remaining request hangs forever. So both
|
|
340
|
+
// ends of the pipe get error handling, and each end tears the other down.
|
|
341
|
+
const stream = Readable.fromWeb(upstream.body);
|
|
342
|
+
stream.on("error", (error) => {
|
|
343
|
+
console.warn(`[vidfarm] serve: upstream stream broke for ${pathWithSearch} (${errorText(error)})`);
|
|
344
|
+
// Headers are already on the wire, so there is no status left to send —
|
|
345
|
+
// cut the response so the browser reports a failed request instead of
|
|
346
|
+
// waiting on a body that will never arrive.
|
|
347
|
+
res.destroy();
|
|
348
|
+
});
|
|
349
|
+
// The browser navigating away or cancelling must not leave us pulling the
|
|
350
|
+
// upstream body forever.
|
|
351
|
+
res.on("close", () => stream.destroy());
|
|
352
|
+
res.on("error", (error) => {
|
|
353
|
+
console.warn(`[vidfarm] serve: client stream broke for ${pathWithSearch} (${errorText(error)})`);
|
|
354
|
+
stream.destroy();
|
|
355
|
+
});
|
|
356
|
+
stream.pipe(res);
|
|
291
357
|
}
|
|
292
358
|
else {
|
|
293
359
|
res.end();
|
|
@@ -709,8 +775,28 @@ function renderLoginRequiredPage(pathname) {
|
|
|
709
775
|
</html>
|
|
710
776
|
`;
|
|
711
777
|
}
|
|
778
|
+
// Last-resort net. Every known stream path now handles its own errors, but a
|
|
779
|
+
// local dev server must never disappear on a stray async throw — the user sees
|
|
780
|
+
// only a page that stopped loading, with no clue the server is gone. Log it
|
|
781
|
+
// loudly and stay up; this process is a stateless proxy plus a static file
|
|
782
|
+
// server, so there is no shared state left corrupt by a failed request.
|
|
783
|
+
let crashGuardInstalled = false;
|
|
784
|
+
function installServeCrashGuard() {
|
|
785
|
+
if (crashGuardInstalled)
|
|
786
|
+
return;
|
|
787
|
+
crashGuardInstalled = true;
|
|
788
|
+
process.on("uncaughtException", (error) => {
|
|
789
|
+
console.error(`[vidfarm] serve: recovered from an uncaught error — ${errorText(error)}`);
|
|
790
|
+
if (error instanceof Error && error.stack)
|
|
791
|
+
console.error(error.stack);
|
|
792
|
+
});
|
|
793
|
+
process.on("unhandledRejection", (reason) => {
|
|
794
|
+
console.error(`[vidfarm] serve: recovered from an unhandled rejection — ${errorText(reason)}`);
|
|
795
|
+
});
|
|
796
|
+
}
|
|
712
797
|
export function startLocalFrontendServer(opts) {
|
|
713
798
|
const host = opts.host.replace(/\/+$/, "");
|
|
799
|
+
installServeCrashGuard();
|
|
714
800
|
const server = createServer((req, res) => {
|
|
715
801
|
void handleRequest(req, res, host, opts.auth).catch((error) => {
|
|
716
802
|
if (!res.headersSent) {
|
|
@@ -816,7 +902,7 @@ async function handleRequest(req, res, host, auth) {
|
|
|
816
902
|
res.setHeader("content-type", "video/mp4");
|
|
817
903
|
res.setHeader("content-length", String(statSync(record.outputPath).size));
|
|
818
904
|
res.setHeader("content-disposition", `inline; filename="vidfarm-${path.basename(record.outputPath)}"`);
|
|
819
|
-
|
|
905
|
+
pipeFile(res, record.outputPath);
|
|
820
906
|
return;
|
|
821
907
|
}
|
|
822
908
|
// 3b. Studio-contract render endpoints (the sealed StudioApp's Export
|
|
@@ -909,7 +995,7 @@ async function handleRequest(req, res, host, auth) {
|
|
|
909
995
|
res.setHeader("content-type", STUDIO_RENDER_MIME[path.extname(job.outputPath)] ?? "video/mp4");
|
|
910
996
|
res.setHeader("content-length", String(statSync(job.outputPath).size));
|
|
911
997
|
res.setHeader("content-disposition", `inline; filename="${filename}"`);
|
|
912
|
-
|
|
998
|
+
pipeFile(res, job.outputPath);
|
|
913
999
|
return;
|
|
914
1000
|
}
|
|
915
1001
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@officexapp/vidfarm-devcli",
|
|
3
|
-
"version": "0.21.
|
|
3
|
+
"version": "0.21.37",
|
|
4
4
|
"description": "Local bridge for the Vidfarm Trackpad Editor. `vidfarm serve <template_id>` boots the FULL editor on localhost (disk-backed records/storage, free in-process render); edit composition.html on disk (Claude Code, Codex, etc.) and the browser live-morphs it.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|