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
@@ -0,0 +1,10 @@
|
||||
CREATE TABLE "job_events" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"job_id" uuid NOT NULL,
|
||||
"seq" integer NOT NULL,
|
||||
"payload" jsonb NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "job_events" ADD CONSTRAINT "job_events_job_id_jobs_id_fk" FOREIGN KEY ("job_id") REFERENCES "public"."jobs"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
CREATE INDEX "job_events_job_id_seq_idx" ON "job_events" USING btree ("job_id","seq");
|
||||
@@ -0,0 +1,461 @@
|
||||
{
|
||||
"id": "de9b94b8-cefb-4d2a-a848-79a552976943",
|
||||
"prevId": "e076bd1e-a7ce-40ab-abd1-9eb11d00ddb9",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.api_keys": {
|
||||
"name": "api_keys",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"key_hash": {
|
||||
"name": "key_hash",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"label": {
|
||||
"name": "label",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"rate_limit": {
|
||||
"name": "rate_limit",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"revoked_at": {
|
||||
"name": "revoked_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"api_keys_key_hash_unique": {
|
||||
"name": "api_keys_key_hash_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"key_hash"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.cache": {
|
||||
"name": "cache",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"cache_key": {
|
||||
"name": "cache_key",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"job_id": {
|
||||
"name": "job_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"url": {
|
||||
"name": "url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"options_hash": {
|
||||
"name": "options_hash",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"compiler_version": {
|
||||
"name": "compiler_version",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"cache_job_id_jobs_id_fk": {
|
||||
"name": "cache_job_id_jobs_id_fk",
|
||||
"tableFrom": "cache",
|
||||
"tableTo": "jobs",
|
||||
"columnsFrom": [
|
||||
"job_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.clones": {
|
||||
"name": "clones",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"job_id": {
|
||||
"name": "job_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"url": {
|
||||
"name": "url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"route_count": {
|
||||
"name": "route_count",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 1
|
||||
},
|
||||
"file_manifest": {
|
||||
"name": "file_manifest",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"bundle_s3_key": {
|
||||
"name": "bundle_s3_key",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"verify": {
|
||||
"name": "verify",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"capture_meta": {
|
||||
"name": "capture_meta",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"clones_job_id_jobs_id_fk": {
|
||||
"name": "clones_job_id_jobs_id_fk",
|
||||
"tableFrom": "clones",
|
||||
"tableTo": "jobs",
|
||||
"columnsFrom": [
|
||||
"job_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.job_events": {
|
||||
"name": "job_events",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"job_id": {
|
||||
"name": "job_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"seq": {
|
||||
"name": "seq",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"payload": {
|
||||
"name": "payload",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"job_events_job_id_seq_idx": {
|
||||
"name": "job_events_job_id_seq_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "job_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "seq",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"job_events_job_id_jobs_id_fk": {
|
||||
"name": "job_events_job_id_jobs_id_fk",
|
||||
"tableFrom": "job_events",
|
||||
"tableTo": "jobs",
|
||||
"columnsFrom": [
|
||||
"job_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.jobs": {
|
||||
"name": "jobs",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"kind": {
|
||||
"name": "kind",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"url": {
|
||||
"name": "url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"options": {
|
||||
"name": "options",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'{}'::jsonb"
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'queued'"
|
||||
},
|
||||
"cache_key": {
|
||||
"name": "cache_key",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"attempts": {
|
||||
"name": "attempts",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 0
|
||||
},
|
||||
"error": {
|
||||
"name": "error",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"compiler_version": {
|
||||
"name": "compiler_version",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"timings": {
|
||||
"name": "timings",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"started_at": {
|
||||
"name": "started_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"finished_at": {
|
||||
"name": "finished_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.signup_tokens": {
|
||||
"name": "signup_tokens",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"token_hash": {
|
||||
"name": "token_hash",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"consumed_at": {
|
||||
"name": "consumed_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"signup_tokens_token_hash_unique": {
|
||||
"name": "signup_tokens_token_hash_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"token_hash"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
}
|
||||
},
|
||||
"enums": {},
|
||||
"schemas": {},
|
||||
"sequences": {},
|
||||
"roles": {},
|
||||
"policies": {},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,13 @@
|
||||
"when": 1782790200000,
|
||||
"tag": "0001_signup_tokens",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 2,
|
||||
"version": "7",
|
||||
"when": 1783202414928,
|
||||
"tag": "0002_productive_wallflower",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
+22
-1
@@ -1,6 +1,6 @@
|
||||
import { and, desc, eq, gt, isNull, sql } from "drizzle-orm";
|
||||
import type { Db } from "./client.js";
|
||||
import { jobs, clones, cache, apiKeys, signupTokens, type Job, type NewJob, type Clone, type NewClone, type CacheRow, type ApiKey, type SignupToken } from "./schema.js";
|
||||
import { jobs, clones, cache, apiKeys, signupTokens, jobEvents, type Job, type NewJob, type Clone, type NewClone, type CacheRow, type ApiKey, type SignupToken } from "./schema.js";
|
||||
|
||||
// ---- jobs ----
|
||||
|
||||
@@ -111,3 +111,24 @@ export async function consumeSignupToken(db: Db, tokenHash: string): Promise<Sig
|
||||
.returning();
|
||||
return row;
|
||||
}
|
||||
|
||||
// ---- job events ----
|
||||
|
||||
export async function appendJobEvent(db: Db, jobId: string, payload: Record<string, unknown>): Promise<number> {
|
||||
const [row] = await db
|
||||
.select({ n: sql<number>`coalesce(max(${jobEvents.seq}), 0)` })
|
||||
.from(jobEvents)
|
||||
.where(eq(jobEvents.jobId, jobId));
|
||||
const seq = (row?.n ?? 0) + 1;
|
||||
await db.insert(jobEvents).values({ jobId, seq, payload: { t: Date.now(), ...payload } });
|
||||
return seq;
|
||||
}
|
||||
|
||||
export async function listJobEvents(db: Db, jobId: string, after = 0): Promise<Array<Record<string, unknown>>> {
|
||||
const rows = await db
|
||||
.select()
|
||||
.from(jobEvents)
|
||||
.where(and(eq(jobEvents.jobId, jobId), sql`${jobEvents.seq} > ${after}`))
|
||||
.orderBy(jobEvents.seq);
|
||||
return rows.map((r) => r.payload as Record<string, unknown>);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { pgTable, text, jsonb, integer, timestamp, uuid } from "drizzle-orm/pg-core";
|
||||
import { pgTable, text, jsonb, integer, timestamp, uuid, index } from "drizzle-orm/pg-core";
|
||||
|
||||
/** Job lifecycle row. Status mirrors the queue; `cacheKey` ties it to the cache row.
|
||||
* `options`/`timings` are jsonb (the service's typed shapes). */
|
||||
@@ -66,6 +66,17 @@ export const signupTokens = pgTable("signup_tokens", {
|
||||
consumedAt: timestamp("consumed_at", { withTimezone: true }),
|
||||
});
|
||||
|
||||
/** Structured clone progress events (mirrors in-memory backend event stream). */
|
||||
export const jobEvents = pgTable("job_events", {
|
||||
id: uuid("id").defaultRandom().primaryKey(),
|
||||
jobId: uuid("job_id")
|
||||
.notNull()
|
||||
.references(() => jobs.id, { onDelete: "cascade" }),
|
||||
seq: integer("seq").notNull(),
|
||||
payload: jsonb("payload").notNull(),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
}, (t) => [index("job_events_job_id_seq_idx").on(t.jobId, t.seq)]);
|
||||
|
||||
export type Job = typeof jobs.$inferSelect;
|
||||
export type NewJob = typeof jobs.$inferInsert;
|
||||
export type Clone = typeof clones.$inferSelect;
|
||||
@@ -74,3 +85,4 @@ export type CacheRow = typeof cache.$inferSelect;
|
||||
export type ApiKey = typeof apiKeys.$inferSelect;
|
||||
export type SignupToken = typeof signupTokens.$inferSelect;
|
||||
export type NewSignupToken = typeof signupTokens.$inferInsert;
|
||||
export type JobEvent = typeof jobEvents.$inferSelect;
|
||||
|
||||
Reference in New Issue
Block a user