diff options
author | Elizabeth Hunt <me@liz.coffee> | 2025-06-20 14:53:38 -0700 |
---|---|---|
committer | Elizabeth Hunt <me@liz.coffee> | 2025-06-20 14:53:38 -0700 |
commit | d4791f3d357634daf506fb8f91cc5332a794c421 (patch) | |
tree | 1bb01d2d4d8fa74d83bb6f99f2c8aa4146ca2d11 /u/server | |
parent | d7e8d31c94cd713a2f4cf799e20e993acc69e361 (diff) | |
download | ci-d4791f3d357634daf506fb8f91cc5332a794c421.tar.gz ci-d4791f3d357634daf506fb8f91cc5332a794c421.zip |
Move to nodejs
Diffstat (limited to 'u/server')
-rw-r--r-- | u/server/activity/fourohfour.ts | 37 | ||||
-rw-r--r-- | u/server/activity/health.ts | 108 | ||||
-rw-r--r-- | u/server/activity/index.ts | 8 | ||||
-rw-r--r-- | u/server/activity/mod.ts | 13 | ||||
-rw-r--r-- | u/server/filter/index.ts | 34 | ||||
-rw-r--r-- | u/server/filter/json.ts | 92 | ||||
-rw-r--r-- | u/server/filter/method.ts | 67 | ||||
-rw-r--r-- | u/server/filter/mod.ts | 35 | ||||
-rw-r--r-- | u/server/index.ts | 7 | ||||
-rw-r--r-- | u/server/mod.ts | 7 | ||||
-rw-r--r-- | u/server/request.ts | 74 | ||||
-rw-r--r-- | u/server/response.ts | 139 |
12 files changed, 291 insertions, 330 deletions
diff --git a/u/server/activity/fourohfour.ts b/u/server/activity/fourohfour.ts index 33cfe5f..cd90ba0 100644 --- a/u/server/activity/fourohfour.ts +++ b/u/server/activity/fourohfour.ts @@ -1,29 +1,28 @@ import { - type IActivity, - type ITraceable, - JsonResponse, - type PenguenoRequest, - type ServerTrace, -} from "@emprespresso/pengueno"; + type IActivity, + type ITraceable, + JsonResponse, + type PenguenoRequest, + type ServerTrace, +} from '@emprespresso/pengueno'; const messages = [ - "D: meow-t found! your api call ran away!", - "404-bidden! but like...in a cute way >:3 !", - ":< your data went on a paw-sible vacation!", - "uwu~ not found, but found our hearts instead!", + 'D: meow-t found! your api call ran away!', + '404-bidden! but like...in a cute way >:3 !', + ':< your data went on a paw-sible vacation!', + 'uwu~ not found, but found our hearts instead!', ]; -const randomFourOhFour = () => - messages[Math.floor(Math.random() * messages.length)]; +const randomFourOhFour = () => messages[Math.floor(Math.random() * messages.length)]!; export interface IFourOhFourActivity { - fourOhFour: IActivity; + fourOhFour: IActivity; } export class FourOhFourActivityImpl implements IFourOhFourActivity { - public fourOhFour(req: ITraceable<PenguenoRequest, ServerTrace>) { - return req - .move(new JsonResponse(req, randomFourOhFour(), { status: 404 })) - .map((resp) => Promise.resolve(resp.get())) - .get(); - } + public fourOhFour(req: ITraceable<PenguenoRequest, ServerTrace>) { + return req + .move(new JsonResponse(req, randomFourOhFour(), { status: 404 })) + .map((resp) => Promise.resolve(resp.get())) + .get(); + } } diff --git a/u/server/activity/health.ts b/u/server/activity/health.ts index 95dfa97..b3ae559 100644 --- a/u/server/activity/health.ts +++ b/u/server/activity/health.ts @@ -1,71 +1,67 @@ import { - type IActivity, - type IEither, - type ITraceable, - JsonResponse, - LogLevel, - type Mapper, - Metric, - type PenguenoRequest, - type ServerTrace, - TraceUtil, -} from "@emprespresso/pengueno"; + type IActivity, + type IEither, + IMetric, + type ITraceable, + JsonResponse, + LogLevel, + type Mapper, + Metric, + type PenguenoRequest, + type ServerTrace, + TraceUtil, +} from '@emprespresso/pengueno'; export enum HealthCheckInput { - CHECK, + CHECK, } export enum HealthCheckOutput { - YAASSSLAYQUEEN, + YAASSSLAYQUEEN, } export interface IHealthCheckActivity { - checkHealth: IActivity; + checkHealth: IActivity; } -const healthCheckMetric = Metric.fromName("Health"); +const healthCheckMetric: IMetric = Metric.fromName('Health'); export interface HealthChecker - extends Mapper< - ITraceable<HealthCheckInput, ServerTrace>, - Promise<IEither<Error, HealthCheckOutput>> - > {} + extends Mapper<ITraceable<HealthCheckInput, ServerTrace>, Promise<IEither<Error, HealthCheckOutput>>> {} export class HealthCheckActivityImpl implements IHealthCheckActivity { - constructor(private readonly check: HealthChecker) {} + constructor(private readonly check: HealthChecker) {} - public checkHealth(req: ITraceable<PenguenoRequest, ServerTrace>) { - return req - .bimap(TraceUtil.withFunctionTrace(this.checkHealth)) - .bimap(TraceUtil.withMetricTrace(healthCheckMetric)) - .flatMap((r) => - r.move(HealthCheckInput.CHECK).map((input) => this.check(input)), - ) - .peek( - TraceUtil.promiseify((h) => - h.get().fold(({ isLeft, value }) => { - if (!isLeft) { - h.trace.trace(healthCheckMetric.success); - return; - } - h.trace.trace(healthCheckMetric.failure); - h.trace.addTrace(LogLevel.ERROR).trace(value); - }), - ), - ) - .map( - TraceUtil.promiseify((h) => - h - .get() - .mapBoth( - () => "oh no, i need to eat more vegetables (。•́︿•̀。)...", - () => "think im healthy!! (✿˘◡˘) ready to do work~", + public checkHealth(req: ITraceable<PenguenoRequest, ServerTrace>) { + return req + .bimap(TraceUtil.withFunctionTrace(this.checkHealth)) + .bimap(TraceUtil.withMetricTrace(healthCheckMetric)) + .flatMap((r) => r.move(HealthCheckInput.CHECK).map((input) => this.check(input))) + .peek( + TraceUtil.promiseify((h) => + h.get().fold(({ isLeft, value }) => { + if (!isLeft) { + h.trace.trace(healthCheckMetric.success); + return; + } + h.trace.trace(healthCheckMetric.failure); + h.trace.addTrace(LogLevel.ERROR).trace(value); + }), + ), ) - .fold( - ({ isLeft, value: message }) => - new JsonResponse(req, message, { - status: isLeft ? 500 : 200, - }), - ), - ), - ) - .get(); - } + .map( + TraceUtil.promiseify((h) => + h + .get() + .mapBoth( + () => 'oh no, i need to eat more vegetables (。•́︿•̀。)...', + () => 'think im healthy!! (✿˘◡˘) ready to do work~', + ) + .fold( + ({ isLeft, value: message }) => + new JsonResponse(req, message, { + status: isLeft ? 500 : 200, + }), + ), + ), + ) + .get(); + } } diff --git a/u/server/activity/index.ts b/u/server/activity/index.ts new file mode 100644 index 0000000..fa0a6b2 --- /dev/null +++ b/u/server/activity/index.ts @@ -0,0 +1,8 @@ +import type { ITraceable, PenguenoRequest, PenguenoResponse, ServerTrace } from '@emprespresso/pengueno'; + +export interface IActivity { + (req: ITraceable<PenguenoRequest, ServerTrace>): Promise<PenguenoResponse>; +} + +export * from './health.js'; +export * from './fourohfour.js'; diff --git a/u/server/activity/mod.ts b/u/server/activity/mod.ts deleted file mode 100644 index 82d8ec4..0000000 --- a/u/server/activity/mod.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { - ITraceable, - PenguenoRequest, - PenguenoResponse, - ServerTrace, -} from "@emprespresso/pengueno"; - -export interface IActivity { - (req: ITraceable<PenguenoRequest, ServerTrace>): Promise<PenguenoResponse>; -} - -export * from "./health.ts"; -export * from "./fourohfour.ts"; diff --git a/u/server/filter/index.ts b/u/server/filter/index.ts new file mode 100644 index 0000000..62a584d --- /dev/null +++ b/u/server/filter/index.ts @@ -0,0 +1,34 @@ +import { + type IEither, + type ITraceable, + LogLevel, + type PenguenoRequest, + type ServerTrace, +} from '@emprespresso/pengueno'; + +export enum ErrorSource { + USER = LogLevel.WARN, + SYSTEM = LogLevel.ERROR, +} + +export class PenguenoError extends Error { + public readonly source: ErrorSource; + constructor( + override readonly message: string, + public readonly status: number, + ) { + super(message); + this.source = Math.floor(status / 100) === 4 ? ErrorSource.USER : ErrorSource.SYSTEM; + } +} + +export interface RequestFilter< + T, + Err extends PenguenoError = PenguenoError, + RIn = ITraceable<PenguenoRequest, ServerTrace>, +> { + (req: RIn): Promise<IEither<Err, T>>; +} + +export * from './method.js'; +export * from './json.js'; diff --git a/u/server/filter/json.ts b/u/server/filter/json.ts index 145d1be..527d483 100644 --- a/u/server/filter/json.ts +++ b/u/server/filter/json.ts @@ -1,54 +1,50 @@ import { - Either, - type IEither, - type ITraceable, - LogLevel, - Metric, - PenguenoError, - type PenguenoRequest, - type RequestFilter, - type ServerTrace, - TraceUtil, -} from "@emprespresso/pengueno"; + Either, + type IEither, + type ITraceable, + LogLevel, + Metric, + PenguenoError, + type PenguenoRequest, + type RequestFilter, + type ServerTrace, + TraceUtil, +} from '@emprespresso/pengueno'; export interface JsonTransformer<R, ParsedJson = unknown> { - (json: ITraceable<ParsedJson, ServerTrace>): IEither<PenguenoError, R>; + (json: ITraceable<ParsedJson, ServerTrace>): IEither<PenguenoError, R>; } -const ParseJsonMetric = Metric.fromName("JsonParse"); +const ParseJsonMetric = Metric.fromName('JsonParse'); export const jsonModel = - <MessageT>( - jsonTransformer: JsonTransformer<MessageT>, - ): RequestFilter<MessageT> => - (r: ITraceable<PenguenoRequest, ServerTrace>) => - r - .bimap(TraceUtil.withFunctionTrace(jsonModel)) - .bimap(TraceUtil.withMetricTrace(ParseJsonMetric)) - .map((j) => - Either.fromFailableAsync<Error, MessageT>(() => j.get().json()).then( - (either) => - either.mapLeft((errReason) => { - j.trace.addTrace(LogLevel.WARN).trace(errReason); - return new PenguenoError( - "seems to be invalid JSON (>//<) can you fix?", - 400, - ); - }), - ), - ) - .peek( - TraceUtil.promiseify((traceableEither) => - traceableEither.get().fold(({ isLeft }) => - traceableEither.trace.trace(ParseJsonMetric[isLeft ? "failure" : "success"]) - ), - ), - ) - .map( - TraceUtil.promiseify((traceableEitherJson) => - traceableEitherJson - .get() - .mapRight((j) => traceableEitherJson.move(j)) - .flatMap(jsonTransformer), - ), - ) - .get(); + <MessageT>(jsonTransformer: JsonTransformer<MessageT>): RequestFilter<MessageT> => + (r: ITraceable<PenguenoRequest, ServerTrace>) => + r + .bimap(TraceUtil.withFunctionTrace(jsonModel)) + .bimap(TraceUtil.withMetricTrace(ParseJsonMetric)) + .map((j) => + Either.fromFailableAsync<Error, MessageT>(<Promise<MessageT>>j.get().json()).then((either) => + either.mapLeft((errReason) => { + j.trace.addTrace(LogLevel.WARN).trace(errReason); + return new PenguenoError('seems to be invalid JSON (>//<) can you fix?', 400); + }), + ), + ) + .peek( + TraceUtil.promiseify((traceableEither) => + traceableEither + .get() + .fold(({ isLeft }) => + traceableEither.trace.trace(ParseJsonMetric[isLeft ? 'failure' : 'success']), + ), + ), + ) + .map( + TraceUtil.promiseify((traceableEitherJson) => + traceableEitherJson + .get() + .mapRight((j) => traceableEitherJson.move(j)) + .flatMap(jsonTransformer), + ), + ) + .get(); diff --git a/u/server/filter/method.ts b/u/server/filter/method.ts index 9901c6f..5ca5716 100644 --- a/u/server/filter/method.ts +++ b/u/server/filter/method.ts @@ -1,43 +1,32 @@ import { - Either, - type ITraceable, - LogLevel, - PenguenoError, - type PenguenoRequest, - type RequestFilter, - type ServerTrace, - TraceUtil, -} from "@emprespresso/pengueno"; + Either, + type ITraceable, + LogLevel, + PenguenoError, + type PenguenoRequest, + type RequestFilter, + type ServerTrace, + TraceUtil, +} from '@emprespresso/pengueno'; -type HttpMethod = - | "POST" - | "GET" - | "HEAD" - | "PUT" - | "DELETE" - | "CONNECT" - | "OPTIONS" - | "TRACE" - | "PATCH"; +type HttpMethod = 'POST' | 'GET' | 'HEAD' | 'PUT' | 'DELETE' | 'CONNECT' | 'OPTIONS' | 'TRACE' | 'PATCH'; export const requireMethod = - (methods: Array<HttpMethod>): RequestFilter<HttpMethod> => - (req: ITraceable<PenguenoRequest, ServerTrace>) => - req - .bimap(TraceUtil.withFunctionTrace(requireMethod)) - .move(Promise.resolve(req.get())) - .map( - TraceUtil.promiseify((t) => { - const { method: _method } = t.get(); - const method = <HttpMethod>_method; - if (!methods.includes(method)) { - const msg = "that's not how you pet me (⋟﹏⋞)~"; - t.trace.addTrace(LogLevel.WARN).trace(msg); - return Either.left<PenguenoError, HttpMethod>( - new PenguenoError(msg, 405), - ); - } - return Either.right<PenguenoError, HttpMethod>(method); - }), - ) - .get(); + (methods: Array<HttpMethod>): RequestFilter<HttpMethod> => + (req: ITraceable<PenguenoRequest, ServerTrace>) => + req + .bimap(TraceUtil.withFunctionTrace(requireMethod)) + .move(Promise.resolve(req.get())) + .map( + TraceUtil.promiseify((t) => { + const { method: _method } = t.get(); + const method = <HttpMethod>_method; + if (!methods.includes(method)) { + const msg = "that's not how you pet me (⋟﹏⋞)~"; + t.trace.addTrace(LogLevel.WARN).trace(msg); + return Either.left<PenguenoError, HttpMethod>(new PenguenoError(msg, 405)); + } + return Either.right<PenguenoError, HttpMethod>(method); + }), + ) + .get(); diff --git a/u/server/filter/mod.ts b/u/server/filter/mod.ts deleted file mode 100644 index 0e0a4cb..0000000 --- a/u/server/filter/mod.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { - type IEither, - type ITraceable, - LogLevel, - type PenguenoRequest, - type ServerTrace, -} from "@emprespresso/pengueno"; - -export enum ErrorSource { - USER = LogLevel.WARN, - SYSTEM = LogLevel.ERROR, -} - -export class PenguenoError extends Error { - public readonly source: ErrorSource; - constructor( - override readonly message: string, - public readonly status: number, - ) { - super(message); - this.source = - Math.floor(status / 100) === 4 ? ErrorSource.USER : ErrorSource.SYSTEM; - } -} - -export interface RequestFilter< - T, - Err extends PenguenoError = PenguenoError, - RIn = ITraceable<PenguenoRequest, ServerTrace>, -> { - (req: RIn): Promise<IEither<Err, T>>; -} - -export * from "./method.ts"; -export * from "./json.ts"; diff --git a/u/server/index.ts b/u/server/index.ts new file mode 100644 index 0000000..17cbbdf --- /dev/null +++ b/u/server/index.ts @@ -0,0 +1,7 @@ +import type { LogMetricTraceSupplier } from '@emprespresso/pengueno'; +export type ServerTrace = LogMetricTraceSupplier; + +export * from './activity/index.js'; +export * from './filter/index.js'; +export * from './response.js'; +export * from './request.js'; diff --git a/u/server/mod.ts b/u/server/mod.ts deleted file mode 100644 index 866b5f9..0000000 --- a/u/server/mod.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { LogMetricTraceSupplier } from "@emprespresso/pengueno"; -export type ServerTrace = LogMetricTraceSupplier; - -export * from "./activity/mod.ts"; -export * from "./filter/mod.ts"; -export * from "./response.ts"; -export * from "./request.ts"; diff --git a/u/server/request.ts b/u/server/request.ts index 72e812a..10610f1 100644 --- a/u/server/request.ts +++ b/u/server/request.ts @@ -1,49 +1,39 @@ -import { LogMetricTraceable, LogTraceable } from "@emprespresso/pengueno"; -import { TraceUtil } from "../trace/util.ts"; +import { TraceUtil, LogMetricTraceable, LogTraceable } from '@emprespresso/pengueno'; -const greetings = [ - "hewwo :D", - "hiya cutie", - "boop!", - "sending virtual hugs!", - "stay pawsitive", -]; -const penguenoGreeting = () => - greetings[Math.floor(Math.random() * greetings.length)]; +const greetings = ['hewwo :D', 'hiya cutie', 'boop!', 'sending virtual hugs!', 'stay pawsitive']; +const penguenoGreeting = () => greetings[Math.floor(Math.random() * greetings.length)]; export class PenguenoRequest extends Request { - private constructor( - _input: Request, - public readonly id: string, - public readonly at: Date, - ) { - super(_input); - } + private constructor( + _input: Request, + public readonly id: string, + public readonly at: Date, + ) { + super(_input); + } - public baseResponseHeaders(): Record<string, string> { - const ServerRequestTime = this.at.getTime(); - const ServerResponseTime = Date.now(); - const DeltaTime = ServerResponseTime - ServerRequestTime; - const RequestId = this.id; + public baseResponseHeaders(): Record<string, string> { + const ServerRequestTime = this.at.getTime(); + const ServerResponseTime = Date.now(); + const DeltaTime = ServerResponseTime - ServerRequestTime; + const RequestId = this.id; - return Object.entries({ - RequestId, - ServerRequestTime, - ServerResponseTime, - DeltaTime, - Hai: penguenoGreeting(), - }).reduce((acc, [key, val]) => ({ ...acc, [key]: val.toString() }), {}); - } + return Object.entries({ + RequestId, + ServerRequestTime, + ServerResponseTime, + DeltaTime, + Hai: penguenoGreeting(), + }).reduce((acc, [key, val]) => ({ ...acc, [key]: val!.toString() }), {}); + } - public static from(request: Request): LogMetricTraceable<PenguenoRequest> { - const id = crypto.randomUUID(); - const url = new URL(request.url); - const { pathname } = url; - const logTraceable = LogTraceable.of( - new PenguenoRequest(request, id, new Date()), - ).bimap( - TraceUtil.withTrace(`RequestId = ${id}, Method = ${request.method}, Path = ${pathname}`), - ); - return LogMetricTraceable.ofLogTraceable(logTraceable); - } + public static from(request: Request): LogMetricTraceable<PenguenoRequest> { + const id = crypto.randomUUID(); + const url = new URL(request.url); + const { pathname } = url; + const logTraceable = LogTraceable.of(new PenguenoRequest(request, id, new Date())).bimap( + TraceUtil.withTrace(`RequestId = ${id}, Method = ${request.method}, Path = ${pathname}`), + ); + return LogMetricTraceable.ofLogTraceable(logTraceable); + } } diff --git a/u/server/response.ts b/u/server/response.ts index 629dbb5..18d70b5 100644 --- a/u/server/response.ts +++ b/u/server/response.ts @@ -1,86 +1,83 @@ import { - type IEither, - isEither, - type ITraceable, - Metric, - type PenguenoRequest, - type ServerTrace, -} from "@emprespresso/pengueno"; + type IEither, + isEither, + type ITraceable, + Metric, + type PenguenoRequest, + type ServerTrace, +} from '@emprespresso/pengueno'; +export type BodyInit = + | ArrayBuffer + | AsyncIterable<Uint8Array> + | Blob + | FormData + | Iterable<Uint8Array> + | NodeJS.ArrayBufferView + | URLSearchParams + | null + | string; export type ResponseBody = object | string; -export type TResponseInit = ResponseInit & { - status: number; - headers?: Record<string, string>; +export type TResponseInit = Omit<ResponseInit, 'headers'> & { + status: number; + headers?: Record<string, string>; }; -const getResponse = ( - req: PenguenoRequest, - opts: TResponseInit, -): TResponseInit => { - return { - ...opts, - headers: { - ...req.baseResponseHeaders(), - ...opts?.headers, - "Content-Type": - (opts?.headers?.["Content-Type"] ?? "text/plain") + "; charset=utf-8", - }, - }; +const getResponse = (req: PenguenoRequest, opts: TResponseInit): ResponseInit => { + const baseHeaders = req.baseResponseHeaders(); + const optHeaders = opts.headers || {}; + + return { + ...opts, + headers: { + ...baseHeaders, + ...optHeaders, + 'Content-Type': (optHeaders['Content-Type'] ?? 'text/plain') + '; charset=utf-8', + } as Record<string, string>, + }; }; -const ResponseCodeMetrics = [0, 1, 2, 3, 4, 5].map((x) => - Metric.fromName(`response.${x}xx`), -); -export const getResponseMetric = (status: number) => { - const index = Math.floor(status / 100); - return ResponseCodeMetrics[index] ?? ResponseCodeMetrics[5]; +const ResponseCodeMetrics = [0, 1, 2, 3, 4, 5].map((x) => Metric.fromName(`response.${x}xx`)); +export const getResponseMetrics = (status: number) => { + const index = Math.floor(status / 100); + return ResponseCodeMetrics.map((metric, i) => metric.count.withValue(i === index ? 1.0 : 0.0)); }; export class PenguenoResponse extends Response { - constructor( - req: ITraceable<PenguenoRequest, ServerTrace>, - msg: BodyInit, - opts: TResponseInit, - ) { - const responseOpts = getResponse(req.get(), opts); - const resMetric = getResponseMetric(opts.status); - req.trace.trace(resMetric.count.withValue(1.0)); - responseOpts.headers; - super(msg, responseOpts); - } + constructor(req: ITraceable<PenguenoRequest, ServerTrace>, msg: BodyInit, opts: TResponseInit) { + const responseOpts = getResponse(req.get(), opts); + for (const metric of getResponseMetrics(opts.status)) { + req.trace.trace(metric); + } + super(msg, responseOpts); + } } export class JsonResponse extends PenguenoResponse { - constructor( - req: ITraceable<PenguenoRequest, ServerTrace>, - e: BodyInit | IEither<ResponseBody, ResponseBody>, - opts: TResponseInit, - ) { - const optsWithJsonContentType = { - ...opts, - headers: { - ...opts?.headers, - "Content-Type": "application/json", - }, - }; - if (isEither<ResponseBody, ResponseBody>(e)) { - super( - req, - JSON.stringify( - e.fold(({ isLeft, value }) => - isLeft ? { error: value } : { ok: value }, - ), - ), - optsWithJsonContentType, - ); - return; + constructor( + req: ITraceable<PenguenoRequest, ServerTrace>, + e: BodyInit | IEither<ResponseBody, ResponseBody>, + opts: TResponseInit, + ) { + const optsWithJsonContentType: TResponseInit = { + ...opts, + headers: { + ...opts.headers, + 'Content-Type': 'application/json', + }, + }; + if (isEither<ResponseBody, ResponseBody>(e)) { + super( + req, + JSON.stringify(e.fold(({ isLeft, value }) => (isLeft ? { error: value } : { ok: value }))), + optsWithJsonContentType, + ); + return; + } + super( + req, + JSON.stringify(Math.floor(opts.status / 100) > 4 ? { error: e } : { ok: e }), + optsWithJsonContentType, + ); } - super( - req, - JSON.stringify( - Math.floor(opts.status / 100) > 4 ? { error: e } : { ok: e }, - ), - optsWithJsonContentType, - ); - } } |