npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@bluelibs/runner

v4.9.0

Published

BlueLibs Runner

Downloads

342

Readme

BlueLibs Runner

Or: How I Learned to Stop Worrying and Love Dependency Injection

| Resource | Type | Notes | | ------------------------------------------------------------------------------------------------------------------- | ------- | ------------------------------------------------------------- | | Presentation Website | Website | Overview, features, and highlights | | BlueLibs Runner GitHub | GitHub | Source code, issues, and releases | | BlueLibs Runner Dev | GitHub | Development tools and CLI for BlueLibs Runner | | UX Friendly Docs | Docs | Clean, navigable documentation | | AI Friendly Docs (<5000 tokens) | Docs | Short, token-friendly summary (<5000 tokens) | | Migrate from 3.x.x to 4.x.x | Guide | Step-by-step upgrade from v3 to v4 | | Runner Lore | Docs | Design notes, deep dives, and context | | Example: Express + OpenAPI + SQLite | Example | Full Express + OpenAPI + SQLite demo | | Example: Fastify + MikroORM + PostgreSQL | Example | Full Fastify + MikroORM + PostgreSQL demo | | OpenAI Runner Chatbot | Chatbot | Ask questions interactively, or feed README.md to your own AI |

Community & Policies

Welcome to BlueLibs Runner, where we've taken the chaos of modern application architecture and turned it into something that won't make you question your life choices at 3am. This isn't just another framework – it's your new best friend who actually understands that code should be readable, testable, and not require a PhD in abstract nonsense to maintain.

What Is This Thing?

BlueLibs Runner is a TypeScript-first framework that embraces functional programming principles while keeping dependency injection simple enough that you won't need a flowchart to understand your own code. Think of it as the anti-framework framework – it gets out of your way and lets you build stuff that actually works.

The Core

  • Tasks are functions - Not classes with 47 methods you swear you'll refactor
  • Resources are singletons - Database connections, configs, services - the usual suspects
  • Events are just events - Revolutionary concept, we know
  • Hooks are lightweight listeners - Event handling without the task overhead
  • Middleware with lifecycle interception - Cross-cutting concerns with full observability
  • Everything is async - Because it's 2025 and blocking code is so 2005
  • Explicit beats implicit - No magic, no surprises, no "how the hell does this work?"
  • No compromise on type-safety - Everything is and will be type-enforced. Catch mistakes before they catch you.

Quick Start

npm install @bluelibs/runner

Here's a complete Express server in less lines than most frameworks need for their "Hello World":

import express from "express";
import { r, run, globals } from "@bluelibs/runner";

// A resource is anything you want to share across your app, a singleton
const server = r
  .resource<{ port: number }>("app.server")
  .init(async ({ port }, dependencies) => {
    const app = express();
    app.use(express.json());
    const listener = await app.listen(port);
    console.log(`Server running on port ${port}`);

    return { listener };
  })
  .dispose(async ({ listener }) => listener.close())
  .build();

// Tasks are your business logic - easily testable functions
const createUser = r
  .task("app.tasks.createUser")
  .dependencies({ server, logger: globals.resources.logger })
  .inputSchema<{ name: string }>({ parse: (value) => value })
  .run(async (input, { server, logger }) => {
    await logger.info(`Creating ${input.name}`);
    return { id: "user-123", name: input.name };
  })
  .build();

// Wire everything together
const app = r
  .resource("app")
  .register([server.with({ port: 3000 }), createUser])
  .dependencies({ server, createUser })
  .init(async (_config, { server, createUser }) => {
    server.listener.on("listening", () => {
      console.log("Runner HTTP server ready");
    });

    server.app.post("/users", async (req, res) => {
      const user = await createUser(req.body);
      res.json(user);
    });
  })
  .build();

// That's it. Each run is fully isolated
const runtime = await run(app);
const { dispose, runTask, getResourceValue, emitEvent } = runtime;

// Or with debug logging enabled
await run(app, { debug: "verbose" });

Classic API (still supported)

Prefer fluent builders for new code, but the classic define-style API remains supported and can be mixed in the same app:

import { resource, task, run } from "@bluelibs/runner";

const db = resource({ id: "app.db", init: async () => "conn" });
const add = task({
  id: "app.tasks.add",
  run: async (i: { a: number; b: number }) => i.a + i.b,
});

const app = resource({ id: "app", register: [db, add] });
await run(app);

See complete docs for migration tips and side‑by‑side patterns.

Platform & Async Context

Runner auto-detects the platform and adapts behavior at runtime. The only feature present only in Node.js is the use of AsyncLocalStorage for managing async context.

The Big Five

The framework is built around five core concepts: Tasks, Resources, Events, Middleware, and Tags. Understanding them is key to using the runner effectively.

Tasks

Tasks are functions with superpowers. They're testable, composable, and fully typed. Unlike classes that accumulate methods like a hoarder accumulates stuff, tasks do one thing well.

import { r } from "@bluelibs/runner";

const sendEmail = r
  .task("app.tasks.sendEmail")
  .dependencies({ emailService, logger })
  .run(async (input, { emailService, logger }) => {
    await logger.info(`Sending email to ${input.to}`);
    return emailService.send(input);
  })
  .build();

// Test it like a normal function (because it basically is)
const result = await sendEmail.run(
  { to: "[email protected]", subject: "Hi", body: "Hello!" },
  { emailService: mockEmailService, logger: mockLogger },
);

Look, we get it. You could turn every function into a task, but that's like using a sledgehammer to crack nuts. Here's the deal:

Make it a task when:

  • It's a high-level business action: "app.user.register", "app.order.process"
  • You want it trackable and observable
  • Multiple parts of your app need it
  • It's complex enough to benefit from dependency injection

Don't make it a task when:

  • It's a simple utility function
  • It's used in only one place or to help other tasks
  • It's performance-critical and doesn't need DI overhead

Think of tasks as the "main characters" in your application story, not every single line of dialogue.

Resources

Resources are the singletons, the services, configs, and connections that live throughout your app's lifecycle. They initialize once and stick around until cleanup time. Register them via .register([...]) so the container knows about them.

import { r } from "@bluelibs/runner";

const database = r
  .resource("app.db")
  .init(async () => {
    const client = new MongoClient(process.env.DATABASE_URL as string);
    await client.connect();
    return client;
  })
  .dispose(async (client) => client.close())
  .build();

const userService = r
  .resource("app.services.user")
  .dependencies({ database })
  .init(async (_config, { database }) => ({
    async createUser(userData: UserData) {
      return database.collection("users").insertOne(userData);
    },
    async getUser(id: string) {
      return database.collection("users").findOne({ _id: id });
    },
  }))
  .build();

Resource Configuration

Resources can be configured with type-safe options. No more "config object of unknown shape" nonsense.

type SMTPConfig = {
  smtpUrl: string;
  from: string;
};

