Skip to content

Monadic Node

What It Is

Monadic Node is a small base-class pattern for application nodes that must return declared output ports on every code path. The example registers SearchCatalogueNode on a two-placement DAG and runs it through both success and error routes.

Use this when node authors want structure around execute: declared outputs, a protected run hook, and a consistent error route instead of uncaught throws escaping the node boundary.

How It Works

MonadicNode requires concrete subclasses to declare name and outputs. A logging intermediate class can wrap the protected run method, while the concrete node owns domain behavior. The dispatcher sees a normal NodeInterface.

Diagrams, Examples, and Outputs

The example is intentionally tiny: one SingleNode placement followed by one completed terminal. The CLI output demonstrates both routes.

Run

bash
npx tsx examples/monadic-node.ts

What It Lets You Do

The monadic node pattern lets applications build node classes where every code path returns a declared output port. Use it when node authors need a small base class that catches implementation errors, routes failures explicitly, and keeps node behavior testable outside the dispatcher.

MonadicNode is the abstract base for canonical DAG node patterns. Concrete subclasses declare name, outputs, and implement execute (or, as shown here, a protected run method called by a logging intermediate class). Every code path must return a declared output port — nothing throws past the node boundary.

SearchCatalogueNode is registered on a two-placement DAG (search → end) and executed twice:

  • With a real query — routes 'success'.
  • With an empty query — routes 'error'.

Code Samples

ts
/**
 * monadic-node: runs a concrete MonadicNode subclass end-to-end in a DAG.
 *
 * MonadicNode is the abstract base for every canonical DAG pattern. Concrete
 * subclasses declare `name`, `outputs`, and implement `execute` (or, as here,
 * a protected `run` method called by a logging intermediate class). Every code
 * path must return a declared output port — nothing throws past the node
 * boundary.
 *
 * The SearchCatalogueNode from dags/monadic-node.ts is registered on a tiny
 * two-placement DAG (search → end) and executed twice: once with a real query
 * (routes 'success') and once with an empty query (routes 'error').
 *
 * DAG definition (state, abstract base, concrete node): examples/dags/monadic-node.ts
 *
 * Run: npx tsx examples/monadic-node.ts
 */

import { DAG_CONTEXT, Dagonizer } from '@studnicky/dagonizer';
import type { DAGType } from '@studnicky/dagonizer';
import { CatalogueState, SearchCatalogueNode } from './dags/monadic-node.js';

// ── Build a minimal DAG: search-catalogue → end ─────────────────────────────

const dag: DAGType = {
  '@context':  DAG_CONTEXT,
  '@id': 'urn:noocodec:dag:catalogue-search',
  '@type':     'DAG',
  name:        'catalogue-search',
  version:     '1',
  entrypoints: { main: 'urn:noocodec:dag:catalogue-search/node/search' },
  nodes: [
    {
      '@id': 'urn:noocodec:dag:catalogue-search/node/search',
      '@type': 'SingleNode',
      name:    'search',
      node:    'urn:noocodec:node:search-catalogue',
      outputs: {
        success: 'urn:noocodec:dag:catalogue-search/node/end',
        empty:   'urn:noocodec:dag:catalogue-search/node/end',
        error:   'urn:noocodec:dag:catalogue-search/node/end',
      },
    },
    {
      '@id': 'urn:noocodec:dag:catalogue-search/node/end',
      '@type':   'TerminalNode',
      name:      'end',
      outcome:   'completed',
    },
  ],
};

// ── Dispatcher ───────────────────────────────────────────────────────────────

const dispatcher = new Dagonizer<CatalogueState>();
dispatcher.registerNode(new SearchCatalogueNode());
dispatcher.registerDAG(dag);

// ── Run 1: valid query → 'success' output, results populated ─────────────────

process.stdout.write('\n=== MonadicNode: concrete subclass in a live DAG ===\n\n');

const validState = new CatalogueState();
validState.query = 'Mechanicus Codex';
await dispatcher.execute('urn:noocodec:dag:catalogue-search', validState);

process.stdout.write(`results: ${JSON.stringify(validState.results)}\n`);

// ── Run 2: empty query → 'error' output, results empty ───────────────────────

const emptyState = new CatalogueState();
emptyState.query = '';
await dispatcher.execute('urn:noocodec:dag:catalogue-search', emptyState);

process.stdout.write(`results (empty query): ${JSON.stringify(emptyState.results)}\n`);

process.stdout.write('\nLesson: MonadicNode subclasses declare typed outputs;\n');
process.stdout.write('        every path returns a named port — nothing throws past the node.\n');

Details for Nerds

  • MonadicNode abstract base. Subclass it to get structural enforcement: name and outputs are required abstract members; execute is the hook point. The base class catches any unhandled throws from run and routes them to 'error' automatically.
  • Protected run method. A logging intermediate class can override execute to wrap run with metrics or tracing. The node implementation goes in run; the infrastructure goes in execute.
  • Every path returns a declared output. Returning a non-declared output port is a type error. The node must handle all cases (including empty input, network failure) by routing to a declared port rather than throwing.
  • Two-placement DAG. A SingleNode placement followed by a TerminalNode with outcome: 'completed'. The minimal shape that verifies the node's routing behavior end-to-end.

Watched over by the Order of Dagon.