From ef51b25e4388cbdf3a27e23d9f1fa381ae20a5ad Mon Sep 17 00:00:00 2001 From: Elizabeth Hunt Date: Fri, 16 May 2025 16:17:13 -0700 Subject: snapshot --- u/fn/callable.ts | 4 ++ u/fn/either.ts | 28 +++++++--- u/server/activity/fourohfour.ts | 30 +++++++++++ u/server/activity/health.ts | 12 +++-- u/server/activity/mod.ts | 13 +++-- u/server/filter/json.ts | 37 +++++++------ u/server/filter/method.ts | 38 +++++++------- u/server/filter/mod.ts | 2 +- u/server/metrics.ts | 112 ++++++++++++++++++++++++++++++++++++++++ u/server/mod.ts | 3 +- u/server/request.ts | 27 ++++++++++ u/server/response.ts | 44 ++++++++++++++++ 12 files changed, 299 insertions(+), 51 deletions(-) create mode 100644 u/server/activity/fourohfour.ts create mode 100644 u/server/metrics.ts create mode 100644 u/server/request.ts create mode 100644 u/server/response.ts (limited to 'u') 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 extends Callable { export interface Mapper extends Callable { (t: T): U; } + +export interface BiMapper 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 { + readonly _tag: IEitherTag; mapBoth: ( errBranch: Mapper, okBranch: Mapper, ) => IEither; + fold: (folder: BiMapper) => Tt; moveRight: (t: Tt) => IEither; mapRight: (mapper: Mapper) => IEither; mapLeft: (mapper: Mapper) => IEither; @@ -15,14 +21,20 @@ export interface IEither { } export class Either implements IEither { - 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( - t: Tt, - ) { + public moveRight(t: Tt) { return this.mapRight(() => t); } + public fold(folder: BiMapper): R { + return folder(this.err ?? null, this.ok ?? null); + } + public mapBoth( errBranch: Mapper, okBranch: Mapper, @@ -37,7 +49,7 @@ export class Either implements IEither { } public mapRight(mapper: Mapper): IEither { - if (this.ok !== undefined) return Either.right(mapper(this.ok)); + if (this.ok !== undefined) return Either.right(mapper(this.ok)); return Either.left(this.err!); } @@ -79,3 +91,7 @@ export class Either implements IEither { } } } + +export const isEither = (o: unknown): o is IEither => { + 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 = (req: ITraceable) => + 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 = ( 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 extends RequestFilter { +export interface IActivity + extends RequestFilter { } 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 = ( - json: ITraceable, +type JsonTransformer = ( + json: ITraceable, ) => IEither; -export const json = ( - jsonTransformer: JsonTransformer, -): RequestFilter => +export const json = ( + jsonTransformer: JsonTransformer, +): RequestFilter => (r: ITraceable) => 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(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 = - (methods: Array): RequestFilter => - (req: ITraceable) => - req.bimap(TraceUtil.withFunctionTrace(requireMethod)) - .map(({ item }) => Promise.resolve(item)) - .map(TraceUtil.promiseify(({ item: request, trace }) => { - const { method: _method } = request; - const method = _method; - if (!methods.includes(method)) { - const msg = "that's not how you pet me (⋟﹏⋞)~"; - trace.addTrace(LogLevel.WARN).trace(msg); - return Either.left( - new Response(msg + "\n", { status: 405 }), - ); - } +export const requireMethod = ( + methods: Array, +): RequestFilter => +(req: ITraceable) => + req.bimap(TraceUtil.withFunctionTrace(requireMethod)) + .move(Promise.resolve(req.item)) + .map(TraceUtil.promiseify(({ item: request, trace }) => { + const { method: _method } = request; + const method = _method; + if (!methods.includes(method)) { + const msg = "that's not how you pet me (⋟﹏⋞)~"; + trace.addTrace(LogLevel.WARN).trace(msg); + return Either.left( + new JsonResponse(msg, { status: 405 }), + ); + } - return Either.right(method); - })) - .item; + return Either.right(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, - Err = Response, > { (req: RIn): Promise>; } 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 { + readonly metric: MetricT; + readonly unit: TUnit; + readonly value: number; + readonly emissionTimestamp: Date; +} + +export type BaseMetricT = string; +export interface CountMetric + extends IMetric { + readonly unit: Unit.COUNT; +} + +export interface TimeMetric + extends IMetric { + readonly unit: Unit.MILLISECONDS; +} + +export interface IMetricsData< + MetricT extends BaseMetricT, + Tracing, + TraceW, +> { + addCount: BiMapper>; + + stopwatch: BiMapper< + MetricT, + ITraceable, + ITraceable + >; + endStopwatch: Mapper< + ITraceable, + IEither> + >; + + flush: Supplier>>; +} + +export class TraceableMetricsData + implements IMetricsData { + private readonly timers: Map, Date> = new Map(); + private metricBuffer: Array> = []; + + private constructor() {} + + private addMetric( + metric: MetricT, + unit: TUnit, + value: number, + ): IMetric { + 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 { + return this.addMetric(metric, Unit.COUNT, count); + } + + public stopwatch(metric: MetricT, traceable: ITraceable) { + const timer = traceable.move(metric); + this.timers.set(timer, new Date()); + return timer; + } + + public endStopwatch( + stopwatch: ITraceable, + ): IEither> { + 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>( + this.addMetric(stopwatch.item, Unit.MILLISECONDS, diff) as TimeMetric< + MetricT + >, + ); + } + return Either.left>( + 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 { + public readonly requestTrace: RequestTraceWith; + constructor(reques); + + public addTrace(_t: ITraceWith) { + return; + } + + addTrace: Mapper, ITrace>; + trace: SideEffect>; +} 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>, + opts: TResponseInit, + ) { + const responseOpts = withJsonResponseType(opts); + const baseBody = { + responseTime: Date.now(), + }; + if (isEither(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, + ); + } +} -- cgit v1.2.3-70-g09d2