summaryrefslogtreecommitdiff
path: root/u/server
diff options
context:
space:
mode:
Diffstat (limited to 'u/server')
-rw-r--r--u/server/activity/health.ts40
-rw-r--r--u/server/filter/index.ts2
-rw-r--r--u/server/filter/json.ts20
-rw-r--r--u/server/filter/method.ts30
-rw-r--r--u/server/http/body.ts10
-rw-r--r--u/server/http/index.ts3
-rw-r--r--u/server/http/method.ts1
-rw-r--r--u/server/http/status.ts71
-rw-r--r--u/server/index.ts12
-rw-r--r--u/server/request.ts39
-rw-r--r--u/server/request/index.ts18
-rw-r--r--u/server/request/pengueno.ts44
-rw-r--r--u/server/response.ts83
-rw-r--r--u/server/response/index.ts18
-rw-r--r--u/server/response/json_pengueno.ts29
-rw-r--r--u/server/response/pengueno.ts59
16 files changed, 294 insertions, 185 deletions
diff --git a/u/server/activity/health.ts b/u/server/activity/health.ts
index b3ae559..9396490 100644
--- a/u/server/activity/health.ts
+++ b/u/server/activity/health.ts
@@ -23,7 +23,7 @@ export interface IHealthCheckActivity {
checkHealth: IActivity;
}
-const healthCheckMetric: IMetric = Metric.fromName('Health');
+const healthCheckMetric = Metric.fromName('Health').asResult();
export interface HealthChecker
extends Mapper<ITraceable<HealthCheckInput, ServerTrace>, Promise<IEither<Error, HealthCheckOutput>>> {}
export class HealthCheckActivityImpl implements IHealthCheckActivity {
@@ -31,36 +31,18 @@ export class HealthCheckActivityImpl implements IHealthCheckActivity {
public checkHealth(req: ITraceable<PenguenoRequest, ServerTrace>) {
return req
- .bimap(TraceUtil.withFunctionTrace(this.checkHealth))
- .bimap(TraceUtil.withMetricTrace(healthCheckMetric))
+ .flatMap(TraceUtil.withFunctionTrace(this.checkHealth))
+ .flatMap(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);
- }),
- ),
- )
+ .peek(TraceUtil.promiseify(TraceUtil.traceResultingEither(healthCheckMetric)))
.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,
- }),
- ),
- ),
+ TraceUtil.promiseify((h) => {
+ const { status, message } = h.get().fold(
+ () => ({ status: 500, message: 'err' }),
+ () => ({ status: 200, message: 'ok' }),
+ );
+ return new JsonResponse(req, message, { status });
+ }),
)
.get();
}
diff --git a/u/server/filter/index.ts b/u/server/filter/index.ts
index 62a584d..75168c7 100644
--- a/u/server/filter/index.ts
+++ b/u/server/filter/index.ts
@@ -27,7 +27,7 @@ export interface RequestFilter<
Err extends PenguenoError = PenguenoError,
RIn = ITraceable<PenguenoRequest, ServerTrace>,
> {
- (req: RIn): Promise<IEither<Err, T>>;
+ (req: RIn): IEither<Err, T> | Promise<IEither<Err, T>>;
}
export * from './method.js';
diff --git a/u/server/filter/json.ts b/u/server/filter/json.ts
index 527d483..bc53d47 100644
--- a/u/server/filter/json.ts
+++ b/u/server/filter/json.ts
@@ -15,30 +15,22 @@ export interface JsonTransformer<R, ParsedJson = unknown> {
(json: ITraceable<ParsedJson, ServerTrace>): IEither<PenguenoError, R>;
}
-const ParseJsonMetric = Metric.fromName('JsonParse');
+const ParseJsonMetric = Metric.fromName('JsonParse').asResult();
export const jsonModel =
<MessageT>(jsonTransformer: JsonTransformer<MessageT>): RequestFilter<MessageT> =>
(r: ITraceable<PenguenoRequest, ServerTrace>) =>
r
- .bimap(TraceUtil.withFunctionTrace(jsonModel))
- .bimap(TraceUtil.withMetricTrace(ParseJsonMetric))
+ .flatMap(TraceUtil.withFunctionTrace(jsonModel))
+ .flatMap(TraceUtil.withMetricTrace(ParseJsonMetric))
.map((j) =>
- Either.fromFailableAsync<Error, MessageT>(<Promise<MessageT>>j.get().json()).then((either) =>
+ Either.fromFailableAsync<Error, MessageT>(<Promise<MessageT>>j.get().req.json()).then((either) =>
either.mapLeft((errReason) => {
- j.trace.addTrace(LogLevel.WARN).trace(errReason);
+ j.trace.traceScope(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']),
- ),
- ),
- )
+ .flatMapAsync(TraceUtil.promiseify(TraceUtil.traceResultingEither(ParseJsonMetric)))
.map(
TraceUtil.promiseify((traceableEitherJson) =>
traceableEitherJson
diff --git a/u/server/filter/method.ts b/u/server/filter/method.ts
index 5ca5716..7d6aa76 100644
--- a/u/server/filter/method.ts
+++ b/u/server/filter/method.ts
@@ -1,5 +1,7 @@
import {
Either,
+ HttpMethod,
+ IEither,
type ITraceable,
LogLevel,
PenguenoError,
@@ -9,24 +11,20 @@ import {
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);
- }),
- )
+ .flatMap(TraceUtil.withFunctionTrace(requireMethod))
+ .map((t): IEither<PenguenoError, HttpMethod> => {
+ const {
+ req: { method },
+ } = t.get();
+ if (!methods.includes(method)) {
+ const msg = "that's not how you pet me (⋟﹏⋞)~";
+ t.trace.traceScope(LogLevel.WARN).trace(msg);
+ return Either.left(new PenguenoError(msg, 405));
+ }
+ return Either.right(method);
+ })
.get();
diff --git a/u/server/http/body.ts b/u/server/http/body.ts
new file mode 100644
index 0000000..5fc4caa
--- /dev/null
+++ b/u/server/http/body.ts
@@ -0,0 +1,10 @@
+export type Body =
+ | ArrayBuffer
+ | AsyncIterable<Uint8Array>
+ | Blob
+ | FormData
+ | Iterable<Uint8Array>
+ | NodeJS.ArrayBufferView
+ | URLSearchParams
+ | null
+ | string;
diff --git a/u/server/http/index.ts b/u/server/http/index.ts
new file mode 100644
index 0000000..ef1c039
--- /dev/null
+++ b/u/server/http/index.ts
@@ -0,0 +1,3 @@
+export * from './body.js';
+export * from './status.js';
+export * from './method.js';
diff --git a/u/server/http/method.ts b/u/server/http/method.ts
new file mode 100644
index 0000000..172d77a
--- /dev/null
+++ b/u/server/http/method.ts
@@ -0,0 +1 @@
+export type HttpMethod = 'POST' | 'GET' | 'HEAD' | 'PUT' | 'DELETE' | 'CONNECT' | 'OPTIONS' | 'TRACE' | 'PATCH';
diff --git a/u/server/http/status.ts b/u/server/http/status.ts
new file mode 100644
index 0000000..15cb30c
--- /dev/null
+++ b/u/server/http/status.ts
@@ -0,0 +1,71 @@
+export const HttpStatusCodes: Record<number, string> = {
+ 100: 'Continue',
+ 101: 'Switching Protocols',
+ 102: 'Processing (WebDAV)',
+ 200: 'OK',
+ 201: 'Created',
+ 202: 'Accepted',
+ 203: 'Non-Authoritative Information',
+ 204: 'No Content',
+ 205: 'Reset Content',
+ 206: 'Partial Content',
+ 207: 'Multi-Status (WebDAV)',
+ 208: 'Already Reported (WebDAV)',
+ 226: 'IM Used',
+ 300: 'Multiple Choices',
+ 301: 'Moved Permanently',
+ 302: 'Found',
+ 303: 'See Other',
+ 304: 'Not Modified',
+ 305: 'Use Proxy',
+ 306: '(Unused)',
+ 307: 'Temporary Redirect',
+ 308: 'Permanent Redirect (experimental)',
+ 400: 'Bad Request',
+ 401: 'Unauthorized',
+ 402: 'Payment Required',
+ 403: 'Forbidden',
+ 404: 'Not Found',
+ 405: 'Method Not Allowed',
+ 406: 'Not Acceptable',
+ 407: 'Proxy Authentication Required',
+ 408: 'Request Timeout',
+ 409: 'Conflict',
+ 410: 'Gone',
+ 411: 'Length Required',
+ 412: 'Precondition Failed',
+ 413: 'Request Entity Too Large',
+ 414: 'Request-URI Too Long',
+ 415: 'Unsupported Media Type',
+ 416: 'Requested Range Not Satisfiable',
+ 417: 'Expectation Failed',
+ 418: "I'm a teapot (RFC 2324)",
+ 420: 'Enhance Your Calm (Twitter)',
+ 422: 'Unprocessable Entity (WebDAV)',
+ 423: 'Locked (WebDAV)',
+ 424: 'Failed Dependency (WebDAV)',
+ 425: 'Reserved for WebDAV',
+ 426: 'Upgrade Required',
+ 428: 'Precondition Required',
+ 429: 'Too Many Requests',
+ 431: 'Request Header Fields Too Large',
+ 444: 'No Response (Nginx)',
+ 449: 'Retry With (Microsoft)',
+ 450: 'Blocked by Windows Parental Controls (Microsoft)',
+ 451: 'Unavailable For Legal Reasons',
+ 499: 'Client Closed Request (Nginx)',
+ 500: 'Internal Server Error',
+ 501: 'Not Implemented',
+ 502: 'Bad Gateway',
+ 503: 'Service Unavailable',
+ 504: 'Gateway Timeout',
+ 505: 'HTTP Version Not Supported',
+ 506: 'Variant Also Negotiates (Experimental)',
+ 507: 'Insufficient Storage (WebDAV)',
+ 508: 'Loop Detected (WebDAV)',
+ 509: 'Bandwidth Limit Exceeded (Apache)',
+ 510: 'Not Extended',
+ 511: 'Network Authentication Required',
+ 598: 'Network read timeout error',
+ 599: 'Network connect timeout error',
+};
diff --git a/u/server/index.ts b/u/server/index.ts
index 17cbbdf..1cefb71 100644
--- a/u/server/index.ts
+++ b/u/server/index.ts
@@ -1,7 +1,13 @@
-import type { LogMetricTraceSupplier } from '@emprespresso/pengueno';
+import type { ITraceable, LogMetricTraceSupplier, Mapper } from '@emprespresso/pengueno';
export type ServerTrace = LogMetricTraceSupplier;
+export * from './http/index.js';
+export * from './response/index.js';
+export * from './request/index.js';
export * from './activity/index.js';
export * from './filter/index.js';
-export * from './response.js';
-export * from './request.js';
+
+import { PenguenoRequest, PenguenoResponse } from '@emprespresso/pengueno';
+export interface Server {
+ readonly serve: Mapper<ITraceable<PenguenoRequest, ServerTrace>, Promise<PenguenoResponse>>;
+}
diff --git a/u/server/request.ts b/u/server/request.ts
deleted file mode 100644
index 10610f1..0000000
--- a/u/server/request.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-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)];
-
-export class PenguenoRequest extends Request {
- 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;
-
- 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);
- }
-}
diff --git a/u/server/request/index.ts b/u/server/request/index.ts
new file mode 100644
index 0000000..41d59b7
--- /dev/null
+++ b/u/server/request/index.ts
@@ -0,0 +1,18 @@
+import { HttpMethod } from '@emprespresso/pengueno';
+
+export interface BaseRequest {
+ url: string;
+ method: HttpMethod;
+
+ header(): Record<string, string>;
+
+ formData(): Promise<FormData>;
+ json(): Promise<unknown>;
+ text(): Promise<string>;
+
+ param(key: string): string | undefined;
+ query(): Record<string, string>;
+ queries(): Record<string, string[]>;
+}
+
+export * from './pengueno.js';
diff --git a/u/server/request/pengueno.ts b/u/server/request/pengueno.ts
new file mode 100644
index 0000000..31563e9
--- /dev/null
+++ b/u/server/request/pengueno.ts
@@ -0,0 +1,44 @@
+import { BaseRequest, ITraceable, ServerTrace } 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 {
+ private constructor(
+ public readonly req: BaseRequest,
+ private readonly id: string,
+ private readonly at: Date,
+ ) {}
+
+ public elapsedTimeMs(after = () => Date.now()): number {
+ return after() - this.at.getTime();
+ }
+
+ public getResponseHeaders(): Record<string, string> {
+ const RequestId = this.id;
+ const RequestReceivedUnix = this.at.getTime();
+ const RequestHandleUnix = Date.now();
+ const DeltaUnix = this.elapsedTimeMs(() => RequestHandleUnix);
+ const Hai = penguenoGreeting();
+
+ return Object.entries({
+ RequestId,
+ RequestReceivedUnix,
+ RequestHandleUnix,
+ DeltaUnix,
+ Hai,
+ }).reduce((acc, [key, val]) => ({ ...acc, [key]: val!.toString() }), {});
+ }
+
+ public static from(request: ITraceable<BaseRequest, ServerTrace>): ITraceable<PenguenoRequest, ServerTrace> {
+ const id = crypto.randomUUID();
+ return request.bimap((tRequest) => {
+ const request = tRequest.get();
+ const url = new URL(request.url);
+ const { pathname } = url;
+ const trace = `RequestId = ${id}, Method = ${request.method}, Path = ${pathname}`;
+
+ return { item: new PenguenoRequest(request, id, new Date()), trace };
+ });
+ }
+}
diff --git a/u/server/response.ts b/u/server/response.ts
deleted file mode 100644
index 18d70b5..0000000
--- a/u/server/response.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-import {
- 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 = Omit<ResponseInit, 'headers'> & {
- status: number;
- headers?: Record<string, string>;
-};
-
-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 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);
- 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: 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,
- );
- }
-}
diff --git a/u/server/response/index.ts b/u/server/response/index.ts
new file mode 100644
index 0000000..17a2d97
--- /dev/null
+++ b/u/server/response/index.ts
@@ -0,0 +1,18 @@
+import { Body } from '@emprespresso/pengueno';
+
+export interface BaseResponse {
+ status: number;
+ statusText: string;
+ headers: Record<string, string>;
+
+ body(): Body;
+}
+
+export interface ResponseOpts {
+ status: number;
+ statusText?: string;
+ headers?: Record<string, string>;
+}
+
+export * from './pengueno.js';
+export * from './json_pengueno.js';
diff --git a/u/server/response/json_pengueno.ts b/u/server/response/json_pengueno.ts
new file mode 100644
index 0000000..d0b74a8
--- /dev/null
+++ b/u/server/response/json_pengueno.ts
@@ -0,0 +1,29 @@
+import {
+ isEither,
+ ITraceable,
+ PenguenoRequest,
+ PenguenoResponse,
+ ResponseOpts,
+ ServerTrace,
+} from '@emprespresso/pengueno';
+
+type Jsonable = any;
+export class JsonResponse extends PenguenoResponse {
+ constructor(req: ITraceable<PenguenoRequest, ServerTrace>, e: Jsonable, _opts: ResponseOpts) {
+ const opts = { ..._opts, headers: { ..._opts.headers, 'Content-Type': 'application/json' } };
+ if (isEither<Jsonable, Jsonable>(e)) {
+ super(
+ req,
+ JSON.stringify(
+ e.fold(
+ (error) => ({ error, ok: undefined }),
+ (ok) => ({ ok }),
+ ),
+ ),
+ opts,
+ );
+ return;
+ }
+ super(req, JSON.stringify(Math.floor(opts.status / 100) > 4 ? { error: e } : { ok: e }), opts);
+ }
+}
diff --git a/u/server/response/pengueno.ts b/u/server/response/pengueno.ts
new file mode 100644
index 0000000..5a953db
--- /dev/null
+++ b/u/server/response/pengueno.ts
@@ -0,0 +1,59 @@
+import {
+ BaseResponse,
+ Body,
+ HttpStatusCodes,
+ ITraceable,
+ Metric,
+ Optional,
+ PenguenoRequest,
+ ResponseOpts,
+ ServerTrace,
+} from '@emprespresso/pengueno';
+
+const getHeaders = (req: PenguenoRequest, extraHeaders: Record<string, string>) => {
+ const optHeaders = {
+ ...req.getResponseHeaders(),
+ ...extraHeaders,
+ };
+ optHeaders['Content-Type'] = (optHeaders['Content-Type'] ?? 'text/plain') + '; charset=utf-8';
+ return optHeaders;
+};
+
+const ResponseCodeMetrics = [0, 1, 2, 3, 4, 5].map((x) => Metric.fromName(`response.${x}xx`).asResult());
+export const getResponseMetrics = (status: number, elapsedMs?: number) => {
+ const index = Math.floor(status / 100);
+ return ResponseCodeMetrics.flatMap((metric, i) =>
+ Optional.from(i)
+ .filter((i) => i === index)
+ .map(() => [metric.count.withValue(1.0)])
+ .flatMap((metricValues) =>
+ Optional.from(elapsedMs)
+ .map((ms) => metricValues.concat(metric.time.withValue(ms)))
+ .orSome(() => metricValues),
+ )
+ .orSome(() => [metric.count.withValue(0.0)])
+ .get(),
+ );
+};
+
+export class PenguenoResponse implements BaseResponse {
+ public readonly statusText: string;
+ public readonly status: number;
+ public readonly headers: Record<string, string>;
+
+ constructor(
+ req: ITraceable<PenguenoRequest, ServerTrace>,
+ private readonly _body: Body,
+ opts: ResponseOpts,
+ ) {
+ this.headers = getHeaders(req.get(), opts?.headers ?? {});
+ this.status = opts.status;
+ this.statusText = opts.statusText ?? HttpStatusCodes[this.status]!;
+
+ req.trace.trace(getResponseMetrics(opts.status, req.get().elapsedTimeMs()));
+ }
+
+ public body() {
+ return this._body;
+ }
+}