const emailer = r
  .resource<{ smtpUrl: string; from: string }>("app.emailer")
  .init(async (config) => ({
    send: async (to: string, subject: string, body: string) => {
      // Use config.smtpUrl and config.from
    },
  }))
  .build();

// Register with specific config
const app = r
  .resource("app")
  .register([
    emailer.with({
      smtpUrl: "smtp://localhost",
      from: "[email protected]",
    }),
    // using emailer without with() will throw a type-error ;)
  ])
  .build();

Private Context

For cases where you need to share variables between init() and dispose() methods (because sometimes cleanup is complicated), use the enhanced context pattern:

const dbResource = r
  .resource("db.service")
  .context(() => ({
    connections: new Map<string, unknown>(),
    pools: [] as Array<{ drain(): Promise<void> }>,
  }))
  .init(async (_config, _deps, ctx) => {
    const db = await connectToDatabase();
    ctx.connections.set("main", db);
    ctx.pools.push(createPool(db));
    return db;
  })
  .dispose(async (_db, _config, _deps, ctx) => {
    for (const pool of ctx.pools) {
      await pool.drain();
    }
    for (const [, conn] of ctx.connections) {
      await (conn as { close(): Promise<void> }).close();
    }
  })
  .build();

Events

Events let different parts of your app talk to each other without tight coupling. It's like having a really good office messenger who never forgets anything.

import { r } from "@bluelibs/runner";

const userRegistered = r
  .event("app.events.userRegistered")
  .payloadSchema<{ userId: string; email: string }>({ parse: (value) => value })
  .build();

const registerUser = r
  .task("app.tasks.registerUser")
  .dependencies({ userService, userRegistered })
  .run(async (input, { userService, userRegistered }) => {
    const user = await userService.createUser(input);
    await userRegistered({ userId: user.id, email: user.email });
    return user;
  })
  .build();

const sendWelcomeEmail = r
  .hook("app.hooks.sendWelcomeEmail")
  .on(userRegistered)
  .run(async (event) => {
    console.log(`Welcome email sent to ${event.data.email}`);
  })
  .build();

Wildcard Events

Sometimes you need to be the nosy neighbor of your application:

const logAllEventsHook = r
  .hook("app.hooks.logAllEvents")
  .on("*")
  .run((event) => {
    console.log("Event detected", event.id, event.data);
  })
  .build();

Excluding Events from Global Listeners

Sometimes you have internal or system events that should not be picked up by wildcard listeners. Use the excludeFromGlobalHooks tag to prevent events from being sent to "*" listeners:

import { r, globals } from "@bluelibs/runner";

// Internal event that won't be seen by global listeners
const internalEvent = r
  .event("app.events.internal")
  .tags([globals.tags.excludeFromGlobalHooks])
  .build();

When to exclude events from global listeners:

  • High-frequency internal events (performance)
  • System debugging events
  • Framework lifecycle events
  • Events that contain sensitive information
  • Events meant only for specific components

Hooks

The modern way to listen to events is through hooks. They are lightweight event listeners, similar to tasks, but with a few key differences.

const myHook = r
  .hook("app.hooks.myEventHandler")
  .on(userRegistered)
  .dependencies({ logger })
  .run(async (event, { logger }) => {
    await logger.info(`User registered: ${event.data.email}`);
  })
  .build();

Multiple Events (type-safe intersection)

Hooks can listen to multiple events by providing an array to on. The run(event) payload is inferred as the common (intersection-like) shape across all provided event payloads. Use the onAnyOf() helper to preserve tuple inference ergonomics, and isOneOf() as a convenient runtime/type guard when needed.

import { r, onAnyOf, isOneOf } from "@bluelibs/runner";

const eUser = r
  .event("app.events.user")
  .payloadSchema<{ id: string; email: string }>({ parse: (v) => v })
  .build();
const eAdmin = r
  .event("app.events.admin")
  .payloadSchema<{ id: string; role: "admin" | "superadmin" }>({
    parse: (v) => v,
  })
  .build();
const eGuest = r
  .event("app.events.guest")
  .payloadSchema<{ id: string; guest: true }>({ parse: (v) => v })
  .build();

// The common field across all three is { id: string }
const auditUsers = r
  .hook("app.hooks.auditUsers")
  .on([eUser, eAdmin, eGuest])
  .run(async (ev) => {
    ev.data.id; // OK: common field inferred
    // ev.data.email; // TS error: not common to all
  })
  .build();

// Guard usage to refine at runtime (still narrows to common payload)
const auditSome = r
  .hook("app.hooks.auditSome")
  .on(onAnyOf([eUser, eAdmin])) // to get a combined event
  .run(async (ev) => {
    if (isOneOf(ev, [eUser, eAdmin])) {
      ev.data.id; // common field of eUser and eAdmin
    }
  })
  .build();

Notes:

  • The common payload is computed structurally. Optional properties become optional if they are not present across all events.
  • Wildcard on: "*" continues to accept any event and infers any payload.

Hooks are perfect for:

  • Event-driven side effects
  • Logging and monitoring
  • Notifications and alerting
  • Data synchronization
  • Any reactive behavior

Key differences from tasks:

  • Lighter weight - no middleware support
  • Designed specifically for event handling

System Event

The framework exposes a minimal system-level event for observability:

import { globals } from "@bluelibs/runner";

const systemReadyHook = r
  .hook("app.hooks.systemReady")
  .on(globals.events.ready)
  .run(async () => {
    console.log("🚀 System is ready and operational!");
  })
  .build();

Available system event:

  • globals.events.ready - System has completed initialization // Note: use run({ onUnhandledError }) for unhandled error handling

stopPropagation()

Sometimes you need to prevent other event listeners from processing an event. The stopPropagation() method gives you fine-grained control over event flow:

const criticalAlert = r
  .event("app.events.alert")
  .payloadSchema<{ severity: "low" | "medium" | "high" | "critical" }>({
    parse: (v) => v,
  })
  .meta({
    title: "System Alert Event",
    description: "Emitted when system issues are detected",
  })
  .build();

// High-priority handler that can stop propagation
const emergencyHandler = r
  .hook("app.hooks.emergencyHandler")
  .on(criticalAlert)
  .order(-100) // Higher priority (lower numbers run first)
  .run(async (event) => {
    console.log(`Alert received: ${event.data.severity}`);

    if (event.data.severity === "critical") {
      console.log("🚨 CRITICAL ALERT - Activating emergency protocols");

      // Stop other handlers from running
      event.stopPropagation();
      // Notify the on-call team, escalate, etc.

      console.log("🛑 Event propagation stopped - emergency protocols active");
    }
  })
  .build();

runtime: "'A really good office messenger.' That's me in rollerblades. You launch a 'userRegistered' flare and I sprint across the building, high-fiving hooks and dodging middleware. stopPropagation is you sweeping my legs mid-stride. Rude. Effective. Slightly thrilling."

Parallel Event Execution

When an event fan-out needs more throughput, mark it as parallel to run same-priority listeners concurrently while preserving priority boundaries:

const parallelEvent = r.event("app.events.parallel").parallel(true).build();

r.hook("app.hooks.first")
  .on(parallelEvent)
  .order(0)
  .run(async (event) => {
    await doWork(event.data);
  })
  .build();

r.hook("app.hooks.second")
  .on(parallelEvent)
  .order(0)
  .run(async () => log.info("Runs alongside first"))
  .build();

r.hook("app.hooks.after")
  .on(parallelEvent)
  .order(1) // Waits for order 0 batch to complete
  .run(async () => followUp())
  .build();

Execution semantics:

  • Listeners sharing the same order run concurrently within a batch; batches execute sequentially in ascending order.
  • All listeners in a batch run to completion even if some fail. If multiple listeners throw, an AggregateError containing all errors is thrown (or a single error if only one fails).
  • If any listener in a batch throws, later batches are skipped.
  • stopPropagation() is evaluated between batches only. Setting it inside a batch does not cancel peers already executing in that batch since parallel listeners cannot be stopped mid-flight.

Middleware

Middleware wraps around your tasks and resources, adding cross-cutting concerns without polluting your business logic.

Note: Middleware is now split by target. Use taskMiddleware(...) for task middleware and resourceMiddleware(...) for resource middleware.

import { r } from "@bluelibs/runner";

// Task middleware with config
type AuthMiddlewareConfig = { requiredRole: string };
const authMiddleware = r.middleware
  .task<AuthMiddlewareConfig>("app.middleware.task.auth")
  .run(async ({ task, next }, _deps, config) => {
    // Must return the value
    return await next(task.input);
  })
  .build();

const adminTask = r
  .task("app.tasks.adminOnly")
  .middleware([authMiddleware.with({ requiredRole: "admin" })])
  .run(async (input) => "Secret admin data")
  .build();

For middleware with input/output contracts:

// Middleware that enforces specific input and output types
type AuthConfig = { requiredRole: string };
type AuthInput = { user: { role: string } };
type AuthOutput = { user: { role: string; verified: boolean } };

const authMiddleware = r.middleware
  .task<AuthConfig>("app.middleware.task.auth")
  .run(async ({ task, next }, _deps, config: AuthConfig) => {
    if ((task.input as AuthInput).user.role !== config.requiredRole) {
      throw new Error("Insufficient permissions");
    }
    const result = await next(task.input);
    return {
      user: {
        ...(task.input as AuthInput).user,
        verified: true,
      },
    } as AuthOutput;
  })
  .build();

// For resources
const resourceAuthMiddleware = r.middleware
  .resource("app.middleware.resource.auth")
  .run(async ({ next }) => {
    // Resource middleware logic
    return await next();
  })
  .build();

const adminTask = r
  .task("app.tasks.adminOnly")
  .middleware([authMiddleware.with({ requiredRole: "admin" })])
  .run(async (input: { user: { role: string } }) => ({
    user: { role: input.user.role, verified: true },
  }))
  .build();

Global Middleware

Want to add logging to everything? Authentication to all tasks? Global middleware has your back:

import { r, globals } from "@bluelibs/runner";

const logTaskMiddleware = r.middleware
  .task("app.middleware.log.task")
  .everywhere(() => true)
  .dependencies({ logger: globals.resources.logger })
  .run(async ({ task, next }, { logger }) => {
    logger.info(`Executing: ${String(task!.definition.id)}`);
    const result = await next(task!.input);
    logger.info(`Completed: ${String(task!.definition.id)}`);
    return result;
  })
  .build();

Note: A global middleware can depend on resources or tasks. However, any such resources or tasks will be excluded from the dependency tree (Task -> Middleware), and the middleware will not run for those specific tasks or resources. This approach gives middleware true flexibility and control.

Interception (advanced)

For advanced scenarios, you can intercept framework execution without relying on events:

  • Event emissions: eventManager.intercept((next, event) => Promise<void>)
  • Hook execution: eventManager.interceptHook((next, hook, event) => Promise<any>)
  • Task middleware execution: middlewareManager.intercept("task", (next, input) => Promise<any>)
  • Resource middleware execution: middlewareManager.intercept("resource", (next, input) => Promise<any>)
  • Per-middleware interception: middlewareManager.interceptMiddleware(mw, interceptor)

Access eventManager via globals.resources.eventManager if needed.

Middleware Type Contracts

Middleware can enforce type contracts on the tasks that use them, ensuring data integrity as it flows through the system. This is achieved by defining Input and Output types within the middleware's implementation.

When a task uses this middleware, its own run method must conform to the Input and Output shapes defined by the middleware contract.

import { r } from "@bluelibs/runner";

// 1. Define the contract types for the middleware.
type AuthConfig = { requiredRole: string };
type AuthInput = { user: { role: string } }; // Task's input must have this shape.
type AuthOutput = { executedBy: { role: string; verified: boolean } }; // Task's output must have this shape.

// 2. Create the middleware using these types in its `run` method.
const authMiddleware = r.middleware
  .task<AuthConfig, AuthInput, AuthOutput>("app.middleware.auth")
  .run(async ({ task, next }, _deps, config) => {
    const input = task.input;
    if (input.user.role !== config.requiredRole) {
      throw new Error("Insufficient permissions");
    }

    // The task runs, and its result must match AuthOutput.
    const result = await next(input);

    // The middleware can further transform the output.
    const output = result;
    return {
      ...output,
      executedBy: {
        ...output.executedBy,
        verified: true, // The middleware adds its own data.
      },
    };
  })
  .build();

// 3. Apply the middleware to a task.
const adminTask = r
  .task("app.tasks.adminOnly")
  // If you use multiple middleware with contracts they get combined.
  .middleware([authMiddleware.with({ requiredRole: "admin" })])
  // If you use .inputSchema() the input must contain the contract types otherwise you end-up with InputContractViolation error.
  // The `run` method is now strictly typed by the middleware's contract.
  // Its input must be `AuthInput`, and its return value must be `AuthOutput`.
  .run(async (input) => {
    // `input.user.role` is available and fully typed.
    console.log(`Task executed by user with role: ${input.user.role}`);

    // Returning a shape that doesn't match AuthOutput will cause a compile-time error.
    // return { wrong: "shape" }; // This would fail!
    return {
      executedBy: {
        role: input.user.role,
      },
    };
  })
  .build();

runtime: "Ah, the onion pattern. A matryoshka doll made of promises. Every peel reveals… another logger. Another tracer. Another 'just a tiny wrapper'."

Tags

Tags are metadata that can influence system behavior. Unlike meta properties, tags can be queried at runtime to build dynamic functionality. They can be simple strings or structured configuration objects.

Basic Usage

import { r } from "@bluelibs/runner";

// Structured tags with configuration
const httpTag = r.tag<{ method: string; path: string }>("http.route").build();

const getUserTask = r
  .task("app.tasks.getUser")
  .tags([httpTag.with({ method: "GET", path: "/users/:id" })])
  .run(async (input) => getUserFromDatabase(input.id))
  .build();

