diff options
33 files changed, 388 insertions, 253 deletions
@@ -1,3 +1,3 @@ { - "workspace": ["./model", "./worker", "./hooks", "./utils"] + "workspace": ["./model", "./worker", "./hooks", "./utils", "./lizutils"] } diff --git a/hooks/job/queue.ts b/hooks/job/queue.ts new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/hooks/job/queue.ts diff --git a/hooks/main.ts b/hooks/main.ts new file mode 100644 index 0000000..1348e57 --- /dev/null +++ b/hooks/main.ts @@ -0,0 +1 @@ +#!/usr/bin/env -S deno run --allow-env --allow-net --allow-run diff --git a/hooks/server.ts b/hooks/server.ts index 000c391..9a3f716 100755 --- a/hooks/server.ts +++ b/hooks/server.ts @@ -1,5 +1,3 @@ -#!/usr/bin/env -S deno run --allow-env --allow-net --allow-run - import { Either, getRequiredEnv, @@ -20,61 +18,6 @@ interface IHealthCheckActivity<R> { healthCheck(req: R): Promise<Response>; } -class HealthCheckActivity implements IHealthCheckActivity<Traceable<Request>> { - public async healthCheck( - req: Traceable<Request>, - ) { - return await req.bimap(Traceable.withClassTrace(this)) - .map(async (r) => { - const { logger } = r; - try { - getRequiredEnv("LAMINAR_HOST"); - await getStdout(r.map(() => ["laminarc", "show-jobs"])); - const msg = `think im healthy!! (✿˘◡˘) ready to do work~\n`; - logger.log(msg); - return new Response( - msg, - { status: 200 }, - ); - } catch (error) { - logger.error(error); - return new Response( - "oh no, i need to eat more vegetables (。•́︿•̀。)...\n", - { status: 500 }, - ); - } - }).item; - } -} - -const aPost = (req: Traceable<Request>): IEither<Response, Request> => { - const { item: request, logger: _logger } = req; - const logger = _logger.addTracer(() => "[aPost]"); - - if (request.method !== "POST") { - const msg = "that's not how you pet me (⋟﹏⋞) try post instead~"; - logger.warn(msg); - return Either.left(new Response(msg + "\n", { status: 405 })); - } - return Either.right(request); -}; - -type JsonTransformer<JsonT, R> = (json: Traceable<JsonT>) => Either<string, R>; -const aJson = - <BodyT, JsonT = unknown>(jsonTransformer: JsonTransformer<JsonT, BodyT>) => - async (r: Traceable<Request>): Promise<Either<string, BodyT>> => { - const { item: request, logger } = r; - try { - return Either.right<string, JsonT>(await request.json()) - .mapRight(r.move) - .flatMap(jsonTransformer); - } catch (_e) { - const err = "seems to be invalid JSON (>//<) can you fix?"; - logger.warn(err); - return Either.left(err); - } - }; - interface IJobHookActivity<R> { processHook(req: R): Promise<Response>; } @@ -82,9 +25,7 @@ type GetJobRequest = { jobType: string; args: unknown }; class JobHookActivityImpl implements IJobHookActivity<Traceable<Request>> { constructor(private readonly queuer: IJobQueuer<Traceable<Job>>) {} - private getJob<JsonT>( - u: Traceable<JsonT>, - ): Either<string, Job> { + private getJob<JsonT>(u: Traceable<JsonT>): Either<string, Job> { const { logger: _logger, item } = u; const logger = _logger.addTracer(() => "[getJob]"); const couldBeJsonJob = isObject(item) && "arguments" in item && diff --git a/hooks/server/activity/health.ts b/hooks/server/activity/health.ts new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/hooks/server/activity/health.ts diff --git a/hooks/server/activity/hook.ts b/hooks/server/activity/hook.ts new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/hooks/server/activity/hook.ts diff --git a/hooks/server/activity/mod.ts b/hooks/server/activity/mod.ts new file mode 100644 index 0000000..0dee57b --- /dev/null +++ b/hooks/server/activity/mod.ts @@ -0,0 +1,8 @@ +import type { RequestFilter } from "../filter/mod.ts"; + +export class r200 extends Response { + public override readonly status = 200; +} + +export interface IActivity extends RequestFilter<r200> { +} diff --git a/hooks/server/filter/json.ts b/hooks/server/filter/json.ts new file mode 100644 index 0000000..bcdd3ee --- /dev/null +++ b/hooks/server/filter/json.ts @@ -0,0 +1,25 @@ +import { Either, type IEither, type Traceable } from "@emprespresso/utils"; +import type { RequestFilter } from "./mod.ts"; + +type JsonTransformer<JsonT, R> = ( + json: Traceable<JsonT>, +) => IEither<Error, R>; +export const json = <BodyT, JsonT = unknown>( + jsonTransformer: JsonTransformer<JsonT, BodyT>, +): RequestFilter<BodyT> => +async (r: Traceable<Request>) => { + const { item: request, logger: _logger } = r; + const logger = _logger.addTracer(() => "[jsonVerification]"); + + const getJson = request.json().catch((errReason) => { + const err = "seems to be invalid JSON (>//<) can you fix?"; + logger.warn(err, errReason); + return new Error(err); + }); + return (await Either.fromAsync<Error, JsonT>(getJson)) + .mapRight(r.move).flatMap(jsonTransformer) + .mapLeft((err) => { + logger.warn(err); + return new Response(err.message, { status: 400 }); + }); +}; diff --git a/hooks/server/filter/method.ts b/hooks/server/filter/method.ts new file mode 100644 index 0000000..07fa5ee --- /dev/null +++ b/hooks/server/filter/method.ts @@ -0,0 +1,22 @@ +import { Either, type Traceable } from "@liz-ci/utils"; +import type { RequestFilter } from "./mod.ts"; + +type HttpMethod = "POST" | "GET" | "HEAD" | "PUT" | "DELETE"; +export const requireMethod = + (methods: Array<HttpMethod>): RequestFilter<HttpMethod> => + (req: Traceable<Request>) => { + const { item: request, logger: _logger } = req; + const logger = _logger.addTracer(() => "[aPost]"); + + const { method: _method } = request; + const method = <HttpMethod> _method; + if (!methods.includes(method)) { + const msg = "that's not how you pet me (⋟﹏⋞)~"; + logger.warn(msg); + return Promise.resolve(Either.left<Response, HttpMethod>( + new Response(msg + "\n", { status: 405 }), + )); + } + + return Promise.resolve(Either.right(method)); + }; diff --git a/hooks/server/filter/mod.ts b/hooks/server/filter/mod.ts new file mode 100644 index 0000000..bedc678 --- /dev/null +++ b/hooks/server/filter/mod.ts @@ -0,0 +1,18 @@ +import type { + IEither, + ITraceable, + ITraceableLogger, + TraceableLogger, +} from "@liz-ci/utils"; + +export interface RequestFilter< + T, + L extends ITraceableLogger<L> = TraceableLogger, + RIn = ITraceable<Request, L>, + Err = Response, +> { + (req: RIn): Promise<IEither<Err, T>>; +} + +export * from "./method.ts"; +export * from "./json.ts"; diff --git a/hooks/server/mod.ts b/hooks/server/mod.ts new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/hooks/server/mod.ts diff --git a/u/deno.json b/u/deno.json new file mode 100644 index 0000000..46e74d6 --- /dev/null +++ b/u/deno.json @@ -0,0 +1,6 @@ +{ + "name": "@emprespresso/utils", + "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..2749947 --- /dev/null +++ b/u/fn/callable.ts @@ -0,0 +1,15 @@ +// 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 SideEffect<T> extends Callable<void, T> { +} + +export interface Mapper<T, U> extends Callable<U, T> { + (t: T): U; +} diff --git a/u/fn/either.ts b/u/fn/either.ts new file mode 100644 index 0000000..eaf77fd --- /dev/null +++ b/u/fn/either.ts @@ -0,0 +1,62 @@ +import type { Mapper, Supplier } from "./mod.ts"; + +export interface IEither<E, T> { + mapBoth: <Ee, Tt>( + errBranch: Mapper<E, Ee>, + okBranch: Mapper<T, Tt>, + ) => IEither<Ee, Tt>; + flatMap: <Tt>(mapper: Mapper<T, IEither<E, Tt>>) => IEither<E, Tt>; + mapRight: <Tt>(mapper: Mapper<T, Tt>) => IEither<E, Tt>; + mapLeft: <Ee>(mapper: Mapper<E, Ee>) => IEither<Ee, T>; +} + +export class Either<E, T> implements IEither<E, T> { + private constructor(private readonly err?: E, private readonly ok?: T) {} + + public mapBoth<Ee, Tt>( + errBranch: Mapper<E, Ee>, + okBranch: Mapper<T, Tt>, + ): Either<Ee, Tt> { + if (this.err) return Either.left(errBranch(this.err)); + return Either.right(okBranch(this.ok!)); + } + + public flatMap<Tt>(mapper: Mapper<T, Either<E, Tt>>) { + if (this.ok) return mapper(this.ok); + return Either.left<E, Tt>(this.err!); + } + + public mapRight<Tt>(mapper: Mapper<T, Tt>): IEither<E, Tt> { + if (this.ok) return Either.right(mapper(this.ok)); + return Either.left<E, Tt>(this.err!); + } + + public mapLeft<Ee>(mapper: Mapper<E, Ee>) { + if (this.err) return Either.left<Ee, T>(mapper(this.err)); + return Either.right<Ee, T>(this.ok!); + } + + 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); + } + } +} 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/mod.ts b/u/leftpadesque/mod.ts new file mode 100644 index 0000000..801846a --- /dev/null +++ b/u/leftpadesque/mod.ts @@ -0,0 +1,3 @@ +export * from "./object.ts"; +export * from "./prepend.ts"; +export * from "./debug.ts"; diff --git a/utils/isObject.ts b/u/leftpadesque/object.ts index 73f7f80..73f7f80 100644 --- a/utils/isObject.ts +++ b/u/leftpadesque/object.ts diff --git a/utils/prepend.ts b/u/leftpadesque/prepend.ts index 9b77aff..9b77aff 100644 --- a/utils/prepend.ts +++ b/u/leftpadesque/prepend.ts diff --git a/u/mod.ts b/u/mod.ts new file mode 100644 index 0000000..fab6804 --- /dev/null +++ b/u/mod.ts @@ -0,0 +1,4 @@ +export * from "./fn/mod.ts"; +export * from "./leftpadesque/mod.ts"; +export * from "./process/mod.ts"; +export * from "./trace/mod.ts"; diff --git a/u/process/env.ts b/u/process/env.ts new file mode 100644 index 0000000..e80ec4a --- /dev/null +++ b/u/process/env.ts @@ -0,0 +1,10 @@ +import { Either, type IEither } from "@emprespresso/utils"; + +export const getRequiredEnv = (name: string): IEither<Error, string> => + Either.fromFailable(() => { + const value = Deno.env.get(name); // could throw when no permission. + if (!value) { + throw new Error(`environment variable "${name}" is required D:`); + } + return value; + }); 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/utils/run.ts b/u/process/run.ts index 9093863..6dc37d0 100644 --- a/utils/run.ts +++ b/u/process/run.ts @@ -1,4 +1,4 @@ -import { Either, type Traceable } from "./mod.ts"; +import { Either, type Traceable } from "@emprespresso/utils"; export class ProcessError extends Error {} export const getStdout = async ( diff --git a/utils/validate_identifier.ts b/u/process/validate_identifier.ts index ec8b77b..ec8b77b 100644 --- a/utils/validate_identifier.ts +++ b/u/process/validate_identifier.ts diff --git a/u/server/mod.ts b/u/server/mod.ts new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/u/server/mod.ts diff --git a/u/trace/itrace.ts b/u/trace/itrace.ts new file mode 100644 index 0000000..b483067 --- /dev/null +++ b/u/trace/itrace.ts @@ -0,0 +1,72 @@ +import { Mapper, SideEffect } from "../fn/mod.ts"; + +export interface ITrace<TracingW> { + addTrace: Mapper<TracingW, ITrace<TracingW>>; + trace: SideEffect<TracingW>; +} + +export type ITraceableTuple<T, Trace> = [T, Trace]; +export type ITraceableMapper<T, Trace, U, W = ITraceable<T, Trace>> = ( + w: W, +) => U; + +export interface ITraceable<T, Trace> { + readonly item: T; + readonly trace: ITrace<Trace>; + + move<U>(u: U): ITraceable<U, Trace>; + map: <U>( + mapper: ITraceableMapper<T, Trace, U>, + ) => ITraceable<U, Trace>; + bimap: <U>( + mapper: ITraceableMapper<T, Trace, ITraceableTuple<U, Trace>>, + ) => ITraceable<U, Trace>; + peek: (peek: ITraceableMapper<T, Trace, void>) => ITraceable<T, Trace>; + flatMap: <U>( + mapper: ITraceableMapper<T, Trace, ITraceable<U, Trace>>, + ) => ITraceable<U, Trace>; + flatMapAsync<U>( + mapper: ITraceableMapper<T, Trace, Promise<ITraceable<U, Trace>>>, + ): ITraceable<Promise<U>, Trace>; +} + +export class TraceableImpl<T, L> implements ITraceable<T, L> { + protected constructor( + readonly item: T, + readonly trace: ITrace<L>, + ) {} + + public map<U>(mapper: ITraceableMapper<T, L, U>) { + const result = mapper(this); + return new TraceableImpl(result, this.trace); + } + + public flatMap<U>( + mapper: ITraceableMapper<T, L, ITraceable<U, L>>, + ): ITraceable<U, L> { + return mapper(this); + } + + public flatMapAsync<U>( + mapper: ITraceableMapper<T, L, Promise<ITraceable<U, L>>>, + ): ITraceable<Promise<U>, L> { + return new TraceableImpl( + mapper(this).then(({ item }) => item), + this.trace, + ); + } + + public peek(peek: ITraceableMapper<T, L, void>) { + peek(this); + return this; + } + + public move<Tt>(t: Tt): ITraceable<Tt, L> { + return this.map(() => t); + } + + public bimap<U>(mapper: ITraceableMapper<T, L, ITraceableTuple<U, L>>) { + const [item, trace] = mapper(this); + return new TraceableImpl(item, this.trace.addTrace(trace)); + } +} diff --git a/u/trace/logger.ts b/u/trace/logger.ts new file mode 100644 index 0000000..79da367 --- /dev/null +++ b/u/trace/logger.ts @@ -0,0 +1,86 @@ +import { + isDebug, + isObject, + type ITrace, + type SideEffect, + type Supplier, +} from "@emprespresso/utils"; + +export interface ILogger { + log: (...args: unknown[]) => void; + debug: (...args: unknown[]) => void; + warn: (...args: unknown[]) => void; + error: (...args: unknown[]) => void; +} +export type ILoggerLevel = "UNKNOWN" | "INFO" | "WARN" | "DEBUG" | "ERROR"; +const logLevelOrder: Array<ILoggerLevel> = ["DEBUG", "INFO", "WARN", "ERROR"]; +const defaultAllowedLevels = () => + [ + "UNKNOWN", + ...(isDebug() ? ["DEBUG"] : []), + "INFO", + "WARN", + "ERROR", + ] as Array<ILoggerLevel>; + +export const logWithLevel = ( + logger: ILogger, + level: ILoggerLevel, +): SideEffect<unknown> => { + switch (level) { + case "UNKNOWN": + case "INFO": + return logger.log; + case "DEBUG": + return logger.debug; + case "WARN": + return logger.warn; + case "ERROR": + return logger.error; + } +}; + +export const LoggerImpl = console; + +export type LogTraceSupplier = string | Supplier<string> | { + level: ILoggerLevel; +}; + +const foldTraces = (traces: Array<LogTraceSupplier>) => { + const { line, level } = traces.reduce( + (acc: { line: string; level: number }, t) => { + if (isObject(t) && "level" in t) { + return { + ...acc, + level: Math.max(logLevelOrder.indexOf(t.level), acc.level), + }; + } + const prefix = [ + acc.line, + typeof t === "function" ? t() : t, + ].join(" "); + return { ...acc, prefix }; + }, + { line: "", level: -1 }, + ); + return { line, level: logLevelOrder[level] ?? "UNKNOWN" }; +}; + +const defaultTrace = () => `[${new Date().toISOString()}]`; +export const LogTrace = ( + logger: ILogger, + traces: Array<LogTraceSupplier> = [defaultTrace], + allowedLevels: Supplier<Array<ILoggerLevel>> = defaultAllowedLevels, + defaultLevel: ILoggerLevel = "INFO", +): ITrace<LogTraceSupplier> => { + return { + addTrace: (trace: LogTraceSupplier) => + LogTrace(logger, traces.concat(trace)), + trace: (trace: LogTraceSupplier) => { + const { line, level: _level } = foldTraces(traces.concat(trace)); + if (!allowedLevels().includes(_level)) return; + const level = _level === "UNKNOWN" ? defaultLevel : _level; + logWithLevel(logger, level)(`[${level}]${line}`); + }, + }; +}; diff --git a/u/trace/mod.ts b/u/trace/mod.ts new file mode 100644 index 0000000..9c42858 --- /dev/null +++ b/u/trace/mod.ts @@ -0,0 +1,3 @@ +export * from "./itrace.ts"; +export * from "./logger.ts"; +export * from "./trace.ts"; diff --git a/u/trace/trace.ts b/u/trace/trace.ts new file mode 100644 index 0000000..5d5c59b --- /dev/null +++ b/u/trace/trace.ts @@ -0,0 +1,34 @@ +import type { Callable } from "@emprespresso/utils"; +import { + type ITraceableMapper, + type ITraceableTuple, + TraceableImpl, + TraceableLogger, +} from "./mod.ts"; + +export class Traceable<T> extends TraceableImpl<T, TraceableLogger> { + static from<T>(t: T) { + return new Traceable(t, new TraceableLogger()); + } + + static withFunctionTrace<F extends Callable, T>( + f: F, + ): ITraceableMapper<T, TraceableLogger, ITraceableTuple<T>> { + return (t) => [t.item, f.name]; + } + + static withClassTrace<C extends object, T>( + c: C, + ): ITraceableMapper<T, TraceableLogger, ITraceableTuple<T>> { + return (t) => [t.item, c.constructor.name]; + } + + static promiseify<T, U>( + mapper: ITraceableMapper<T, TraceableLogger, U>, + ): ITraceableMapper<Promise<T>, TraceableLogger, Promise<U>> { + return (traceablePromise) => + traceablePromise.flatMapAsync(async (t) => + t.move(await t.item).map(mapper) + ).item; + } +} diff --git a/utils/either.ts b/utils/either.ts deleted file mode 100644 index 10e4f43..0000000 --- a/utils/either.ts +++ /dev/null @@ -1,48 +0,0 @@ -export interface IEither<E, T> { - ok?: T; - err?: E; - mapBoth: <Ee, Tt>( - errBranch: (e: E) => Ee, - okBranch: (o: T) => Tt, - ) => IEither<Ee, Tt>; - mapRight: <Tt>(mapper: (t: T) => Tt) => Either<E, Tt>; - mapLeft: <Ee>(mapper: (e: E) => Ee) => Either<Ee, T>; - flatMap: <Ee extends E, Tt>( - mapper: (e: T) => Either<Ee, Tt>, - ) => Either<Ee, Tt>; -} - -export class Either<E, T> implements IEither<E, T> { - private constructor(readonly err?: E, readonly ok?: T) {} - - public mapBoth<Ee, Tt>( - errBranch: (e: E) => Ee, - okBranch: (t: T) => Tt, - ): Either<Ee, Tt> { - if (this.err) return Either.left(errBranch(this.err)); - return Either.right(okBranch(this.ok!)); - } - - public flatMap<Ee extends E, Tt>(mapper: (t: T) => Either<Ee, Tt>) { - if (this.ok) return mapper(this.ok); - return this; - } - - public mapRight<Tt>(mapper: (t: T) => Tt): Either<E, Tt> { - if (this.ok) return Either.right(mapper(this.ok)); - return Either.left(this.err!); - } - - public mapLeft<Ee>(mapper: (e: E) => Ee): Either<Ee, T> { - if (this.err) return Either.left(mapper(this.err)); - return Either.right(this.ok!); - } - - 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); - } -} diff --git a/utils/env.ts b/utils/env.ts deleted file mode 100644 index 31b7ccf..0000000 --- a/utils/env.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const getRequiredEnv = (name: string): string => { - const value = Deno.env.get(name); - if (!value) throw new Error(`environment variable "${name}" is required D:`); - return value; -}; diff --git a/utils/mod.ts b/utils/mod.ts deleted file mode 100644 index d8cb526..0000000 --- a/utils/mod.ts +++ /dev/null @@ -1,8 +0,0 @@ -export * from "./isObject.ts"; -export * from "./trace.ts"; -export * from "./either.ts"; -export * from "./env.ts"; -export * from "./run.ts"; -export * from "./secret.ts"; -export * from "./validate_identifier.ts"; -export * from "./prepend.ts"; diff --git a/utils/trace.ts b/utils/trace.ts deleted file mode 100644 index 1a5e51d..0000000 --- a/utils/trace.ts +++ /dev/null @@ -1,130 +0,0 @@ -export interface Logger { - log: (...args: unknown[]) => void; - debug: (...args: unknown[]) => void; - warn: (...args: unknown[]) => void; - error: (...args: unknown[]) => void; -} - -type Supplier<T> = () => T; -type TraceSupplier = Supplier<string>; -export interface ITraceableLogger<L extends ITraceableLogger<L>> - extends Logger { - addTracer: (traceSupplier: TraceSupplier) => L; -} - -export type ITraceableTuple<T> = [T, TraceSupplier]; -export type ITraceableMapper<T, L extends ITraceableLogger<L>, U> = ( - t: ITraceable<T, L>, -) => U; -export interface ITraceable<T, L extends ITraceableLogger<L>> { - item: T; - logger: L; - - map: <U>(mapper: ITraceableMapper<T, L, U>) => ITraceable<U, L>; - bimap: <U>( - mapper: ITraceableMapper<T, L, ITraceableTuple<U>>, - ) => ITraceable<U, L>; - peek: (peek: ITraceableMapper<T, L, void>) => ITraceable<T, L>; - flatMap: <U>( - mapper: ITraceableMapper<T, L, ITraceable<U, L>>, - ) => ITraceable<U, L>; - flatMapAsync<U>( - mapper: ITraceableMapper<T, L, Promise<ITraceable<U, L>>>, - ): ITraceable<Promise<U>, L>; - move<Tt>(t: Tt): ITraceable<Tt, L>; -} - -export class TraceableLogger implements ITraceableLogger<TraceableLogger> { - private readonly logger: Logger = console; - constructor( - private readonly traces = [() => `[${new Date().toISOString()}]`], - ) { - } - - public debug(...args: unknown[]) { - this.logger.debug("[DEBUG]", ...this.getPrefix(), args); - } - - public log(...args: unknown[]) { - this.logger.log("[INFO]", ...this.getPrefix(), args); - } - - public warn(...args: unknown[]) { - this.logger.warn("[WARN]", ...this.getPrefix(), args); - } - - public error(...args: unknown[]) { - this.logger.error("[ERROR]", ...this.getPrefix(), args); - } - - public addTracer(traceSupplier: TraceSupplier) { - return new TraceableLogger(this.traces.concat(traceSupplier)); - } - - private getPrefix() { - return this.traces.map((tracer) => tracer()); - } -} - -class TraceableImpl< - T, - L extends ITraceableLogger<L>, -> implements ITraceable<T, L> { - protected constructor(readonly item: T, readonly logger: L) {} - - public map<U>(mapper: ITraceableMapper<T, L, U>) { - const result = mapper(this); - return new TraceableImpl(result, this.logger); - } - - public flatMap<U>( - mapper: ITraceableMapper<T, L, ITraceable<U, L>>, - ): ITraceable<U, L> { - return mapper(this); - } - - public flatMapAsync<U>( - mapper: ITraceableMapper<T, L, Promise<ITraceable<U, L>>>, - ): ITraceable<Promise<U>, L> { - return new TraceableImpl( - mapper(this).then(({ item }) => item), - this.logger, - ); - } - - public peek(peek: ITraceableMapper<T, L, void>) { - peek(this); - return this; - } - - public move<Tt>(t: Tt) { - return this.map(() => t); - } - - public bimap<U>(mapper: ITraceableMapper<T, L, ITraceableTuple<U>>) { - const [item, trace] = mapper(this); - return new TraceableImpl(item, this.logger.addTracer(trace)); - } - - static promiseify<T, L extends ITraceableLogger<L>, U>( - mapper: ITraceableMapper<T, L, U>, - ): ITraceableMapper<Promise<T>, L, Promise<U>> { - return (traceablePromise) => - traceablePromise.flatMapAsync(async (t) => { - const item = await t.item; - return t.map(() => item).map(mapper); - }).item; - } -} - -export class Traceable<T> extends TraceableImpl<T, TraceableLogger> { - static withClassTrace<C extends object, T>( - c: C, - ): ITraceableMapper<T, TraceableLogger, ITraceableTuple<T>> { - return (t) => [t.item, () => c.constructor.name]; - } - - static from<T>(t: T) { - return new Traceable(t, new TraceableLogger()); - } -} |