Service: job event stream, async in-memory backend, preview routes
Ported from PR #15 (packages-side surface only; the fork's dev UI page is intentionally dropped - the API surface is the product): - job_events table + repository, worker emits stage transitions, additive GET /v1/clones/:id/events polling route. The fork's hand-written migration was never registered in Drizzle's journal (it silently never applied); regenerated properly via drizzle-kit from schema.ts with the (job_id, seq) index declared, and verified against a live Postgres - In-memory backend moves to the documented enqueue-202 + poll contract with single-flight BUSY guard; POST honors backend httpStatus - app-preview static serving routes; the preview BUILD half is deferred until the compiler exports buildApp/DEFAULT_HARNESS_DIR - routes 404 cleanly until then - preview option threaded through core types; cache key intentionally unchanged while the flag is inert All workspaces typecheck; api 18/18, core 13/13, worker 1/1 pass; migration verified applying (table + index + FK) on postgres:16. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
f991f469cf
commit
bbe4a5643b
+39
-2
@@ -15,6 +15,7 @@ const OptionsSchema = z
|
||||
mode: z.enum(["single", "multi"]).optional(),
|
||||
styling: z.enum(["tailwind", "css"]).optional(),
|
||||
framework: z.enum(["next", "vite"]).optional(),
|
||||
preview: z.boolean().optional(),
|
||||
verify: z.boolean().optional(),
|
||||
asyncVerify: z.boolean().optional(),
|
||||
maxRoutes: z.number().int().positive().optional(),
|
||||
@@ -238,10 +239,12 @@ export function createApp(deps: AppDeps): Hono {
|
||||
|
||||
try {
|
||||
const out = await backend.submit(url, opts);
|
||||
if (out.status === "queued") return c.json({ jobId: out.jobId, status: "queued" }, 202);
|
||||
if (out.status === "queued") return c.json({ jobId: out.jobId, status: "queued" }, out.httpStatus);
|
||||
return c.json(out.result, 200);
|
||||
} catch (e) {
|
||||
return c.json({ status: "failed", error: String(e).slice(0, 500) }, 500);
|
||||
const msg = String(e);
|
||||
if (msg.startsWith("BUSY:")) return c.json({ error: msg.slice(5).trim() }, 429);
|
||||
return c.json({ status: "failed", error: msg.slice(0, 500) }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -282,6 +285,40 @@ export function createApp(deps: AppDeps): Hono {
|
||||
return c.body(file.bytes);
|
||||
});
|
||||
|
||||
// Pipeline progress events (poll every ~300ms while a clone runs).
|
||||
app.get("/v1/clones/:id/events", async (c) => {
|
||||
const after = Math.max(0, Number(c.req.query("after") ?? "0") || 0);
|
||||
const events = backend.events ? await backend.events(c.req.param("id"), after) : null;
|
||||
if (!events) return c.json({ error: "not found" }, 404);
|
||||
return c.json({ jobId: c.req.param("id"), events });
|
||||
});
|
||||
|
||||
// Browsable preview of the built clone (static export published by the preview
|
||||
// build under public/app-preview/). References are relative, so the export works
|
||||
// from this mount — the only requirement is a trailing slash on the root.
|
||||
const previewFile = async (c: Context, sub: string) => {
|
||||
const id = c.req.param("id") ?? "";
|
||||
const tryPaths = sub.includes(".")
|
||||
? [`public/app-preview/${sub}`]
|
||||
: [
|
||||
`public/app-preview/${sub}`.replace(/\/$/, "") + "/index.html",
|
||||
`public/app-preview/${sub}`,
|
||||
`public/app-preview/${sub}.html`,
|
||||
];
|
||||
for (const p of tryPaths) {
|
||||
const file = await backend.file(id, p);
|
||||
if (file) {
|
||||
c.header("content-type", file.contentType);
|
||||
c.header("content-length", String(file.bytes.length));
|
||||
return c.body(file.bytes);
|
||||
}
|
||||
}
|
||||
return c.json({ error: "no app preview for this clone (preview builds are on by default for single-page clones; pass options.preview=true otherwise)" }, 404);
|
||||
};
|
||||
app.get("/v1/clones/:id/app-preview", (c) => c.redirect(`/v1/clones/${c.req.param("id")}/app-preview/`, 302));
|
||||
app.get("/v1/clones/:id/app-preview/", (c) => previewFile(c, "index.html"));
|
||||
app.get("/v1/clones/:id/app-preview/:path{.+}", (c) => previewFile(c, c.req.param("path") ?? ""));
|
||||
|
||||
app.delete("/v1/clones/:id", async (c) => {
|
||||
const ok = await backend.remove(c.req.param("id"));
|
||||
return c.json({ deleted: ok }, ok ? 200 : 404);
|
||||
|
||||
Reference in New Issue
Block a user