AGENTS.md - Machine-Readable Package Context
Package Identity
name: "@marianmeres/midware"
version: "1.4.0"
type: "middleware-framework"
language: "TypeScript"
runtime: ["Deno", "Node.js"]
registry:
jsr: "jsr:@marianmeres/midware"
npm: "@marianmeres/midware"
license: "MIT"
Purpose
Serial middleware execution framework. Executes functions sequentially with:
- Type-safe generic arguments AND return type
- Per-middleware and total execution timeouts
- Priority-based sorting (re-evaluated on every execute)
- Duplicate detection (works with timeout-wrapped middlewares)
- Early termination via return values
- Cooperative cancellation via AbortSignal
File Map
src/mod.ts → Entry point, re-exports all public API
src/midware.ts → Midware class, MidwareUseFn, MidwareOptions, MidwareExecuteOptions
src/utils/with-timeout.ts → withTimeout function, TimeoutError class
src/utils/sleep.ts → sleep function, SleepTimeoutRef type
tests/midware.test.ts → Test suite (26 tests)
Public API Summary
Classes
class Midware<T extends unknown[], R = unknown> {
constructor(midwares?: MidwareUseFn<T>[], options?: MidwareOptions)
options: MidwareOptions
readonly size: number
readonly middlewares: readonly MidwareUseFn<T>[]
use(midware: MidwareUseFn<T>, timeout?: number): void
unshift(midware: MidwareUseFn<T>, timeout?: number): void
remove(midware: MidwareUseFn<T>): boolean
clear(): void
execute(
args: T,
timeoutOrOptions?: number | MidwareExecuteOptions,
): Promise<R | undefined>
}
class TimeoutError extends Error {
override name = "TimeoutError"
}
Types
type MidwareUseFn<T extends unknown[]> = {
(...args: T): any
__midwarePreExecuteSortOrder?: number // lower = higher priority
__midwareDuplicable?: boolean // allow duplicate registration
__midwareOriginal?: MidwareUseFn<T> // internal: back-ref through timeout wrappers
}
interface MidwareOptions {
preExecuteSortEnabled?: boolean // default: false
duplicatesCheckEnabled?: boolean // default: false
}
interface MidwareExecuteOptions {
timeout?: number // total execution timeout (ms)
signal?: AbortSignal // cooperative cancellation
}
interface SleepTimeoutRef {
id: number
}
Functions
function withTimeout<
TReturn = unknown,
TArgs extends readonly unknown[] = any[],
>(
fn: (...args: TArgs) => TReturn | Promise<TReturn>,
timeout?: number, // default: 1000; <= 0 means "no timeout"
errMessage?: string,
): (...args: TArgs) => Promise<TReturn>
function sleep(
timeout: number,
refOrSignal?: SleepTimeoutRef | AbortSignal,
): Promise<void>
Execution Model
execute(args)calls middlewares sequentially- Each middleware receives same
argstuple - Return
undefined→ continue to next middleware - Return any other value → terminate chain, return that value (typed as
R) - If
preExecuteSortEnabled: sort by__midwarePreExecuteSortOrderon every execute (no caching) - If
signalprovided: checked before each middleware, throwssignal.reasonif aborted - Non-array
argsis auto-wrapped to a single-element array (legacy BC)
Error Conditions
| Error | Trigger |
|---|---|
TypeError | Non-function passed to use()/unshift() |
Error | Duplicate middleware when duplicatesCheckEnabled=true |
TimeoutError | Middleware or total execution exceeds timeout |
signal.reason | AbortSignal fired before/between middlewares |
Important Implementation Notes
-
Timeout wrapping preserves identity via
__midwareOriginal: Callinguse(fn, timeout)wrapsfnin a new function, but stores the original aswrapped.__midwareOriginal.remove(fn), duplicate detection, and priority sorting all look through this back-reference, so they work correctly with timeout-wrapped middlewares. -
No sort caching: Priority sort is re-evaluated on every
execute(). Mutating__midwarePreExecuteSortOrderbetween runs is safe. Cost is O(n log n) per execute; negligible for typical stacks. -
Args normalization:
execute()wraps non-array args in array automatically. This is a documented legacy convenience; prefer always passing a tuple that matchesT. -
Options are mutable:
optionsproperty is public and can be modified after construction. Since sorting is no longer cached, togglingpreExecuteSortEnabledat runtime is safe. -
AbortSignal is cooperative, not coercive: The signal check happens between middlewares, not during. An in-flight middleware continues until it yields. Middlewares that want true cancellation must accept the signal through the shared context (via
args) and check it themselves. -
withTimeout(fn, 0)is a no-op: A non-positive timeout returns an async-wrapped pass-through. Previously fired on next tick (bug). -
Synchronous throws in
withTimeout-wrappedfnbecome rejections: Previously they escaped as sync throws.
Commands
deno test # Run tests once (26 tests)
deno task test:watch # Run tests in watch mode
deno check src/mod.ts # Type-check
deno task npm:build # Build for npm
deno task npm:publish # Build and publish to npm
Code Style
- Indentation: Tabs
- Line width: 90
- Private fields:
#prefix - Lint:
no-explicit-anydisabled
Common Patterns
Request/Response Pipeline
const app = new Midware<[Request, Response]>();
app.use((req, res) => { /* logging */ });
app.use((req, res) => { /* auth - may return early */ });
app.use((req, res) => { /* handler */ });
await app.execute([req, res]);
Context Object
const app = new Midware<[Context]>();
app.use((ctx) => { ctx.startTime = Date.now(); });
app.use((ctx) => { /* process */ });
await app.execute([{ data: "input" }]);
Typed Return Value
const app = new Midware<[Ctx], { status: number }>();
app.use((ctx) => {
if (!ctx.user) return { status: 401 };
});
const result = await app.execute([ctx]);
// result: { status: number } | undefined
Guard/Early Return
app.use((ctx) => {
if (!ctx.authorized) return { error: "Forbidden" };
// returning non-undefined stops chain
});
Priority Execution
const auth: MidwareUseFn<[Ctx]> = (ctx) => { /* ... */ };
auth.__midwarePreExecuteSortOrder = 1; // runs first
const app = new Midware<[Ctx]>([], { preExecuteSortEnabled: true });
app.use(auth);
AbortSignal Cancellation
const ac = new AbortController();
setTimeout(() => ac.abort(), 1000);
await app.execute([ctx], { timeout: 5000, signal: ac.signal });
Dependencies
None (zero external dependencies).
Test Coverage
26 tests covering:
- Basic flow and early termination
- Error propagation (sync + async)
- Per-middleware timeout
- Total execution timeout
- Duplicate detection (incl. timeout-wrapped)
- Priority sorting (incl. timeout-wrapped, mid-run mutation)
- Remove/clear operations (incl. timeout-wrapped)
sizeandmiddlewaresgetters- Typed return (
Rgeneric) - AbortSignal (pre-aborted, mid-chain, combined with timeout)
sleepwith AbortSignalwithTimeoutwithtimeout <= 0(no-op)withTimeoutsync-throw handlingTimeoutError.name
Migration from v1.3.x
See the Changelog in README.md for the full list. Short version:
- Runtime-BC-safe: all documented runtime behaviors preserved. Legacy
forms (
execute(args, number),sleep(ms, { id }), auto-array-wrap inexecute()) still work. - Potential BC (edge cases):
withTimeout(fn, 0)is now a no-op (was: immediateTimeoutError).TimeoutError.nameis now"TimeoutError"(was:"Error").withTimeouttype signature refined; rare type-only break if callers passed an opaqueCallableFunction.