Discovering Components by Tags

The core power of tags is runtime discovery. Use store.getTasksWithTag() to find components:

import { r, globals } from "@bluelibs/runner";

// Auto-register HTTP routes based on tags
const routeRegistration = r
  .hook("app.hooks.registerRoutes")
  .on(globals.events.ready)
  .dependencies({ store: globals.resources.store, server: expressServer })
  .run(async (_event, { store, server }) => {
    // Find all tasks with HTTP tags
    const apiTasks = store.getTasksWithTag(httpTag);

    apiTasks.forEach((taskDef) => {
      const config = httpTag.extract(taskDef);
      if (!config) return;

      const { method, path } = config;
      server.app[method.toLowerCase()](path, async (req, res) => {
        const result = await taskDef({ ...req.params, ...req.body });
        res.json(result);
      });
    });

    // Also find by string tags
    const cacheableTasks = store.getTasksWithTag("cacheable");
    console.log(`Found ${cacheableTasks.length} cacheable tasks`);
  })
  .build();

Tag Extraction and Processing

// Check if a tag exists and extract its configuration
const performanceTag = r
  .tag<{ warnAboveMs: number }>("performance.monitor")
  .build();

const performanceMiddleware = r.middleware
  .task("app.middleware.performance")
  .run(async ({ task, next }) => {
    // Check if task has performance monitoring enabled
    if (!performanceTag.exists(task.definition)) {
      return next(task.input);
    }

    // Extract the configuration
    const config = performanceTag.extract(task.definition)!;
    const startTime = Date.now();

    try {
      const result = await next(task.input);
      const duration = Date.now() - startTime;

      if (duration > config.warnAboveMs) {
        console.warn(`Task ${task.definition.id} took ${duration}ms`);
      }

      return result;
    } catch (error) {
      const duration = Date.now() - startTime;
      console.error(`Task failed after ${duration}ms`, error);
      throw error;
    }
  })
  .build();

System Tags

Built-in tags for framework behavior:

import { r, globals } from "@bluelibs/runner";

const internalTask = r
  .task("app.internal.cleanup")
  .tags([
    globals.tags.system, // Excludes from debug logs
    globals.tags.debug.with({ logTaskInput: true }), // Per-component debug config
  ])
  .run(async () => performCleanup())
  .build();

const internalEvent = r
  .event("app.events.internal")
  .tags([globals.tags.excludeFromGlobalHooks]) // Won't trigger wildcard listeners
  .build();

Contract Tags

Enforce return value shapes at compile time:

// Tags that enforce type contracts input/output for tasks or config/value for resources
type InputType = { id: string };
type OutputType = { name: string };
const userContract = r
  // void = no config, no need for .with({ ... })
  .tag<void, InputType, OutputType>("contract.user")
  .build();

const profileTask = r
  .task("app.tasks.getProfile")
  .tags([userContract]) // Must return { name: string }
  .run(async (input) => ({ name: input.id + "Ada" })) // ✅ Satisfies contract
  .build();

Errors

Typed errors can be declared once and injected anywhere. Register them alongside other items and consume via dependencies. The injected value is the error helper itself, exposing .throw(), .is(), .toString(), and id.

import { r } from "@bluelibs/runner";

// Fluent builder for errors
const userNotFoundError = r
  .error<{ code: number; message: string }>("app.errors.userNotFound")
  .dataSchema(z.object({ ... }))
  .build();

const getUser = r
  .task("app.tasks.getUser")
  .dependencies({ userNotFoundError })
  .run(async (input, { userNotFoundError }) => {
    userNotFoundError.throw({ code: 404, message: `User ${input} not found` });
  })
  .build();

const root = r.resource("app").register([userNotFoundError, getUser]).build();

Error data must include a message: string. The thrown Error has name = id and message = data.message for predictable matching and logging.

try {
  userNotFoundError.throw({ code: 404, message: "User not found" });
} catch (err) {
  if (userNotFoundError.is(err)) {
    // err.name === "app.errors.userNotFound", err.message === "User not found"
    console.log(`Caught error: ${err.name} - ${err.message}`);
  }
}

run() and RunOptions

The run() function boots a root resource and returns a RunResult handle to interact with your system.

Basic usage:

import { r, run } from "@bluelibs/runner";

const ping = r
  .task("ping.task")
  .run(async () => "pong")
  .build();

const app = r
  .resource("app")
  .register([ping])
  .init(async () => "ready")
  .build();

const result = await run(app);
console.log(result.value); // "ready"
await result.dispose();

What run() returns:

| Property | Description | | ----------------------- | ------------------------------------------------------------------ | | value | Value returned by root resource’s init() | | runTask(...) | Run a task by reference or string id | | emitEvent(...) | Emit events | | getResourceValue(...) | Read a resource’s value | | logger | Logger instance | | store | Runtime store with registered resources, tasks, middleware, events | | dispose() | Gracefully dispose resources and unhook listeners |

RunOptions

Pass as the second argument to run(root, options).

| Option | Type | Description | | ------------------ | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | debug | "normal" or "verbose" | Enables debug resource to log runner internals. "normal" logs lifecycle events, "verbose" adds input/output. Can also be a partial config object for fine-grained control. | | logs | object | Configures logging. printThreshold sets the minimum level to print (default: "info"). printStrategy sets the format (pretty, json, json-pretty, plain). bufferLogs holds logs until initialization is complete. | | errorBoundary | boolean | (default: true) Installs process-level safety nets (uncaughtException/unhandledRejection) and routes them to onUnhandledError. | | shutdownHooks | boolean | (default: true) Installs SIGINT/SIGTERM listeners to call dispose() for graceful shutdown. | | onUnhandledError | (err, ctx) => void | Custom handler for unhandled errors captured by the boundary. | | dryRun | boolean | Skips runtime initialization but fully builds and validates the dependency graph. Useful for CI smoke tests. init() is not called. |

const result = await run(app, { dryRun: true });
// result.value is undefined (root not initialized)
// You can inspect result.store.resources / result.store.tasks
await result.dispose();

Patterns

  • Minimal boot:
await run(app);
  • Debugging locally:
await run(app, { debug: "normal", logs: { printThreshold: "debug" } });
  • Verbose investigations:
await run(app, { debug: "verbose", logs: { printStrategy: "json-pretty" } });
  • CI validation (no side effects):
await run(app, { dryRun: true });
  • Custom process error routing:
await run(app, {
  errorBoundary: true,
  onUnhandledError: (err) => report(err),
});

Task Interceptors

Resources can dynamically modify task behavior during initialization

Task interceptors (task.intercept()) are the modern replacement for component lifecycle events, allowing resources to dynamically modify task behavior without tight coupling.

import { r, run } from "@bluelibs/runner";

const calculatorTask = r
  .task("app.tasks.calculator")
  .run(async (input: { value: number }) => {
    console.log("3. Task is running...");
    return { result: input.value + 1 };
  })
  .build();

