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 🙏

© 2026 – Pkg Stats / Ryan Hefner

@orria/dispatchkit

v1.1.0

Published

Lightweight CQRS-lite application framework and runtime toolkit for Bun.

Downloads

120

Readme

Features

  • CQRS operation definitions: defineQuery, defineMutation, defineAction
  • Runtime modules: defineConfig, defineLogger, defineInfra, defineTransport
  • Runtime builder: buildRuntime()
  • Strict CQRS call guards at runtime
  • Standard Schema validation for config and operation input and return
  • Resource cleanup through onDispose() and runtime.dispose()
  • Context helpers: getModuleCtx(), getTransportCtx()
  • Runtime artifact generation into src/generated/runtime
  • CLI: dispatchkit generate and dispatchkit generate --watch

Installation

bun add @orria/dispatchkit

Install a validator separately. The Zod examples below use bun add zod; Valibot uses bun add valibot. Dispatchkit has no mandatory Zod dependency. dotenv is included. A custom logger such as pino is optional through defineLogger.

Minimal app structure

src/
├── index.ts
├── config.ts              # optional (defineConfig)
├── logger.ts              # optional (defineLogger)
├── modules/
│   └── widget/
│       ├── get.query.ts
│       ├── upsert.mutation.ts
│       └── upsert.action.ts
├── infra/
│   ├── repo.ts
│   └── db/index.ts
└── transport/
    ├── http.ts
    └── cli/index.ts

Quick start

1) Define operations

// src/modules/widget/get.query.ts
import { defineQuery } from "@orria/dispatchkit";
import { z } from "zod";

export default defineQuery({
  input: z.object({ id: z.string() }),
  return: z.object({ id: z.string() }).nullable(),
  handler: async (ctx) => {
    return ctx.infra.repo.get(ctx.input.id);
  },
});

Each operation becomes available on runtime.bus:

  • runtime.bus.query.widget.get(input)
  • runtime.bus.query.widget.get.$unsafe(input)
  • runtime.bus.query.widget.get.$input
  • runtime.bus.query.widget.get.$return

Nested module paths also generate grouped keys:

  • modules/widget/get.query.ts -> runtime.bus.query.widget.get(...)
  • Flat alias is also present: runtime.bus.query.widgetGet(...)

2) Optional runtime config

import { defineConfig } from "@orria/dispatchkit";
import { z } from "zod";

export default defineConfig(
  z.object({
    PORT: z.coerce.number().int().min(1).max(65535).default(3000),
  }),
);

Built-in runtime config keys:

  • SERVICE_NAME
  • SERVICE_DESCRIPTION
  • SERVICE_VERSION
  • LOG_LEVEL (fatal|error|warn|info|debug|trace|silent)
  • NODE_ENV (development|production)

Config merge priority, with later sources overriding earlier ones:

  1. Defaults from package.json
  2. process.env
  3. .env file
  4. buildRuntime(options).config
  5. Top-level config keys in buildRuntime(options)

The schema receives this merged input and must produce an object. Its parsed output is merged with the built-in keys, so a schema that strips unknown properties keeps SERVICE_NAME and the other defaults. Built-in fields are validated again after custom transforms. Without defineConfig, runtime config contains only the built-in keys. NODE_ENV=test is normalized to development.

3) Optional logger

import { defineLogger } from "@orria/dispatchkit";
import pino from "pino";

export default defineLogger((config) => {
  const logger = pino({
    name: String(config.SERVICE_NAME),
    level: String(config.LOG_LEVEL),
  });

  return {
    logger,
    console,
  };
});

If src/logger.ts is missing, Dispatchkit uses a fallback console-based logger filtered by LOG_LEVEL.

4) Infra modules

// src/infra/repo.ts
import { defineInfra } from "@orria/dispatchkit";

export default defineInfra(({ config, logger, onDispose }) => {
  logger.info("infra init", { service: config.SERVICE_NAME });
  const records = new Map<string, { id: string }>();
  onDispose(() => records.clear());

  return {
    get: (id: string) => records.get(id) ?? null,
  };
});

defineInfra() receives { config, logger, onDispose }.

Return behavior:

  • src/infra/database.ts -> runtime.infra.database
  • src/infra/database/index.ts -> runtime.infra.database
  • The module return value is assigned to that key, including class instances.

5) Transport modules

import { defineTransport } from "@orria/dispatchkit";

export default defineTransport(
  () => ({
    ping: () => "pong",
  }),
  {
    allowGetTransportCtxFrom: ["http", "transport/http/**/*.ts"],
  },
);

