Skip to content

Repository files navigation

@orria/dispatchkit logo

Dispatchkit

@orria/dispatchkit — Lightweight CQRS-lite runtime toolkit for Bun.

Features

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

Installation

bun add @orria/dispatchkit zod

dotenv is included as a dependency. pino (or any logger) is optional via 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/
│   ├── storage.ts
│   └── db/index.ts
└── transport/
    ├── http.ts
    └── cli/index.ts

Quick Start

1) Define operations

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.userGet(input)
  • runtime.bus.query.userGet.$unsafe(input)
  • runtime.bus.query.userGet.$input
  • runtime.bus.query.userGet.$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({
    FEATURE_FLAG: z.boolean().default(false),
  }),
);

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 (later overrides earlier):

  1. Defaults from package.json
  2. .env file
  3. buildRuntime(options) overrides (options.config and top-level keys)

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

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

export default defineInfra(async ({ config, logger }) => {
  logger.info("infra init", { service: config.SERVICE_NAME });

  return {
    repo: {
      get: (id: string) => ({ id }),
    },
  };
});

defineInfra() receives only { config, logger }.

Return behavior:

  • Each infra module is exposed by its domain key:
  • src/infra/database.ts -> runtime.infra.database
  • src/infra/database/index.ts -> runtime.infra.database
  • Module return value is assigned as-is to that key (plain object or class instance).

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.

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

globalThis.runtime is also mounted after successful build.

Context Helpers

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

Factory/handler context matrix:

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

Invalid context access throws structured errors:

  • DISPATCHKIT_CONTEXT_UNAVAILABLE
  • DISPATCHKIT_CONTEXT_FORBIDDEN

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

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.

CLI

# one-time generation
dispatchkit generate

# watch mode
dispatchkit generate --watch

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

Options:

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

Build This Package Locally

bun run build

Documentation

About

Lightweight CQRS-lite application framework and runtime toolkit for Bun

Topics

Resources

Stars

76 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages