Services container
Nodes often need shared dependencies — loggers, database clients, registries, retry pools. The dispatcher accepts a typed services bag at construction; the same reference flows through every node's context.services.
Defining the bag
The services bag is a plain interface defined by the consumer. There is no DI container, no provider scope, no factory step.
interface AppServices {
readonly logger: Logger;
readonly db: Database;
readonly cache: Cache;
}How services flow
Constructing the dispatcher
Dagonizer<TState, TServices> carries the services type as a generic parameter:
import { Dagonizer, NodeStateBase } from '@noocodex/dagonizer';
class S extends NodeStateBase {
out: unknown = null;
}
const dispatcher = new Dagonizer<S, AppServices>({
services: {
logger,
db,
cache,
},
});TServices defaults to undefined — dispatchers that don't need services work unchanged with new Dagonizer<S>().
Receiving services in a node
NodeInterface<TState, TOutput, TServices> propagates the same parameter to context.services:
import type { NodeInterface } from '@noocodex/dagonizer';
const fetchNode: NodeInterface<S, 'success' | 'error', AppServices> = {
name: 'fetch',
outputs: ['success', 'error'],
async execute(state, context) {
context.services.logger.info('fetch start');
const cached = await context.services.cache.get(state.key);
if (cached) {
state.out = cached;
return { output: 'success' };
}
try {
const rows = await context.services.db.query('SELECT 1');
state.out = rows;
return { output: 'success' };
} catch (error) {
context.services.logger.error({ err: error }, 'fetch failed');
return { output: 'error' };
}
},
};The generic parameter ensures context.services is fully typed inside the node body.
Mixing services-aware and services-free nodes
Nodes without a services parameter (NodeInterface<S, 'success'>, default TServices = undefined) cannot register on a dispatcher with non-undefined services because the registration signature requires the same TServices. Either:
- Always declare the bag (most consistent).
- Or split into two dispatchers — one with services, one without — when nodes truly cannot share a bag.
In practice the bag is wide enough to cover everything a flow needs, and every node accepts the same parameter.
Lifetime
Services live on the dispatcher instance. There is no per-execution scope; the same bag is handed to every node in every execution, including sub-DAG nested calls and fan-out items.
If a service needs per-execution state (e.g. a request ID), put the per-execution data in state instead. The bag is for things that outlive any one execution.