allowGetTransportCtxFrom extends default allowed locations for getTransportCtx(). Shorthand values like "http" are supported.

Transport wrappers support methods on class prototypes. JavaScript requires non-configurable, non-writable own properties to return their exact values. Functions and objects held in those properties do not get additional context wrappers.

6) Build runtime

import { buildRuntime } from "@orria/dispatchkit";

const runtime = await buildRuntime({
  rootDir: process.cwd(),
  srcDir: "./src",
  generatedDir: "./src/generated/runtime",
  envFile: "./.env",
  SERVICE_NAME: "my-service",
});

Runtime shape:

  • runtime.config
  • runtime.logger
  • runtime.infra
  • runtime.bus
  • runtime.transport
  • runtime.dispose()

After a successful build, Dispatchkit mounts globalThis.runtime and the configured console. Set mountGlobal: false to keep runtime instances separate from those globals. Context helpers still work inside their handlers and transport calls.

Relative srcDir, generatedDir, and envFile paths resolve from rootDir. By default, rootDir is the current working directory. Set generateArtifacts: false when generated types are already part of the build or the runtime filesystem is read-only:

const runtime = await buildRuntime({
  rootDir: import.meta.dir,
  srcDir: "src",
  mountGlobal: false,
  generateArtifacts: false,
});

Standard Schema validation

defineConfig and operation input and return accept Standard Schema v1 validators. Zod 4 and Valibot 1 schemas work directly. Validation can be synchronous or asynchronous, including refinements and transforms. Different validators can be used in the same operation:

// src/modules/increment.query.ts
import { defineQuery } from "@orria/dispatchkit";
import { z } from "zod";
import * as v from "valibot";

export default defineQuery({
  input: z.string().regex(/^\d+$/).transform(Number),
  return: v.pipe(v.number(), v.transform(String)),
  handler: ({ input }) => input + 1,
});

The bus accepts the input schema's input type. The handler receives its output type, after parsing. The handler returns the return schema's input type, and the bus resolves to its output type. Without a return schema, the bus infers the handler's result.

If a handler calls its own generated bus method, give the handler an explicit return type so TypeScript can resolve the recursion.

await runtime.bus.query.increment("41"); // "42"
await runtime.bus.query.increment.$unsafe(41); // 42

$unsafe skips both validators and all their transforms. It takes the value the handler normally receives and returns the handler's raw result. CQRS guards still apply. $input and $return expose the original schema objects, or undefined when omitted.

Elysia and TypeBox

Elysia's t schemas need an adapter. Choose the subpath that matches the TypeBox version:

| Schema source | Install alongside Dispatchkit | Adapter import | | --- | --- | --- | | Elysia 1, @sinclair/typebox 0.34 | bun add @sinclair/typebox | @orria/dispatchkit/typebox | | Elysia 2, typebox 1 | bun add typebox | @orria/dispatchkit/typebox-v1 |

The adapters are optional imports; the core does not load TypeBox. Elysia 2 integration is tested with [email protected] and [email protected].

// Elysia 1
import { t } from "elysia";
import { standardSchema } from "@orria/dispatchkit/typebox";

const input = standardSchema(t.Object({ id: t.String() }));

For Elysia 2, use standardSchema(schema, context?) from @orria/dispatchkit/typebox-v1. The optional context maps names to referenced schemas. Elysia 2's t.Numeric() keeps separate input and output types:

// Elysia 2
import { defineQuery } from "@orria/dispatchkit";
import { t } from "elysia";
import { standardSchema } from "@orria/dispatchkit/typebox-v1";

export default defineQuery({
  input: standardSchema(t.Object({ count: t.Numeric() })),
  handler: ({ input }) => input.count + 1,
});
// Bus input: { count: string | number }. Result: number.

The TypeBox 0.34 adapter accepts standardSchema(schema, references?) and preserves validation and Decode transforms. For explicit encoded and decoded types, use TypeBox's own Type.Transform:

import { defineQuery } from "@orria/dispatchkit";
import { Type } from "@sinclair/typebox";
import { standardSchema } from "@orria/dispatchkit/typebox";

export default defineQuery({
  input: standardSchema(
    Type.Transform(Type.String()).Decode((value) => value.length).Encode(String),
  ),
  handler: ({ input }) => input * 2,
});
// Bus input: string. Handler input and bus result: number.

