summaryrefslogtreecommitdiff
path: root/u
diff options
context:
space:
mode:
authorElizabeth Alexander Hunt <me@liz.coffee>2025-05-12 09:40:12 -0700
committerElizabeth <me@liz.coffee>2025-05-26 14:15:42 -0700
commitd51c9d74857aca3c2f172609297266968bc7f809 (patch)
tree64327f9cc4219729aa11af32d7d4c70cddfc2292 /u
parent30729a0cf707d9022bae0a7baaba77379dc31fd5 (diff)
downloadci-d51c9d74857aca3c2f172609297266968bc7f809.tar.gz
ci-d51c9d74857aca3c2f172609297266968bc7f809.zip
The big refactor TM
Diffstat (limited to 'u')
-rw-r--r--u/deno.json6
-rw-r--r--u/fn/callable.ts22
-rw-r--r--u/fn/either.ts97
-rw-r--r--u/fn/mod.ts2
-rw-r--r--u/leftpadesque/debug.ts11
-rw-r--r--u/leftpadesque/memoize.ts14
-rw-r--r--u/leftpadesque/mod.ts4
-rw-r--r--u/leftpadesque/object.ts2
-rw-r--r--u/leftpadesque/prepend.ts4
-rw-r--r--u/mod.ts5
-rw-r--r--u/process/env.ts36
-rw-r--r--u/process/mod.ts3
-rw-r--r--u/process/run.ts64
-rw-r--r--u/process/validate_identifier.ts24
-rw-r--r--u/server/activity/fourohfour.ts36
-rw-r--r--u/server/activity/health.ts67
-rw-r--r--u/server/activity/mod.ts13
-rw-r--r--u/server/filter/json.ts51
-rw-r--r--u/server/filter/method.ts41
-rw-r--r--u/server/filter/mod.ts33
-rw-r--r--u/server/mod.ts7
-rw-r--r--u/server/request.ts57
-rw-r--r--u/server/response.ts84
-rw-r--r--u/trace/itrace.ts107
-rw-r--r--u/trace/logger.ts108
-rw-r--r--u/trace/metrics.ts143
-rw-r--r--u/trace/mod.ts5
-rw-r--r--u/trace/trace.ts82
-rw-r--r--u/trace/util.ts58
29 files changed, 1186 insertions, 0 deletions
diff --git a/u/deno.json b/u/deno.json
new file mode 100644
index 0000000..26b08bf
--- /dev/null
+++ b/u/deno.json
@@ -0,0 +1,6 @@
+{
+ "name": "@emprespresso/pengueno",
+ "version": "0.1.0",
+ "exports": "./mod.ts",
+ "workspace": ["./*"]
+}
diff --git a/u/fn/callable.ts b/u/fn/callable.ts
new file mode 100644
index 0000000..fc6ea81
--- /dev/null
+++ b/u/fn/callable.ts
@@ -0,0 +1,22 @@
+// deno-lint-ignore no-explicit-any
+export interface Callable<T = any, ArgT = any> {
+ (...args: Array<ArgT>): T;
+}
+
+export interface Supplier<T> extends Callable<T, undefined> {
+ (): 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;
+}
+
+export interface SideEffect<T> extends Mapper<T, void> {
+}
+
+export interface BiSideEffect<T, U> extends BiMapper<T, U, void> {
+}
diff --git a/u/fn/either.ts b/u/fn/either.ts
new file mode 100644
index 0000000..8b233bf
--- /dev/null
+++ b/u/fn/either.ts
@@ -0,0 +1,97 @@
+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 | undefined, T | undefined, 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>;
+ flatMap: <Tt>(mapper: Mapper<T, IEither<E, Tt>>) => IEither<E, Tt>;
+ flatMapAsync: <Tt>(
+ mapper: Mapper<T, Promise<IEither<E, Tt>>>,
+ ) => Promise<IEither<E, Tt>>;
+}
+
+export class Either<E, T> implements IEither<E, T> {
+ private constructor(
+ private readonly err?: E,
+ private readonly ok?: T,
+ public readonly _tag: IEitherTag = iEitherTag,
+ ) {}
+
+ public moveRight<Tt>(t: Tt) {
+ return this.mapRight(() => t);
+ }
+
+ public fold<R>(folder: BiMapper<E | undefined, T | undefined, R>): R {
+ return folder(this.err ?? undefined, this.ok ?? undefined);
+ }
+
+ public mapBoth<Ee, Tt>(
+ errBranch: Mapper<E, Ee>,
+ okBranch: Mapper<T, Tt>,
+ ): Either<Ee, Tt> {
+ if (this.err !== undefined) return Either.left(errBranch(this.err));
+ return Either.right(okBranch(this.ok!));
+ }
+
+ public flatMap<Tt>(mapper: Mapper<T, Either<E, Tt>>): Either<E, Tt> {
+ if (this.ok !== undefined) return mapper(this.ok);
+ return Either.left<E, Tt>(this.err!);
+ }
+
+ public mapRight<Tt>(mapper: Mapper<T, Tt>): IEither<E, Tt> {
+ if (this.ok !== undefined) return Either.right<E, Tt>(mapper(this.ok));
+ return Either.left<E, Tt>(this.err!);
+ }
+
+ public mapLeft<Ee>(mapper: Mapper<E, Ee>): IEither<Ee, T> {
+ if (this.err !== undefined) return Either.left<Ee, T>(mapper(this.err));
+ return Either.right<Ee, T>(this.ok!);
+ }
+
+ public async flatMapAsync<Tt>(
+ mapper: Mapper<T, Promise<IEither<E, Tt>>>,
+ ): Promise<IEither<E, Tt>> {
+ if (this.err !== undefined) {
+ return Promise.resolve(Either.left<E, Tt>(this.err));
+ }
+ return await mapper(this.ok!).catch((err) => Either.left<E, Tt>(err as E));
+ }
+
+ static left<E, T>(e: E) {
+ return new Either<E, T>(e);
+ }
+
+ static right<E, T>(t: T) {
+ return new Either<E, T>(undefined, t);
+ }
+
+ static fromFailable<E, T>(s: Supplier<T>) {
+ try {
+ return Either.right<E, T>(s());
+ } catch (e) {
+ return Either.left<E, T>(e as E);
+ }
+ }
+
+ static async fromFailableAsync<E, T>(s: Promise<T>) {
+ try {
+ return Either.right<E, T>(await s);
+ } catch (e) {
+ return Either.left<E, T>(e as E);
+ }
+ }
+}
+
+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/fn/mod.ts b/u/fn/mod.ts
new file mode 100644
index 0000000..f0fbe88
--- /dev/null
+++ b/u/fn/mod.ts
@@ -0,0 +1,2 @@
+export * from "./callable.ts";
+export * from "./either.ts";
diff --git a/u/leftpadesque/debug.ts b/u/leftpadesque/debug.ts
new file mode 100644
index 0000000..a9da1f3
--- /dev/null
+++ b/u/leftpadesque/debug.ts
@@ -0,0 +1,11 @@
+const _hasEnv = !Deno.permissions.querySync({ name: "env" });
+
+const _env: "development" | "production" =
+ _hasEnv && (Deno.env.get("ENVIRONMENT") ?? "").toLowerCase().includes("prod")
+ ? "production"
+ : "development";
+export const isProd = () => _env === "production";
+
+const _debug = !isProd() || (_hasEnv &&
+ ["y", "t"].some((Deno.env.get("DEBUG") ?? "").toLowerCase().startsWith));
+export const isDebug = () => _debug;
diff --git a/u/leftpadesque/memoize.ts b/u/leftpadesque/memoize.ts
new file mode 100644
index 0000000..95e6019
--- /dev/null
+++ b/u/leftpadesque/memoize.ts
@@ -0,0 +1,14 @@
+import type { Callable } from "@emprespresso/pengueno";
+
+export const memoize = <R, F extends Callable<R>>(fn: F): F => {
+ const cache = new Map<string, R>();
+ return ((...args: unknown[]): R => {
+ const key = JSON.stringify(args);
+ if (cache.has(key)) {
+ return cache.get(key)!;
+ }
+ const res = fn.apply(args);
+ cache.set(key, res);
+ return res;
+ }) as F;
+};
diff --git a/u/leftpadesque/mod.ts b/u/leftpadesque/mod.ts
new file mode 100644
index 0000000..63d8d7a
--- /dev/null
+++ b/u/leftpadesque/mod.ts
@@ -0,0 +1,4 @@
+export * from "./object.ts";
+export * from "./prepend.ts";
+export * from "./debug.ts";
+export * from "./memoize.ts";
diff --git a/u/leftpadesque/object.ts b/u/leftpadesque/object.ts
new file mode 100644
index 0000000..73f7f80
--- /dev/null
+++ b/u/leftpadesque/object.ts
@@ -0,0 +1,2 @@
+export const isObject = (o: unknown): o is object =>
+ typeof o === "object" && !Array.isArray(o) && !!o;
diff --git a/u/leftpadesque/prepend.ts b/u/leftpadesque/prepend.ts
new file mode 100644
index 0000000..9b77aff
--- /dev/null
+++ b/u/leftpadesque/prepend.ts
@@ -0,0 +1,4 @@
+export const prependWith = (arr: string[], prep: string) =>
+ Array(arr.length * 2).fill(0)
+ .map((_, i) => i % 2 === 0)
+ .map((isPrep, i) => isPrep ? prep : arr[i]);
diff --git a/u/mod.ts b/u/mod.ts
new file mode 100644
index 0000000..8397ce6
--- /dev/null
+++ b/u/mod.ts
@@ -0,0 +1,5 @@
+export * from "./fn/mod.ts";
+export * from "./leftpadesque/mod.ts";
+export * from "./process/mod.ts";
+export * from "./trace/mod.ts";
+export * from "./server/mod.ts";
diff --git a/u/process/env.ts b/u/process/env.ts
new file mode 100644
index 0000000..0e41b4f
--- /dev/null
+++ b/u/process/env.ts
@@ -0,0 +1,36 @@
+import { Either, type IEither } from "@emprespresso/pengueno";
+
+export const getRequiredEnv = <V extends string>(name: V): IEither<Error, V> =>
+ Either
+ .fromFailable<Error, V>(() => Deno.env.get(name) as V) // could throw when no permission.
+ .flatMap((v) =>
+ (v && Either.right(v)) ||
+ Either.left(
+ new Error(`environment variable "${name}" is required D:`),
+ )
+ );
+
+type ObjectFromList<T extends ReadonlyArray<string>, V = string> = {
+ [K in (T extends ReadonlyArray<infer U> ? U : never)]: V;
+};
+
+export const getRequiredEnvVars = <V extends string>(vars: ReadonlyArray<V>) =>
+ vars
+ .map((envVar) => [envVar, getRequiredEnv(envVar)] as [V, IEither<Error, V>])
+ .reduce(
+ (
+ acc: IEither<Error, ObjectFromList<typeof vars>>,
+ x: [V, IEither<Error, V>],
+ ) => {
+ const [envVar, eitherVal] = x;
+ return acc.flatMap((args) => {
+ return eitherVal.mapRight((envValue) =>
+ ({
+ ...args,
+ [envVar]: envValue,
+ }) as ObjectFromList<typeof vars>
+ );
+ });
+ },
+ Either.right({} as ObjectFromList<typeof vars>),
+ );
diff --git a/u/process/mod.ts b/u/process/mod.ts
new file mode 100644
index 0000000..3f02d46
--- /dev/null
+++ b/u/process/mod.ts
@@ -0,0 +1,3 @@
+export * from "./env.ts";
+export * from "./run.ts";
+export * from "./validate_identifier.ts";
diff --git a/u/process/run.ts b/u/process/run.ts
new file mode 100644
index 0000000..4954438
--- /dev/null
+++ b/u/process/run.ts
@@ -0,0 +1,64 @@
+import {
+ Either,
+ type IEither,
+ type ITraceable,
+ LogLevel,
+ TraceUtil,
+} from "@emprespresso/pengueno";
+
+export type Command = string[] | string;
+type CommandOutputDecoded = {
+ code: number;
+ stdoutText: string;
+ stderrText: string;
+};
+
+export const getStdout = <Trace>(
+ c: ITraceable<Command, Trace>,
+ options: Deno.CommandOptions = {},
+): Promise<IEither<Error, string>> =>
+ c.bimap(TraceUtil.withFunctionTrace(getStdout))
+ .map((tCmd) => {
+ const cmd = tCmd.get();
+ tCmd.trace.trace(`:> im gonna run this command! ${cmd}`);
+ const [exec, ...args] = (typeof cmd === "string") ? cmd.split(" ") : cmd;
+ return new Deno.Command(exec, {
+ args,
+ stdout: "piped",
+ stderr: "piped",
+ ...options,
+ }).output();
+ })
+ .map((tOut) =>
+ Either.fromFailableAsync<Error, Deno.CommandOutput>(tOut.get())
+ )
+ .map(
+ TraceUtil.promiseify((tEitherOut) =>
+ tEitherOut.get().flatMap(({ code, stderr, stdout }) =>
+ Either
+ .fromFailable<Error, CommandOutputDecoded>(() => {
+ const stdoutText = new TextDecoder().decode(stdout);
+ const stderrText = new TextDecoder().decode(stderr);
+ return { code, stdoutText, stderrText };
+ })
+ .mapLeft((e) => {
+ tEitherOut.trace.addTrace(LogLevel.ERROR).trace(`o.o wat ${e}`);
+ return new Error(`${e}`);
+ })
+ .flatMap((decodedOutput): Either<Error, string> => {
+ const { code, stdoutText, stderrText } = decodedOutput;
+ tEitherOut.trace.addTrace(LogLevel.DEBUG).trace(
+ `stderr hehehe ${stderrText}`,
+ );
+ if (code !== 0) {
+ const msg =
+ `i weceived an exit code of ${code} i wanna zewoooo :<`;
+ tEitherOut.trace.addTrace(LogLevel.ERROR).trace(msg);
+ return Either.left(new Error(msg));
+ }
+ return Either.right(stdoutText);
+ })
+ )
+ ),
+ )
+ .get();
diff --git a/u/process/validate_identifier.ts b/u/process/validate_identifier.ts
new file mode 100644
index 0000000..32952a6
--- /dev/null
+++ b/u/process/validate_identifier.ts
@@ -0,0 +1,24 @@
+import { Either, type IEither } from "@emprespresso/pengueno";
+
+export const validateIdentifier = (token: string) => {
+ return (/^[a-zA-Z0-9_\-:. \/]+$/).test(token) && !token.includes("..");
+};
+
+// ensure {@param obj} is a Record<string, string> with stuff that won't
+// have the potential for shell injection, just to be super safe.
+type InvalidEntry<K, T> = [K, T];
+export const validateExecutionEntries = <
+ T,
+ K extends symbol | number | string = symbol | number | string,
+>(
+ obj: Record<K, T>,
+): IEither<
+ Array<InvalidEntry<K, T>>,
+ Record<string, string>
+> => {
+ const invalidEntries = <Array<InvalidEntry<K, T>>> Object.entries(obj).filter(
+ (e) => !e.every((x) => typeof x === "string" && validateIdentifier(x)),
+ );
+ if (invalidEntries.length > 0) return Either.left(invalidEntries);
+ return Either.right(<Record<string, string>> obj);
+};
diff --git a/u/server/activity/fourohfour.ts b/u/server/activity/fourohfour.ts
new file mode 100644
index 0000000..6449abd
--- /dev/null
+++ b/u/server/activity/fourohfour.ts
@@ -0,0 +1,36 @@
+import {
+ type IActivity,
+ type ITraceable,
+ JsonResponse,
+ type PenguenoRequest,
+ type ServerTrace,
+} from "@emprespresso/pengueno";
+
+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~",
+];
+const randomFourOhFour = () => messages[Math.random() * messages.length];
+
+export interface IFourOhFourActivity {
+ 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();
+ }
+}
diff --git a/u/server/activity/health.ts b/u/server/activity/health.ts
new file mode 100644
index 0000000..0f54a99
--- /dev/null
+++ b/u/server/activity/health.ts
@@ -0,0 +1,67 @@
+import {
+ type IActivity,
+ type IEither,
+ type ITraceable,
+ JsonResponse,
+ LogLevel,
+ type Mapper,
+ Metric,
+ type PenguenoRequest,
+ type ServerTrace,
+ TraceUtil,
+} from "@emprespresso/pengueno";
+
+export enum HealthCheckInput {
+ CHECK,
+}
+export enum HealthCheckOutput {
+ YAASSSLAYQUEEN,
+}
+
+export interface IHealthCheckActivity {
+ checkHealth: IActivity;
+}
+
+const healthCheckMetric = Metric.fromName("Health");
+export interface HealthChecker extends
+ Mapper<
+ ITraceable<HealthCheckInput, ServerTrace>,
+ Promise<IEither<Error, HealthCheckOutput>>
+ > {}
+export class HealthCheckActivityImpl implements IHealthCheckActivity {
+ 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(this.check))
+ .peek(TraceUtil.promiseify((h) =>
+ h.get().fold((err) => {
+ if (err) {
+ h.trace.trace(healthCheckMetric.failure);
+ h.trace.addTrace(LogLevel.ERROR).trace(`${err}`);
+ return;
+ }
+ h.trace.trace(healthCheckMetric.success);
+ })
+ ))
+ .map(TraceUtil.promiseify((h) =>
+ h.get()
+ .mapBoth(
+ () => "oh no, i need to eat more vegetables (。•́︿•̀。)...",
+ () => "think im healthy!! (✿˘◡˘) ready to do work~",
+ )
+ .fold((errMsg, okMsg) =>
+ new JsonResponse(
+ req,
+ errMsg ?? okMsg,
+ { status: errMsg ? 500 : 200 },
+ )
+ )
+ ))
+ .get();
+ }
+}
diff --git a/u/server/activity/mod.ts b/u/server/activity/mod.ts
new file mode 100644
index 0000000..82d8ec4
--- /dev/null
+++ b/u/server/activity/mod.ts
@@ -0,0 +1,13 @@
+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/json.ts b/u/server/filter/json.ts
new file mode 100644
index 0000000..4a2961e
--- /dev/null
+++ b/u/server/filter/json.ts
@@ -0,0 +1,51 @@
+import {
+ 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>;
+}
+
+const ParseJsonMetric = Metric.fromName("JsonParse");
+export const jsonModel = <MessageT>(
+ jsonTransformer: JsonTransformer<MessageT>,
+): RequestFilter<MessageT> =>
+(r: ITraceable<PenguenoRequest, ServerTrace>) =>
+ r.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().mapBoth(
+ () => traceableEither.trace.trace(ParseJsonMetric.failure),
+ () => traceableEither.trace.trace(ParseJsonMetric.success),
+ )
+ ),
+ )
+ .map(
+ TraceUtil.promiseify((traceableEitherJson) =>
+ traceableEitherJson.get()
+ .mapRight(traceableEitherJson.move)
+ .flatMap(jsonTransformer)
+ ),
+ )
+ .get();
diff --git a/u/server/filter/method.ts b/u/server/filter/method.ts
new file mode 100644
index 0000000..6b0419d
--- /dev/null
+++ b/u/server/filter/method.ts
@@ -0,0 +1,41 @@
+import {
+ 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";
+
+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();
diff --git a/u/server/filter/mod.ts b/u/server/filter/mod.ts
new file mode 100644
index 0000000..bbf37df
--- /dev/null
+++ b/u/server/filter/mod.ts
@@ -0,0 +1,33 @@
+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(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/mod.ts b/u/server/mod.ts
new file mode 100644
index 0000000..866b5f9
--- /dev/null
+++ b/u/server/mod.ts
@@ -0,0 +1,7 @@
+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
new file mode 100644
index 0000000..7aa9917
--- /dev/null
+++ b/u/server/request.ts
@@ -0,0 +1,57 @@
+import { LogMetricTraceable } from "@emprespresso/pengueno";
+
+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: URL,
+ _requestInit: RequestInit,
+ public readonly id: string,
+ public readonly at: Date,
+ ) {
+ super(_input, _requestInit);
+ }
+
+ 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()) }), {});
+ }
+
+ public static from(
+ request: Request,
+ ): LogMetricTraceable<PenguenoRequest> {
+ const id = crypto.randomUUID();
+ const url = new URL(request.url);
+ const { pathname } = url;
+ const traceSupplier = () => `[${id} <- ${request.method}'d @ ${pathname}]`;
+ return LogMetricTraceable
+ .from(
+ new PenguenoRequest(
+ url,
+ { ...request },
+ id,
+ new Date(),
+ ),
+ )
+ .bimap((_request) => [_request.get(), traceSupplier]);
+ }
+}
diff --git a/u/server/response.ts b/u/server/response.ts
new file mode 100644
index 0000000..c21819a
--- /dev/null
+++ b/u/server/response.ts
@@ -0,0 +1,84 @@
+import {
+ type IEither,
+ isEither,
+ type ITraceable,
+ Metric,
+ type PenguenoRequest,
+ type ServerTrace,
+} from "@emprespresso/pengueno";
+
+export type ResponseBody = object | string;
+export type TResponseInit = ResponseInit & {
+ 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 ResponseCodeMetrics = [1, 2, 3, 4, 5].map((x) =>
+ Metric.fromName(`response.${x}xx`)
+);
+export const getResponseMetric = (status: number) => {
+ const index = (Math.floor(status / 100)) + 1;
+ return ResponseCodeMetrics[index] ?? ResponseCodeMetrics[5 - 1];
+};
+
+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);
+ }
+}
+
+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((err, ok) => err ? ({ error: err! }) : ({ ok: ok! })),
+ ),
+ optsWithJsonContentType,
+ );
+ return;
+ }
+ super(
+ req,
+ JSON.stringify(
+ (Math.floor(opts.status / 100) < 4) ? { ok: e } : { error: e },
+ ),
+ optsWithJsonContentType,
+ );
+ }
+}
diff --git a/u/trace/itrace.ts b/u/trace/itrace.ts
new file mode 100644
index 0000000..e6189d3
--- /dev/null
+++ b/u/trace/itrace.ts
@@ -0,0 +1,107 @@
+import type { Mapper, SideEffect, Supplier } from "@emprespresso/pengueno";
+
+// the "thing" every Trace writer must "trace()"
+type BaseTraceWith = string;
+export type ITraceWith<T> = BaseTraceWith | T;
+export interface ITrace<TraceWith> {
+ addTrace: Mapper<ITraceWith<TraceWith>, ITrace<TraceWith>>;
+ trace: SideEffect<ITraceWith<TraceWith>>;
+}
+
+export type ITraceableTuple<T, TraceWith> = [T, BaseTraceWith | TraceWith];
+export type ITraceableMapper<
+ T,
+ U,
+ TraceWith,
+ W = ITraceable<T, TraceWith>,
+> = (
+ w: W,
+) => U;
+
+export interface ITraceable<T, Trace = BaseTraceWith> {
+ readonly trace: ITrace<Trace>;
+ get: Supplier<T>;
+ move: <U>(u: U) => ITraceable<U, Trace>;
+ map: <U>(
+ mapper: ITraceableMapper<T, U, Trace>,
+ ) => ITraceable<U, Trace>;
+ bimap: <U>(
+ mapper: ITraceableMapper<
+ T,
+ ITraceableTuple<U, Array<Trace> | Trace>,
+ Trace
+ >,
+ ) => ITraceable<U, Trace>;
+ peek: (peek: ITraceableMapper<T, void, Trace>) => ITraceable<T, Trace>;
+ flatMap: <U>(
+ mapper: ITraceableMapper<T, ITraceable<U, Trace>, Trace>,
+ ) => ITraceable<U, Trace>;
+ flatMapAsync<U>(
+ mapper: ITraceableMapper<T, Promise<ITraceable<U, Trace>>, Trace>,
+ ): ITraceable<Promise<U>, Trace>;
+}
+
+export class TraceableImpl<T, TraceWith> implements ITraceable<T, TraceWith> {
+ protected constructor(
+ private readonly item: T,
+ public readonly trace: ITrace<TraceWith>,
+ ) {}
+
+ public map<U>(
+ mapper: ITraceableMapper<T, U, TraceWith>,
+ ) {
+ const result = mapper(this);
+ return new TraceableImpl(result, this.trace);
+ }
+
+ public flatMap<U>(
+ mapper: ITraceableMapper<
+ T,
+ ITraceable<U, TraceWith>,
+ TraceWith
+ >,
+ ): ITraceable<U, TraceWith> {
+ return mapper(this);
+ }
+
+ public flatMapAsync<U>(
+ mapper: ITraceableMapper<
+ T,
+ Promise<ITraceable<U, TraceWith>>,
+ TraceWith
+ >,
+ ): ITraceable<Promise<U>, TraceWith> {
+ return new TraceableImpl(
+ mapper(this).then((t) => t.get()),
+ this.trace,
+ );
+ }
+
+ public peek(peek: ITraceableMapper<T, void, TraceWith>) {
+ peek(this);
+ return this;
+ }
+
+ public move<Tt>(t: Tt): ITraceable<Tt, TraceWith> {
+ return this.map(() => t);
+ }
+
+ public bimap<U>(
+ mapper: ITraceableMapper<
+ T,
+ ITraceableTuple<U, Array<TraceWith> | TraceWith>,
+ TraceWith
+ >,
+ ) {
+ const [item, trace] = mapper(this);
+ const traces = Array.isArray(trace) ? trace : [trace];
+ return new TraceableImpl(
+ item,
+ traces.reduce((trace, _trace) => trace.addTrace(_trace), this.trace),
+ );
+ }
+
+ public get() {
+ return this.item;
+ }
+}
diff --git a/u/trace/logger.ts b/u/trace/logger.ts
new file mode 100644
index 0000000..a5739c8
--- /dev/null
+++ b/u/trace/logger.ts
@@ -0,0 +1,108 @@
+import {
+ isDebug,
+ type ITrace,
+ type ITraceWith,
+ type SideEffect,
+ type Supplier,
+} from "@emprespresso/pengueno";
+
+export interface ILogger {
+ log: (...args: unknown[]) => void;
+ debug: (...args: unknown[]) => void;
+ warn: (...args: unknown[]) => void;
+ error: (...args: unknown[]) => void;
+}
+export enum LogLevel {
+ UNKNOWN = "UNKNOWN",
+ INFO = "INFO",
+ WARN = "WARN",
+ DEBUG = "DEBUG",
+ ERROR = "ERROR",
+}
+const logLevelOrder: Array<LogLevel> = [
+ LogLevel.DEBUG,
+ LogLevel.INFO,
+ LogLevel.WARN,
+ LogLevel.ERROR,
+];
+export const isLogLevel = (l: string): l is LogLevel =>
+ logLevelOrder.some((level) => <string> level === l);
+
+const defaultAllowedLevels = () =>
+ [
+ LogLevel.UNKNOWN,
+ ...(isDebug() ? [LogLevel.DEBUG] : []),
+ LogLevel.INFO,
+ LogLevel.WARN,
+ LogLevel.ERROR,
+ ] as Array<LogLevel>;
+
+export const logWithLevel = (
+ logger: ILogger,
+ level: LogLevel,
+): SideEffect<unknown> => {
+ switch (level) {
+ case LogLevel.UNKNOWN:
+ case LogLevel.INFO:
+ return logger.log;
+ case LogLevel.DEBUG:
+ return logger.debug;
+ case LogLevel.WARN:
+ return logger.warn;
+ case LogLevel.ERROR:
+ return logger.error;
+ }
+};
+
+export type LogTraceSupplier = ITraceWith<Supplier<string>>;
+
+const defaultTrace = () => `[${new Date().toISOString()}]`;
+export const LoggerImpl = console;
+export class LogTrace implements ITrace<LogTraceSupplier> {
+ constructor(
+ private readonly logger: ILogger = LoggerImpl,
+ private readonly traces: Array<LogTraceSupplier> = [defaultTrace],
+ private readonly allowedLevels: Supplier<Array<LogLevel>> =
+ defaultAllowedLevels,
+ private readonly defaultLevel: LogLevel = LogLevel.INFO,
+ ) {
+ }
+
+ public addTrace(trace: LogTraceSupplier): ITrace<LogTraceSupplier> {
+ return new LogTrace(
+ this.logger,
+ this.traces.concat(trace),
+ this.allowedLevels,
+ this.defaultLevel,
+ );
+ }
+
+ public trace(trace: LogTraceSupplier) {
+ const { line, level: _level } = this.foldTraces(this.traces.concat(trace));
+ if (!this.allowedLevels().includes(_level)) return;
+
+ const level = _level === LogLevel.UNKNOWN ? this.defaultLevel : _level;
+ logWithLevel(this.logger, level)(`[${level}]${line}`);
+ }
+
+ private foldTraces(traces: Array<LogTraceSupplier>) {
+ const { line, level } = traces.reduce(
+ (acc: { line: string; level: number }, t) => {
+ const val = typeof t === "function" ? t() : t;
+ if (isLogLevel(val)) {
+ return {
+ ...acc,
+ level: Math.max(logLevelOrder.indexOf(val), acc.level),
+ };
+ }
+ const prefix = [
+ acc.line,
+ val,
+ ].join(" ");
+ return { ...acc, prefix };
+ },
+ { line: "", level: -1 },
+ );
+ return { line, level: logLevelOrder[level] ?? LogLevel.UNKNOWN };
+ }
+}
diff --git a/u/trace/metrics.ts b/u/trace/metrics.ts
new file mode 100644
index 0000000..4ddde06
--- /dev/null
+++ b/u/trace/metrics.ts
@@ -0,0 +1,143 @@
+import {
+ isObject,
+ type ITrace,
+ type ITraceWith,
+ type Mapper,
+ type SideEffect,
+ type Supplier,
+} from "@emprespresso/pengueno";
+
+export enum Unit {
+ COUNT,
+ MILLISECONDS,
+}
+
+export interface IMetric {
+ readonly count: IEmittableMetric;
+ readonly time: IEmittableMetric;
+ readonly failure: IMetric;
+ readonly success: IMetric;
+ readonly warn: IMetric;
+ readonly children: Supplier<Array<IMetric>>;
+
+ readonly _tag: "IMetric";
+}
+export const isIMetric = (t: unknown): t is IMetric =>
+ isObject(t) && "_tag" in t && t._tag === "IMetric";
+
+export interface IEmittableMetric {
+ readonly name: string;
+ readonly unit: Unit;
+ withValue: Mapper<number, MetricValue>;
+}
+
+export class EmittableMetric implements IEmittableMetric {
+ constructor(public readonly name: string, public readonly unit: Unit) {
+ }
+
+ public withValue(value: number): MetricValue {
+ return {
+ name: this.name,
+ unit: this.unit,
+ emissionTimestamp: Date.now(),
+ value,
+ _tag: "MetricValue",
+ };
+ }
+}
+
+export class Metric implements IMetric {
+ constructor(
+ public readonly count: IEmittableMetric,
+ public readonly time: IEmittableMetric,
+ public readonly failure: Metric,
+ public readonly success: Metric,
+ public readonly warn: Metric,
+ public readonly _tag: "IMetric" = "IMetric",
+ ) {}
+
+ public children() {
+ return [this.failure, this.success, this.warn];
+ }
+
+ static fromName(name: string): Metric {
+ return new Metric(
+ new EmittableMetric(`${name}.count`, Unit.COUNT),
+ new EmittableMetric(`${name}.elapsed`, Unit.MILLISECONDS),
+ Metric.fromName(`${name}.failure`),
+ Metric.fromName(`${name}.success`),
+ Metric.fromName(`${name}.warn`),
+ );
+ }
+}
+
+export interface MetricValue {
+ readonly name: string;
+ readonly unit: Unit;
+ readonly value: number;
+ readonly emissionTimestamp: number;
+ readonly _tag: "MetricValue";
+}
+export const isMetricValue = (t: unknown): t is MetricValue =>
+ isObject(t) && "_tag" in t && t._tag === "MetricValue";
+
+export const isMetricsTraceSupplier = (t: unknown): t is MetricsTraceSupplier =>
+ isMetricValue(t) || isIMetric(t);
+
+export type MetricsTraceSupplier = ITraceWith<IMetric | MetricValue>;
+type MetricTracingTuple = [IMetric, Date];
+export class MetricsTrace implements ITrace<MetricsTraceSupplier> {
+ constructor(
+ private readonly metricConsumer: SideEffect<Array<MetricValue>>,
+ private readonly tracing: Array<MetricTracingTuple> = [],
+ private readonly flushed: Set<IMetric> = new Set(),
+ ) {}
+
+ public addTrace(trace: MetricsTraceSupplier) {
+ if (isMetricValue(trace) || typeof trace === "string") return this;
+ return new MetricsTrace(this.metricConsumer)._nowTracing(trace);
+ }
+
+ public trace(metric: MetricsTraceSupplier) {
+ if (typeof metric === "string") return this;
+ if (isMetricValue(metric)) {
+ this.metricConsumer([metric]);
+ return this;
+ }
+
+ const foundMetricValues = this.tracing.flatMap((
+ [tracing, startedTracing],
+ ) =>
+ [tracing, ...tracing.children()]
+ .filter((_tracing) => metric === _tracing)
+ .flatMap((metric) => [
+ this.addMetric(metric, startedTracing),
+ this.addMetric(tracing, startedTracing),
+ ])
+ ).flatMap((values) => values);
+
+ if (foundMetricValues.length === 0) {
+ return this._nowTracing(metric);
+ }
+
+ this.metricConsumer(foundMetricValues);
+ return this;
+ }
+
+ private addMetric(metric: IMetric, startedTracing: Date): Array<MetricValue> {
+ if (this.flushed.has(metric)) {
+ return [];
+ }
+
+ this.flushed.add(metric);
+ return [
+ metric.count.withValue(1.0),
+ metric.time.withValue(Date.now() - startedTracing.getTime()),
+ ];
+ }
+
+ private _nowTracing(metric: IMetric): MetricsTrace {
+ this.tracing.push([metric, new Date()]);
+ return this;
+ }
+}
diff --git a/u/trace/mod.ts b/u/trace/mod.ts
new file mode 100644
index 0000000..0f9b61b
--- /dev/null
+++ b/u/trace/mod.ts
@@ -0,0 +1,5 @@
+export * from "./itrace.ts";
+export * from "./util.ts";
+export * from "./logger.ts";
+export * from "./metrics.ts";
+export * from "./trace.ts";
diff --git a/u/trace/trace.ts b/u/trace/trace.ts
new file mode 100644
index 0000000..e942066
--- /dev/null
+++ b/u/trace/trace.ts
@@ -0,0 +1,82 @@
+import {
+ isMetricsTraceSupplier,
+ type ITrace,
+ type ITraceWith,
+ LogTrace,
+ type LogTraceSupplier,
+ MetricsTrace,
+ type MetricsTraceSupplier,
+ type MetricValue,
+ TraceableImpl,
+} from "@emprespresso/pengueno";
+
+export class LogTraceable<T> extends TraceableImpl<T, LogTraceSupplier> {
+ public static LogTrace = new LogTrace();
+ static from<T>(t: T) {
+ return new LogTraceable(t, LogTraceable.LogTrace);
+ }
+}
+
+const getEmbeddedMetricConsumer =
+ (logTrace: LogTrace) => (metrics: Array<MetricValue>) =>
+ logTrace.addTrace("<metrics>").trace(
+ JSON.stringify(metrics, null, 2) + "</metrics>",
+ );
+export class EmbeddedMetricsTraceable<T>
+ extends TraceableImpl<T, MetricsTraceSupplier> {
+ public static MetricsTrace = new MetricsTrace(
+ getEmbeddedMetricConsumer(LogTraceable.LogTrace),
+ );
+
+ static from<T>(t: T) {
+ return new EmbeddedMetricsTraceable(
+ t,
+ EmbeddedMetricsTraceable.MetricsTrace,
+ );
+ }
+}
+
+export type LogMetricTraceSupplier = ITraceWith<
+ LogTraceSupplier | MetricsTraceSupplier
+>;
+export class LogMetricTrace implements ITrace<LogMetricTraceSupplier> {
+ constructor(
+ private logTrace: ITrace<LogTraceSupplier>,
+ private metricsTrace: ITrace<MetricsTraceSupplier>,
+ ) {}
+
+ public addTrace(
+ trace: LogTraceSupplier | MetricsTraceSupplier,
+ ): LogMetricTrace {
+ if (isMetricsTraceSupplier(trace)) {
+ this.metricsTrace = this.metricsTrace.addTrace(trace);
+ return this;
+ }
+ this.logTrace = this.logTrace.addTrace(trace);
+ return this;
+ }
+
+ public trace(trace: LogTraceSupplier | MetricsTraceSupplier) {
+ if (isMetricsTraceSupplier(trace)) {
+ this.metricsTrace.trace(trace);
+ return this;
+ }
+ this.logTrace.trace(trace);
+ return this;
+ }
+}
+
+export class LogMetricTraceable<T>
+ extends TraceableImpl<T, MetricsTraceSupplier | LogTraceSupplier> {
+ public static LogMetricTrace = new LogMetricTrace(
+ LogTraceable.LogTrace,
+ EmbeddedMetricsTraceable.MetricsTrace,
+ );
+
+ static from<T>(t: T) {
+ return new LogMetricTraceable(
+ t,
+ LogMetricTraceable.LogMetricTrace,
+ );
+ }
+}
diff --git a/u/trace/util.ts b/u/trace/util.ts
new file mode 100644
index 0000000..302c8e4
--- /dev/null
+++ b/u/trace/util.ts
@@ -0,0 +1,58 @@
+import type {
+ Callable,
+ IMetric,
+ ITraceableMapper,
+ ITraceableTuple,
+ MetricsTraceSupplier,
+} from "@emprespresso/pengueno";
+
+export class TraceUtil {
+ static withTrace<T, Trace>(
+ trace: string,
+ ): ITraceableMapper<
+ T,
+ ITraceableTuple<T, Trace | Array<Trace>>,
+ Trace
+ > {
+ return (t) => [t.get(), `[${trace}]`];
+ }
+
+ static withMetricTrace<T, Trace extends MetricsTraceSupplier>(
+ metric: IMetric,
+ ): ITraceableMapper<
+ T,
+ ITraceableTuple<T, Trace | Array<Trace>>,
+ Trace
+ > {
+ return (t) => [t.get(), metric as Trace];
+ }
+
+ static withFunctionTrace<F extends Callable, T, Trace>(
+ f: F,
+ ): ITraceableMapper<
+ T,
+ ITraceableTuple<T, Trace | Array<Trace>>,
+ Trace
+ > {
+ return TraceUtil.withTrace(f.name);
+ }
+
+ static withClassTrace<C extends object, T, Trace>(
+ c: C,
+ ): ITraceableMapper<
+ T,
+ ITraceableTuple<T, Trace | Array<Trace>>,
+ Trace
+ > {
+ return TraceUtil.withTrace(c.constructor.name);
+ }
+
+ static promiseify<T, U, Trace>(
+ mapper: ITraceableMapper<T, U, Trace>,
+ ): ITraceableMapper<Promise<T>, Promise<U>, Trace> {
+ return (traceablePromise) =>
+ traceablePromise.flatMapAsync(async (t) =>
+ t.move(await t.get()).map(mapper)
+ ).get();
+ }
+}