summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorElizabeth Hunt <me@liz.coffee>2025-06-29 17:31:30 -0700
committerElizabeth Hunt <me@liz.coffee>2025-06-29 17:31:30 -0700
commit58be1809c46cbe517a18d86d0af52179dcc5cbf6 (patch)
tree9ccc678b3fd48c1a52fe501600dd2c2051740a55
parentd4791f3d357634daf506fb8f91cc5332a794c421 (diff)
downloadci-58be1809c46cbe517a18d86d0af52179dcc5cbf6.tar.gz
ci-58be1809c46cbe517a18d86d0af52179dcc5cbf6.zip
Move to nodejs and also lots of significant refactoring that should've been broken up but idgaf
-rwxr-xr-xindex.ts16
-rw-r--r--model/index.ts4
-rw-r--r--model/job/index.ts2
-rw-r--r--model/pipeline/builder.ts6
-rw-r--r--model/pipeline/impl.ts2
-rw-r--r--model/pipeline/index.ts6
-rw-r--r--package.json2
-rw-r--r--server/ci.ts30
-rw-r--r--server/health.ts2
-rw-r--r--server/hono_proxy.ts71
-rw-r--r--server/index.ts32
-rw-r--r--server/job/index.ts4
-rw-r--r--server/job/queue.ts40
-rw-r--r--server/job/run_activity.ts55
-rw-r--r--tsconfig.json2
-rw-r--r--u/fn/either.ts100
-rw-r--r--u/history.ts36
-rw-r--r--u/index.ts5
-rw-r--r--u/leftpadesque/index.ts1
-rw-r--r--u/process/argv.ts42
-rw-r--r--u/process/env.ts13
-rw-r--r--u/process/index.ts1
-rw-r--r--u/process/run.ts36
-rw-r--r--u/process/signals.ts49
-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
-rw-r--r--u/trace/index.ts6
-rw-r--r--u/trace/itrace.ts75
-rw-r--r--u/trace/log/ansi.ts15
-rw-r--r--u/trace/log/index.ts5
-rw-r--r--u/trace/log/level.ts19
-rw-r--r--u/trace/log/logger.ts5
-rw-r--r--u/trace/log/pretty_json_console.ts39
-rw-r--r--u/trace/log/trace.ts60
-rw-r--r--u/trace/logger.ts126
-rw-r--r--u/trace/metric/emittable.ts18
-rw-r--r--u/trace/metric/index.ts41
-rw-r--r--u/trace/metric/metric.ts54
-rw-r--r--u/trace/metric/trace.ts59
-rw-r--r--u/trace/metrics.ts140
-rw-r--r--u/trace/trace.ts14
-rw-r--r--u/trace/util.ts72
-rw-r--r--u/types/collections/cons.ts40
-rw-r--r--u/types/collections/index.ts2
-rw-r--r--u/types/collections/list_zipper.ts70
-rw-r--r--u/types/fn/callable.ts (renamed from u/fn/callable.ts)1
-rw-r--r--u/types/fn/either.ts106
-rw-r--r--u/types/fn/index.ts (renamed from u/fn/index.ts)1
-rw-r--r--u/types/fn/optional.ts93
-rw-r--r--u/types/index.ts5
-rw-r--r--u/types/object.ts (renamed from u/leftpadesque/object.ts)0
-rw-r--r--u/types/tagged.ts8
-rw-r--r--worker/executor.ts76
-rwxr-xr-xworker/scripts/ansible_playbook.ts4
-rwxr-xr-xworker/scripts/build_docker_image.ts49
-rwxr-xr-xworker/scripts/checkout_ci.ts72
-rw-r--r--worker/secret.ts73
71 files changed, 1416 insertions, 968 deletions
diff --git a/index.ts b/index.ts
index a9defca..86119ca 100755
--- a/index.ts
+++ b/index.ts
@@ -3,7 +3,7 @@
import { argv, IEither, Either } from '@emprespresso/pengueno';
import { runServer } from '@emprespresso/ci_server';
-const main = (_argv = process.argv.slice(2)): Promise<IEither<Error, 0>> =>
+const main = (_argv = process.argv.slice(2)): Promise<IEither<Error, void>> =>
argv(
['--run-server', '--port', '--host'],
{
@@ -18,19 +18,17 @@ const main = (_argv = process.argv.slice(2)): Promise<IEither<Error, 0>> =>
port: args['--port'],
host: args['--host'],
}))
- .flatMapAsync((runConfig) => {
- if (runConfig.server_mode) {
- return runServer(runConfig.port, runConfig.host);
+ .flatMapAsync(async (runConfig) => {
+ if (!runConfig.server_mode) {
+ return Either.right(<void>undefined);
}
- return Promise.resolve(Either.right(0));
+ return runServer(runConfig.port, runConfig.host);
});
if (process.argv[1] === import.meta.filename) {
await main().then((eitherDone) =>
- eitherDone.fold(({ isLeft, value }) => {
- if (!isLeft) return;
-
- console.error(`failed to start`, value);
+ eitherDone.mapLeft((err) => {
+ console.error(`failed to start`, err);
process.exit(1);
}),
);
diff --git a/model/index.ts b/model/index.ts
index 094c693..5e91071 100644
--- a/model/index.ts
+++ b/model/index.ts
@@ -1,2 +1,2 @@
-export * from './job';
-export * from './pipeline';
+export * from './job/index.js';
+export * from './pipeline/index.js';
diff --git a/model/job/index.ts b/model/job/index.ts
index 78f69d6..b7fb3b0 100644
--- a/model/job/index.ts
+++ b/model/job/index.ts
@@ -8,4 +8,4 @@ export interface Job {
export const isJob = (j: unknown): j is Job =>
!!(isObject(j) && 'arguments' in j && isObject(j.arguments) && 'type' in j && typeof j.type === 'string' && j);
-export * from './jobs';
+export * from './jobs.js';
diff --git a/model/pipeline/builder.ts b/model/pipeline/builder.ts
index e95e89c..dac9452 100644
--- a/model/pipeline/builder.ts
+++ b/model/pipeline/builder.ts
@@ -1,6 +1,6 @@
-import { Pipeline, PipelineStage } from '.';
-import { FetchCodeJob } from '../job';
-import { PipelineImpl } from './impl';
+import { Pipeline, PipelineStage } from './index.js';
+import { FetchCodeJob } from '../job/index.js';
+import { PipelineImpl } from './impl.js';
export interface PipelineBuilder {
addStage(stage: PipelineStage): PipelineBuilder;
diff --git a/model/pipeline/impl.ts b/model/pipeline/impl.ts
index 2e08d6e..406a05e 100644
--- a/model/pipeline/impl.ts
+++ b/model/pipeline/impl.ts
@@ -1,5 +1,5 @@
import { Either, IEither } from '@emprespresso/pengueno';
-import { isPipeline, Pipeline, PipelineStage } from '.';
+import { isPipeline, Pipeline, PipelineStage } from './index.js';
export class PipelineImpl implements Pipeline {
constructor(public readonly serialJobs: Array<PipelineStage>) {}
diff --git a/model/pipeline/index.ts b/model/pipeline/index.ts
index adf902b..fd14e8d 100644
--- a/model/pipeline/index.ts
+++ b/model/pipeline/index.ts
@@ -1,5 +1,5 @@
import { isObject } from '@emprespresso/pengueno';
-import { isJob, Job } from '../job';
+import { isJob, Job } from '../job/index.js';
export interface PipelineStage {
readonly parallelJobs: Array<Job>;
@@ -15,5 +15,5 @@ export interface Pipeline {
serialize(): string;
}
-export * from './builder';
-export * from './impl';
+export * from './builder.js';
+export * from './impl.js';
diff --git a/package.json b/package.json
index 8107bdf..9ed995f 100644
--- a/package.json
+++ b/package.json
@@ -18,7 +18,7 @@
"format": "prettier --write .",
"format:check": "prettier --check .",
"type-check": "tsc --noEmit",
- "clean": "npm run clean --workspaces --if-present && rm -rf dist",
+ "clean": "npm run clean --workspaces --if-present && rm -rf dist node_modules",
"start": "node dist/index.js"
},
"devDependencies": {
diff --git a/server/ci.ts b/server/ci.ts
index f57c426..c8aa6a1 100644
--- a/server/ci.ts
+++ b/server/ci.ts
@@ -1,34 +1,41 @@
import {
FourOhFourActivityImpl,
- getRequiredEnv,
+ getEnv,
HealthCheckActivityImpl,
type HealthChecker,
type IFourOhFourActivity,
type IHealthCheckActivity,
type ITraceable,
PenguenoRequest,
+ Server,
type ServerTrace,
- TraceUtil,
} from '@emprespresso/pengueno';
import type { Job } from '@emprespresso/ci_model';
-import { type IJobHookActivity, type IJobQueuer, JobHookActivityImpl, LaminarJobQueuer } from './job';
-import { healthCheck as _healthCheck } from '.';
+import {
+ healthCheck as _healthCheck,
+ type IJobHookActivity,
+ type IJobQueuer,
+ JobHookActivityImpl,
+ LaminarJobQueuer,
+} from '@emprespresso/ci_server';
export const DEFAULT_CI_SERVER = 'https://ci.liz.coffee';
-export class CiHookServer {
+export class CiHookServer implements Server {
constructor(
healthCheck: HealthChecker = _healthCheck,
jobQueuer: IJobQueuer<ITraceable<Job, ServerTrace>> = new LaminarJobQueuer(
- getRequiredEnv('LAMINAR_URL').fold(({ isLeft, value }) => (isLeft ? DEFAULT_CI_SERVER : value)),
+ getEnv('LAMINAR_URL')
+ .orSome(() => DEFAULT_CI_SERVER)
+ .get(),
),
private readonly healthCheckActivity: IHealthCheckActivity = new HealthCheckActivityImpl(healthCheck),
private readonly jobHookActivity: IJobHookActivity = new JobHookActivityImpl(jobQueuer),
private readonly fourOhFourActivity: IFourOhFourActivity = new FourOhFourActivityImpl(),
) {}
- private route(req: ITraceable<PenguenoRequest, ServerTrace>) {
- const url = new URL(req.get().url);
+ public serve(req: ITraceable<PenguenoRequest, ServerTrace>) {
+ const url = new URL(req.get().req.url);
if (url.pathname === '/health') {
return this.healthCheckActivity.checkHealth(req);
}
@@ -37,11 +44,4 @@ export class CiHookServer {
}
return this.fourOhFourActivity.fourOhFour(req);
}
-
- public serve(req: Request): Promise<Response> {
- return PenguenoRequest.from(req)
- .bimap(TraceUtil.withClassTrace(this))
- .map((req) => this.route(req))
- .get();
- }
}
diff --git a/server/health.ts b/server/health.ts
index 8435865..6a1f77f 100644
--- a/server/health.ts
+++ b/server/health.ts
@@ -14,7 +14,7 @@ export const healthCheck: HealthChecker = (
input: ITraceable<HealthCheckInput, ServerTrace>,
): Promise<IEither<Error, HealthCheckOutput>> =>
input
- .bimap(TraceUtil.withFunctionTrace(healthCheck))
+ .flatMap(TraceUtil.withFunctionTrace(healthCheck))
.move(getRequiredEnv('LAMINAR_HOST'))
// ensure LAMINAR_HOST is propagated to getStdout for other procedures
.map((tEitherEnv) =>
diff --git a/server/hono_proxy.ts b/server/hono_proxy.ts
new file mode 100644
index 0000000..f729819
--- /dev/null
+++ b/server/hono_proxy.ts
@@ -0,0 +1,71 @@
+import {
+ BaseRequest,
+ Either,
+ IEither,
+ LogMetricTraceable,
+ Metric,
+ PenguenoRequest,
+ Server,
+ Signals,
+ TraceUtil,
+} from '@emprespresso/pengueno';
+
+import { serve, ServerType } from '@hono/node-server';
+import { Hono } from 'hono';
+
+const AppLifetimeMetric = Metric.fromName('HonoAppLifetime').asResult();
+const AppRequestMetric = Metric.fromName('HonoAppRequest');
+
+export class HonoProxy {
+ private readonly app = LogMetricTraceable.of(new Hono())
+ .flatMap(TraceUtil.withTrace(`AppId = ${crypto.randomUUID()}`))
+ .flatMap(TraceUtil.withMetricTrace(AppLifetimeMetric));
+
+ constructor(private readonly server: Server) {}
+
+ public async serve(port: number, hostname: string): Promise<IEither<Error, void>> {
+ return this.app
+ .map((tApp) =>
+ Either.fromFailable<Error, ServerType>(() => {
+ const app = tApp.get();
+ app.all('*', async (c) =>
+ tApp
+ .flatMap(TraceUtil.withMetricTrace(AppRequestMetric))
+ .move(<BaseRequest>c.req)
+ .flatMap((tRequest) => PenguenoRequest.from(tRequest))
+ .map((req) => this.server.serve(req))
+ .map(
+ TraceUtil.promiseify((tResponse) => {
+ tResponse.trace.trace(AppRequestMetric.count.withValue(1.0));
+ return new Response(tResponse.get().body(), tResponse.get());
+ }),
+ )
+ .get(),
+ );
+ return serve({
+ fetch: (_r) => app.fetch(_r),
+ port,
+ hostname,
+ });
+ }),
+ )
+ .peek(TraceUtil.traceResultingEither())
+ .peek((tServe) =>
+ tServe
+ .get()
+ .mapRight(() =>
+ tServe.trace.trace(
+ `haii im still listening at http://${hostname}:${port} ~uwu dont think i forgot`,
+ ),
+ ),
+ )
+ .map((tEitherServer) =>
+ tEitherServer
+ .get()
+ .mapRight((server) => tEitherServer.move(server))
+ .flatMapAsync((tServer) => Signals.awaitClose(tServer)),
+ )
+ .peek(TraceUtil.promiseify(TraceUtil.traceResultingEither(AppLifetimeMetric)))
+ .get();
+ }
+}
diff --git a/server/index.ts b/server/index.ts
index c33b43e..d018a4e 100644
--- a/server/index.ts
+++ b/server/index.ts
@@ -1,31 +1,13 @@
#!/usr/bin/env node
-export * from './job';
-export * from './ci';
-export * from './health';
+export * from './job/index.js';
+export * from './ci.js';
+export * from './health.js';
+export * from './hono_proxy.js';
-import { CiHookServer } from '.';
-import { Either, type IEither } from '@emprespresso/pengueno';
-import { serve } from '@hono/node-server';
-import { Hono } from 'hono';
+import { CiHookServer, HonoProxy } from '@emprespresso/ci_server';
const server = new CiHookServer();
+const hono = new HonoProxy(server);
-const neverEndingPromise = new Promise<IEither<Error, 0>>(() => {});
-export const runServer = (port: number, host: string): Promise<IEither<Error, 0>> =>
- Either.fromFailable<Error, void>(() => {
- const app = new Hono();
-
- app.all('*', async (c) => {
- const response = await server.serve(c.req.raw);
- return response;
- });
-
- serve({
- fetch: app.fetch,
- port,
- hostname: host,
- });
-
- console.log(`server running on http://${host}:${port} :D`);
- }).flatMapAsync(() => neverEndingPromise);
+export const runServer = (port: number, hostname: string) => hono.serve(port, hostname);
diff --git a/server/job/index.ts b/server/job/index.ts
index ecf0984..92c4682 100644
--- a/server/job/index.ts
+++ b/server/job/index.ts
@@ -1,2 +1,2 @@
-export * from './queue';
-export * from './run_activity';
+export * from './queue.js';
+export * from './run_activity.js';
diff --git a/server/job/queue.ts b/server/job/queue.ts
index 2392222..4b21186 100644
--- a/server/job/queue.ts
+++ b/server/job/queue.ts
@@ -2,13 +2,13 @@ import {
getStdout,
type Mapper,
memoize,
- Either,
type IEither,
type ITraceable,
LogLevel,
Metric,
type ServerTrace,
TraceUtil,
+ PenguenoError,
} from '@emprespresso/pengueno';
import { type Job } from '@emprespresso/ci_model';
@@ -23,7 +23,7 @@ export class LaminarJobQueuer implements IJobQueuer<ITraceable<Job, ServerTrace>
private static GetJobTypeTrace = (jobType: string) => `LaminarJobQueue.Queue.${jobType}`;
private static JobTypeMetrics = memoize((jobType: string) =>
- Metric.fromName(LaminarJobQueuer.GetJobTypeTrace(jobType)),
+ Metric.fromName(LaminarJobQueuer.GetJobTypeTrace(jobType)).asResult(),
);
public queue(j: ITraceable<Job, ServerTrace>) {
@@ -32,8 +32,8 @@ export class LaminarJobQueuer implements IJobQueuer<ITraceable<Job, ServerTrace>
const metric = LaminarJobQueuer.JobTypeMetrics(jobType);
return j
- .bimap(TraceUtil.withTrace(trace))
- .bimap(TraceUtil.withMetricTrace(metric))
+ .flatMap(TraceUtil.withTrace(trace))
+ .flatMap(TraceUtil.withMetricTrace(metric))
.map((j) => {
const { type: jobType, arguments: args } = j.get();
const laminarCommand = [
@@ -47,26 +47,24 @@ export class LaminarJobQueuer implements IJobQueuer<ITraceable<Job, ServerTrace>
.peek((c) =>
c.trace.trace(`im so excited to see how this queue job will end!! (>ᴗ<): ${c.get().toString()}`),
)
- .map(getStdout)
- .peek(
- TraceUtil.promiseify((q) =>
- q.trace.trace(q.get().fold(({ isLeft }) => (isLeft ? metric.failure : metric.success))),
- ),
- )
+ .map((c) => getStdout(c))
+ .peek(TraceUtil.promiseify(TraceUtil.traceResultingEither(metric)))
.map(
TraceUtil.promiseify((q) =>
- q.get().fold(({ isLeft, value }) => {
- if (isLeft) {
- q.trace.addTrace(LogLevel.ERROR).trace(value.toString());
- return Either.left<Error, string>(value);
- }
- q.trace.addTrace(LogLevel.DEBUG).trace(`stdout ${value}`);
- const [jobName, jobId] = value.split(':');
- const jobUrl = `${this.queuePositionPrefix}/jobs/${jobName}/${jobId}`;
+ q.get().mapBoth(
+ (err) => {
+ q.trace.traceScope(LogLevel.ERROR).trace(err);
+ return err;
+ },
+ (ok) => {
+ q.trace.traceScope(LogLevel.DEBUG).trace(`stdout ${ok}`);
+ const [jobName, jobId] = ok.split(':');
+ const jobUrl = `${this.queuePositionPrefix}/jobs/${jobName}/${jobId}`;
- q.trace.trace(`all queued up and weady to go~ (˘ω˘) => ${jobUrl}`);
- return Either.right<Error, string>(jobUrl);
- }),
+ q.trace.trace(`all queued up and weady to go~ (˘ω˘) => ${jobUrl}`);
+ return jobUrl;
+ },
+ ),
),
)
.get();
diff --git a/server/job/run_activity.ts b/server/job/run_activity.ts
index 9f25cf8..22bc4c7 100644
--- a/server/job/run_activity.ts
+++ b/server/job/run_activity.ts
@@ -7,7 +7,9 @@ import {
jsonModel,
JsonResponse,
LogLevel,
+ LogMetricTraceSupplier,
Metric,
+ MetricsTraceSupplier,
PenguenoError,
type PenguenoRequest,
type ServerTrace,
@@ -15,39 +17,35 @@ import {
validateExecutionEntries,
} from '@emprespresso/pengueno';
import { isJob, type Job } from '@emprespresso/ci_model';
-import { IJobQueuer } from './queue';
+import { IJobQueuer } from './queue.js';
-const wellFormedJobMetric = Metric.fromName('Job.WellFormed');
+const wellFormedJobMetric = Metric.fromName('Job.WellFormed').asResult();
const jobJsonTransformer = (j: ITraceable<unknown, ServerTrace>): IEither<PenguenoError, Job> =>
j
- .bimap(TraceUtil.withMetricTrace(wellFormedJobMetric))
+ .flatMap(TraceUtil.withMetricTrace(wellFormedJobMetric))
.map((tJson): IEither<PenguenoError, Job> => {
const tJob = tJson.get();
if (!isJob(tJob) || !validateExecutionEntries(tJob)) {
const err = 'seems like a pwetty mawfomed job (-.-)';
- tJson.trace.addTrace(LogLevel.WARN).trace(err);
+ tJson.trace.traceScope(LogLevel.WARN).trace(err);
return Either.left(new PenguenoError(err, 400));
}
return Either.right(tJob);
})
- .peek((tJob) =>
- tJob.trace.trace(
- tJob.get().fold(({ isLeft }) => (isLeft ? wellFormedJobMetric.failure : wellFormedJobMetric.success)),
- ),
- )
+ .peek(TraceUtil.traceResultingEither(wellFormedJobMetric))
.get();
export interface IJobHookActivity {
processHook: IActivity;
}
-const jobHookRequestMetric = Metric.fromName('JobHook.process');
+const jobHookRequestMetric = Metric.fromName('JobHook.process').asResult();
export class JobHookActivityImpl implements IJobHookActivity {
constructor(private readonly queuer: IJobQueuer<ITraceable<Job, ServerTrace>>) {}
private trace(r: ITraceable<PenguenoRequest, ServerTrace>) {
- return r.bimap(TraceUtil.withClassTrace(this)).bimap(TraceUtil.withMetricTrace(jobHookRequestMetric));
+ return r.flatMap(TraceUtil.withClassTrace(this)).flatMap(TraceUtil.withMetricTrace(jobHookRequestMetric));
}
public processHook(r: ITraceable<PenguenoRequest, ServerTrace>) {
@@ -63,29 +61,34 @@ export class JobHookActivityImpl implements IJobHookActivity {
return eitherQueued.mapLeft((e) => new PenguenoError(e.message, 500));
});
})
+ .flatMapAsync(
+ TraceUtil.promiseify((tEitherQueued) => {
+ const errorSource = tEitherQueued
+ .get()
+ .left()
+ .map(({ source }) => source)
+ .orSome(() => ErrorSource.SYSTEM)
+ .get();
+ const shouldWarn = errorSource === ErrorSource.USER;
+ return TraceUtil.traceResultingEither<PenguenoError, string, LogMetricTraceSupplier>(
+ jobHookRequestMetric,
+ shouldWarn,
+ )(tEitherQueued);
+ }),
+ )
.peek(
TraceUtil.promiseify((tJob) =>
- tJob.get().fold(({ isRight, value }) => {
- if (isRight) {
- tJob.trace.trace(jobHookRequestMetric.success);
- tJob.trace.trace(`all queued up and weady to go :D !! ${value}`);
- return;
- }
-
- tJob.trace.trace(
- value.source === ErrorSource.SYSTEM
- ? jobHookRequestMetric.failure
- : jobHookRequestMetric.warn,
- );
- tJob.trace.addTrace(value.source).trace(`${value}`);
- }),
+ tJob.get().mapRight((job) => tJob.trace.trace(`all queued up and weady to go :D !! ${job}`)),
),
)
.map(
TraceUtil.promiseify(
(tEitherQueuedJob) =>
new JsonResponse(r, tEitherQueuedJob.get(), {
- status: tEitherQueuedJob.get().fold(({ isRight, value }) => (isRight ? 200 : value.status)),
+ status: tEitherQueuedJob.get().fold(
+ ({ status }) => status,
+ () => 200,
+ ),
}),
),
)
diff --git a/tsconfig.json b/tsconfig.json
index 105c510..a28cdaf 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -20,7 +20,7 @@
"@emprespresso/pengueno": ["./u/index.ts"],
"@emprespresso/ci_model": ["./model/index.ts"],
"@emprespresso/ci_server": ["./server/index.ts"],
- "@emprespresso/ci_worker": ["./worker/index.ts"],
+ "@emprespresso/ci_worker": ["./worker/index.ts"]
}
},
"include": ["**/*.ts", "**/*.js"],
diff --git a/u/fn/either.ts b/u/fn/either.ts
deleted file mode 100644
index 8c47b64..0000000
--- a/u/fn/either.ts
+++ /dev/null
@@ -1,100 +0,0 @@
-import { type Mapper, type Supplier, isObject } from '@emprespresso/pengueno';
-
-type IEitherTag = 'IEither';
-const iEitherTag: IEitherTag = 'IEither';
-
-export interface _Either<LeftT, RightT, T> {
- readonly isLeft: LeftT;
- readonly isRight: RightT;
- readonly value: T;
-}
-export type Left<E> = _Either<true, false, E>;
-export type Right<T> = _Either<false, true, T>;
-
-export interface IEither<E, T> {
- readonly _tag: IEitherTag;
-
- mapBoth: <_E, _T>(errBranch: Mapper<E, _E>, okBranch: Mapper<T, _T>) => IEither<_E, _T>;
- fold: <_T>(folder: Mapper<Left<E> | Right<T>, _T>) => _T;
- moveRight: <_T>(t: _T) => IEither<E, _T>;
- mapRight: <_T>(mapper: Mapper<T, _T>) => IEither<E, _T>;
- mapLeft: <_E>(mapper: Mapper<E, _E>) => IEither<_E, T>;
- flatMap: <_T>(mapper: Mapper<T, IEither<E, _T>>) => IEither<E, _T>;
- flatMapAsync: <_T>(mapper: Mapper<T, Promise<IEither<E, _T>>>) => Promise<IEither<E, _T>>;
-}
-
-export class Either<E, T> implements IEither<E, T> {
- private readonly self: Left<E> | Right<T>;
-
- private constructor(
- init: { err?: E; ok?: T },
- public readonly _tag: IEitherTag = iEitherTag,
- ) {
- this.self = <Left<E> | Right<T>>{
- isLeft: 'err' in init,
- isRight: 'ok' in init,
- value: init.err ?? init.ok!,
- };
- }
-
- public moveRight<_T>(t: _T) {
- return this.mapRight(() => t);
- }
-
- public fold<_T>(folder: Mapper<Left<E> | Right<T>, _T>): _T {
- return folder(this.self);
- }
-
- public mapBoth<_E, _T>(errBranch: Mapper<E, _E>, okBranch: Mapper<T, _T>): IEither<_E, _T> {
- if (this.self.isLeft) return Either.left(errBranch(this.self.value));
- return Either.right(okBranch(this.self.value));
- }
-
- public flatMap<_T>(mapper: Mapper<T, IEither<E, _T>>): IEither<E, _T> {
- if (this.self.isRight) return mapper(this.self.value);
- return Either.left<E, _T>(this.self.value);
- }
-
- public mapRight<_T>(mapper: Mapper<T, _T>): IEither<E, _T> {
- if (this.self.isRight) return Either.right<E, _T>(mapper(this.self.value));
- return Either.left<E, _T>(this.self.value);
- }
-
- public mapLeft<_E>(mapper: Mapper<E, _E>): IEither<_E, T> {
- if (this.self.isLeft) return Either.left<_E, T>(mapper(this.self.value));
- return Either.right<_E, T>(this.self.value);
- }
-
- public async flatMapAsync<_T>(mapper: Mapper<T, Promise<IEither<E, _T>>>): Promise<IEither<E, _T>> {
- if (this.self.isLeft) {
- return Promise.resolve(Either.left<E, _T>(this.self.value));
- }
- return await mapper(this.self.value).catch((err) => Either.left<E, _T>(err));
- }
-
- static left<E, T>(e: E): IEither<E, T> {
- return new Either<E, T>({ err: e });
- }
-
- static right<E, T>(t: T): IEither<E, T> {
- return new Either<E, T>({ ok: t });
- }
-
- static fromFailable<E, T>(s: Supplier<T>): IEither<E, T> {
- try {
- return Either.right<E, T>(s());
- } catch (e) {
- return Either.left<E, T>(e as E);
- }
- }
-
- static async fromFailableAsync<E, T>(s: Supplier<Promise<T>> | Promise<T>): Promise<IEither<E, T>> {
- return await (typeof s === 'function' ? s() : s)
- .then((t: T) => Either.right<E, T>(t))
- .catch((e: E) => Either.left<E, T>(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/history.ts b/u/history.ts
deleted file mode 100644
index 5b13961..0000000
--- a/u/history.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-export interface History<T> {
- undo: () => History<T> | undefined;
- redo: () => History<T> | undefined;
-
- get: () => T;
- add: (value: T) => History<T>;
-}
-
-export class HistoryImpl<T> implements History<T> {
- private readonly item: T;
- private previous?: History<T>;
- private next?: History<T>;
-
- constructor(item: T) {
- this.item = item;
- }
-
- public get(): T {
- return this.item;
- }
-
- public undo(): History<T> | undefined {
- return this.previous;
- }
-
- public redo(): History<T> | undefined {
- return this.next;
- }
-
- public add(value: T): History<T> {
- const newHistory = new HistoryImpl(value);
- newHistory.previous = this;
- this.next = newHistory;
- return newHistory;
- }
-}
diff --git a/u/index.ts b/u/index.ts
index 0c8c760..e2e0768 100644
--- a/u/index.ts
+++ b/u/index.ts
@@ -1,6 +1,5 @@
-export * from './fn/index.js';
export * from './leftpadesque/index.js';
-export * from './process/index.js';
export * from './trace/index.js';
+export * from './process/index.js';
export * from './server/index.js';
-export * from './history.js';
+export * from './types/index.js';
diff --git a/u/leftpadesque/index.ts b/u/leftpadesque/index.ts
index 6403e4a..09a0bd1 100644
--- a/u/leftpadesque/index.ts
+++ b/u/leftpadesque/index.ts
@@ -1,4 +1,3 @@
-export * from './object.js';
export * from './prepend.js';
export * from './debug.js';
export * from './memoize.js';
diff --git a/u/process/argv.ts b/u/process/argv.ts
index dcdba85..dca5098 100644
--- a/u/process/argv.ts
+++ b/u/process/argv.ts
@@ -1,4 +1,4 @@
-import { Either, type Mapper, type IEither } from '@emprespresso/pengueno';
+import { Either, type Mapper, type IEither, Optional } from '@emprespresso/pengueno';
export const isArgKey = <K extends string>(k: string): k is K => k.startsWith('--');
@@ -13,22 +13,27 @@ export const getArg = <K extends string, V>(
argv: Array<string>,
whenValue: ArgHandler<V>,
): IEither<Error, V> => {
- const value =
- argv
- .filter((_argv) => isArgKey(_argv) && _argv.split('=')[0] === arg)
- .map((_argv, i) => {
- const next = _argv.includes('=') ? _argv.split('=')[1] : argv.at(i + 1);
- if (next) {
- if (isArgKey(next)) return whenValue.unspecified;
- return whenValue.present(next);
- }
- return whenValue.unspecified;
- })
- .find((x) => x) ?? whenValue.absent;
- if (value === undefined) {
- return Either.left(new Error('no value specified for ' + arg));
+ const argIndex = Optional.from(argv.findIndex((_argv) => isArgKey(_argv) && _argv.split('=')[0] === arg)).filter(
+ (index) => index >= 0 && index < argv.length,
+ );
+ if (!argIndex.present()) {
+ return Optional.from(whenValue.absent)
+ .map((v) => Either.right<Error, V>(v))
+ .orSome(() =>
+ Either.left(
+ new Error(`arg ${arg} is not present in arguments list and does not have an 'absent' value`),
+ ),
+ )
+ .get();
}
- return Either.right(value);
+
+ return argIndex
+ .flatMap((idx) =>
+ Optional.from(argv.at(idx)).map((_argv) => (_argv.includes('=') ? _argv.split('=')[1] : argv.at(idx + 1))),
+ )
+ .map((next) => (isArgKey(next) ? whenValue.unspecified : whenValue.present(next)))
+ .map((v) => Either.right<Error, V>(<V>v))
+ .get();
};
type MappedArgs<
@@ -55,10 +60,10 @@ export const argv = <
return getArg(arg, argv, handler).mapRight((value) => [arg, value] as const);
};
- return args
+ const res = args
.map(processArg)
.reduce(
- (acc: IEither<Error, Partial<Result>>, current: IEither<Error, readonly [Args[number], unknown]>) =>
+ (acc: IEither<Error, Partial<Result>>, current: IEither<Error, [Args[number], unknown]>) =>
acc.flatMap((accValue) =>
current.mapRight(([key, value]) => ({
...accValue,
@@ -68,4 +73,5 @@ export const argv = <
Either.right(<Partial<Result>>{}),
)
.mapRight((result) => <Result>result);
+ return res;
};
diff --git a/u/process/env.ts b/u/process/env.ts
index 1e4fd32..9a55488 100644
--- a/u/process/env.ts
+++ b/u/process/env.ts
@@ -1,10 +1,11 @@
-import { Either, type IEither } from '@emprespresso/pengueno';
+import { IOptional, Either, Optional, type IEither } from '@emprespresso/pengueno';
-export const getRequiredEnv = <V extends string>(name: V): IEither<Error, V> =>
- Either.fromFailable<Error, V | undefined>(() => process.env[name] as V | undefined) // could throw when no permission.
- .flatMap(
- (v) => (v && Either.right(v)) || Either.left(new Error(`environment variable "${name}" is required D:`)),
- );
+export const getEnv = <V extends string>(name: string): IOptional<V> => Optional.from(<V>process.env[name]);
+
+export const getRequiredEnv = <V extends string>(name: string): IEither<Error, V> =>
+ Either.fromFailable(() => getEnv<V>(name).get()).mapLeft(
+ () => 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;
diff --git a/u/process/index.ts b/u/process/index.ts
index 4ffbf2a..6945a0f 100644
--- a/u/process/index.ts
+++ b/u/process/index.ts
@@ -2,3 +2,4 @@ export * from './env.js';
export * from './run.js';
export * from './validate_identifier.js';
export * from './argv.js';
+export * from './signals.js';
diff --git a/u/process/run.ts b/u/process/run.ts
index e3c4c3d..1d19129 100644
--- a/u/process/run.ts
+++ b/u/process/run.ts
@@ -1,9 +1,10 @@
import {
Either,
- type IEither,
+ IEither,
type ITraceable,
LogLevel,
- type LogTraceSupplier,
+ LogMetricTraceSupplier,
+ Metric,
TraceUtil,
} from '@emprespresso/pengueno';
import { promisify } from 'node:util';
@@ -13,34 +14,27 @@ const exec = promisify(execCallback);
export type Command = string[] | string;
export type StdStreams = { stdout: string; stderr: string };
+export const CmdMetric = Metric.fromName('Exec').asResult();
export const getStdout = (
- c: ITraceable<Command, LogTraceSupplier>,
+ c: ITraceable<Command, LogMetricTraceSupplier>,
options: { env?: Record<string, string>; clearEnv?: boolean } = {},
): Promise<IEither<Error, string>> =>
c
- .bimap(TraceUtil.withFunctionTrace(getStdout))
- .bimap((tCmd) => {
+ .flatMap(TraceUtil.withFunctionTrace(getStdout))
+ .flatMap((tCmd) => tCmd.traceScope(() => `Command = ${tCmd.get()}`))
+ .map((tCmd) => {
const cmd = tCmd.get();
- tCmd.trace.trace(`Command = ${cmd} :> im gonna run this command! `);
-
const _exec = typeof cmd === 'string' ? cmd : cmd.join(' ');
const env = options.clearEnv ? options.env : { ...process.env, ...options.env };
-
- const p: Promise<IEither<Error, StdStreams>> = Either.fromFailableAsync(exec(_exec, { env }));
- return [p, `Command = ${_exec}`];
+ return Either.fromFailableAsync<Error, StdStreams>(exec(_exec, { env }));
})
.map(
- TraceUtil.promiseify(
- (tEitherProcess): IEither<Error, string> =>
- tEitherProcess.get().fold(({ isLeft, value }) => {
- if (isLeft) {
- return Either.left(value);
- }
- if (value.stderr) {
- tEitherProcess.trace.addTrace(LogLevel.DEBUG).trace(`StdErr = ${value.stderr}`);
- }
- return Either.right(value.stdout);
- }),
+ TraceUtil.promiseify((tEitherStdStreams) =>
+ tEitherStdStreams.get().mapRight(({ stderr, stdout }) => {
+ if (stderr) tEitherStdStreams.trace.traceScope(LogLevel.DEBUG).trace(`StdErr = ${stderr}`);
+ return stdout;
+ }),
),
)
+ .peek(TraceUtil.promiseify(TraceUtil.traceResultingEither(CmdMetric)))
.get();
diff --git a/u/process/signals.ts b/u/process/signals.ts
new file mode 100644
index 0000000..c4feb7a
--- /dev/null
+++ b/u/process/signals.ts
@@ -0,0 +1,49 @@
+import {
+ Either,
+ IEither,
+ IMetric,
+ ITraceable,
+ LogMetricTrace,
+ LogMetricTraceSupplier,
+ Mapper,
+ Metric,
+ Optional,
+ ResultMetric,
+ SideEffect,
+ TraceUtil,
+} from '@emprespresso/pengueno';
+
+export const SigIntMetric = Metric.fromName('SigInt').asResult();
+export const SigTermMetric = Metric.fromName('SigTerm').asResult();
+
+export interface Closeable<TFailure> {
+ readonly close: SideEffect<SideEffect<TFailure | undefined>>;
+}
+
+export class Signals {
+ public static async awaitClose<E extends Error>(
+ t: ITraceable<Closeable<E>, LogMetricTraceSupplier>,
+ ): Promise<IEither<Error, void>> {
+ const success: IEither<Error, void> = Either.right(<void>undefined);
+ return new Promise<IEither<Error, void>>((res) => {
+ const metricizedInterruptHandler = (metric: ResultMetric) => (err: Error | undefined) =>
+ t
+ .flatMap(TraceUtil.withMetricTrace(metric))
+ .peek((_t) => _t.trace.trace('closing'))
+ .move(
+ Optional.from(err)
+ .map((e) => Either.left<Error, void>(e))
+ .orSome(() => success)
+ .get(),
+ )
+ .flatMap(TraceUtil.traceResultingEither(metric))
+ .map((e) => res(e.get()))
+ .peek((_t) => _t.trace.trace('finished'))
+ .get();
+ const sigintCloser = metricizedInterruptHandler(SigIntMetric);
+ const sigtermCloser = metricizedInterruptHandler(SigTermMetric);
+ process.on('SIGINT', () => t.flatMap(TraceUtil.withTrace('SIGINT')).get().close(sigintCloser));
+ process.on('SIGTERM', () => t.flatMap(TraceUtil.withTrace('SIGTERM')).get().close(sigtermCloser));
+ });
+ }
+}
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;
+ }
+}
diff --git a/u/trace/index.ts b/u/trace/index.ts
index 18da87a..332fb52 100644
--- a/u/trace/index.ts
+++ b/u/trace/index.ts
@@ -1,5 +1,5 @@
export * from './itrace.js';
-export * from './util.js';
-export * from './logger.js';
-export * from './metrics.js';
+export * from './metric/index.js';
+export * from './log/index.js';
export * from './trace.js';
+export * from './util.js';
diff --git a/u/trace/itrace.ts b/u/trace/itrace.ts
index 8cf123a..9c33ad2 100644
--- a/u/trace/itrace.ts
+++ b/u/trace/itrace.ts
@@ -1,69 +1,90 @@
import type { Mapper, SideEffect, Supplier } from '@emprespresso/pengueno';
-// the "thing" every Trace writer must "trace()"
+/**
+ * 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>>;
+ /**
+ * creates a new trace scope which inherits from this trace.
+ */
+ traceScope: Mapper<ITraceWith<TraceWith>, ITrace<TraceWith>>;
+
+ /**
+ * does the tracing.
+ */
trace: SideEffect<ITraceWith<TraceWith>>;
}
-export type ITraceableTuple<T, TraceWith> = [T, BaseTraceWith | TraceWith];
-export type ITraceableMapper<T, _T, TraceWith, W = ITraceable<T, TraceWith>> = (w: W) => _T;
+export type ITraceableTuple<T, TraceWith> = { item: T; trace: BaseTraceWith | TraceWith };
+export type ITraceableMapper<T, _T, TraceWith> = (w: ITraceable<T, TraceWith>) => _T;
export interface ITraceable<T, Trace = BaseTraceWith> {
readonly trace: ITrace<Trace>;
- get: Supplier<T>;
- move: <_T>(t: _T) => ITraceable<_T, Trace>;
- map: <_T>(mapper: ITraceableMapper<T, _T, Trace>) => ITraceable<_T, Trace>;
- bimap: <_T>(mapper: ITraceableMapper<T, ITraceableTuple<_T, Array<Trace> | Trace>, Trace>) => ITraceable<_T, Trace>;
- peek: (peek: ITraceableMapper<T, void, Trace>) => ITraceable<T, Trace>;
- flatMap: <_T>(mapper: ITraceableMapper<T, ITraceable<_T, Trace>, Trace>) => ITraceable<_T, Trace>;
- flatMapAsync<_T>(
+ readonly get: Supplier<T>;
+
+ readonly move: <_T>(t: _T) => ITraceable<_T, Trace>;
+ readonly map: <_T>(mapper: ITraceableMapper<T, _T, Trace>) => ITraceable<_T, Trace>;
+ readonly bimap: <_T>(mapper: ITraceableMapper<T, ITraceableTuple<_T, Trace>, Trace>) => ITraceable<_T, Trace>;
+ readonly coExtend: <_T>(
+ mapper: ITraceableMapper<T, ReadonlyArray<_T>, Trace>,
+ ) => ReadonlyArray<ITraceable<_T, Trace>>;
+ readonly peek: (peek: ITraceableMapper<T, void, Trace>) => ITraceable<T, Trace>;
+
+ readonly traceScope: (mapper: ITraceableMapper<T, Trace, Trace>) => ITraceable<T, Trace>;
+
+ readonly flatMap: <_T>(mapper: ITraceableMapper<T, ITraceable<_T, Trace>, Trace>) => ITraceable<_T, Trace>;
+ readonly flatMapAsync: <_T>(
mapper: ITraceableMapper<T, Promise<ITraceable<_T, Trace>>, Trace>,
- ): ITraceable<Promise<_T>, Trace>;
+ ) => ITraceable<Promise<_T>, Trace>;
}
-export class TraceableImpl<T, TraceWith> implements ITraceable<T, TraceWith> {
+export class TraceableImpl<T, Trace> implements ITraceable<T, Trace> {
protected constructor(
private readonly item: T,
- public readonly trace: ITrace<TraceWith>,
+ public readonly trace: ITrace<Trace>,
) {}
- public map<_T>(mapper: ITraceableMapper<T, _T, TraceWith>) {
+ public map<_T>(mapper: ITraceableMapper<T, _T, Trace>) {
const result = mapper(this);
return new TraceableImpl(result, this.trace);
}
- public flatMap<_T>(mapper: ITraceableMapper<T, ITraceable<_T, TraceWith>, TraceWith>): ITraceable<_T, TraceWith> {
+ public coExtend<_T>(mapper: ITraceableMapper<T, ReadonlyArray<_T>, Trace>): ReadonlyArray<ITraceable<_T, Trace>> {
+ const results = mapper(this);
+ return Array.from(results).map((result) => this.move(result));
+ }
+
+ public flatMap<_T>(mapper: ITraceableMapper<T, ITraceable<_T, Trace>, Trace>): ITraceable<_T, Trace> {
return mapper(this);
}
public flatMapAsync<_T>(
- mapper: ITraceableMapper<T, Promise<ITraceable<_T, TraceWith>>, TraceWith>,
- ): ITraceable<Promise<_T>, TraceWith> {
+ mapper: ITraceableMapper<T, Promise<ITraceable<_T, Trace>>, Trace>,
+ ): ITraceable<Promise<_T>, Trace> {
return new TraceableImpl(
mapper(this).then((t) => t.get()),
this.trace,
);
}
- public peek(peek: ITraceableMapper<T, void, TraceWith>) {
+ public traceScope(mapper: ITraceableMapper<T, Trace, Trace>): ITraceable<T, Trace> {
+ return new TraceableImpl(this.get(), this.trace.traceScope(mapper(this)));
+ }
+
+ public peek(peek: ITraceableMapper<T, void, Trace>) {
peek(this);
return this;
}
- public move<_T>(t: _T): ITraceable<_T, TraceWith> {
+ public move<_T>(t: _T): ITraceable<_T, Trace> {
return this.map(() => t);
}
- public bimap<_T>(mapper: ITraceableMapper<T, ITraceableTuple<_T, 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 bimap<_T>(mapper: ITraceableMapper<T, ITraceableTuple<_T, Trace>, Trace>) {
+ const { item, trace: _trace } = mapper(this);
+ return this.move(item).traceScope(() => <Trace>_trace);
}
public get() {
diff --git a/u/trace/log/ansi.ts b/u/trace/log/ansi.ts
new file mode 100644
index 0000000..7ff16a3
--- /dev/null
+++ b/u/trace/log/ansi.ts
@@ -0,0 +1,15 @@
+export const ANSI = {
+ RESET: '\x1b[0m',
+ BOLD: '\x1b[1m',
+ DIM: '\x1b[2m',
+ RED: '\x1b[31m',
+ GREEN: '\x1b[32m',
+ YELLOW: '\x1b[33m',
+ BLUE: '\x1b[34m',
+ MAGENTA: '\x1b[35m',
+ CYAN: '\x1b[36m',
+ WHITE: '\x1b[37m',
+ BRIGHT_RED: '\x1b[91m',
+ BRIGHT_YELLOW: '\x1b[93m',
+ GRAY: '\x1b[90m',
+};
diff --git a/u/trace/log/index.ts b/u/trace/log/index.ts
new file mode 100644
index 0000000..670e333
--- /dev/null
+++ b/u/trace/log/index.ts
@@ -0,0 +1,5 @@
+export * from './ansi.js';
+export * from './level.js';
+export * from './logger.js';
+export * from './pretty_json_console.js';
+export * from './trace.js';
diff --git a/u/trace/log/level.ts b/u/trace/log/level.ts
new file mode 100644
index 0000000..027dd71
--- /dev/null
+++ b/u/trace/log/level.ts
@@ -0,0 +1,19 @@
+export enum LogLevel {
+ UNKNOWN = 'UNKNOWN',
+ INFO = 'INFO',
+ WARN = 'WARN',
+ DEBUG = 'DEBUG',
+ ERROR = 'ERROR',
+ SYS = 'SYS',
+}
+
+export const logLevelOrder: Array<LogLevel> = [
+ LogLevel.DEBUG,
+ LogLevel.INFO,
+ LogLevel.WARN,
+ LogLevel.ERROR,
+ LogLevel.SYS,
+];
+
+export const isLogLevel = (l: unknown): l is LogLevel =>
+ typeof l === 'string' && logLevelOrder.some((level) => level === l);
diff --git a/u/trace/log/logger.ts b/u/trace/log/logger.ts
new file mode 100644
index 0000000..3ced60a
--- /dev/null
+++ b/u/trace/log/logger.ts
@@ -0,0 +1,5 @@
+import { LogLevel } from './level.js';
+
+export interface ILogger {
+ readonly log: (level: LogLevel, ...args: string[]) => void;
+}
diff --git a/u/trace/log/pretty_json_console.ts b/u/trace/log/pretty_json_console.ts
new file mode 100644
index 0000000..758af51
--- /dev/null
+++ b/u/trace/log/pretty_json_console.ts
@@ -0,0 +1,39 @@
+import { ANSI, LogLevel, ILogger } from './index.js';
+
+export class PrettyJsonConsoleLogger implements ILogger {
+ public log(level: LogLevel, ...trace: string[]) {
+ const message = JSON.stringify(
+ {
+ level,
+ trace,
+ },
+ null,
+ 4,
+ );
+ const styled = `${this.getStyle(level)}${message}${ANSI.RESET}\n`;
+ this.getStream(level)(styled);
+ }
+
+ private getStream(level: LogLevel) {
+ if (level === LogLevel.ERROR) {
+ return console.error;
+ }
+ return console.log;
+ }
+
+ private getStyle(level: LogLevel) {
+ switch (level) {
+ case LogLevel.UNKNOWN:
+ case LogLevel.INFO:
+ return `${ANSI.MAGENTA}`;
+ case LogLevel.DEBUG:
+ return `${ANSI.CYAN}`;
+ case LogLevel.WARN:
+ return `${ANSI.BRIGHT_YELLOW}`;
+ case LogLevel.ERROR:
+ return `${ANSI.BRIGHT_RED}`;
+ case LogLevel.SYS:
+ return `${ANSI.DIM}${ANSI.BLUE}`;
+ }
+ }
+}
diff --git a/u/trace/log/trace.ts b/u/trace/log/trace.ts
new file mode 100644
index 0000000..3f9f1b2
--- /dev/null
+++ b/u/trace/log/trace.ts
@@ -0,0 +1,60 @@
+import { isDebug, ITrace, ITraceWith, memoize, Supplier } from '@emprespresso/pengueno';
+import { ILogger, isLogLevel, LogLevel, logLevelOrder, PrettyJsonConsoleLogger } from './index.js';
+
+export type LogTraceSupplier = ITraceWith<Supplier<string>> | ITraceWith<Error>;
+
+export class LogTrace implements ITrace<LogTraceSupplier> {
+ constructor(
+ private readonly logger: ILogger = new PrettyJsonConsoleLogger(),
+ private readonly traces: Array<LogTraceSupplier> = [defaultTrace],
+ private readonly defaultLevel: LogLevel = LogLevel.INFO,
+ private readonly allowedLevels: Supplier<Set<LogLevel>> = defaultAllowedLevelsSupplier,
+ ) {}
+
+ public traceScope(trace: LogTraceSupplier): ITrace<LogTraceSupplier> {
+ return new LogTrace(this.logger, this.traces.concat(trace), this.defaultLevel, this.allowedLevels);
+ }
+
+ public trace(trace: LogTraceSupplier) {
+ const { traces, level: _level } = this.foldTraces(this.traces.concat(trace));
+ if (!this.allowedLevels().has(_level)) return;
+
+ const level = _level === LogLevel.UNKNOWN ? this.defaultLevel : _level;
+ this.logger.log(level, ...traces);
+ }
+
+ private foldTraces(_traces: Array<LogTraceSupplier>) {
+ const _logTraces = _traces.map((trace) => (typeof trace === 'function' ? trace() : trace));
+ const _level = _logTraces
+ .filter((trace) => isLogLevel(trace))
+ .reduce((acc, level) => Math.max(logLevelOrder.indexOf(level), acc), -1);
+ const level = logLevelOrder[_level] ?? LogLevel.UNKNOWN;
+
+ const traces = _logTraces
+ .filter((trace) => !isLogLevel(trace))
+ .map((trace) => {
+ if (typeof trace === 'object') {
+ return `TracedException.Name = ${trace.name}, TracedException.Message = ${trace.message}, TracedException.Stack = ${trace.stack}`;
+ }
+ return trace;
+ });
+ return {
+ level,
+ traces,
+ };
+ }
+}
+
+const defaultTrace = () => `TimeStamp = ${new Date().toISOString()}`;
+const defaultAllowedLevels = memoize(
+ (isDebug: boolean) =>
+ new Set([
+ LogLevel.UNKNOWN,
+ ...(isDebug ? [LogLevel.DEBUG] : []),
+ LogLevel.INFO,
+ LogLevel.WARN,
+ LogLevel.ERROR,
+ LogLevel.SYS,
+ ]),
+);
+const defaultAllowedLevelsSupplier = () => defaultAllowedLevels(isDebug());
diff --git a/u/trace/logger.ts b/u/trace/logger.ts
deleted file mode 100644
index 91432fe..0000000
--- a/u/trace/logger.ts
+++ /dev/null
@@ -1,126 +0,0 @@
-import { isDebug, type ITrace, type ITraceWith, type Supplier } from '@emprespresso/pengueno';
-
-export type LogTraceSupplier = ITraceWith<Supplier<string> | Error>;
-const defaultTrace = () => `TimeStamp = ${new Date().toISOString()}`;
-export class LogTrace implements ITrace<LogTraceSupplier> {
- constructor(
- private readonly logger: ILogger = new LoggerImpl(),
- private readonly traces: Array<LogTraceSupplier> = [defaultTrace],
- private readonly defaultLevel: LogLevel = LogLevel.INFO,
- private readonly allowedLevels: Supplier<Array<LogLevel>> = defaultAllowedLevels,
- ) {}
-
- public addTrace(trace: LogTraceSupplier): ITrace<LogTraceSupplier> {
- return new LogTrace(this.logger, this.traces.concat(trace), this.defaultLevel, this.allowedLevels);
- }
-
- public trace(trace: LogTraceSupplier) {
- const { traces, level: _level } = this.foldTraces(this.traces.concat(trace));
- if (!this.allowedLevels().includes(_level)) return;
-
- const level = _level === LogLevel.UNKNOWN ? this.defaultLevel : _level;
- this.logger.log(level, ...traces);
- }
-
- private foldTraces(_traces: Array<LogTraceSupplier>) {
- const _logTraces = _traces.map((trace) => (typeof trace === 'function' ? trace() : trace));
- const _level = _logTraces
- .filter((trace) => isLogLevel(trace))
- .reduce((acc, level) => Math.max(logLevelOrder.indexOf(level), acc), -1);
- const level = logLevelOrder[_level] ?? LogLevel.UNKNOWN;
-
- const traces = _logTraces
- .filter((trace) => !isLogLevel(trace))
- .map((trace) => {
- if (typeof trace === 'object') {
- return `TracedException.Name = ${trace.name}, TracedException.Message = ${trace.message}, TracedException.Stack = ${trace.stack}`;
- }
- return trace;
- });
- return {
- level,
- traces,
- };
- }
-}
-
-export enum LogLevel {
- UNKNOWN = 'UNKNOWN',
- INFO = 'INFO',
- WARN = 'WARN',
- DEBUG = 'DEBUG',
- ERROR = 'ERROR',
- SYS = 'SYS',
-}
-const logLevelOrder: Array<LogLevel> = [LogLevel.DEBUG, LogLevel.INFO, LogLevel.WARN, LogLevel.ERROR, LogLevel.SYS];
-export const isLogLevel = (l: unknown): l is LogLevel =>
- typeof l === 'string' && logLevelOrder.some((level) => level === l);
-
-const defaultAllowedLevels = () =>
- [
- LogLevel.UNKNOWN,
- ...(isDebug() ? [LogLevel.DEBUG] : []),
- LogLevel.INFO,
- LogLevel.WARN,
- LogLevel.ERROR,
- LogLevel.SYS,
- ] as Array<LogLevel>;
-
-export interface ILogger {
- readonly log: (level: LogLevel, ...args: string[]) => void;
-}
-class LoggerImpl implements ILogger {
- private readonly textEncoder = new TextEncoder();
-
- public log(level: LogLevel, ...trace: string[]) {
- const message = JSON.stringify(
- {
- level,
- trace,
- },
- null,
- 4,
- );
- const styled = `${this.getStyle(level)}${message}${ANSI.RESET}\n`;
- this.getStream(level)(this.textEncoder.encode(styled));
- }
-
- private getStream(level: LogLevel) {
- if (level === LogLevel.ERROR) {
- return console.error;
- }
- return console.log;
- }
-
- private getStyle(level: LogLevel) {
- switch (level) {
- case LogLevel.UNKNOWN:
- case LogLevel.INFO:
- return `${ANSI.MAGENTA}`;
- case LogLevel.DEBUG:
- return `${ANSI.CYAN}`;
- case LogLevel.WARN:
- return `${ANSI.BRIGHT_YELLOW}`;
- case LogLevel.ERROR:
- return `${ANSI.BRIGHT_RED}`;
- case LogLevel.SYS:
- return `${ANSI.DIM}${ANSI.BLUE}`;
- }
- }
-}
-
-export const ANSI = {
- RESET: '\x1b[0m',
- BOLD: '\x1b[1m',
- DIM: '\x1b[2m',
- RED: '\x1b[31m',
- GREEN: '\x1b[32m',
- YELLOW: '\x1b[33m',
- BLUE: '\x1b[34m',
- MAGENTA: '\x1b[35m',
- CYAN: '\x1b[36m',
- WHITE: '\x1b[37m',
- BRIGHT_RED: '\x1b[91m',
- BRIGHT_YELLOW: '\x1b[93m',
- GRAY: '\x1b[90m',
-};
diff --git a/u/trace/metric/emittable.ts b/u/trace/metric/emittable.ts
new file mode 100644
index 0000000..f3441ec
--- /dev/null
+++ b/u/trace/metric/emittable.ts
@@ -0,0 +1,18 @@
+import { IEmittableMetric, MetricValue, MetricValueTag, Unit } from './index.js';
+
+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: MetricValueTag,
+ };
+ }
+}
diff --git a/u/trace/metric/index.ts b/u/trace/metric/index.ts
new file mode 100644
index 0000000..72c37d2
--- /dev/null
+++ b/u/trace/metric/index.ts
@@ -0,0 +1,41 @@
+import { isTagged, Tagged, type Mapper } from '@emprespresso/pengueno';
+
+export enum Unit {
+ COUNT = 'COUNT',
+ MILLISECONDS = 'MILLISECONDS',
+}
+
+export const MetricValueTag = 'MetricValue' as const;
+export type MetricValueTag = typeof MetricValueTag;
+export const isMetricValue = (t: unknown): t is MetricValue => isTagged(t, MetricValueTag);
+export interface MetricValue extends Tagged<MetricValueTag> {
+ readonly name: string;
+ readonly unit: Unit;
+ readonly value: number;
+ readonly emissionTimestamp: number;
+}
+
+export interface IEmittableMetric {
+ readonly name: string;
+ readonly unit: Unit;
+ readonly withValue: Mapper<number, MetricValue>;
+}
+
+export const IMetricTag = 'IMetric' as const;
+export type IMetricTag = typeof IMetricTag;
+export const isIMetric = (t: unknown): t is IMetric => isTagged(t, IMetricTag);
+export interface IMetric extends Tagged<IMetricTag> {
+ readonly count: IEmittableMetric;
+ readonly time: IEmittableMetric;
+ readonly parent: undefined | IMetric;
+}
+
+export interface IResultMetric extends IMetric {
+ readonly failure: IMetric;
+ readonly success: IMetric;
+ readonly warn: IMetric;
+}
+
+export * from './emittable.js';
+export * from './metric.js';
+export * from './trace.js';
diff --git a/u/trace/metric/metric.ts b/u/trace/metric/metric.ts
new file mode 100644
index 0000000..8ef339f
--- /dev/null
+++ b/u/trace/metric/metric.ts
@@ -0,0 +1,54 @@
+import { EmittableMetric, IMetric, IMetricTag, IResultMetric, Unit } from './index.js';
+
+class _Tagged {
+ protected constructor(public readonly _tag = IMetricTag) {}
+}
+
+export class Metric extends _Tagged implements IMetric {
+ private static DELIM = '.';
+
+ protected constructor(
+ public readonly name: string,
+ public readonly parent: undefined | IMetric = undefined,
+ public readonly count = new EmittableMetric(Metric.join(name, 'count'), Unit.COUNT),
+ public readonly time = new EmittableMetric(Metric.join(name, 'time'), Unit.MILLISECONDS),
+ ) {
+ super();
+ }
+
+ public child(_name: string): Metric {
+ const childName = Metric.join(this.name, _name);
+ return new Metric(childName, this);
+ }
+
+ public asResult() {
+ return ResultMetric.from(this);
+ }
+
+ static join(...name: Array<string>) {
+ return name.join(Metric.DELIM);
+ }
+
+ static fromName(name: string): Metric {
+ return new Metric(name);
+ }
+}
+
+export class ResultMetric extends Metric implements IResultMetric {
+ protected constructor(
+ public readonly name: string,
+ public readonly parent: undefined | IMetric = undefined,
+ public readonly failure: IMetric,
+ public readonly success: IMetric,
+ public readonly warn: IMetric,
+ ) {
+ super(name, parent);
+ }
+
+ static from(metric: Metric) {
+ const failure = metric.child('failure');
+ const success = metric.child('success');
+ const warn = metric.child('warn');
+ return new ResultMetric(metric.name, metric.parent, failure, success, warn);
+ }
+}
diff --git a/u/trace/metric/trace.ts b/u/trace/metric/trace.ts
new file mode 100644
index 0000000..0c5fe37
--- /dev/null
+++ b/u/trace/metric/trace.ts
@@ -0,0 +1,59 @@
+import { IMetric, isIMetric, isMetricValue, ITrace, ITraceWith, MetricValue, SideEffect } from '@emprespresso/pengueno';
+
+export type MetricsTraceSupplier =
+ | ITraceWith<IMetric | MetricValue | undefined>
+ | Array<ITraceWith<IMetric | MetricValue | undefined>>;
+export const isMetricsTraceSupplier = (t: unknown): t is MetricsTraceSupplier =>
+ isMetricValue(t) || isIMetric(t) || (Array.isArray(t) && t.every((_m) => isMetricValue(_m) || isIMetric(_m)));
+
+export class MetricsTrace implements ITrace<MetricsTraceSupplier> {
+ constructor(
+ private readonly metricConsumer: SideEffect<Array<MetricValue>>,
+ private readonly activeTraces: ReadonlyMap<IMetric, number> = new Map(),
+ private readonly completedTraces: ReadonlySet<IMetric> = new Set(),
+ ) {}
+
+ public traceScope(trace: MetricsTraceSupplier): MetricsTrace {
+ const now = Date.now();
+ const metricsToTrace = (Array.isArray(trace) ? trace : [trace]).filter(isIMetric);
+
+ const initialTraces = new Map(metricsToTrace.map((metric) => [metric, now]));
+
+ return new MetricsTrace(this.metricConsumer, initialTraces);
+ }
+
+ public trace(metrics: MetricsTraceSupplier): MetricsTrace {
+ if (!metrics || typeof metrics === 'string') {
+ return this;
+ }
+
+ const now = Date.now();
+ const allMetrics = Array.isArray(metrics) ? metrics : [metrics];
+
+ // partition the incoming metrics
+ const valuesToEmit = allMetrics.filter(isMetricValue);
+ const traceableMetrics = allMetrics.filter(isIMetric);
+
+ const metricsToStart = traceableMetrics.filter((m) => !this.activeTraces.has(m));
+ const metricsToEnd = traceableMetrics.filter((m) => this.activeTraces.has(m) && !this.completedTraces.has(m));
+
+ // the new metrics to emit based on traces ending *now*
+ const endedMetricValues = metricsToEnd.flatMap((metric) => [
+ metric.count.withValue(1.0),
+ metric.time.withValue(now - this.activeTraces.get(metric)!),
+ ]);
+
+ const allMetricsToEmit = [...valuesToEmit, ...endedMetricValues];
+ if (allMetricsToEmit.length > 0) {
+ this.metricConsumer(allMetricsToEmit);
+ }
+
+ // the next immutable state
+ const nextActiveTraces = new Map([
+ ...this.activeTraces,
+ ...metricsToStart.map((m): [IMetric, number] => [m, now]),
+ ]);
+ const nextCompletedTraces = new Set([...this.completedTraces, ...metricsToEnd]);
+ return new MetricsTrace(this.metricConsumer, nextActiveTraces, nextCompletedTraces);
+ }
+}
diff --git a/u/trace/metrics.ts b/u/trace/metrics.ts
deleted file mode 100644
index 2301afd..0000000
--- a/u/trace/metrics.ts
+++ /dev/null
@@ -1,140 +0,0 @@
-import {
- isObject,
- type ITrace,
- type ITraceWith,
- type Mapper,
- type SideEffect,
- type Supplier,
-} from '@emprespresso/pengueno';
-
-export enum Unit {
- COUNT = 'COUNT',
- MILLISECONDS = 'MILLISECONDS',
-}
-
-export interface IMetric {
- readonly count: IEmittableMetric;
- readonly time: IEmittableMetric;
- readonly failure: undefined | IMetric;
- readonly success: undefined | IMetric;
- readonly warn: undefined | 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: undefined | Metric = undefined,
- public readonly success: undefined | Metric = undefined,
- public readonly warn: undefined | Metric = undefined,
- public readonly _tag: 'IMetric' = 'IMetric',
- ) {}
-
- public children() {
- return [this.failure, this.success, this.warn].filter((x) => x) as IMetric[];
- }
-
- static fromName(name: string, addChildren = true): Metric {
- return new Metric(
- new EmittableMetric(`${name}.count`, Unit.COUNT),
- new EmittableMetric(`${name}.elapsed`, Unit.MILLISECONDS),
- addChildren ? Metric.fromName(`${name}.failure`, false) : undefined,
- addChildren ? Metric.fromName(`${name}.success`, false) : undefined,
- addChildren ? Metric.fromName(`${name}.warn`, false) : undefined,
- );
- }
-}
-
-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 | undefined>;
-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 (!isIMetric(trace)) return this;
- return new MetricsTrace(this.metricConsumer)._nowTracing(trace);
- }
-
- public trace(metric: MetricsTraceSupplier) {
- if (typeof metric === 'undefined' || 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 {
- if (!metric) return this;
- this.tracing.push([metric, new Date()]);
- return this;
- }
-}
diff --git a/u/trace/trace.ts b/u/trace/trace.ts
index acc116f..e316ca8 100644
--- a/u/trace/trace.ts
+++ b/u/trace/trace.ts
@@ -10,7 +10,7 @@ import {
type MetricsTraceSupplier,
type MetricValue,
TraceableImpl,
-} from '@emprespresso/pengueno';
+} from './index.js';
export class LogTraceable<T> extends TraceableImpl<T, LogTraceSupplier> {
public static LogTrace = new LogTrace();
@@ -19,8 +19,10 @@ export class LogTraceable<T> extends TraceableImpl<T, LogTraceSupplier> {
}
}
-const getEmbeddedMetricConsumer = (logTrace: ITrace<LogTraceSupplier>) => (metrics: Array<MetricValue>) =>
- logTrace.addTrace(LogLevel.SYS).trace(`Metrics = <metrics>${JSON.stringify(metrics)}</metrics>`);
+const getEmbeddedMetricConsumer = (logTrace: ITrace<LogTraceSupplier>) => (metrics: Array<MetricValue>) => {
+ if (metrics.length === 0) return;
+ logTrace.traceScope(LogLevel.SYS).trace(`Metrics = <metrics>${JSON.stringify(metrics)}</metrics>`);
+};
export class EmbeddedMetricsTraceable<T> extends TraceableImpl<T, MetricsTraceSupplier> {
public static MetricsTrace = new MetricsTrace(getEmbeddedMetricConsumer(LogTraceable.LogTrace));
@@ -37,12 +39,12 @@ export class LogMetricTrace implements ITrace<LogMetricTraceSupplier> {
private metricsTrace: ITrace<MetricsTraceSupplier>,
) {}
- public addTrace(trace: LogTraceSupplier | MetricsTraceSupplier): LogMetricTrace {
+ public traceScope(trace: LogTraceSupplier | MetricsTraceSupplier): LogMetricTrace {
if (isMetricsTraceSupplier(trace)) {
- this.metricsTrace = this.metricsTrace.addTrace(trace);
+ this.metricsTrace = this.metricsTrace.traceScope(trace);
return this;
}
- this.logTrace = this.logTrace.addTrace(trace);
+ this.logTrace = this.logTrace.traceScope(trace);
return this;
}
diff --git a/u/trace/util.ts b/u/trace/util.ts
index db1db63..ec67571 100644
--- a/u/trace/util.ts
+++ b/u/trace/util.ts
@@ -1,45 +1,59 @@
import {
- ANSI,
+ IEither,
+ IMetric,
+ isEither,
+ ITraceable,
+ ITraceWith,
+ LogLevel,
+ ResultMetric,
type Callable,
- type IMetric,
type ITraceableMapper,
- type ITraceableTuple,
- type MetricsTraceSupplier,
} from '@emprespresso/pengueno';
export class TraceUtil {
- static withTrace<T, Trace>(
- trace: string,
- ansi?: Array<keyof typeof ANSI>,
- ): ITraceableMapper<T, ITraceableTuple<T, Trace | Array<Trace>>, Trace> {
- if (ansi) {
- return (t) => [t.get(), `${ansi.join('')}${trace}${ANSI.RESET}`];
- }
- return (t) => [t.get(), trace];
+ 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();
}
- static withMetricTrace<T, Trace extends MetricsTraceSupplier>(
- metric: IMetric,
- ): ITraceableMapper<T, ITraceableTuple<T, Trace | Array<Trace>>, Trace> {
- return (t) => [t.get(), metric as Trace];
+ static traceResultingEither<TErr, TOk, Trace>(
+ metric?: ResultMetric,
+ warnOnFailure = false,
+ ): ITraceableMapper<IEither<TErr, TOk>, ITraceable<IEither<TErr, TOk>, Trace>, Trace> {
+ return (t) => {
+ if (metric)
+ t.trace.trace(
+ t.get().fold(
+ (_err) => <Trace>(warnOnFailure ? metric.warn : metric.failure),
+ (_ok) => <Trace>metric.success,
+ ),
+ );
+ return t.traceScope((_t) =>
+ _t.get().fold(
+ (_err) => <Trace>(warnOnFailure ? LogLevel.WARN : LogLevel.ERROR),
+ (_ok) => <Trace>LogLevel.INFO,
+ ),
+ );
+ };
}
- static withFunctionTrace<F extends Callable, T, Trace>(
- f: F,
- ): ITraceableMapper<T, ITraceableTuple<T, Trace | Array<Trace>>, Trace> {
- return TraceUtil.withTrace(`fn.${f.name}`);
+ static withTrace<T, Trace, _Trace extends ITraceWith<Trace>>(
+ trace: _Trace,
+ ): ITraceableMapper<T, ITraceable<T, Trace>, Trace> {
+ return (t) => t.traceScope(() => <Trace>trace);
}
- static withClassTrace<C extends object, T, Trace>(
- c: C,
- ): ITraceableMapper<T, ITraceableTuple<T, Trace | Array<Trace>>, Trace> {
- return TraceUtil.withTrace(`class.${c.constructor.name}`);
+ static withMetricTrace<T, Trace>(metric: IMetric): ITraceableMapper<T, ITraceable<T, Trace>, Trace> {
+ return TraceUtil.withTrace(<Trace>metric);
}
- 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();
+ static withFunctionTrace<F extends Callable, T, Trace>(f: F): ITraceableMapper<T, ITraceable<T, Trace>, Trace> {
+ return TraceUtil.withTrace(<Trace>`fn.${f.name}`);
+ }
+
+ static withClassTrace<C extends object, T, Trace>(c: C): ITraceableMapper<T, ITraceable<T, Trace>, Trace> {
+ return TraceUtil.withTrace(<Trace>`class.${c.constructor.name}`);
}
}
diff --git a/u/types/collections/cons.ts b/u/types/collections/cons.ts
new file mode 100644
index 0000000..05dbe7c
--- /dev/null
+++ b/u/types/collections/cons.ts
@@ -0,0 +1,40 @@
+import { IOptional, Mapper, Optional } from '@emprespresso/pengueno';
+
+export interface ICons<T> extends Iterable<T> {
+ readonly value: T;
+ readonly next: IOptional<ICons<T>>;
+
+ readonly replace: Mapper<T, ICons<T>>;
+ readonly before: Mapper<IOptional<ICons<T>>, ICons<T>>;
+}
+
+export class Cons<T> implements ICons<T> {
+ constructor(
+ public readonly value: T,
+ public readonly next: IOptional<ICons<T>> = Optional.none(),
+ ) {}
+
+ public before(head: IOptional<ICons<T>>): ICons<T> {
+ return new Cons<T>(this.value, head);
+ }
+
+ public replace(_value: T): ICons<T> {
+ return new Cons<T>(_value, this.next);
+ }
+
+ *[Symbol.iterator]() {
+ for (let cur = Optional.some<ICons<T>>(this); cur.present(); cur = cur.flatMap((cur) => cur.next)) {
+ yield cur.get().value;
+ }
+ }
+
+ static addOnto<T>(items: Iterable<T>, tail: IOptional<ICons<T>>): IOptional<ICons<T>> {
+ return Array.from(items)
+ .reverse()
+ .reduce((cons, value) => Optional.from<ICons<T>>(new Cons<T>(value, cons)), tail);
+ }
+
+ static from<T>(items: Iterable<T>): IOptional<ICons<T>> {
+ return Cons.addOnto(items, Optional.none());
+ }
+}
diff --git a/u/types/collections/index.ts b/u/types/collections/index.ts
new file mode 100644
index 0000000..69e5d0b
--- /dev/null
+++ b/u/types/collections/index.ts
@@ -0,0 +1,2 @@
+export * from './cons.js';
+export * from './list_zipper.js';
diff --git a/u/types/collections/list_zipper.ts b/u/types/collections/list_zipper.ts
new file mode 100644
index 0000000..3df15b5
--- /dev/null
+++ b/u/types/collections/list_zipper.ts
@@ -0,0 +1,70 @@
+import { Cons, ICons } from './cons.js';
+import { IOptional, Mapper, Optional, Supplier } from '@emprespresso/pengueno';
+
+export interface IZipper<T> extends Iterable<T> {
+ readonly read: Supplier<IOptional<T>>;
+ readonly next: Supplier<IOptional<IZipper<T>>>;
+ readonly previous: Supplier<IOptional<IZipper<T>>>;
+
+ readonly prependChunk: Mapper<Iterable<T>, IZipper<T>>;
+ readonly prepend: Mapper<T, IZipper<T>>;
+ readonly remove: Supplier<IZipper<T>>;
+ readonly replace: Mapper<T, IZipper<T>>;
+}
+
+export class ListZipper<T> implements IZipper<T> {
+ private constructor(
+ private readonly reversedPathToHead: IOptional<ICons<T>>,
+ private readonly currentHead: IOptional<ICons<T>>,
+ ) {}
+
+ public read(): IOptional<T> {
+ return this.currentHead.map(({ value }) => value);
+ }
+
+ public next(): IOptional<IZipper<T>> {
+ return this.currentHead.map<IZipper<T>>(
+ (head) => new ListZipper<T>(Optional.some(head.before(this.reversedPathToHead)), head.next),
+ );
+ }
+
+ public previous(): IOptional<IZipper<T>> {
+ return this.reversedPathToHead.map<IZipper<T>>(
+ (lastVisited) => new ListZipper<T>(lastVisited.next, Optional.some(lastVisited.before(this.currentHead))),
+ );
+ }
+
+ public prependChunk(values: Iterable<T>): IZipper<T> {
+ return new ListZipper<T>(Cons.addOnto(Array.from(values).reverse(), this.reversedPathToHead), this.currentHead);
+ }
+
+ public prepend(value: T): IZipper<T> {
+ return this.prependChunk([value]);
+ }
+
+ public remove(): IZipper<T> {
+ const newHead = this.currentHead.flatMap((right) => right.next);
+ return new ListZipper<T>(this.reversedPathToHead, newHead);
+ }
+
+ public replace(value: T): IZipper<T> {
+ const newHead = this.currentHead.map((right) => right.replace(value));
+ return new ListZipper<T>(this.reversedPathToHead, newHead);
+ }
+
+ *[Symbol.iterator]() {
+ let head: ListZipper<T> = this;
+ for (let prev = head.previous(); prev.present(); prev = prev.flatMap((p) => p.previous())) {
+ head = <ListZipper<T>>prev.get();
+ }
+ if (head.currentHead.present()) yield* head.currentHead.get();
+ }
+
+ public collection() {
+ return Array.from(this);
+ }
+
+ static from<T>(iterable: Iterable<T>): ListZipper<T> {
+ return new ListZipper(Optional.none(), Cons.from(iterable));
+ }
+}
diff --git a/u/fn/callable.ts b/u/types/fn/callable.ts
index 8a61057..51756d7 100644
--- a/u/fn/callable.ts
+++ b/u/types/fn/callable.ts
@@ -1,4 +1,3 @@
-// deno-lint-ignore no-explicit-any
export interface Callable<T = any, ArgT = any> {
(...args: Array<ArgT>): T;
}
diff --git a/u/types/fn/either.ts b/u/types/fn/either.ts
new file mode 100644
index 0000000..6140ada
--- /dev/null
+++ b/u/types/fn/either.ts
@@ -0,0 +1,106 @@
+import { IOptional, type Mapper, Optional, type Supplier, Tagged, isTagged } from '@emprespresso/pengueno';
+
+export const IEitherTag = 'IEither' as const;
+export type IEitherTag = typeof IEitherTag;
+export const isEither = <E, T>(o: unknown): o is IEither<E, T> => isTagged(o, IEitherTag);
+export interface IEither<E, T> extends Tagged<IEitherTag> {
+ readonly mapBoth: <_E, _T>(errBranch: Mapper<E, _E>, okBranch: Mapper<T, _T>) => IEither<_E, _T>;
+ readonly fold: <_T>(leftFolder: Mapper<E, _T>, rightFolder: Mapper<T, _T>) => _T;
+ readonly left: Supplier<IOptional<E>>;
+ readonly right: Supplier<IOptional<T>>;
+ readonly moveRight: <_T>(t: _T) => IEither<E, _T>;
+ readonly mapRight: <_T>(mapper: Mapper<T, _T>) => IEither<E, _T>;
+ readonly mapLeft: <_E>(mapper: Mapper<E, _E>) => IEither<_E, T>;
+ readonly flatMap: <_T>(mapper: Mapper<T, IEither<E, _T>>) => IEither<E, _T>;
+ readonly flatMapAsync: <_T>(mapper: Mapper<T, Promise<IEither<E, _T>>>) => Promise<IEither<E, _T>>;
+}
+
+const ELeftTag = 'E.Left' as const;
+type ELeftTag = typeof ELeftTag;
+export const isLeft = <E>(o: unknown): o is Left<E> => isTagged(o, ELeftTag);
+interface Left<E> extends Tagged<ELeftTag> {
+ err: E;
+}
+
+const ERightTag = 'E.Right' as const;
+type ERightTag = typeof ERightTag;
+export const isRight = <T>(o: unknown): o is Right<T> => isTagged(o, ERightTag);
+interface Right<T> extends Tagged<ERightTag> {
+ ok: T;
+}
+
+class _Tagged implements Tagged<IEitherTag> {
+ protected constructor(public readonly _tag = IEitherTag) {}
+}
+
+export class Either<E, T> extends _Tagged implements IEither<E, T> {
+ protected constructor(private readonly self: Left<E> | Right<T>) {
+ super();
+ }
+
+ public moveRight<_T>(t: _T) {
+ return this.mapRight(() => t);
+ }
+
+ public mapBoth<_E, _T>(errBranch: Mapper<E, _E>, okBranch: Mapper<T, _T>): IEither<_E, _T> {
+ if (isLeft(this.self)) return Either.left(errBranch(this.self.err));
+ return Either.right(okBranch(this.self.ok));
+ }
+
+ public mapRight<_T>(mapper: Mapper<T, _T>): IEither<E, _T> {
+ if (isRight(this.self)) return Either.right(mapper(this.self.ok));
+ return Either.left(this.self.err);
+ }
+
+ public mapLeft<_E>(mapper: Mapper<E, _E>): IEither<_E, T> {
+ if (isLeft(this.self)) return Either.left(mapper(this.self.err));
+ return Either.right(this.self.ok);
+ }
+
+ public flatMap<_T>(mapper: Mapper<T, IEither<E, _T>>): IEither<E, _T> {
+ if (isRight(this.self)) return mapper(this.self.ok);
+ return Either.left<E, _T>(this.self.err);
+ }
+
+ public async flatMapAsync<_T>(mapper: Mapper<T, Promise<IEither<E, _T>>>): Promise<IEither<E, _T>> {
+ if (isLeft(this.self)) return Promise.resolve(Either.left(this.self.err));
+ return await mapper(this.self.ok).catch((err) => Either.left(err));
+ }
+
+ public fold<_T>(leftFolder: Mapper<E, _T>, rightFolder: Mapper<T, _T>): _T {
+ if (isLeft(this.self)) return leftFolder(this.self.err);
+ return rightFolder(this.self.ok);
+ }
+
+ public left(): IOptional<E> {
+ if (isLeft(this.self)) return Optional.from(this.self.err) as IOptional<E>;
+ return Optional.none();
+ }
+
+ public right(): IOptional<T> {
+ if (isRight(this.self)) return Optional.from(this.self.ok) as IOptional<T>;
+ return Optional.none();
+ }
+
+ static left<E, T>(e: E): IEither<E, T> {
+ return new Either({ err: e, _tag: ELeftTag });
+ }
+
+ static right<E, T>(t: T): IEither<E, T> {
+ return new Either({ ok: t, _tag: ERightTag });
+ }
+
+ static fromFailable<E, T>(s: Supplier<T>): IEither<E, T> {
+ try {
+ return Either.right(s());
+ } catch (e) {
+ return Either.left(e as E);
+ }
+ }
+
+ static async fromFailableAsync<E, T>(s: Supplier<Promise<T>> | Promise<T>): Promise<IEither<E, T>> {
+ return await (typeof s === 'function' ? s() : s)
+ .then((t: T) => Either.right<E, T>(t))
+ .catch((e: E) => Either.left<E, T>(e));
+ }
+}
diff --git a/u/fn/index.ts b/u/types/fn/index.ts
index 1ec71aa..780c86c 100644
--- a/u/fn/index.ts
+++ b/u/types/fn/index.ts
@@ -1,2 +1,3 @@
export * from './callable.js';
export * from './either.js';
+export * from './optional.js';
diff --git a/u/types/fn/optional.ts b/u/types/fn/optional.ts
new file mode 100644
index 0000000..3396a45
--- /dev/null
+++ b/u/types/fn/optional.ts
@@ -0,0 +1,93 @@
+import { type Mapper, type Supplier, Tagged, isTagged } from '@emprespresso/pengueno';
+
+export type MaybeGiven<T> = T | undefined | null;
+
+export const IOptionalTag = 'IOptional' as const;
+export type IOptionalTag = typeof IOptionalTag;
+export const isOptional = <T>(o: unknown): o is IOptional<T> => isTagged(o, IOptionalTag);
+export class IOptionalEmptyError extends Error {}
+export interface IOptional<t, T extends NonNullable<t> = NonNullable<t>> extends Tagged<IOptionalTag>, Iterable<T> {
+ readonly move: <_T>(t: MaybeGiven<_T>) => IOptional<_T>;
+ readonly map: <_T>(mapper: Mapper<T, MaybeGiven<_T>>) => IOptional<_T>;
+ readonly filter: (mapper: Mapper<T, boolean>) => IOptional<T>;
+ readonly flatMap: <_T>(mapper: Mapper<T, MaybeGiven<IOptional<_T>>>) => IOptional<_T>;
+ readonly orSome: (supplier: Supplier<MaybeGiven<t>>) => IOptional<T>;
+ readonly get: Supplier<T>;
+ readonly present: Supplier<boolean>;
+}
+
+type OSomeTag = typeof OSomeTag;
+const OSomeTag = 'O.Some' as const;
+interface Some<T> extends Tagged<OSomeTag> {
+ value: NonNullable<T>;
+}
+
+const ONoneTag = 'O.None' as const;
+type ONoneTag = typeof ONoneTag;
+interface None extends Tagged<ONoneTag> {}
+
+const isNone = (o: unknown): o is None => isTagged(o, ONoneTag);
+const isSome = <T>(o: unknown): o is Some<T> => isTagged(o, OSomeTag);
+
+class _Tagged implements Tagged<IOptionalTag> {
+ protected constructor(public readonly _tag = IOptionalTag) {}
+}
+
+export class Optional<t, T extends NonNullable<t> = NonNullable<t>> extends _Tagged implements IOptional<T> {
+ private constructor(private readonly self: Some<T> | None) {
+ super();
+ }
+
+ public move<_T>(t: MaybeGiven<_T>): IOptional<_T> {
+ return this.map(() => t);
+ }
+
+ public orSome(supplier: Supplier<MaybeGiven<t>>): IOptional<T> {
+ if (isNone(this.self)) return Optional.from(supplier());
+ return this;
+ }
+
+ public get(): T {
+ if (isNone(this.self)) throw new IOptionalEmptyError('empty value');
+ return this.self.value;
+ }
+
+ public filter(mapper: Mapper<T, boolean>): IOptional<T> {
+ if (isNone(this.self) || !mapper(this.self.value)) return Optional.none();
+ return Optional.some(this.self.value);
+ }
+
+ public map<_T>(mapper: Mapper<T, MaybeGiven<_T>>): IOptional<_T> {
+ if (isNone(this.self)) return Optional.none();
+ return Optional.from(mapper(this.self.value)) as IOptional<_T>;
+ }
+
+ public flatMap<_T>(mapper: Mapper<T, MaybeGiven<IOptional<_T>>>): IOptional<_T> {
+ if (isNone(this.self)) return Optional.none();
+ return Optional.from(mapper(this.self.value))
+ .orSome(() => Optional.none())
+ .get();
+ }
+
+ public present() {
+ return isSome(this.self);
+ }
+
+ *[Symbol.iterator]() {
+ if (isSome(this.self)) yield this.self.value;
+ }
+
+ static some<t, T extends NonNullable<t> = NonNullable<t>>(value: T): IOptional<T> {
+ return new Optional({ value, _tag: OSomeTag });
+ }
+
+ private static readonly _none = new Optional({ _tag: ONoneTag });
+ static none<T>(): IOptional<T> {
+ return this._none as unknown as IOptional<T>;
+ }
+
+ static from<t, T extends NonNullable<t> = NonNullable<t>>(value: MaybeGiven<t>): IOptional<T> {
+ if (value === null || value === undefined) return Optional.none<T>();
+ return Optional.some(<T>value);
+ }
+}
diff --git a/u/types/index.ts b/u/types/index.ts
new file mode 100644
index 0000000..c68cedc
--- /dev/null
+++ b/u/types/index.ts
@@ -0,0 +1,5 @@
+export * from './object.js';
+export * from './tagged.js';
+
+export * from './fn/index.js';
+export * from './collections/index.js';
diff --git a/u/leftpadesque/object.ts b/u/types/object.ts
index fe97999..fe97999 100644
--- a/u/leftpadesque/object.ts
+++ b/u/types/object.ts
diff --git a/u/types/tagged.ts b/u/types/tagged.ts
new file mode 100644
index 0000000..351e4c9
--- /dev/null
+++ b/u/types/tagged.ts
@@ -0,0 +1,8 @@
+import { isObject } from './index.js';
+
+export interface Tagged<TTag> {
+ _tag: TTag;
+}
+
+export const isTagged = <TTag>(o: unknown, tag: TTag): o is Tagged<TTag> =>
+ !!(isObject(o) && '_tag' in o && o._tag === tag);
diff --git a/worker/executor.ts b/worker/executor.ts
index f4b7906..bfcbc37 100644
--- a/worker/executor.ts
+++ b/worker/executor.ts
@@ -13,74 +13,58 @@ import {
import type { Job, JobArgT, Pipeline } from '@emprespresso/ci_model';
// -- <job.exectuor> --
-const jobTypeMetric = memoize((type: string) => Metric.fromName(`run.${type}`));
-export const executeJob = (tJob: ITraceable<Job, LogMetricTraceSupplier>) =>
- tJob
- .bimap(TraceUtil.withMetricTrace(jobTypeMetric(tJob.get().type)))
+const jobTypeMetric = memoize((type: string) => Metric.fromName(`run.${type}`).asResult());
+export const executeJob = (tJob: ITraceable<Job, LogMetricTraceSupplier>) => {
+ const metric = jobTypeMetric(tJob.get().type);
+ return tJob
+ .flatMap(TraceUtil.withMetricTrace(metric))
.peek((tJob) => tJob.trace.trace(`let's do this little job ok!! ${tJob.get()}`))
.map((tJob) =>
validateExecutionEntries(tJob.get().arguments)
.mapLeft((badEntries) => {
- tJob.trace.addTrace(LogLevel.ERROR).trace(badEntries.toString());
+ tJob.trace.traceScope(LogLevel.ERROR).trace(badEntries.toString());
return new Error('invalid job arguments');
})
.flatMapAsync((args) => getStdout(tJob.move(tJob.get().type), { env: args })),
)
- .peek(
- TraceUtil.promiseify((q) =>
- q.trace.trace(
- q.get().fold(({ isLeft }) => jobTypeMetric(tJob.get().type)[isLeft ? 'failure' : 'success']),
- ),
- ),
- )
+ .flatMapAsync(TraceUtil.promiseify(TraceUtil.traceResultingEither(metric)))
.get();
+};
// -- </job.exectuor> --
// -- <pipeline.executor> --
-const pipelinesMetric = Metric.fromName('pipelines');
+const pipelinesMetric = Metric.fromName('pipelines').asResult();
export const executePipeline = (
tPipeline: ITraceable<Pipeline, LogMetricTraceSupplier>,
baseEnv?: JobArgT,
): Promise<IEither<Error, void>> =>
tPipeline
- .bimap(TraceUtil.withFunctionTrace(executePipeline))
- .bimap(TraceUtil.withMetricTrace(pipelinesMetric))
- .map(async (tJobs): Promise<IEither<Error, void>> => {
- for (const [i, serialStage] of tJobs.get().serialJobs.entries()) {
- tJobs.trace.trace(`executing stage ${i}. do your best little stage :>\n${serialStage}`);
- const jobResults = await Promise.all(
- serialStage.parallelJobs.map((job) =>
- tJobs
- .bimap((_) => [job, `stage ${i}`])
- .map(
- (tJob) =>
- <Job>{
- ...tJob.get(),
- arguments: {
- ...baseEnv,
- ...tJob.get().arguments,
- },
- },
- )
+ .flatMap(TraceUtil.withFunctionTrace(executePipeline))
+ .flatMap(TraceUtil.withMetricTrace(pipelinesMetric))
+ .map(async (_tPipeline): Promise<IEither<Error, void>> => {
+ for (const [i, serialStage] of tPipeline.get().serialJobs.entries()) {
+ const tPipeline = _tPipeline.flatMap(TraceUtil.withTrace(`Stage = ${i}`));
+ const parallelJobs = tPipeline
+ .peek((t) => t.trace.trace(`do your best little stage :> ${serialStage}`))
+ .move(serialStage.parallelJobs)
+ .coExtend((jobs) =>
+ jobs.get().map((job) => <Job>{ ...job, arguments: { ...baseEnv, ...job.arguments } }),
+ )
+ .map((job) => {
+ const metric = jobTypeMetric(job.get().type);
+ return job
+ .flatMap(TraceUtil.withMetricTrace(metric))
.map(executeJob)
- .peek(
- TraceUtil.promiseify((tEitherJobOutput) =>
- tEitherJobOutput
- .get()
- .mapRight((stdout) => tEitherJobOutput.trace.addTrace('STDOUT').trace(stdout)),
- ),
- )
- .get(),
- ),
- );
- const failures = jobResults.filter((e) => e.fold(({ isLeft }) => isLeft));
+ .flatMapAsync(TraceUtil.promiseify(TraceUtil.traceResultingEither(metric)));
+ });
+ const results = await Promise.all(parallelJobs.map((job) => job.get()));
+ const failures = results.filter((e) => e.left);
if (failures.length > 0) {
- tJobs.trace.trace(pipelinesMetric.failure);
return Either.left(new Error(failures.toString()));
}
}
- tJobs.trace.trace(pipelinesMetric.success);
- return Either.right(undefined);
+ return Either.right(<void>undefined);
})
+ .flatMapAsync(TraceUtil.promiseify(TraceUtil.traceResultingEither(pipelinesMetric)))
.get();
// -- </pipeline.executor> --
diff --git a/worker/scripts/ansible_playbook.ts b/worker/scripts/ansible_playbook.ts
index c6d8f2c..4a22984 100755
--- a/worker/scripts/ansible_playbook.ts
+++ b/worker/scripts/ansible_playbook.ts
@@ -28,9 +28,9 @@ const eitherJob = getRequiredEnvVars(['path', 'playbooks']).mapRight(
const eitherVault = Bitwarden.getConfigFromEnvironment().mapRight((config) => new Bitwarden(config));
const playbookMetric = Metric.fromName('ansiblePlaybook.playbook');
-const _logJob = LogTraceable.of(eitherJob).bimap(TraceUtil.withTrace('ansible_playbook'));
+const _logJob = LogTraceable.of(eitherJob).flatMap(TraceUtil.withTrace('ansible_playbook'));
await LogMetricTraceable.ofLogTraceable(_logJob)
- .bimap(TraceUtil.withMetricTrace(playbookMetric))
+ .flatMap(TraceUtil.withMetricTrace(playbookMetric))
.peek((tEitherJob) => tEitherJob.trace.trace('starting ansible playbook job! (⑅˘꒳˘)'))
.map((tEitherJob) =>
tEitherJob.get().flatMapAsync((job) =>
diff --git a/worker/scripts/build_docker_image.ts b/worker/scripts/build_docker_image.ts
index 228dfcc..b35031a 100755
--- a/worker/scripts/build_docker_image.ts
+++ b/worker/scripts/build_docker_image.ts
@@ -3,7 +3,6 @@
import {
getRequiredEnvVars,
getStdout,
- LogLevel,
LogTraceable,
LogMetricTraceable,
Metric,
@@ -29,17 +28,18 @@ const eitherJob = getRequiredEnvVars([
);
const eitherVault = Bitwarden.getConfigFromEnvironment().mapRight((config) => new Bitwarden(config));
-const buildImageMetric = Metric.fromName('dockerImage.build');
-const loginMetric = Metric.fromName('dockerRegistry.login');
-const _logJob = LogTraceable.of(eitherJob).bimap((tEitherJob) => {
- const trace =
- 'build_docker_image.' +
- tEitherJob.get().fold(({ isRight, value }) => (isRight ? value.arguments.buildTarget : ''));
- return [tEitherJob.get(), trace];
+const buildImageMetric = Metric.fromName('dockerImage.build').asResult();
+const loginMetric = Metric.fromName('dockerRegistry.login').asResult();
+const _logJob = LogTraceable.of(eitherJob).flatMap((tEitherJob) => {
+ const trace = tEitherJob.get().fold(
+ () => 'NO_BUILD_TARGET',
+ ({ arguments: { buildTarget } }) => buildTarget,
+ );
+ return tEitherJob.traceScope(() => `build_docker_image.${trace}`);
});
await LogMetricTraceable.ofLogTraceable(_logJob)
- .bimap(TraceUtil.withMetricTrace(buildImageMetric))
- .bimap(TraceUtil.withMetricTrace(loginMetric))
+ .flatMap(TraceUtil.withMetricTrace(buildImageMetric))
+ .flatMap(TraceUtil.withMetricTrace(loginMetric))
.peek((tEitherJob) => tEitherJob.trace.trace('starting docker image build job! (⑅˘꒳˘)'))
.map((tEitherJob) =>
tEitherJob.get().flatMapAsync((job) =>
@@ -68,12 +68,7 @@ await LogMetricTraceable.ofLogTraceable(_logJob)
}),
);
})
- .peek(async (tEitherWithAuthdRegistry) => {
- const eitherWithAuthdRegistry = await tEitherWithAuthdRegistry.get();
- return tEitherWithAuthdRegistry.trace.trace(
- eitherWithAuthdRegistry.fold(({ isLeft }) => loginMetric[isLeft ? 'failure' : 'success']),
- );
- })
+ .peek(TraceUtil.promiseify(TraceUtil.traceResultingEither(loginMetric)))
.map(async (tEitherWithAuthdRegistryBuildJob) => {
const eitherWithAuthdRegistryBuildJob = await tEitherWithAuthdRegistryBuildJob.get();
tEitherWithAuthdRegistryBuildJob.trace.trace('finally building the image~ (◕ᴗ◕✿)');
@@ -92,19 +87,15 @@ await LogMetricTraceable.ofLogTraceable(_logJob)
eitherWithAuthdRegistryBuildJob.mapRight((job) => ({ buildOutput, job })),
);
})
- .peek(async (tEitherWithBuiltImage) => {
- const eitherWithBuiltImage = await tEitherWithBuiltImage.get();
- eitherWithBuiltImage.fold(({ isLeft, value }) => {
- tEitherWithBuiltImage.trace.trace(buildImageMetric[isLeft ? 'failure' : 'success']);
- if (isLeft) {
- tEitherWithBuiltImage.trace
- .addTrace(LogLevel.ERROR)
- .trace(`oh nyoo we couldn't buiwd the img :(( ${value}`);
- return;
- }
- tEitherWithBuiltImage.trace.addTrace('buildOutput').trace(value.buildOutput);
- });
- })
+ .flatMapAsync(TraceUtil.promiseify(TraceUtil.traceResultingEither(buildImageMetric)))
+ .peek(
+ TraceUtil.promiseify((tBuilt) =>
+ tBuilt.get().fold(
+ (err) => tBuilt.trace.trace(`oh nyoo we couldn't buiwd the img :(( ${err}`),
+ (ok) => tBuilt.trace.traceScope('buildOutput').trace(ok.buildOutput),
+ ),
+ ),
+ )
.map(async (tEitherWithBuiltImage) => {
const eitherWithBuiltImage = await tEitherWithBuiltImage.get();
return eitherWithBuiltImage
diff --git a/worker/scripts/checkout_ci.ts b/worker/scripts/checkout_ci.ts
index 8e4dcca..fb71a16 100755
--- a/worker/scripts/checkout_ci.ts
+++ b/worker/scripts/checkout_ci.ts
@@ -11,6 +11,7 @@ import {
Metric,
prependWith,
TraceUtil,
+ IEither,
} from '@emprespresso/pengueno';
import { mkdir, readFile, rm } from 'fs/promises';
import { join } from 'path';
@@ -29,11 +30,14 @@ const eitherJob = getRequiredEnvVars(['remote', 'refname', 'rev']).mapRight(
},
},
);
+const afterJob = eitherJob.flatMapAsync((job) =>
+ Either.fromFailableAsync(() => rm(getWorkingDirectoryForCiJob(job), { recursive: true })),
+);
const ciRunMetric = Metric.fromName('checkout_ci.run');
-const _logJob = LogTraceable.of(eitherJob).bimap(TraceUtil.withTrace(`checkout_ci.${run}`));
+const _logJob = LogTraceable.of(eitherJob).flatMap(TraceUtil.withTrace(`checkout_ci.${run}`));
await LogMetricTraceable.ofLogTraceable(_logJob)
- .bimap(TraceUtil.withMetricTrace(ciRunMetric))
+ .flatMap(TraceUtil.withMetricTrace(ciRunMetric))
.map((tEitherJob) =>
tEitherJob.get().flatMapAsync((ciJob) => {
const wd = getWorkingDirectoryForCiJob(ciJob);
@@ -53,29 +57,28 @@ await LogMetricTraceable.ofLogTraceable(_logJob)
);
}),
)
- .map((tEitherCiJob) =>
- tEitherCiJob.get().then((eitherCiJob) =>
- eitherCiJob.flatMapAsync<{ cmd: Command; job: CheckoutCiJob }>((ciJob) =>
- Either.fromFailableAsync<Error, string>(() =>
- readFile(join(getSrcDirectoryForCiJob(ciJob), CI_WORKFLOW_FILE), 'utf-8'),
- ).then((eitherWorkflowJson) =>
- eitherWorkflowJson
- .flatMap((json) => Either.fromFailable<Error, unknown>(JSON.parse(json)))
- .flatMap((eitherWorkflowParse) => {
- if (isCiWorkflow(eitherWorkflowParse)) {
- return Either.right({
- cmd: getPipelineGenerationCommand(ciJob, eitherWorkflowParse.workflow),
- job: ciJob,
- });
- }
- return Either.left(
- new Error("couldn't find any valid ci configuration (。•́︿•̀。), that's okay~"),
- );
- }),
- ),
+ .map(async (tEitherCiJob) => {
+ const eitherCiJob = await tEitherCiJob.get();
+ const repoCiFileContents = await eitherCiJob.flatMapAsync((ciJob) =>
+ Either.fromFailableAsync<Error, string>(() =>
+ readFile(join(getSrcDirectoryForCiJob(ciJob), CI_WORKFLOW_FILE), 'utf-8'),
),
- ),
- )
+ );
+ return repoCiFileContents
+ .flatMap((fileText) => Either.fromFailable<Error, unknown>(() => JSON.parse(fileText)))
+ .flatMap((json) => {
+ return eitherCiJob.flatMap((ciJob): IEither<Error, { cmd: Command; job: CheckoutCiJob }> => {
+ if (!isCiWorkflow(json)) {
+ const e = new Error("couldn't find any valid ci configuration (。•́︿•̀。), that's okay~");
+ return Either.left(e);
+ }
+ return Either.right({
+ cmd: getPipelineGenerationCommand(ciJob, json.workflow),
+ job: ciJob,
+ });
+ });
+ });
+ })
.map(async (tEitherPipelineGenerationCommand) => {
const eitherJobCommand = await tEitherPipelineGenerationCommand.get();
const eitherPipeline = await eitherJobCommand.flatMapAsync((jobCommand) =>
@@ -107,17 +110,14 @@ await LogMetricTraceable.ofLogTraceable(_logJob)
.get(),
);
})
- .get()
- .then((e) =>
- e
- .flatMap(() => eitherJob)
- .fold(({ isLeft, isRight, value }) => {
- if (isLeft || !isRight) throw value;
- return rm(getWorkingDirectoryForCiJob(value), {
- recursive: true,
- });
- }),
- );
+ .map(async (tCompletePipeline) => {
+ const completePipeline = await tCompletePipeline.get();
+ return completePipeline.fold(
+ (e) => Promise.reject(e),
+ () => afterJob,
+ );
+ })
+ .get();
const getWorkingDirectoryForCiJob = (job: CheckoutCiJob) => `${job.arguments.returnPath}/${job.arguments.run}`;
@@ -130,7 +130,7 @@ const getPipelineGenerationCommand = (
pipelineGeneratorPath: string,
image = _image,
runFlags = _runFlags,
-) => [
+): Command => [
'docker',
'run',
...runFlags,
diff --git a/worker/secret.ts b/worker/secret.ts
index e3edb2d..e533b16 100644
--- a/worker/secret.ts
+++ b/worker/secret.ts
@@ -10,18 +10,21 @@ import {
} from '@emprespresso/pengueno';
// -- <ISecret> --
-export interface LoginItem {
+export interface SecretItem {
+ name: string;
+}
+
+export interface LoginItem extends SecretItem {
login: {
username: string;
password: string;
};
}
-export interface SecureNote {
+export interface SecureNote extends SecretItem {
notes: string;
}
-export type SecretItem = LoginItem | SecureNote;
export interface IVault<TClient, TKey, TItemId> {
unlock: (client: TClient) => Promise<IEither<Error, TKey>>;
lock: (client: TClient, key: TKey) => Promise<IEither<Error, TKey>>;
@@ -30,7 +33,7 @@ export interface IVault<TClient, TKey, TItemId> {
}
// -- </ISecret> --
-// -- <IVault> --
+// -- <Vault> --
type TClient = ITraceable<unknown, LogMetricTraceSupplier>;
type TKey = string;
type TItemId = string;
@@ -38,9 +41,9 @@ export class Bitwarden implements IVault<TClient, TKey, TItemId> {
constructor(private readonly config: BitwardenConfig) {}
public unlock(client: TClient) {
- return client
+ const authed = client
.move(this.config)
- .bimap(TraceUtil.withMetricTrace(Bitwarden.loginMetric))
+ .flatMap(TraceUtil.withMetricTrace(Bitwarden.loginMetric))
.flatMap((tConfig) => tConfig.move(`bw config server ${tConfig.get().server}`).map(getStdout))
.map(async (tEitherWithConfig) => {
const eitherWithConfig = await tEitherWithConfig.get();
@@ -49,12 +52,9 @@ export class Bitwarden implements IVault<TClient, TKey, TItemId> {
tEitherWithConfig.move('bw login --apikey --quiet').map(getStdout).get(),
);
})
- .peek(async (tEitherWithAuthd) => {
- const eitherWithAuthd = await tEitherWithAuthd.get();
- return tEitherWithAuthd.trace.trace(
- eitherWithAuthd.fold(({ isLeft }) => Bitwarden.loginMetric[isLeft ? 'failure' : 'success']),
- );
- })
+ .peek(TraceUtil.promiseify(TraceUtil.traceResultingEither(Bitwarden.loginMetric)));
+ const unlocked = authed
+ .flatMap(TraceUtil.withMetricTrace(Bitwarden.unlockVaultMetric))
.map(async (tEitherWithAuthd) => {
const eitherWithAuthd = await tEitherWithAuthd.get();
tEitherWithAuthd.trace.trace('unlocking the secret vault~ (◕ᴗ◕✿)');
@@ -62,19 +62,14 @@ export class Bitwarden implements IVault<TClient, TKey, TItemId> {
tEitherWithAuthd.move('bw unlock --passwordenv BW_PASSWORD --raw').map(getStdout).get(),
);
})
- .peek(async (tEitherWithSession) => {
- const eitherWithAuthd = await tEitherWithSession.get();
- return tEitherWithSession.trace.trace(
- eitherWithAuthd.fold(({ isLeft }) => Bitwarden.unlockVaultMetric[isLeft ? 'failure' : 'success']),
- );
- })
- .get();
+ .peek(TraceUtil.promiseify(TraceUtil.traceResultingEither(Bitwarden.unlockVaultMetric)));
+ return unlocked.get();
}
public fetchSecret<T extends SecretItem>(client: TClient, key: string, item: string): Promise<IEither<Error, T>> {
return client
.move(key)
- .bimap(TraceUtil.withMetricTrace(Bitwarden.fetchSecretMetric))
+ .flatMap(TraceUtil.withMetricTrace(Bitwarden.fetchSecretMetric))
.peek((tSession) => tSession.trace.trace(`looking for your secret ${item} (⑅˘꒳˘)`))
.flatMap((tSession) =>
tSession.move('bw list items').map((listCmd) =>
@@ -88,8 +83,7 @@ export class Bitwarden implements IVault<TClient, TKey, TItemId> {
tEitherItemsJson
.get()
.flatMap(
- (itemsJson): IEither<Error, Array<T & { name: string }>> =>
- Either.fromFailable(() => JSON.parse(itemsJson)),
+ (itemsJson): IEither<Error, Array<T>> => Either.fromFailable(() => JSON.parse(itemsJson)),
)
.flatMap((itemsList): IEither<Error, T> => {
const secret = itemsList.find(({ name }) => name === item);
@@ -100,19 +94,14 @@ export class Bitwarden implements IVault<TClient, TKey, TItemId> {
}),
),
)
- .peek(async (tEitherWithSecret) => {
- const eitherWithSecret = await tEitherWithSecret.get();
- return tEitherWithSecret.trace.trace(
- eitherWithSecret.fold(({ isLeft }) => Bitwarden.fetchSecretMetric[isLeft ? 'failure' : 'success']),
- );
- })
+ .flatMapAsync(TraceUtil.promiseify(TraceUtil.traceResultingEither(Bitwarden.fetchSecretMetric)))
.get();
}
public lock(client: TClient, key: TKey) {
return client
.move(key)
- .bimap(TraceUtil.withMetricTrace(Bitwarden.lockVaultMetric))
+ .flatMap(TraceUtil.withMetricTrace(Bitwarden.lockVaultMetric))
.peek((tSession) => tSession.trace.trace(`taking care of locking the vault :3`))
.flatMap((tSession) =>
tSession.move('bw lock').map((lockCmd) =>
@@ -121,14 +110,14 @@ export class Bitwarden implements IVault<TClient, TKey, TItemId> {
}),
),
)
- .peek(async (tEitherWithLocked) => {
- const eitherWithLocked = await tEitherWithLocked.get();
- return eitherWithLocked.fold(({ isLeft }) => {
- tEitherWithLocked.trace.trace(Bitwarden.lockVaultMetric[isLeft ? 'failure' : 'success']);
- if (isLeft) return;
- tEitherWithLocked.trace.trace('all locked up and secure now~ (。•̀ᴗ-)✧');
- });
- })
+ .peek(TraceUtil.promiseify(TraceUtil.traceResultingEither(Bitwarden.lockVaultMetric)))
+ .peek(
+ TraceUtil.promiseify((tEitherWithLocked) =>
+ tEitherWithLocked
+ .get()
+ .mapRight(() => tEitherWithLocked.trace.trace('all locked up and secure now~ (。•̀ᴗ-)✧')),
+ ),
+ )
.get();
}
@@ -142,10 +131,10 @@ export class Bitwarden implements IVault<TClient, TKey, TItemId> {
);
}
- private static loginMetric = Metric.fromName('Bitwarden.login');
- private static unlockVaultMetric = Metric.fromName('Bitwarden.unlockVault');
- private static fetchSecretMetric = Metric.fromName('Bitwarden.fetchSecret');
- private static lockVaultMetric = Metric.fromName('Bitwarden.lock');
+ private static loginMetric = Metric.fromName('Bitwarden.login').asResult();
+ private static unlockVaultMetric = Metric.fromName('Bitwarden.unlockVault').asResult();
+ private static fetchSecretMetric = Metric.fromName('Bitwarden.fetchSecret').asResult();
+ private static lockVaultMetric = Metric.fromName('Bitwarden.lock').asResult();
}
export interface BitwardenConfig {
@@ -153,4 +142,4 @@ export interface BitwardenConfig {
secret: string;
clientId: string;
}
-// -- </IVault> --
+// -- </Vault> --