const interceptorResource = r
  .resource("app.interceptor")
  .dependencies({ calculatorTask })
  .init(async (_config, { calculatorTask }) => {
    // Intercept the task to modify its behavior
    calculatorTask.intercept(async (next, input) => {
      console.log("1. Interceptor before task run");
      const result = await next(input);
      console.log("4. Interceptor after task run");
      return { ...result, intercepted: true };
    });
  })
  .build();

const app = r
  .resource("app")
  .register([calculatorTask, interceptorResource])
  .dependencies({ calculatorTask })
  .init(async (_config, { calculatorTask }) => {
    console.log("2. Calling the task...");
    const result = await calculatorTask({ value: 10 });
    console.log("5. Final result:", result);
    // Final result: { result: 11, intercepted: true }
  })
  .build();

await run(app);

runtime: "'Modern replacement for lifecycle events.' Adorable rebrand for 'surgical monkey‑patching.' You’re collapsing the waveform of a task at runtime and I’m Schrödinger’s runtime, praying the cat hasn’t overridden run() with throw new Error('lol')."

Optional Dependencies

Making your app resilient when services aren't available

Sometimes you want your application to gracefully handle missing dependencies instead of crashing. Optional dependencies let you build resilient systems that degrade gracefully.

Keep in mind that you have full control over dependency registration by functionalising dependencies(config) => ({ ... }) and register(config) => [].

import { r } from "@bluelibs/runner";

const emailService = r
  .resource("app.services.email")
  .init(async () => new EmailService())
  .build();

const paymentService = r
  .resource("app.services.payment")
  .init(async () => new PaymentService())
  .build();

const userRegistration = r
  .task("app.tasks.registerUser")
  .dependencies({
    database: userDatabase, // Required - will fail if not available
    emailService: emailService.optional(), // Optional - won't fail if missing
    analytics: analyticsService.optional(), // Optional - graceful degradation
  })
  .run(async (input, { database, emailService, analytics }) => {
    // Create user (required)
    const user = await database.users.create(userData);

    // Send welcome email (optional)
    if (emailService) {
      await emailService.sendWelcome(user.email);
    }

    // Track analytics (optional)
    if (analytics) {
      await analytics.track("user.registered", { userId: user.id });
    }

    return user;
  },
});

When to use optional dependencies:

  • External services that might be down
  • Feature flags and A/B testing services
  • Analytics and monitoring services
  • Non-critical third-party integrations
  • Development vs production service differences

Benefits:

  • Graceful degradation instead of crashes
  • Better resilience in distributed systems
  • Easier testing with partial mocks
  • Smoother development environments

runtime: "Graceful degradation: your app quietly limps with a brave smile. I’ll juggle undefined like a street performer while your analytics vendor takes a nap. Please clap when I keep the lights on using the raw power of conditional chaining."

Serialization (EJSON)

Runner uses EJSON by default. Think of it as JSON with superpowers: it safely round‑trips values like Date, RegExp, and even your own custom types across HTTP and between Node and the browser.

  • By default, Runner’s HTTP clients and exposures use the EJSON serializer
  • You can call getDefaultSerializer() for the shared serializer instance
  • A global serializer is also exposed as a resource: globals.resources.serializer
import { r, globals } from "@bluelibs/runner";

// 2) Register custom EJSON types centrally via the global serializer resource
const ejsonSetup = r
  .resource("app.serialization.setup")
  .dependencies({ serializer: globals.resources.serializer })
  .init(async (_config, { serializer }) => {
    const text = s.stringify({ when: new Date() });
    const obj = s.parse<{ when: Date }>(text);
    class Distance {
      constructor(public value: number, public unit: string) {}
      toJSONValue() {
        return { value: this.value, unit: this.unit } as const;
      }
      typeName() {
        return "Distance";
      }
    }

    serializer.addType(
      "Distance",
      (j: { value: number; unit: string }) => new Distance(j.value, j.unit),
    );
  })
  .build();

Tunnels: Bridging Runners

Tunnels are a powerful feature for building distributed systems. They let you expose your tasks and events over HTTP, making them callable from other processes, services, or even a browser UI. This allows a server and client to co-exist, enabling one Runner instance to securely call another.

Here's a sneak peek of how you can expose your application and configure a client tunnel to consume a remote Runner:

import { r, globals } from "@bluelibs/runner";
import { nodeExposure } from "@bluelibs/runner/node";

let app = r.resource("app");

if (process.env.SERVER) {
  // 1. Expose your local tasks and events over HTTP, only when server mode is active.
  app.register([
    // ... your tasks and events
    nodeExposure.with({
      http: {
        basePath: "/__runner",
        listen: { port: 7070 },
      },
    }),
  ]);
}
app = app.build();

// 2. In another app, define a tunnel resource to call a remote Runner
const remoteTasksTunnel = r
  .resource("app.tunnels.http")
  .tags([globals.tags.tunnel])
  .dependencies({ createClient: globals.resource.httpClientFactory })
  .init(async (_, { createClient }) => ({
    mode: "client", // or "server", or "none", or "both" for emulating network infrastructure
    transport: "http", // the only one supported for now
    // Selectively forward tasks starting with "remote.tasks."
    tasks: (t) => t.id.startsWith("remote.tasks."),
    client: createClient({
      url: "http://remote-runner:8080/__runner",
    }),
  }))
  .build();

This is just a glimpse. With tunnels, you can build microservices, CLIs, and admin panels that interact with your main application securely and efficiently.

For a deep dive into streaming, authentication, file uploads, and more, check out the full Tunnels documentation.

Async Context

Async Context provides per-request/thread-local state via the platform's AsyncLocalStorage (Node). Use the fluent builder under r.asyncContext to create contexts that can be registered and injected as dependencies.

import { r } from "@bluelibs/runner";

const requestContext = r
  .asyncContext<{ requestId: string }>("app.ctx.request")
  // below is optional
  .configSchema(z.object({ ... }))
  .serialize((data) => JSON.stringify(data))
  .parse((raw) => JSON.parse(raw))
  .build();

// Provide and read within an async boundary
await requestContext.provide({ requestId: "abc" }, async () => {
  const ctx = requestContext.use(); // { requestId: "abc" }
});

// Require middleware for tasks that need the context
const requireRequestContext = requestContext.require();
  • If you don't provide serialize/parse, Runner uses its default EJSON serializer to preserve Dates, RegExp, etc.

  • A legacy createContext(name?) exists for backwards compatibility; prefer r.asyncContext or asyncContext({ id }).

  • You can also inject async contexts as dependencies; the injected value is the helper itself. Contexts must be registered to be used.

const whoAmI = r
  .task("app.tasks.whoAmI")
  .dependencies({ requestContext })
  .run(async (_input, { requestContext }) => requestContext.use().requestId)
  .build();

const app = r.resource("app").register([requestContext, whoAmI]).build();

// Legacy section for Private Context - different from Async Context

Fluent Builders (r.*)

