TypeDrop

2026-03-23 Challenge

2026-03-23 Hard

Typed Plugin Middleware Chain Executor

You're building the extensibility core for a developer platform where third-party plugins can register typed middleware that transforms a shared request context. Each plugin declares the exact context shape it reads and the shape it writes, and the chain executor must thread them together in order — surfacing typed errors and a full execution trace through a `Result<T, E>` monad with zero `any`.

Goals

  • Define a `Result<T,E>` discriminated union and implement `ok`/`err` constructors, then build a `PluginError` discriminated union covering validation, timeout, and dependency variants.
  • Model `Plugin<In, Out>` with a generic constraint `Out extends In` that enforces context-only-grows semantics, and implement `PluginRegistry` with a fluent `register()` builder.
  • Implement `executeChain` that runs plugins sequentially, enforces per-plugin timeouts via `Promise.race`, accumulates a `TraceEntry` for every started plugin, and short-circuits on the first error.
  • Implement `formatPluginError` with an exhaustive `switch` over `PluginError` kinds, using a `never`-typed default branch so TypeScript proves no variant is unhandled.
challenge.ts
// Key types — understand these before implementing

export type Result<T, E> =
  | { ok: true;  value: T }
  | { ok: false; error: E };

export interface BaseContext {
  readonly requestId: string;
  meta: Record<string, unknown>;
}

export type MiddlewareFn<In extends BaseContext, Out extends In> = (
  ctx: In
) => Promise<Result<Out, PluginError>>;

export interface Plugin<In extends BaseContext, Out extends In> {
  id: string;
  timeoutMs: number;
  run: MiddlewareFn<In, Out>;
}

// Main entry point
export async function executeChain(
  initCtx: BaseContext,
  registry: PluginRegistry
): Promise<ChainResult<BaseContext>>;
Hints (click to reveal)

Hints

  • For the timeout race, create a `Promise<Result<never, PluginError>>` that rejects (or resolves to `err(...)`) after `timeoutMs` milliseconds — then `Promise.race` it against the plugin's own promise.
  • Store plugins in `PluginRegistry` as `Plugin<BaseContext, BaseContext>[]`; the `register<In, Out>` generic signature is safe to accept because `Plugin<In, Out>` is assignable to `Plugin<BaseContext, BaseContext>` given the covariant/contravariant positions here — but you may need a targeted cast only inside `register`.
  • The `never`-typed default in `formatPluginError` looks like: `default: { const _exhaustive: never = error; throw new Error(...); }` — TypeScript will error if you ever add a new `PluginError` variant without handling it.

Or clone locally

git clone -b challenge/2026-03-23 https://github.com/niltonheck/typedrop.git