summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorElizabeth Hunt <lizhunt@amazon.com>2025-05-16 16:17:13 -0700
committerElizabeth Hunt <lizhunt@amazon.com>2025-05-16 16:17:13 -0700
commitef51b25e4388cbdf3a27e23d9f1fa381ae20a5ad (patch)
treed54be88fa30fd2da97a97fc7006d9ff9d94ed16a
parent1ab20482ab37d7962c8e69701163270e687df3ca (diff)
downloadci-ef51b25e4388cbdf3a27e23d9f1fa381ae20a5ad.tar.gz
ci-ef51b25e4388cbdf3a27e23d9f1fa381ae20a5ad.zip
snapshot
-rw-r--r--hooks/server/health.ts23
-rw-r--r--hooks/server/mod.ts71
-rw-r--r--u/fn/callable.ts4
-rw-r--r--u/fn/either.ts28
-rw-r--r--u/server/activity/fourohfour.ts30
-rw-r--r--u/server/activity/health.ts12
-rw-r--r--u/server/activity/mod.ts13
-rw-r--r--u/server/filter/json.ts37
-rw-r--r--u/server/filter/method.ts38
-rw-r--r--u/server/filter/mod.ts2
-rw-r--r--u/server/metrics.ts112
-rw-r--r--u/server/mod.ts3
-rw-r--r--u/server/request.ts27
-rw-r--r--u/server/response.ts44
14 files changed, 348 insertions, 96 deletions
diff --git a/hooks/server/health.ts b/hooks/server/health.ts
new file mode 100644
index 0000000..41dfcb4
--- /dev/null
+++ b/hooks/server/health.ts
@@ -0,0 +1,23 @@
+import {
+ getRequiredEnv,
+ getStdout,
+ type HealthCheckInput,
+ HealthCheckOutput,
+ type IEither,
+ type ITraceable,
+ TraceUtil,
+} from "@emprespresso/pengueno";
+
+export const healthCheck = <Trace>(
+ input: ITraceable<HealthCheckInput, Trace>,
+): Promise<IEither<Error, HealthCheckOutput>> =>
+ input.bimap(TraceUtil.withFunctionTrace(healthCheck))
+ .move(getRequiredEnv("LAMINAR_HOST"))
+ // we need to test LAMINAR_HOST is propagated to getStdout for other procedures
+ .map(({ item }) => item.moveRight(["laminarc", "show-jobs"]))
+ .map((i) =>
+ i.item.mapRight(i.move.apply)
+ .flatMapAsync(getStdout.apply)
+ .then((gotJobs) => gotJobs.moveRight(HealthCheckOutput.YAASQUEEN))
+ )
+ .item;
diff --git a/hooks/server/mod.ts b/hooks/server/mod.ts
index cc20827..b635b05 100644
--- a/hooks/server/mod.ts
+++ b/hooks/server/mod.ts
@@ -5,52 +5,33 @@ import {
HealthCheckOutput,
type IEither,
type ITraceable,
+ LogTraceable,
TraceUtil,
} from "@emprespresso/pengueno";
-import {} from "../../u/trace/mod.ts";
-const healthCheck = <Trace>(
- input: ITraceable<HealthCheckInput, Trace>,
-): Promise<IEither<Error, HealthCheckOutput>> =>
- input.bimap(TraceUtil.withFunctionTrace(healthCheck))
- .move(getRequiredEnv("LAMINAR_HOST"))
- // we need to test LAMINAR_HOST is propagated to getStdout for other procedures
- .map(({ item }) => item.moveRight(["laminarc", "show-jobs"]))
- .map((i) =>
- i.item.mapRight(i.move.apply)
- .flatMapAsync(getStdout.apply)
- .then((gotJobs) => gotJobs.moveRight(HealthCheckOutput.YAASQUEEN))
- )
- .item;
+export class LizCIServer {
+ private constructor(
+ private readonly healthCheckActivity = HealthCheckActivity(healthCheck),
+ private readonly jobHookActivity = JobHookActivity(jobQueuer),
+ private readonly fourOhFourActivity = FourOhFourActivity(),
+ ) {}
-//export class LizCIServer {
-// private constructor(
-// private readonly healthCheckActivity = HealthCheckActivity(healthCheck),
-// private readonly jobHookActivity = JobHookActivity(jobQueuer)
-// ) {}
-//
-// private async route(req: ITraceable<req: Request, LogTraceable>) {
-// 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<Response> {
-// return LogTraceable(req).bimap(TraceUtil.withClassTrace(this)).map(this.route)
-// }
-//}
-//private route(
-// req: Traceable<Request & { pathname: string }>,
-//): Traceable<Promise<Response>> {
-// return req.flatMap((req) => {
-// const { logger, item: { method, pathname } } = req;
-// if (pathname === "/health") {
-// return this.healthCheckActivity.healthCheck(req);
-// }
-// return this.jobHookActivity.processHook(req);
-// });
-//}
+ private async route(req: LogTraceable<Request>) {
+ return req.flatMap((req) => {
+ const { item: request } = req;
+ const url = new URL(request.url);
+ if (url.pathname === "/health") {
+ return this.healthCheckActivity.healthCheck(req);
+ }
+ if (url.pathname === "/job") {
+ return this.jobHookActivity.processHook(req);
+ }
+ });
+ }
+
+ public async serve(req: Request): Promise<Response> {
+ return LogTraceable(req).bimap(TraceUtil.withClassTrace(this)).map(
+ this.route,
+ );
+ }
+}
diff --git a/u/fn/callable.ts b/u/fn/callable.ts
index 2749947..a087928 100644
--- a/u/fn/callable.ts
+++ b/u/fn/callable.ts
@@ -13,3 +13,7 @@ export interface SideEffect<T> extends Callable<void, T> {
export interface Mapper<T, U> extends Callable<U, T> {
(t: T): U;
}
+
+export interface BiMapper<T, U, R> extends Callable {
+ (t: T, u: U): R;
+}
diff --git a/u/fn/either.ts b/u/fn/either.ts
index 916bb71..9dc1027 100644
--- a/u/fn/either.ts
+++ b/u/fn/either.ts
@@ -1,10 +1,16 @@
-import type { Mapper, Supplier } from "@emprespresso/pengueno";
+import type { BiMapper, Mapper, Supplier } from "@emprespresso/pengueno";
+import { isObject } from "../leftpadesque/mod.ts";
+
+type IEitherTag = "IEither";
+const iEitherTag: IEitherTag = "IEither";
export interface IEither<E, T> {
+ readonly _tag: IEitherTag;
mapBoth: <Ee, Tt>(
errBranch: Mapper<E, Ee>,
okBranch: Mapper<T, Tt>,
) => IEither<Ee, Tt>;
+ fold: <Tt>(folder: BiMapper<E | null, T | null, Tt>) => Tt;
moveRight: <Tt>(t: Tt) => IEither<E, Tt>;
mapRight: <Tt>(mapper: Mapper<T, Tt>) => IEither<E, Tt>;
mapLeft: <Ee>(mapper: Mapper<E, Ee>) => IEither<Ee, T>;
@@ -15,14 +21,20 @@ export interface IEither<E, T> {
}
export class Either<E, T> implements IEither<E, T> {
- private constructor(private readonly err?: E, private readonly ok?: T) {}
+ private constructor(
+ private readonly err?: E,
+ private readonly ok?: T,
+ public readonly _tag: IEitherTag = iEitherTag,
+ ) {}
- public moveRight<Tt>(
- t: Tt,
- ) {
+ public moveRight<Tt>(t: Tt) {
return this.mapRight(() => t);
}
+ public fold<R>(folder: BiMapper<E | null, T | null, R>): R {
+ return folder(this.err ?? null, this.ok ?? null);
+ }
+
public mapBoth<Ee, Tt>(
errBranch: Mapper<E, Ee>,
okBranch: Mapper<T, Tt>,
@@ -37,7 +49,7 @@ export class Either<E, T> implements IEither<E, T> {
}
public mapRight<Tt>(mapper: Mapper<T, Tt>): IEither<E, Tt> {
- if (this.ok !== undefined) return Either.right(mapper(this.ok));
+ if (this.ok !== undefined) return Either.right<E, Tt>(mapper(this.ok));
return Either.left<E, Tt>(this.err!);
}
@@ -79,3 +91,7 @@ export class Either<E, T> implements IEither<E, T> {
}
}
}
+
+export const isEither = <E, T>(o: unknown): o is IEither<E, T> => {
+ return isObject(o) && "_tag" in o && o._tag === "IEither";
+};
diff --git a/u/server/activity/fourohfour.ts b/u/server/activity/fourohfour.ts
new file mode 100644
index 0000000..698dacd
--- /dev/null
+++ b/u/server/activity/fourohfour.ts
@@ -0,0 +1,30 @@
+import {
+ type ITraceable,
+ JsonResponse,
+ TraceUtil,
+} from "@emprespresso/pengueno";
+
+export enum HealthCheckInput {
+ CHECK,
+}
+export enum HealthCheckOutput {
+ YAASQUEEN,
+}
+
+const messages = [
+ "(≧ω≦)ゞ Oopsie! This endpoint has gone a-404-dable!",
+ "。゚(。ノωヽ。)゚。 Meow-t found! Your API call ran away!",
+ "404-bidden! But like...in a cute way (・`ω´・) !",
+ "(=①ω①=) This endpoint is hiss-terically missing!",
+ "┐(´∀`)┌ Whoopsie fluff! No API here!",
+ "(つ≧▽≦)つ Your data went on a paw-sible vacation!",
+ "(ꈍᴗꈍ) Uwu~ not found, but found our hearts instead!",
+ "ヽ(;▽;)ノ Eep! This route has ghosted you~",
+];
+export const FourOhFourActivity = <Trace>(req: ITraceable<Request, Trace>) =>
+ req.bimap(TraceUtil.withFunctionTrace(FourOhFourActivity))
+ .map(() =>
+ new JsonResponse(messages[Math.random() * messages.length], {
+ status: 404,
+ })
+ );
diff --git a/u/server/activity/health.ts b/u/server/activity/health.ts
index 7ee6629..98acbb8 100644
--- a/u/server/activity/health.ts
+++ b/u/server/activity/health.ts
@@ -27,15 +27,19 @@ export const HealthCheckActivity = <Trace>(
health.mapBoth((e) => {
trace.addTrace(LogLevel.ERROR).trace(`${e}`);
return new Response(
- "oh no, i need to eat more vegetables (。•́︿•̀。)...\n",
- { status: 500 },
+ JSON.stringify({
+ message: "oh no, i need to eat more vegetables (。•́︿•̀。)...",
+ }),
+ { status: 500, headers: { "Content-Type": "application/json" } },
);
}, (_healthy) => {
const msg = `think im healthy!! (✿˘◡˘) ready to do work~`;
trace.trace(msg);
return new Response(
- msg + "\n",
- { status: 200 },
+ JSON.stringify({
+ message: "oh no, i need to eat more vegetables (。•́︿•̀。)...",
+ }),
+ { status: 500, headers: { "Content-Type": "application/json" } },
);
});
}));
diff --git a/u/server/activity/mod.ts b/u/server/activity/mod.ts
index 9d05d3c..f0cbed2 100644
--- a/u/server/activity/mod.ts
+++ b/u/server/activity/mod.ts
@@ -1,10 +1,15 @@
-import type { RequestFilter } from "@emprespresso/pengueno";
+import { JsonResponse, type RequestFilter } from "@emprespresso/pengueno";
-export class r200 extends Response {
- public override readonly status = 200;
+export enum StatusOK {
+ FOLLOW = 300,
+ OK = 200,
+}
+export interface ActivityOk {
+ readonly status: StatusOK;
}
-export interface IActivity<Trace> extends RequestFilter<r200, Trace> {
+export interface IActivity<Trace>
+ extends RequestFilter<ActivityOk, Trace, Response> {
}
export * from "./health.ts";
diff --git a/u/server/filter/json.ts b/u/server/filter/json.ts
index f8e4607..1e05bad 100644
--- a/u/server/filter/json.ts
+++ b/u/server/filter/json.ts
@@ -7,27 +7,30 @@ import {
TraceUtil,
} from "@emprespresso/pengueno";
-type JsonTransformer<JsonT, R> = (
- json: ITraceable<JsonT>,
+type JsonTransformer<R, Trace> = (
+ json: ITraceable<unknown, Trace>,
) => IEither<Error, R>;
-export const json = <BodyT, Trace, JsonT = unknown>(
- jsonTransformer: JsonTransformer<JsonT, BodyT>,
-): RequestFilter<BodyT, Trace> =>
+export const json = <BodyT, Trace>(
+ jsonTransformer: JsonTransformer<BodyT, Trace>,
+): RequestFilter<BodyT, Trace, Error> =>
(r: ITraceable<Request, Trace>) =>
r.bimap(TraceUtil.withFunctionTrace(json))
- .map(({ item: request }) => Either.fromFailableAsync(request.json()))
- .map(
- TraceUtil.promiseify(({ item: eitherJson, trace }) =>
- eitherJson.mapLeft((errReason) => {
- trace.addTrace(LogLevel.WARN).trace(`${errReason}`);
- const err = "seems to be invalid JSON (>//<) can you fix?";
- return new Error(err);
- })
- .flatMap(jsonTransformer)
- .mapLeft((err) => {
- trace.addTrace(LogLevel.WARN).trace(`${err}`);
- return new Response(err.message, { status: 400 });
+ .map(({ item: request, trace }) =>
+ Either.fromFailableAsync<Error, BodyT>(request.json())
+ .then((either) =>
+ either.mapLeft((errReason) => {
+ trace.addTrace(LogLevel.WARN).trace(`${errReason}`);
+ return new Error("seems to be invalid JSON (>//<) can you fix?");
})
+ )
+ )
+ .flatMapAsync(
+ TraceUtil.promiseify((traceableEitherJson) =>
+ traceableEitherJson.map(({ item }) =>
+ item.mapRight(traceableEitherJson.move).flatMap(
+ jsonTransformer,
+ )
+ )
),
)
.item;
diff --git a/u/server/filter/method.ts b/u/server/filter/method.ts
index a1401b4..8d13406 100644
--- a/u/server/filter/method.ts
+++ b/u/server/filter/method.ts
@@ -1,6 +1,7 @@
import {
Either,
type ITraceable,
+ JsonResponse,
LogLevel,
type RequestFilter,
TraceUtil,
@@ -17,22 +18,23 @@ type HttpMethod =
| "TRACE"
| "PATCH";
-export const requireMethod =
- <Trace>(methods: Array<HttpMethod>): RequestFilter<HttpMethod, Trace> =>
- (req: ITraceable<Request, Trace>) =>
- req.bimap(TraceUtil.withFunctionTrace(requireMethod))
- .map(({ item }) => Promise.resolve(item))
- .map(TraceUtil.promiseify(({ item: request, trace }) => {
- const { method: _method } = request;
- const method = <HttpMethod> _method;
- if (!methods.includes(method)) {
- const msg = "that's not how you pet me (⋟﹏⋞)~";
- trace.addTrace(LogLevel.WARN).trace(msg);
- return Either.left<Response, HttpMethod>(
- new Response(msg + "\n", { status: 405 }),
- );
- }
+export const requireMethod = <Trace>(
+ methods: Array<HttpMethod>,
+): RequestFilter<HttpMethod, Trace, JsonResponse> =>
+(req: ITraceable<Request, Trace>) =>
+ req.bimap(TraceUtil.withFunctionTrace(requireMethod))
+ .move(Promise.resolve(req.item))
+ .map(TraceUtil.promiseify(({ item: request, trace }) => {
+ const { method: _method } = request;
+ const method = <HttpMethod> _method;
+ if (!methods.includes(method)) {
+ const msg = "that's not how you pet me (⋟﹏⋞)~";
+ trace.addTrace(LogLevel.WARN).trace(msg);
+ return Either.left<JsonResponse, HttpMethod>(
+ new JsonResponse(msg, { status: 405 }),
+ );
+ }
- return Either.right<Response, HttpMethod>(method);
- }))
- .item;
+ return Either.right<JsonResponse, HttpMethod>(method);
+ }))
+ .item;
diff --git a/u/server/filter/mod.ts b/u/server/filter/mod.ts
index 3256d35..78fbd00 100644
--- a/u/server/filter/mod.ts
+++ b/u/server/filter/mod.ts
@@ -3,8 +3,8 @@ import type { IEither, ITraceable } from "@emprespresso/pengueno";
export interface RequestFilter<
T,
Trace,
+ Err,
RIn = ITraceable<Request, Trace>,
- Err = Response,
> {
(req: RIn): Promise<IEither<Err, T>>;
}
diff --git a/u/server/metrics.ts b/u/server/metrics.ts
new file mode 100644
index 0000000..05df967
--- /dev/null
+++ b/u/server/metrics.ts
@@ -0,0 +1,112 @@
+import {
+ type BiMapper,
+ Either,
+ type IEither,
+ type ITraceable,
+ type Mapper,
+ type Supplier,
+} from "@emprespresso/pengueno";
+
+export enum Unit {
+ COUNT,
+ MILLISECONDS,
+}
+
+export interface IMetric<MetricT extends string, TUnit extends Unit> {
+ readonly metric: MetricT;
+ readonly unit: TUnit;
+ readonly value: number;
+ readonly emissionTimestamp: Date;
+}
+
+export type BaseMetricT = string;
+export interface CountMetric<MetricT extends BaseMetricT>
+ extends IMetric<MetricT, Unit.COUNT> {
+ readonly unit: Unit.COUNT;
+}
+
+export interface TimeMetric<MetricT extends BaseMetricT>
+ extends IMetric<MetricT, Unit.MILLISECONDS> {
+ readonly unit: Unit.MILLISECONDS;
+}
+
+export interface IMetricsData<
+ MetricT extends BaseMetricT,
+ Tracing,
+ TraceW,
+> {
+ addCount: BiMapper<MetricT, number, CountMetric<MetricT>>;
+
+ stopwatch: BiMapper<
+ MetricT,
+ ITraceable<Tracing, TraceW>,
+ ITraceable<MetricT, TraceW>
+ >;
+ endStopwatch: Mapper<
+ ITraceable<MetricT, TraceW>,
+ IEither<Error, TimeMetric<MetricT>>
+ >;
+
+ flush: Supplier<Array<IMetric<MetricT, Unit>>>;
+}
+
+export class TraceableMetricsData<MetricT extends BaseMetricT, Tracing, Trace>
+ implements IMetricsData<MetricT, Tracing, Trace> {
+ private readonly timers: Map<ITraceable<MetricT, Trace>, Date> = new Map();
+ private metricBuffer: Array<IMetric<MetricT, Unit>> = [];
+
+ private constructor() {}
+
+ private addMetric<TUnit extends Unit>(
+ metric: MetricT,
+ unit: TUnit,
+ value: number,
+ ): IMetric<MetricT, TUnit> {
+ const _metric = {
+ metric,
+ unit,
+ value,
+ emissionTimestamp: new Date(),
+ };
+ this.metricBuffer.push(_metric);
+ return _metric;
+ }
+
+ public flush() {
+ const metrics = [...this.metricBuffer];
+ this.metricBuffer = [];
+ return metrics;
+ }
+
+ public addCount(
+ metric: MetricT,
+ count: number,
+ ): CountMetric<MetricT> {
+ return this.addMetric(metric, Unit.COUNT, count);
+ }
+
+ public stopwatch(metric: MetricT, traceable: ITraceable<Tracing, Trace>) {
+ const timer = traceable.move(metric);
+ this.timers.set(timer, new Date());
+ return timer;
+ }
+
+ public endStopwatch(
+ stopwatch: ITraceable<MetricT, Trace>,
+ ): IEither<Error, TimeMetric<MetricT>> {
+ const now = new Date();
+ if (this.timers.has(stopwatch)) {
+ const timer = this.timers.get(stopwatch)!;
+ const diff = now.getTime() - timer.getTime();
+ this.timers.delete(stopwatch);
+ return Either.right<Error, TimeMetric<MetricT>>(
+ this.addMetric(stopwatch.item, Unit.MILLISECONDS, diff) as TimeMetric<
+ MetricT
+ >,
+ );
+ }
+ return Either.left<Error, TimeMetric<MetricT>>(
+ new Error("cannot stop stopwatch before starting it"),
+ );
+ }
+}
diff --git a/u/server/mod.ts b/u/server/mod.ts
index 52b26e2..50f82dd 100644
--- a/u/server/mod.ts
+++ b/u/server/mod.ts
@@ -1,2 +1,3 @@
-export * from "./filter/mod.ts";
+export * from "./response.ts";
export * from "./activity/mod.ts";
+export * from "./filter/mod.ts";
diff --git a/u/server/request.ts b/u/server/request.ts
new file mode 100644
index 0000000..6c4e602
--- /dev/null
+++ b/u/server/request.ts
@@ -0,0 +1,27 @@
+import { ITrace } from "@emprespresso/pengueno";
+import { ITraceWith } from "../trace/mod.ts";
+
+class RequestTraceWith {
+ private constructor(
+ public readonly id: string,
+ public readonly received: Date,
+ ) {
+ }
+
+ public static from() {
+ const id = crypto.randomUUID();
+ const received = new Date();
+ return new RequestTraceWith(id, received);
+ }
+}
+export class RequestTrace implements ITrace<RequestTraceWith> {
+ public readonly requestTrace: RequestTraceWith;
+ constructor(reques);
+
+ public addTrace(_t: ITraceWith<RequestTraceWith>) {
+ return;
+ }
+
+ addTrace: Mapper<ITraceWith<TraceWith>, ITrace<TraceWith>>;
+ trace: SideEffect<ITraceWith<TraceWith>>;
+}
diff --git a/u/server/response.ts b/u/server/response.ts
new file mode 100644
index 0000000..59ca43d
--- /dev/null
+++ b/u/server/response.ts
@@ -0,0 +1,44 @@
+import {
+ type IEither,
+ isEither,
+ type ITraceable,
+} from "@emprespresso/pengueno";
+
+export type ResponseBody = object | string;
+export type TResponseInit = ResponseInit & { status: number };
+const withJsonResponseType = (opts: TResponseInit): TResponseInit => {
+ return {
+ ...opts,
+ headers: {
+ "Content-Type": "application/json",
+ ...(opts?.headers),
+ },
+ };
+};
+
+export class JsonResponse extends Response {
+ constructor(
+ e: ITraceable<IEither<ResponseBody, ResponseBody>>,
+ opts: TResponseInit,
+ ) {
+ const responseOpts = withJsonResponseType(opts);
+ const baseBody = {
+ responseTime: Date.now(),
+ };
+ if (isEither<ResponseBody, ResponseBody>(e)) {
+ super(
+ JSON.stringify(
+ e.fold((err, ok) => err ? ({ error: err! }) : ({ ok: ok! })),
+ ),
+ responseOpts,
+ );
+ return;
+ }
+ super(
+ JSON.stringify(
+ (Math.floor(responseOpts.status / 100) < 4) ? { ok: e } : { error: e },
+ ),
+ responseOpts,
+ );
+ }
+}