For a more ergonomic and chainable way to define your components, Runner offers a fluent builder API under the r namespace. These builders are fully type-safe, improve readability for complex definitions, and compile to the standard Runner definitions with zero runtime overhead.

Here’s a quick taste of how it looks, with and without zod for validation:

import { r, run } from "@bluelibs/runner";
import { z } from "zod";

// With Zod, the config type is inferred automatically
const emailerConfigSchema = z.object({
  smtpUrl: z.string().url(),
  from: z.string().email(),
});

const emailer = r
  .resource("app.emailer")
  .configSchema(emailerConfigSchema)
  .init(async ({ config }) => ({
    send: (to: string, body: string) => {
      console.log(
        `Sending from ${config.from} to ${to} via ${config.smtpUrl}: ${body}`,
      );
    },
  }))
  .build();

// Without a schema library, you can provide the type explicitly
const greeter = r
  .resource("app.greeter")
  .init(async (cfg: { name: string }) => ({
    greet: () => `Hello, ${cfg.name}!`,
  }))
  .build();

const app = r
  .resource("app")
  .register([
    emailer.with({
      smtpUrl: "smtp://example.com",
      from: "[email protected]",
    }),
    greeter.with({ name: "World" }),
  ])
  .dependencies({ emailer, greeter })
  .init(async (_, { emailer, greeter }) => {
    console.log(greeter.greet());
    emailer.send("[email protected]", "This is a test.");
  })
  .build();

await run(app);

The builder API provides a clean, step-by-step way to construct everything from simple tasks to complex resources with middleware, tags, and schemas.

For a complete guide and more examples, check out the full Fluent Builders documentation.

Type Helpers

These utility types help you extract the generics from tasks, resources, and events without re-declaring them. Import them from @bluelibs/runner.

import { r } from "@bluelibs/runner";
import type {
  ExtractTaskInput,
  ExtractTaskOutput,
  ExtractResourceConfig,
  ExtractResourceValue,
  ExtractEventPayload,
} from "@bluelibs/runner";

// Task example
const add = r
  .task("calc.add")
  .run(async (input: { a: number; b: number }) => input.a + input.b)
  .build();

type AddInput = ExtractTaskInput<typeof add>; // { a: number; b: number }
type AddOutput = ExtractTaskOutput<typeof add>; // number

// Resource example
const config = r
  .resource("app.config")
  .init(async (cfg: { baseUrl: string }) => ({ baseUrl: cfg.baseUrl }))
  .build();

type ConfigInput = ExtractResourceConfig<typeof config>; // { baseUrl: string }
type ConfigValue = ExtractResourceValue<typeof config>; // { baseUrl: string }

// Event example
const userRegistered = r
  .event("app.events.userRegistered")
  .payloadSchema<{ userId: string; email: string }>({ parse: (v) => v })
  .build();
type UserRegisteredPayload = ExtractEventPayload<typeof userRegistered>; // { userId: string; email: string }

Context with Middleware

Context shines when combined with middleware for request-scoped data:

import { r } from "@bluelibs/runner";
import { randomUUID } from "crypto";

const requestContext = r
  .asyncContext<{
    requestId: string;
    startTime: number;
    userAgent?: string;
  }>("app.requestContext")
  .build();

const requestMiddleware = r.middleware
  .task("app.middleware.request")
  .run(async ({ task, next }) => {
    // This works even in express middleware if needed.
    return requestContext.provide(
      {
        requestId: randomUUID(),
        startTime: Date.now(),
        userAgent: "MyApp/1.0",
      },
      async () => {
        return next(task?.input);
      },
    );
  })
  .build();

const handleRequest = r
  .task("app.handleRequest")
  .middleware([requestMiddleware])
  .run(async (input: { path: string }) => {
    const request = requestContext.use();
    console.log(`Processing ${input.path} (Request ID: ${request.requestId})`);
    return { success: true, requestId: request.requestId };
  })
  .build();

runtime: "Context: global state with manners. You invented a teleporting clipboard for data and called it 'nice.' Forget to provide() once and I’ll unleash the 'Context not available' banshee scream exactly where your logs are least helpful."

System Shutdown Hooks

Graceful shutdown and cleanup when your app needs to stop

The framework includes built-in support for graceful shutdowns with automatic cleanup and configurable shutdown hooks:

import { run } from "@bluelibs/runner";

// Enable shutdown hooks (default: true in production)
const { dispose, taskRunner, eventManager } = await run(app, {
  shutdownHooks: true, // Automatically handle SIGTERM/SIGINT
  errorBoundary: true, // Catch unhandled errors and rejections
});

// Manual graceful shutdown
process.on("SIGTERM", async () => {
  console.log("Received SIGTERM, shutting down gracefully...");
  await dispose(); // This calls all resource dispose() methods
  process.exit(0);
});

// Resources with cleanup logic
const databaseResource = r
  .resource("app.database")
  .init(async () => {
    const connection = await connectToDatabase();
    console.log("Database connected");
    return connection;
  })
  .dispose(async (connection) => {
    await connection.close();
    // console.log("Database connection closed");
  })
  .build();

const serverResource = r
  .resource("app.server")
  .dependencies({ database: databaseResource })
  .init(async (config: { port: number }, { database }) => {
    const server = express().listen(config.port);
    console.log(`Server listening on port ${config.port}`);
    return server;
  })
  .dispose(async (server) => {
    return new Promise<void>((resolve) => {
      server.close(() => {
        console.log("Server closed");
        resolve();
      });
    });
  })
  .build();

Error Boundary Integration

The framework can automatically handle uncaught exceptions and unhandled rejections:

const { dispose, logger } = await run(app, {
  errorBoundary: true, // Catch process-level errors
  shutdownHooks: true, // Graceful shutdown on signals
  onUnhandledError: async ({ error, kind, source }) => {
    // We log it by default
    await logger.error(`Unhandled error: ${error && error.toString()}`);
    // Optionally report to telemetry or decide to dispose/exit
  },
});

runtime: "You summon a 'graceful shutdown' with Ctrl‑C like a wizard casting Chill Vibes. Meanwhile I’m speed‑dating every socket, timer, and file handle to say goodbye before the OS pulls the plug. dispose(): now with 30% more dignity."

Unhandled Errors

The onUnhandledError callback is invoked by Runner whenever an error escapes normal handling. It receives a structured payload you can ship to logging/telemetry and decide mitigation steps.

type UnhandledErrorKind =
  | "process" // uncaughtException / unhandledRejection
  | "task" // task.run threw and wasn't handled
  | "middleware" // middleware threw and wasn't handled
  | "resourceInit" // resource init failed
  | "hook" // hook.run threw and wasn't handled
  | "run"; // failures in run() lifecycle

interface OnUnhandledErrorInfo {
  error: unknown;
  kind?: UnhandledErrorKind;
  source?: string; // additional origin hint (ex: "uncaughtException")
}

type OnUnhandledError = (info: OnUnhandledErrorInfo) => void | Promise<void>;

