#!/usr/bin/env -S deno run --allow-env --allow-net --allow-run import { getRequiredEnv, getStdout, invalidExecutionEntriesOf, Traceable, ITraceable, ITraceableLogger, IEither, Either, } from "@liz-ci/utils"; import type { Job } from "@liz-ci/model"; const SERVER_CONFIG = { host: "0.0.0.0", port: 9000, }; interface IHealthCheckActivity { healthCheck(req: Traceable): Traceable>; } class HealthCheckActivity implements IHealthCheckActivity { public healthCheck( req: Traceable, ) { return req.map(async ({ logger }) => { try { getRequiredEnv("LAMINAR_HOST"); await getStdout(["laminarc", "show-jobs"]); const msg = `think im healthy!! (✿˘◡˘) ready to do work~\n`; logger.log(msg); return new Response( msg, { status: 200 }, ); } catch (error) { logger.error(error); return new Response( "oh no, i need to eat more vegetables (。•́︿•̀。)...\n", { status: 500 }, ); } }); } } const aPost = (req: Traceable): IEither => { const {item: request, logger} = req; const {method} = request; if (method !== "POST") { const msg = "that's not how you pet me (⋟﹏⋞) try post instead~"; logger.warn(msg); return { err: new Response(msg + "\n", {status: 405}) }; } return {ok: request}; }; type JsonTransformer = (json: Traceable) => Either; const aJson = (jsonTransformer: JsonTransformer) => (r: Traceable) => r .map(async ({item: request, logger}): Promise> => { try { return {ok: (await request.json())}; } catch (e) { const err = "seems to be invalid JSON (>//<) can you fix?"; logger.warn(err); return { err }; } }) .flatMapAsync(TraceableImpl.promiseify((t): Traceable> => { const {item: {err, ok: json}} = t; if (err) return t.map(() => ({ err })); return t.map(() => json!).map(jsonTransformer); })) interface IJobHookActivity { processHook(req: Traceable): Traceable>; } type GetJobRequest = { jobType: string; args: unknown }; class JobHookActivityImpl implements IJobHookActivity { constructor(private readonly queuer: IJobQueuer) {} private getJob( { logger, item }: Traceable, ): Either { const isObject = (o: unknown): o is Record => typeof o === "object" && !Array.isArray(o) && !!o; if (!isObject(item) || !isObject(item.arguments)|| typeof item.type !== "string") { const err = "seems like a pwetty mawfomed job \\(-.-)/"; logger.warn(err) return { err }; } const ok = { type: item.type, arguments: item.arguments }; const invalidArgEntries = invalidExecutionEntriesOf({type: ok.type, ...ok.arguments}); if (invalidArgEntries.length > 0) { const err = "your reqwest seems invawid (´。﹏。`) can you fix? uwu\n" + invalidArgEntries; logger.warn(err); return { err }; } return { ok: ok }; } public processHook(r: Traceable) { return r.map(aPost) .map((t) => { either((err) => ({ err }), (request) => ) const {item: {ok: request, err}} = t; if (err) return > t; return t.map(() => request!).map(aJson(this.getJob)); }); // flatMapAsync(aJsonPost(this.getJob)) // .map(TraceableImpl.promiseify((g) => { // if (jobRequest) { // return g.map(() => jobRequest) // .map(this.getJob) // .map( // ({ item: { ok: jobRequest, err } }) => { // if (err) return { err: new Response(err, { status: 400 }) }; // return { ok: jobRequest }; // }, // ); // } // return g.map(() => ({ ok: undefined, err })); // })) // .map(TraceableImpl.promiseify(({ item: t }) => { // const { item: { ok: job, err } } = t; // if (err) return t.map(() => Promise.resolve(err)); // // return t.map(() => job!) // .map(this.queuer.queue) // .map(TraceableImpl.promiseify(({ item, logger }) => { // if (item.ok) { // return new Response(item.ok, { status: 200 }); // } // logger.error(item.err); // return new Response("i messed up D:\n", { status: 500 }); // })); // })); // } } class LizCIServerImpl implements ILizCIServer { constructor( private readonly healthCheckActivity: IHealthCheckActivity, private readonly jobHookActivity: IJobHookActivity, ) {} private route( req: Traceable, ): Traceable> { return req.flatMap((req) => { const { logger, item: { method, pathname } } = req; if (pathname === "/health") { return this.healthCheckActivity.healthCheck(req); } return this.jobHookActivity.processHook(req); }); } public async serve(req: Request): Promise { const traceId = crypto.randomUUID(); const { pathname } = new URL(req.url); const traceSupplier = () => `[${traceId} <- ${req.method}'d @ ${pathname}]`; return TraceableImpl.from(req) .bimap(({ item: req }) => [{ req, pathname }, traceSupplier]) .flatMap(this.route) .map(({ item, logger }) => item.catch((e) => { const errorMessage = `oh noes! something went wrong (ಥ_ಥ) so sowwy!`; logger.error(errorMessage, e); return new Response(`${errorMessage}\n`, { status: 500 }); }) ) .item; } } class JobQueue { private readonly logger: PrefixLogger; private readonly url: URL; private readonly pathname: string; constructor(private readonly request: Request, private readonly) { this.url = new URL(request.url); this.pathname = this.url.pathname; this.logger = this.createLogger(); } /** * Creates a logger with request-specific context */ /** * Performs health checks on dependent services */ private async performHealthCheck(): Promise { } /** * Handles health check requests */ private async handleHealthCheck(): Promise { try { await this.performHealthCheck(); } catch (error) { } } /** * Queues a job in the laminar system */ private async queueJob(jobName: string, args: JobRequest): Promise { } /** * Validates job request parameters */ private validateJobRequest( jobName: string, args: unknown, ): { valid: boolean; response?: Response } { } /** * Main method to handle the request */ public async handle(): Promise { this.logger.log("go! :DDD"); // Handle health check requests if (this.pathname === "/health") { return this.handleHealthCheck(); } // Validate HTTP method if (this.request.method !== "POST") { } // Extract job name from path if (!validation.valid) { return validation.response!; } // Queue the job return this.queueJob(jobName, requestBody as JobRequest); } /** * Handles the entire request lifecycle, including error handling */ public async processRequest(): Promise { try { return await this.handle(); } catch (error) { } finally { this.logger.log("allll done!"); } } } /** * Entry point - starts the server */ Deno.serve(SERVER_CONFIG, async (request: Request) => { const handler = new RequestHandler(request); return handler.processRequest(); });