@orria/dispatchkit — Lightweight CQRS-lite runtime toolkit for Bun.
- CQRS operation definitions:
defineQuery,defineMutation,defineAction - Runtime modules:
defineConfig,defineLogger,defineInfra,defineTransport - Runtime builder:
buildRuntime() - Strict CQRS call guards at runtime
- Optional
zodvalidation for operationinputandreturn - Context helpers:
getModuleCtx(),getTransportCtx() - Runtime artifact generation into
src/generated/runtime - CLI:
dispatchkit generateanddispatchkit generate --watch
bun add @orria/dispatchkit zoddotenv is included as a dependency. pino (or any logger) is optional via defineLogger.
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
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.$inputruntime.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(...)
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_NAMESERVICE_DESCRIPTIONSERVICE_VERSIONLOG_LEVEL(fatal|error|warn|info|debug|trace|silent)NODE_ENV(development|production)
Config merge priority (later overrides earlier):
- Defaults from
package.json .envfilebuildRuntime(options)overrides (options.configand top-level keys)
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.
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.databasesrc/infra/database/index.ts->runtime.infra.database- Module return value is assigned as-is to that key (plain object or class instance).
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.
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.configruntime.loggerruntime.infraruntime.busruntime.transport
globalThis.runtime is also mounted after successful build.
getModuleCtx()returns{ config, logger, infra, bus }getTransportCtx()returns{ config, logger, bus }
Factory/handler context matrix:
defineLogger((config) => ...)->configdefineInfra((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_UNAVAILABLEDISPATCHKIT_CONTEXT_FORBIDDEN
Runtime enforces call chain restrictions:
query-> onlyquerymutation->query,mutationaction->query,mutation,action
Invalid calls throw DISPATCHKIT_CQRS_GUARD.
Dispatchkit scans under srcDir:
modules/**/*.query.tsmodules/**/*.mutation.tsmodules/**/*.action.tsinfra/*.tsandinfra/**/index.tstransport/*.tsandtransport/**/index.ts
Notes:
*.d.tsfiles are ignored- Operation and transport naming collisions throw errors
- An infra module exporting
defineTransport(...)is treated as transport
Default output directory: src/generated/runtime
manifest.jsonbus.d.tsruntime.d.tsindex.ts
manifest.json is rewritten only when the discovery structure changes.
# one-time generation
dispatchkit generate
# watch mode
dispatchkit generate --watch
# custom paths
dispatchkit generate --srcDir ./src --generatedDir ./src/generated/runtimeOptions:
--rootDir <path>--srcDir <path>--generatedDir <path>--watch--intervalMs <ms>
bun run build- Russian version:
docs/README.ru.md