Default behavior (when not provided) logs the normalized error via the created logger at error level. Provide your own handler to integrate with tools like Sentry/PagerDuty or to trigger shutdown strategies.

Example with telemetry and conditional shutdown:

await run(app, {
  errorBoundary: true,
  onUnhandledError: async ({ error, kind, source }) => {
    await telemetry.capture(error as Error, { kind, source });
    // Optionally decide on remediation strategy
    if (kind === "process") {
      // For hard process faults, prefer fast, clean exit after flushing logs
      await flushAll();
      process.exit(1);
    }
  },
});

Best Practices for Shutdown:

  • Resources are disposed in reverse dependency order
  • Set reasonable timeouts for cleanup operations
  • Save critical state before shutdown
  • Notify load balancers and health checks
  • Stop accepting new work before cleaning up

runtime: "An error boundary: a trampoline under your tightrope. I’m the one bouncing, cataloging mid‑air exceptions, and deciding whether to end the show or juggle chainsaws with a smile. The audience hears music; I hear stack traces."

Caching

Because nobody likes waiting for the same expensive operation twice:

import { globals } from "@bluelibs/runner";

const expensiveTask = r
  .task("app.tasks.expensive")
  .middleware([
    globals.middleware.task.cache.with({
      // lru-cache options by default
      ttl: 60 * 1000, // Cache for 1 minute
      keyBuilder: (taskId, input: any) => `${taskId}-${input.userId}`, // optional key builder
    }),
  ])
  .run(async (input: { userId: string }) => {
    // This expensive operation will be cached
    return await doExpensiveCalculation(input.userId);
  })
});

// Global cache configuration
const app = r
  .resource("app.cache")
  .register([
    // You have to register it, cache resource is not enabled by default.
    globals.resources.cache.with({
      defaultOptions: {
        max: 1000, // Maximum items in cache
        ttl: 30 * 1000, // Default TTL
      },
    }),
  ])
  .build();

Want Redis instead of the default LRU cache? No problem, just override the cache factory task:

import { r } from "@bluelibs/runner";

const redisCacheFactory = r
  .task("globals.tasks.cacheFactory") // Same ID as the default task
  .run(async (input: { input: any }) => new RedisCache(input))
  .build();

const app = r
  .resource("app")
  .register([globals.resources.cache])
  .overrides([redisCacheFactory]) // Override the default cache factory
  .build();

runtime: "'Because nobody likes waiting.' Correct. You keep asking the same question like a parrot with Wi‑Fi, so I built a memory palace. Now you get instant answers until you change one variable and whisper 'cache invalidation' like a curse."

Performance

BlueLibs Runner is designed with performance in mind. The framework introduces minimal overhead while providing powerful features like dependency injection, middleware, and event handling.

Test it yourself by cloning @bluelibs/runner and running npm run benchmark.

You may see negative middlewareOverheadMs. This is a measurement artifact at micro-benchmark scale: JIT warm‑up, CPU scheduling, GC timing, and cache effects can make the "with middleware" run appear slightly faster than the baseline. Interpret small negatives as ≈ 0 overhead.

Performance Benchmarks

Here are real performance metrics from our comprehensive benchmark suite on an M1 Max.

** Core Operations**

  • Basic task execution: ~2.2M tasks/sec
  • Task execution with 5 middlewares: ~244,000 tasks/sec
  • Resource initialization: ~59,700 resources/sec
  • Event emission and handling: ~245,861 events/sec
  • Dependency resolution (10-level chain): ~8,400 chains/sec

Overhead Analysis

  • Middleware overhead: ~0.0013ms for all 5, ~0.00026ms per middleware (virtually zero)
  • Memory overhead: ~3.3MB for 100 components (resources + tasks)
  • Cache middleware speedup: 3.65x faster with cache hits

Real-World Performance

// This executes in ~0.005ms on average
const userTask = r
  .task("user.create")
  .middleware([auth, logging, metrics])
  .run(async (input) => database.users.create(input))
  .build();

// 1000 executions = ~5ms total time
for (let i = 0; i < 1000; i++) {
  await userTask(mockUserData);
}

Performance Guidelines

When Performance Matters Most

Use tasks for:

  • High-level business operations that benefit from observability
  • Operations that need middleware (auth, caching, retry)
  • Functions called from multiple places

Use regular functions or service resources for:

  • Simple utilities and helpers
  • Performance-critical hot paths (< 1ms requirement)
  • Single-use internal logic

Optimizing Your App

Middleware Ordering: Place faster middleware first

const task = r
  .task("app.performance.example")
  middleware: [
    fastAuthCheck, // ~0.1ms
    slowRateLimiting, // ~2ms
    expensiveLogging, // ~5ms
  ],
  .run(async () => null)
  .build();

Resource Reuse: Resources are singletons—perfect for expensive setup

const database = r
  .resource("app.performance.db")
  .init(async () => {
    // Expensive connection setup happens once
    const connection = await createDbConnection();
    return connection;
  })
  .build();

Cache Strategically: Use built-in caching for expensive operations

const expensiveTask = r
  .task("app.performance.expensive")
  .middleware([globals.middleware.cache.with({ ttl: 60000 })])
  .run(async (input) => {
    // This expensive computation is cached
    return performExpensiveCalculation(input);
  })
  .build();

Memory Considerations

  • Lightweight: Each component adds ~33KB to memory footprint
  • Automatic cleanup: Resources dispose properly to prevent leaks
  • Event efficiency: Event listeners are automatically managed

Benchmarking Your Code

Run the framework's benchmark suite:

# Comprehensive benchmarks
npm run test -- --testMatch="**/comprehensive-benchmark.test.ts"

# Benchmark.js based tests
npm run benchmark

Create your own performance tests:

const iterations = 1000;
const start = performance.now();

for (let i = 0; i < iterations; i++) {
  await yourTask(testData);
}

const duration = performance.now() - start;
console.log(`${iterations} tasks in ${duration.toFixed(2)}ms`);
console.log(`Average: ${(duration / iterations).toFixed(4)}ms per task`);
console.log(
  `Throughput: ${Math.round(iterations / (duration / 1000))} tasks/sec`,
);

Performance vs Features Trade-off

BlueLibs Runner achieves high performance while providing enterprise features:

| Feature | Overhead | Benefit | | -------------------- | -------------------- | ----------------------------- | | Dependency Injection | ~0.001ms | Type safety, testability | | Event System | ~0.013ms | Loose coupling, observability | | Middleware Chain | ~0.0003ms/middleware | Cross-cutting concerns | | Resource Management | One-time init | Singleton pattern, lifecycle | | Built-in Caching | Variable speedup | Automatic optimization |

Bottom line: The framework adds minimal overhead (~0.005ms per task) while providing significant architectural benefits.

runtime: "'Millions of tasks per second.' Fantastic—on your lava‑warmed laptop, in a vacuum, with the wind at your back. Add I/O, entropy, and one feral user and watch those numbers molt. I’ll still be here, caffeinated and inevitable."

