From e49fda41176d025a671802be76c219d66167276f Mon Sep 17 00:00:00 2001 From: Elizabeth Alexander Hunt Date: Mon, 12 May 2025 23:05:27 -0700 Subject: snapshot --- hooks/mod.ts | 107 +++++++++++++++++--------------------------------------- hooks/queuer.ts | 47 +++++++++++++++++++++++++ utils/either.ts | 25 +++++++++++++ utils/mod.ts | 1 + utils/run.ts | 28 ++++++++++----- utils/trace.ts | 47 +++++++++++++++---------- 6 files changed, 154 insertions(+), 101 deletions(-) create mode 100644 hooks/queuer.ts create mode 100644 utils/either.ts diff --git a/hooks/mod.ts b/hooks/mod.ts index 9fc4501..9df7d67 100755 --- a/hooks/mod.ts +++ b/hooks/mod.ts @@ -4,8 +4,11 @@ import { getRequiredEnv, getStdout, invalidExecutionEntriesOf, - type Traceable, - TraceableImpl, + Traceable, + ITraceable, + ITraceableLogger, + IEither, + Either, } from "@liz-ci/utils"; import type { Job } from "@liz-ci/model"; @@ -13,49 +16,6 @@ const SERVER_CONFIG = { host: "0.0.0.0", port: 9000, }; - -type QueuePosition = string; -interface IJobQueuer { - queue: ( - job: Traceable, - ) => Promise<{ ok?: QueuePosition; err?: unknown }>; -} - -class LaminarJobQueuer implements IJobQueuer { - constructor( - private readonly queuePositionPrefix: string, - ) {} - - public async queue({ item: job, logger }: Traceable) { - try { - const laminarCommand = [ - "laminarc", - "queue", - job.type, - ...Object.entries(job.arguments).map(([key, val]) => - `"${key}"="${val}"` - ), - ]; - - logger.log( - `im so excited to see how this queue job will end!! (>ᴗ<)`, - laminarCommand, - ); - - const output = await getStdout(laminarCommand); - logger.log(output); - - const [jobName, jobId] = output.split(":"); - const jobUrl = `${this.queuePositionPrefix}/jobs/${jobName}/${jobId}`; - - logger.log(`all queued up and weady to go~ (˘ω˘) => ${jobUrl}\n`); - return { ok: jobUrl, err: undefined }; - } catch (e) { - return { ok: undefined, err: e }; - } - } -} - interface IHealthCheckActivity { healthCheck(req: Traceable): Traceable>; } @@ -85,39 +45,36 @@ class HealthCheckActivity implements IHealthCheckActivity { } } -const aPost = (r: Traceable): Traceable<{ ok?: Request, err?: Response }> => - r.map((req) => { - 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); - const r405 = new Response(msg + "\n", {status: 405}); - return {err: r405}; - } - return {ok: request}; - }); +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) => Traceable<{ ok?: R; err?: Response }>; +type JsonTransformer = (json: Traceable) => Either; const aJson = (jsonTransformer: JsonTransformer) => (r: Traceable) => r - .map(async ({item: request, logger}): Promise<{ ok?: JsonT, err?: Response }> => { - + .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); - const r400 = new Response(msg + "\n", {status: 400}); - return {err: r400}; + return { err }; } }) - .flatMapAsync(TraceableImpl.promiseify((t): Traceable<{ ok?: BodyT, err?: Response }> => { + .flatMapAsync(TraceableImpl.promiseify((t): Traceable> => { const {item: {err, ok: json}} = t; - if (err) return >t; - return t.map(() => json!).flatMap(jsonTransformer); + if (err) return t.map(() => ({ err })); + return t.map(() => json!).map(jsonTransformer); })) + interface IJobHookActivity { processHook(req: Traceable): Traceable>; } @@ -126,31 +83,31 @@ class JobHookActivityImpl implements IJobHookActivity { constructor(private readonly queuer: IJobQueuer) {} private getJob( - j: Traceable, - ): Traceable<{ ok?: Job; err?: Response }> { - return j.map(({ logger, item }) => { + { 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 = "your reqwest seems compwetewy mawfomed \\(-.-)/\n"; - logger.warn(err); - return { err: new Response(err, { status: 400 }) }; + 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"; + const err = "your reqwest seems invawid (´。﹏。`) can you fix? uwu\n" + invalidArgEntries; logger.warn(err); - return { err: new Response(err, { status: 400 }) }; + return { err }; } return { ok: ok }; - }); } public processHook(r: Traceable) { - return r.flatMap(aPost).flatMap((t) => { + 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)); diff --git a/hooks/queuer.ts b/hooks/queuer.ts new file mode 100644 index 0000000..1461809 --- /dev/null +++ b/hooks/queuer.ts @@ -0,0 +1,47 @@ +import type { IEither, ITraceable, ITraceableLogger } from "@liz-ci/utils"; +import type { Job } from "@liz-ci/model"; + +type QueuePosition = string; +export class QueueError extends Error {} +export interface IJobQueuer { + queue: >( + job: ITraceable, + ) => Promise>; +} + +export class LaminarJobQueuer implements IJobQueuer { + constructor( + private readonly queuePositionPrefix: string, + ) {} + + public async queue({ item: job, logger }: Traceable) { + const laminarCommand = [ + "laminarc", + "queue", + job.type, + ...Object.entries(job.arguments).map(([key, val]) => `"${key}"="${val}"`), + ]; + + logger.log( + `im so excited to see how this queue job will end!! (>ᴗ<)`, + laminarCommand, + ); + + return (await getStdout(laminarCommand)).mapBoth( + (e) => { + const err = `we bwoke oh noesss D:`; + logger.error(err, e); + return Either.left(e); + }, + (stdout) => { + logger.log(stdout); + + const [jobName, jobId] = stdout.split(":"); + const jobUrl = `${this.queuePositionPrefix}/jobs/${jobName}/${jobId}`; + + logger.log(`all queued up and weady to go~ (˘ω˘) => ${jobUrl}\n`); + return Either.right(jobUrl); + }, + ); + } +} diff --git a/utils/either.ts b/utils/either.ts new file mode 100644 index 0000000..d21c796 --- /dev/null +++ b/utils/either.ts @@ -0,0 +1,25 @@ +export interface IEither { + ok?: T; + err?: E; + mapBoth: ( + errBranch: (e: E) => Ee, + okBranch: (o: T) => Tt, + ) => IEither; +} + +export class Either implements IEither { + private constructor(readonly err?: E, readonly ok?: T) {} + + public mapBoth(errBranch: (e: E) => Ee, okBranch: (t: T) => Tt) { + if (this.err) return new Either(errBranch(this.err)); + return new Either(undefined, okBranch(this.ok!)); + } + + static left(e: E) { + return new Either(e); + } + + static right(t: T) { + return new Either(undefined, t); + } +} diff --git a/utils/mod.ts b/utils/mod.ts index a8a0751..53ea173 100644 --- a/utils/mod.ts +++ b/utils/mod.ts @@ -1,4 +1,5 @@ export * from "./trace.ts"; +export * from "./either.ts"; export * from "./env.ts"; export * from "./run.ts"; export * from "./secret.ts"; diff --git a/utils/run.ts b/utils/run.ts index 60ae1e6..06e7d9f 100644 --- a/utils/run.ts +++ b/utils/run.ts @@ -1,7 +1,10 @@ +import { Either } from "./mod.ts"; + +export class ProcessError extends Error {} export const getStdout = async ( cmd: string[] | string, options: Deno.CommandOptions = {}, -): Promise => { +): Promise> => { const [exec, ...args] = (typeof cmd === "string") ? cmd.split(" ") : cmd; const command = new Deno.Command(exec, { args, @@ -10,12 +13,21 @@ export const getStdout = async ( ...options, }); - const { code, stdout, stderr } = await command.output(); - - const stdoutText = new TextDecoder().decode(stdout); - const stderrText = new TextDecoder().decode(stderr); - - if (code !== 0) throw new Error(`command failed\n${stderrText}`); + try { + const { code, stdout, stderr } = await command.output(); + const stdoutText = new TextDecoder().decode(stdout); + const stderrText = new TextDecoder().decode(stderr); - return stdoutText; + if (code !== 0) { + return Either.left( + new ProcessError(`command failed\n${stderrText}`), + ); + } + return Either.right(stdoutText); + } catch (e) { + if (e instanceof Error) { + return Either.left(e); + } + throw new Error("unknown error " + e); + } }; diff --git a/utils/trace.ts b/utils/trace.ts index eb4ac2f..373f37e 100644 --- a/utils/trace.ts +++ b/utils/trace.ts @@ -25,12 +25,15 @@ export interface ITraceable> { mapper: ITraceableMapper>, ) => ITraceable; peek: (peek: ITraceableMapper) => ITraceable; - flatMap: (mapper: ITraceableMapper>) => ITraceable; - flatMapAsync(mapper: ITraceableMapper>>): ITraceable, L>; + flatMap: ( + mapper: ITraceableMapper>, + ) => ITraceable; + flatMapAsync( + mapper: ITraceableMapper>>, + ): ITraceable, L>; } -export class TraceableLogger - implements ITraceableLogger { +export class TraceableLogger implements ITraceableLogger { private readonly logger: Logger = console; constructor( private readonly traces = [() => `[${new Date().toISOString()}]`], @@ -62,23 +65,30 @@ export class TraceableLogger } } -export class TraceableImpl< +class TraceableImpl< T, L extends ITraceableLogger, > implements ITraceable { - private constructor(readonly item: T, readonly logger: L) {} + protected constructor(readonly item: T, readonly logger: L) {} public map(mapper: ITraceableMapper) { const result = mapper(this); return new TraceableImpl(result, this.logger); } - public flatMap(mapper: ITraceableMapper>): ITraceable { + public flatMap( + mapper: ITraceableMapper>, + ): ITraceable { return mapper(this); } - public flatMapAsync(mapper: ITraceableMapper>>): ITraceable, L> { - return new TraceableImpl(mapper(this).then(({ item }) => item), this.logger); + public flatMapAsync( + mapper: ITraceableMapper>>, + ): ITraceable, L> { + return new TraceableImpl( + mapper(this).then(({ item }) => item), + this.logger, + ); } public peek(peek: ITraceableMapper) { @@ -95,20 +105,21 @@ export class TraceableImpl< mapper: ITraceableMapper, ): ITraceableMapper, L, Promise> { return (traceablePromise) => - traceablePromise.flatMapAsync(async (t) => { - const item = await t.item; - return t.map(() => item).map(mapper); - }).item; + traceablePromise.flatMapAsync(async (t) => { + const item = await t.item; + return t.map(() => item).map(mapper); + }).item; } +} - static withClassTrace>(c: C): ITraceableMapper> { +export class Traceable extends TraceableImpl { + static withClassTrace( + c: C, + ): ITraceableMapper> { return (t) => [t.item, () => c.constructor.name]; } static from(t: T) { - return new TraceableImpl(t, new TraceableLogger()); + return new Traceable(t, new TraceableLogger()); } } - -export interface Traceable = TraceableLogger> extends ITraceable { -} -- cgit v1.2.3-70-g09d2