Elysia 1 declares t.Numeric() as TNumber, although it accepts numeric strings at runtime. Its t.Transform also exposes a different static type than TypeBox's version. The adapter preserves these runtime conversions, but it cannot recover input types that Elysia's declarations omit. Use Type.Transform when encoded input types matter. Both adapters use TypeBox's synchronous decoding APIs. They do not run Elysia's HTTP coercion, defaults, or cleaning pipeline.

Elysia 2 schemas that depend on its private asynchronous refinement queue, including file signature checks with t.File({ type: "image/png" }) or t.Files({ type: "image/png" }), fail with DISPATCHKIT_TYPEBOX_ASYNC_REFINE_UNSUPPORTED. Validate file signatures through Elysia's HTTP pipeline or a Standard Schema validator with asynchronous validation. Basic t.File() and size checks are supported, as are t.Numeric(), t.BooleanString(), and t.Date().

Resource lifecycle

Infra and transport factories receive onDispose(callback). Register cleanup immediately after acquiring a resource:

// src/transport/http.ts
import { defineTransport } from "@orria/dispatchkit";

export default defineTransport(({ onDispose }) => {
  const server = Bun.serve({
    port: 3000,
    fetch: () => new Response("ok"),
  });
  onDispose(() => { server.stop(true); });
  return server;
});

Call await runtime.dispose() during application shutdown. Cleanup runs once, in reverse registration order, and awaits each callback. All callbacks run even if one fails; cleanup errors are reported in an AggregateError. New bus calls fail after disposal. Dispatchkit does not install process signal handlers or close unregistered resources.

A failed buildRuntime() runs the registered cleanup callbacks and leaves the previously mounted runtime and console intact. Building another runtime does not dispose the previous one; keep its reference and call dispose() when it is no longer needed.

Context helpers

  • getModuleCtx() returns { config, logger, infra, bus }
  • getTransportCtx() returns { config, logger, bus }

Factory/handler context matrix:

  • defineLogger((config) => ...) -> config
  • defineInfra((ctx) => ...) -> { config, logger, onDispose }
  • defineTransport((ctx) => ...) -> { config, logger, bus, onDispose }
  • defineQuery/defineMutation/defineAction.handler(ctx) -> { config, logger, infra, bus, input }

Invalid context access throws structured errors:

  • DISPATCHKIT_CONTEXT_UNAVAILABLE
  • DISPATCHKIT_CONTEXT_FORBIDDEN

Bun caches application modules; their top-level code runs once per process. A transport module can read getTransportCtx() during its initial import, but a captured value keeps referring to that first runtime when another runtime reuses the module. For multiple runtime instances, read runtime-dependent values inside factories, handlers, or transport methods.

CQRS guards

Runtime enforces call chain restrictions:

  • query -> only query
  • mutation -> query, mutation
  • action -> query, mutation, action

Invalid calls throw DISPATCHKIT_CQRS_GUARD.

Discovery rules

Dispatchkit scans under srcDir:

  • modules/**/*.query.ts
  • modules/**/*.mutation.ts
  • modules/**/*.action.ts
  • infra/*.ts and infra/**/index.ts
  • transport/*.ts and transport/**/index.ts

Notes:

  • *.d.ts files are ignored
  • Operation and transport naming collisions throw errors
  • An infra module exporting defineTransport(...) is treated as transport

Runtime and CLI share the same discovery rules. Static classification follows named import aliases, namespace imports, const bindings, and local imports and re-exports. The generator does not execute application modules. Dynamic wrappers and mutable definitions that cannot be classified fail with DISPATCHKIT_DISCOVERY_UNKNOWN_DEFINITION. Export defineInfra(...) or defineTransport(...) through one of the supported forms.

Generated artifacts

Default output directory: src/generated/runtime

  • manifest.json
  • bus.d.ts
  • runtime.d.ts
  • index.ts

manifest.json is rewritten only when the discovery structure changes.

Run dispatchkit generate before type checking and include the generated declarations in your TypeScript project. The default src/generated/runtime path is covered by an include pattern such as src/**/*.ts.

CLI

# one-time generation
dispatchkit generate

# watch mode
dispatchkit generate --watch

# custom paths
dispatchkit generate --srcDir ./src --generatedDir ./src/generated/runtime

--watch updates generated types and the manifest as files change. It does not reload application modules in a running process.

Options:

  • --rootDir <path>
  • --srcDir <path>
  • --generatedDir <path>
  • --watch
  • --intervalMs <ms>

Build this package locally

bun run build

Documentation