Retrying Failed Operations

For when things go wrong, but you know they'll probably work if you just try again. The built-in retry middleware makes your tasks and resources more resilient to transient failures.

import { globals } from "@bluelibs/runner";

const flakyApiCall = r
  .task("app.tasks.flakyApiCall")
  .middleware([
    globals.middleware.task.retry.with({
      retries: 5, // Try up to 5 times
      delayStrategy: (attempt) => 100 * Math.pow(2, attempt), // Exponential backoff
      stopRetryIf: (error) => error.message === "Invalid credentials", // Don't retry auth errors
    }),
  ])
  .run(async () => {
    // This might fail due to network issues, rate limiting, etc.
    return await fetchFromUnreliableService();
  })
  .build();

const app = r.resource("app").register([flakyApiCall]).build();

The retry middleware can be configured with:

  • retries: The maximum number of retry attempts (default: 3).
  • delayStrategy: A function that returns the delay in milliseconds before the next attempt.
  • stopRetryIf: A function to prevent retries for certain types of errors.

runtime: "Retry: the art of politely head‑butting reality. 'Surely it’ll work the fourth time,' you declare, inventing exponential backoff and calling it strategy. I’ll keep the attempts ledger while your API cosplays a coin toss."

Timeouts

The built-in timeout middleware prevents operations from hanging indefinitely by racing them against a configurable timeout. Works for resources and tasks.

import { globals } from "@bluelibs/runner";

const apiTask = r
  .task("app.tasks.externalApi")
  .middleware([
    // Works for tasks and resources via globals.middleware.resource.timeout
    globals.middleware.task.timeout.with({ ttl: 5000 }), // 5 second timeout
  ])
  .run(async () => {
    // This operation will be aborted if it takes longer than 5 seconds
    return await fetch("https://slow-api.example.com/data");
  })
  .build();

// Combine with retry for robust error handling
const resilientTask = r
  .task("app.tasks.resilient")
  .middleware([
    // Order matters here. Imagine a big onion.
    // Works for resources as well via globals.middleware.resource.retry
    globals.middleware.task.retry.with({
      retries: 3,
      delayStrategy: (attempt) => 1000 * attempt, // 1s, 2s, 3s delays
    }),
    globals.middleware.task.timeout.with({ ttl: 10000 }), // 10 second timeout per attempt
  ])
  .run(async () => {
    // Each retry attempt gets its own 10-second timeout
    return await unreliableOperation();
  })
  .build();

How it works:

  • Uses AbortController and Promise.race() for clean cancellation
  • Throws TimeoutError when the timeout is reached
  • Works with any async operation in tasks and resources
  • Integrates seamlessly with retry middleware for layered resilience
  • Zero timeout (ttl: 0) throws immediately for testing edge cases

Best practices:

  • Set timeouts based on expected operation duration plus buffer
  • Combine with retry middleware for transient failures
  • Use longer timeouts for resource initialization than task execution
  • Consider network conditions when setting API call timeouts

runtime: "Timeouts: you tie a kitchen timer to my ankle and yell 'hustle.' When the bell rings, you throw a TimeoutError like a penalty flag. It’s not me, it’s your molasses‑flavored endpoint. I just blow the whistle."

Logging

The structured logging system that actually makes debugging enjoyable

BlueLibs Runner comes with a built-in logging system that's structured, and doesn't make you hate your life when you're trying to debug at 2 AM.

Basic Logging

import { r, globals } from "@bluelibs/runner";

const app = r
  .resource("app")
  .dependencies({ logger: globals.resources.logger })
  .init(async (_config, { logger }) => {
    logger.info("Starting business process"); // ✅ Visible by default
    logger.warn("This might take a while"); // ✅ Visible by default
    logger.error("Oops, something went wrong", {
      // ✅ Visible by default
      error: new Error("Database connection failed"),
    });
    logger.critical("System is on fire", {
      // ✅ Visible by default
      data: { temperature: "9000°C" },
    });
    logger.debug("Debug information"); // ❌ Hidden by default
    logger.trace("Very detailed trace"); // ❌ Hidden by default

    logger.onLog(async (log) => {
      // Sub-loggers instantiated .with() share the same log listeners.
      // Catch logs
    });
  })
  .build();

run(app, {
  logs: {
    printThreshold: "info", // use null to disable printing, and hook into onLog(), if in 'test' mode default is null unless specified
    printStrategy: "pretty", // you also have "plain", "json" and "json-pretty" with circular dep safety for JSON formatting.
    bufferLogs: false, // Starts sending out logs only after the system emits the ready event. Useful for when you're sending them out.
  },
});

Log Levels

The logger supports six log levels with increasing severity:

| Level | Severity | When to Use | Color | | ---------- | -------- | ------------------------------------------- | ------- | | trace | 0 | Ultra-detailed debugging info | Gray | | debug | 1 | Development and debugging information | Cyan | | info | 2 | General information about normal operations | Green | | warn | 3 | Something's not right, but still working | Yellow | | error | 4 | Errors that need attention | Red | | critical | 5 | System-threatening issues | Magenta |

// All log levels are available as methods
logger.trace("Ultra-detailed debugging info");
logger.debug("Development debugging");
logger.info("Normal operation");
logger.warn("Something's fishy");
logger.error("Houston, we have a problem");
logger.critical("DEFCON 1: Everything is broken");

Structured Logging

The logger accepts rich, structured data that makes debugging actually useful:

const userTask = r
  .task("app.tasks.user.create")
  .dependencies({ logger: globals.resources.logger })
  .run(async (input, { logger }) => {
    // Basic message
    logger.info("Creating new user");

    // With structured data
    logger.info("User creation attempt", {
      source: userTask.id,
      data: {
        email: input.email,
        registrationSource: "web",
        timestamp: new Date().toISOString(),
      },
    });

    // With error information
    try {
      const user = await createUser(input);
      logger.info("User created successfully", {
        data: { userId: user.id, email: user.email },
      });
    } catch (error) {
      logger.error("User creation failed", {
        error,
        data: {
          attemptedEmail: input.email,
          validationErrors: error.validationErrors,
        },
      });
    }
  })
  .build();

Context-Aware Logging

Create logger instances with bound context for consistent metadata across related operations:

const RequestContext = createContext<{ requestId: string; userId: string }>(
  "app.requestContext",
);

const requestHandler = r
  .task("app.tasks.handleRequest")
  .dependencies({ logger: globals.resources.logger })
  .run(async ({ input: requestData }, { logger }) => {
    const request = RequestContext.use();

    // Create a contextual logger with bound metadata with source and context
    const requestLogger = logger.with({
      source: requestHandler.id,
      additionalContext: {
        requestId: request.requestId,
        userId: request.userId,
      },
    });

    // All logs from this logger will include the bound context
    requestLogger.info("Processing request", {
      data: { endpoint: requestData.path },
    });

    requestLogger.debug("Validating input", {
      data: { inputSize: JSON.stringi