summaryrefslogtreecommitdiff
path: root/u
diff options
context:
space:
mode:
Diffstat (limited to 'u')
-rw-r--r--u/fn/either.ts6
-rw-r--r--u/fn/mod.ts1
-rw-r--r--u/leftpadesque/memoize.ts14
-rw-r--r--u/leftpadesque/mod.ts1
-rw-r--r--u/process/env.ts15
-rw-r--r--u/process/run.ts33
-rw-r--r--u/server/activity/fourohfour.ts27
-rw-r--r--u/server/activity/health.ts77
-rw-r--r--u/server/activity/mod.ts20
-rw-r--r--u/server/filter/json.ts43
-rw-r--r--u/server/filter/method.ts10
-rw-r--r--u/server/filter/mod.ts28
-rw-r--r--u/trace/metrics.ts24
-rw-r--r--u/trace/trace.ts8
-rw-r--r--u/trace/util.ts14
15 files changed, 207 insertions, 114 deletions
diff --git a/u/fn/either.ts b/u/fn/either.ts
index 9dc1027..8b233bf 100644
--- a/u/fn/either.ts
+++ b/u/fn/either.ts
@@ -10,7 +10,7 @@ export interface IEither<E, T> {
errBranch: Mapper<E, Ee>,
okBranch: Mapper<T, Tt>,
) => IEither<Ee, Tt>;
- fold: <Tt>(folder: BiMapper<E | null, T | null, Tt>) => 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>;
@@ -31,8 +31,8 @@ export class Either<E, T> implements IEither<E, T> {
return this.mapRight(() => t);
}
- public fold<R>(folder: BiMapper<E | null, T | null, R>): R {
- return folder(this.err ?? null, this.ok ?? null);
+ public fold<R>(folder: BiMapper<E | undefined, T | undefined, R>): R {
+ return folder(this.err ?? undefined, this.ok ?? undefined);
}
public mapBoth<Ee, Tt>(
diff --git a/u/fn/mod.ts b/u/fn/mod.ts
index f0fbe88..265c6db 100644
--- a/u/fn/mod.ts
+++ b/u/fn/mod.ts
@@ -1,2 +1,3 @@
export * from "./callable.ts";
export * from "./either.ts";
+export * from "./memoize.ts";
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
index 801846a..63d8d7a 100644
--- a/u/leftpadesque/mod.ts
+++ b/u/leftpadesque/mod.ts
@@ -1,3 +1,4 @@
export * from "./object.ts";
export * from "./prepend.ts";
export * from "./debug.ts";
+export * from "./memoize.ts";
diff --git a/u/process/env.ts b/u/process/env.ts
index 26e1158..65f4e63 100644
--- a/u/process/env.ts
+++ b/u/process/env.ts
@@ -10,3 +10,18 @@ export const getRequiredEnv = (name: string): IEither<Error, string> =>
new Error(`environment variable "${name}" is required D:`),
)
);
+
+export const getRequiredEnvVars = (vars: Array<string>) =>
+ vars
+ .map((envVar) =>
+ [envVar, getRequiredEnv(envVar)] as [string, IEither<Error, string>]
+ )
+ .reduce((acc, x: [string, IEither<Error, string>]) => {
+ const [envVar, eitherVal] = x;
+ return acc.flatMap((args) => {
+ return eitherVal.mapRight((envValue) => ({
+ ...args,
+ [envVar]: envValue,
+ }));
+ });
+ }, Either.right<Error, Record<string, string>>({}));
diff --git a/u/process/run.ts b/u/process/run.ts
index cbf8c65..4954438 100644
--- a/u/process/run.ts
+++ b/u/process/run.ts
@@ -6,21 +6,21 @@ import {
TraceUtil,
} from "@emprespresso/pengueno";
-type Command = string[] | string;
+export type Command = string[] | string;
type CommandOutputDecoded = {
code: number;
stdoutText: string;
stderrText: string;
};
-export class ProcessError extends Error {}
export const getStdout = <Trace>(
c: ITraceable<Command, Trace>,
options: Deno.CommandOptions = {},
-): Promise<IEither<ProcessError, string>> =>
+): Promise<IEither<Error, string>> =>
c.bimap(TraceUtil.withFunctionTrace(getStdout))
- .map(({ item: cmd, trace }) => {
- trace.trace(`:> im gonna run this command! ${cmd}`);
+ .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,
@@ -29,12 +29,12 @@ export const getStdout = <Trace>(
...options,
}).output();
})
- .map(({ item: p }) =>
- Either.fromFailableAsync<Error, Deno.CommandOutput>(p)
+ .map((tOut) =>
+ Either.fromFailableAsync<Error, Deno.CommandOutput>(tOut.get())
)
.map(
- TraceUtil.promiseify(({ item: eitherOutput, trace }) =>
- eitherOutput.flatMap(({ code, stderr, stdout }) =>
+ TraceUtil.promiseify((tEitherOut) =>
+ tEitherOut.get().flatMap(({ code, stderr, stdout }) =>
Either
.fromFailable<Error, CommandOutputDecoded>(() => {
const stdoutText = new TextDecoder().decode(stdout);
@@ -42,22 +42,23 @@ export const getStdout = <Trace>(
return { code, stdoutText, stderrText };
})
.mapLeft((e) => {
- trace.addTrace(LogLevel.ERROR).trace(`o.o wat ${e}`);
- return new ProcessError(`${e}`);
+ tEitherOut.trace.addTrace(LogLevel.ERROR).trace(`o.o wat ${e}`);
+ return new Error(`${e}`);
})
- .flatMap((decodedOutput): Either<ProcessError, string> => {
+ .flatMap((decodedOutput): Either<Error, string> => {
const { code, stdoutText, stderrText } = decodedOutput;
- trace.addTrace(LogLevel.DEBUG).trace(
+ tEitherOut.trace.addTrace(LogLevel.DEBUG).trace(
`stderr hehehe ${stderrText}`,
);
if (code !== 0) {
const msg =
`i weceived an exit code of ${code} i wanna zewoooo :<`;
- trace.addTrace(LogLevel.ERROR).trace(msg);
- return Either.left(new ProcessError(msg));
+ tEitherOut.trace.addTrace(LogLevel.ERROR).trace(msg);
+ return Either.left(new Error(msg));
}
return Either.right(stdoutText);
})
)
),
- ).item;
+ )
+ .get();
diff --git a/u/server/activity/fourohfour.ts b/u/server/activity/fourohfour.ts
index 48740df..6449abd 100644
--- a/u/server/activity/fourohfour.ts
+++ b/u/server/activity/fourohfour.ts
@@ -1,4 +1,5 @@
import {
+ type IActivity,
type ITraceable,
JsonResponse,
type PenguenoRequest,
@@ -16,12 +17,20 @@ const messages = [
"ヽ(;▽;)ノ Eep! This route has ghosted you~",
];
const randomFourOhFour = () => messages[Math.random() * messages.length];
-export const FourOhFourActivity = (
- req: ITraceable<PenguenoRequest, ServerTrace>,
-) =>
- req
- .move(
- new JsonResponse(req, randomFourOhFour(), {
- status: 404,
- }),
- );
+
+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
index b9efa3a..0f54a99 100644
--- a/u/server/activity/health.ts
+++ b/u/server/activity/health.ts
@@ -1,4 +1,5 @@
import {
+ type IActivity,
type IEither,
type ITraceable,
JsonResponse,
@@ -14,39 +15,53 @@ export enum HealthCheckInput {
CHECK,
}
export enum HealthCheckOutput {
- YAASQUEEN,
+ YAASSSLAYQUEEN,
+}
+
+export interface IHealthCheckActivity {
+ checkHealth: IActivity;
}
const healthCheckMetric = Metric.fromName("Health");
-export const HealthCheckActivity = (
- check: Mapper<
+export interface HealthChecker extends
+ Mapper<
ITraceable<HealthCheckInput, ServerTrace>,
Promise<IEither<Error, HealthCheckOutput>>
- >,
-) =>
-(req: ITraceable<PenguenoRequest, ServerTrace>) =>
- req
- .bimap(TraceUtil.withFunctionTrace(HealthCheckActivity))
- .bimap(TraceUtil.withMetricTrace(healthCheckMetric))
- .flatMap((r) => r.move(HealthCheckInput.CHECK).map(check))
- .map(TraceUtil.promiseify((h) => {
- const health = h.get();
- health.mapBoth((e) => {
- h.trace.trace(healthCheckMetric.failure);
- h.trace.addTrace(LogLevel.ERROR).trace(`${e}`);
- return new JsonResponse(
- req,
- "oh no, i need to eat more vegetables (。•́︿•̀。)...",
- { status: 500 },
- );
- }, (_healthy) => {
- h.trace.trace(healthCheckMetric.success);
- const msg = `think im healthy!! (✿˘◡˘) ready to do work~`;
- h.trace.trace(msg);
- return new JsonResponse(
- req,
- msg,
- { status: 200 },
- );
- });
- }));
+ > {}
+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
index 9bd512f..82d8ec4 100644
--- a/u/server/activity/mod.ts
+++ b/u/server/activity/mod.ts
@@ -1,15 +1,13 @@
-import type { PenguenoResponse, RequestFilter } from "@emprespresso/pengueno";
+import type {
+ ITraceable,
+ PenguenoRequest,
+ PenguenoResponse,
+ ServerTrace,
+} from "@emprespresso/pengueno";
-export enum StatusOK {
- FOLLOW = 300,
- OK = 200,
-}
-export interface ActivityOk {
- readonly status: StatusOK;
-}
-
-export interface IActivity<Trace>
- extends RequestFilter<ActivityOk, Trace, PenguenoResponse> {
+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
index c839707..4a2961e 100644
--- a/u/server/filter/json.ts
+++ b/u/server/filter/json.ts
@@ -3,46 +3,49 @@ import {
type IEither,
type ITraceable,
LogLevel,
+ Metric,
+ PenguenoError,
type PenguenoRequest,
type RequestFilter,
type ServerTrace,
TraceUtil,
} from "@emprespresso/pengueno";
-import { Metric } from "../../trace/mod.ts";
-type JsonTransformer<R, ParsedJson = unknown> = (
- json: ITraceable<ParsedJson, ServerTrace>,
-) => IEither<Error, R>;
+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, Error> =>
+): RequestFilter<MessageT> =>
(r: ITraceable<PenguenoRequest, ServerTrace>) =>
- r
- .bimap(TraceUtil.withMetricTrace(ParseJsonMetric))
+ 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 Error("seems to be invalid JSON (>//<) can you fix?");
+ return new PenguenoError(
+ "seems to be invalid JSON (>//<) can you fix?",
+ 400,
+ );
})
)
)
- .flatMapAsync(
- TraceUtil.promiseify((traceableEitherJson) =>
- traceableEitherJson.map((t) =>
- t.get().mapRight(traceableEitherJson.move).flatMap(
- jsonTransformer,
- )
+ .peek(
+ TraceUtil.promiseify((traceableEither) =>
+ traceableEither.get().mapBoth(
+ () => traceableEither.trace.trace(ParseJsonMetric.failure),
+ () => traceableEither.trace.trace(ParseJsonMetric.success),
)
),
)
- .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
index 350f04c..6b0419d 100644
--- a/u/server/filter/method.ts
+++ b/u/server/filter/method.ts
@@ -1,8 +1,8 @@
import {
Either,
type ITraceable,
- JsonResponse,
LogLevel,
+ PenguenoError,
type PenguenoRequest,
type RequestFilter,
type ServerTrace,
@@ -22,7 +22,7 @@ type HttpMethod =
export const requireMethod = (
methods: Array<HttpMethod>,
-): RequestFilter<HttpMethod, JsonResponse> =>
+): RequestFilter<HttpMethod> =>
(req: ITraceable<PenguenoRequest, ServerTrace>) =>
req.bimap(TraceUtil.withFunctionTrace(requireMethod))
.move(Promise.resolve(req.get()))
@@ -32,10 +32,10 @@ export const requireMethod = (
if (!methods.includes(method)) {
const msg = "that's not how you pet me (⋟﹏⋞)~";
t.trace.addTrace(LogLevel.WARN).trace(msg);
- return Either.left<JsonResponse, HttpMethod>(
- new JsonResponse(req, msg, { status: 405 }),
+ return Either.left<PenguenoError, HttpMethod>(
+ new PenguenoError(msg, 405),
);
}
- return Either.right<JsonResponse, HttpMethod>(method);
+ return Either.right<PenguenoError, HttpMethod>(method);
}))
.get();
diff --git a/u/server/filter/mod.ts b/u/server/filter/mod.ts
index 22ddad5..bbf37df 100644
--- a/u/server/filter/mod.ts
+++ b/u/server/filter/mod.ts
@@ -1,13 +1,29 @@
-import type {
- IEither,
- ITraceable,
- PenguenoRequest,
- ServerTrace,
+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,
+ Err extends PenguenoError = PenguenoError,
RIn = ITraceable<PenguenoRequest, ServerTrace>,
> {
(req: RIn): Promise<IEither<Err, T>>;
diff --git a/u/trace/metrics.ts b/u/trace/metrics.ts
index a26ee5d..4ddde06 100644
--- a/u/trace/metrics.ts
+++ b/u/trace/metrics.ts
@@ -4,6 +4,7 @@ import {
type ITraceWith,
type Mapper,
type SideEffect,
+ type Supplier,
} from "@emprespresso/pengueno";
export enum Unit {
@@ -16,10 +17,13 @@ export interface IMetric {
readonly time: IEmittableMetric;
readonly failure: IMetric;
readonly success: IMetric;
- readonly _isIMetric: true;
+ readonly warn: IMetric;
+ readonly children: Supplier<Array<IMetric>>;
+
+ readonly _tag: "IMetric";
}
export const isIMetric = (t: unknown): t is IMetric =>
- isObject(t) && "_isIMetric" in t;
+ isObject(t) && "_tag" in t && t._tag === "IMetric";
export interface IEmittableMetric {
readonly name: string;
@@ -35,9 +39,9 @@ export class EmittableMetric implements IEmittableMetric {
return {
name: this.name,
unit: this.unit,
- _isMetricValue: true as true,
emissionTimestamp: Date.now(),
value,
+ _tag: "MetricValue",
};
}
}
@@ -48,15 +52,21 @@ export class Metric implements IMetric {
public readonly time: IEmittableMetric,
public readonly failure: Metric,
public readonly success: Metric,
- public readonly _isIMetric: true = true,
+ 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`),
);
}
}
@@ -66,10 +76,10 @@ export interface MetricValue {
readonly unit: Unit;
readonly value: number;
readonly emissionTimestamp: number;
- readonly _isMetricValue: true;
+ readonly _tag: "MetricValue";
}
export const isMetricValue = (t: unknown): t is MetricValue =>
- isObject(t) && "_isMetricValue" in t;
+ isObject(t) && "_tag" in t && t._tag === "MetricValue";
export const isMetricsTraceSupplier = (t: unknown): t is MetricsTraceSupplier =>
isMetricValue(t) || isIMetric(t);
@@ -98,7 +108,7 @@ export class MetricsTrace implements ITrace<MetricsTraceSupplier> {
const foundMetricValues = this.tracing.flatMap((
[tracing, startedTracing],
) =>
- [tracing, tracing.success, tracing.failure]
+ [tracing, ...tracing.children()]
.filter((_tracing) => metric === _tracing)
.flatMap((metric) => [
this.addMetric(metric, startedTracing),
diff --git a/u/trace/trace.ts b/u/trace/trace.ts
index 72d4eef..e942066 100644
--- a/u/trace/trace.ts
+++ b/u/trace/trace.ts
@@ -41,18 +41,18 @@ export type LogMetricTraceSupplier = ITraceWith<
>;
export class LogMetricTrace implements ITrace<LogMetricTraceSupplier> {
constructor(
- private readonly logTrace: ITrace<LogTraceSupplier>,
- private readonly metricsTrace: ITrace<MetricsTraceSupplier>,
+ private logTrace: ITrace<LogTraceSupplier>,
+ private metricsTrace: ITrace<MetricsTraceSupplier>,
) {}
public addTrace(
trace: LogTraceSupplier | MetricsTraceSupplier,
): LogMetricTrace {
if (isMetricsTraceSupplier(trace)) {
- this.metricsTrace.addTrace(trace);
+ this.metricsTrace = this.metricsTrace.addTrace(trace);
return this;
}
- this.logTrace.addTrace(trace);
+ this.logTrace = this.logTrace.addTrace(trace);
return this;
}
diff --git a/u/trace/util.ts b/u/trace/util.ts
index dd8fb0d..302c8e4 100644
--- a/u/trace/util.ts
+++ b/u/trace/util.ts
@@ -7,6 +7,16 @@ import type {
} 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<
@@ -24,7 +34,7 @@ export class TraceUtil {
ITraceableTuple<T, Trace | Array<Trace>>,
Trace
> {
- return (t) => [t.get(), `[${f.name}]`];
+ return TraceUtil.withTrace(f.name);
}
static withClassTrace<C extends object, T, Trace>(
@@ -34,7 +44,7 @@ export class TraceUtil {
ITraceableTuple<T, Trace | Array<Trace>>,
Trace
> {
- return (t) => [t.get(), `[${c.constructor.name}]`];
+ return TraceUtil.withTrace(c.constructor.name);
}
static promiseify<T, U, Trace>(