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 🙏

© 2025 – Pkg Stats / Ryan Hefner

@codeforbreakfast/eventsourcing-commands

v0.4.9

Published

Wire command validation and dispatch for event sourcing systems - External boundary layer with schema validation

Readme

@codeforbreakfast/eventsourcing-commands

CQRS command types and schemas for event sourcing. This package provides core command handling abstractions with type-safe command and result definitions, featuring a strongly typed command registry system.

Overview

This package contains the fundamental CQRS (Command Query Responsibility Segregation) types that bridge the gap between user intentions and domain events. Commands represent requests to change system state, while command results indicate the outcome of processing those commands.

The package features a typed command registry that ensures each command name is tied to exactly one payload schema, providing compile-time safety and exhaustive validation using Effect's pattern matching for comprehensive command handling.

Installation

npm install @codeforbreakfast/eventsourcing-commands

Key Concepts

  • Commands: Represent user intent to change aggregate state
  • Command Results: Indicate success/failure of command processing
  • Type Safety: Full TypeScript support with Effect schemas
  • Pattern Matching: Effect's exhaustive pattern matching for command handling
  • Typed Registry: Compile-time validation and exhaustive command schemas
  • Payload Validation: Automatic schema-based validation for all commands

Quick Start

1. Define Your Commands

import { Schema } from 'effect';
import { defineCommand } from '@codeforbreakfast/eventsourcing-commands';

// Define command payload schemas
const CreateUserPayload = Schema.Struct({
  name: Schema.String.pipe(Schema.minLength(1)),
  email: Schema.String.pipe(Schema.pattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)),
});

const UpdateEmailPayload = Schema.Struct({
  newEmail: Schema.String.pipe(Schema.pattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)),
});

// Create command definitions
const createUserCommand = defineCommand('CreateUser', CreateUserPayload);
const updateEmailCommand = defineCommand('UpdateEmail', UpdateEmailPayload);

2. Create Command Matcher

import { Effect, Match, pipe, Schema } from 'effect';
import {
  CommandFromDefinitions,
  makeCommandRegistry,
  CommandResult,
  CommandDefinition,
} from '@codeforbreakfast/eventsourcing-commands';
import { EventStreamPosition } from '@codeforbreakfast/eventsourcing-store';

// Declare commands from previous example
declare const createUserCommand: CommandDefinition<'CreateUser', { name: string; email: string }>;
declare const updateEmailCommand: CommandDefinition<'UpdateEmail', { newEmail: string }>;

// Define your command array
const commands = [createUserCommand, updateEmailCommand] as const;

// Extract the command union type
type Commands = CommandFromDefinitions<typeof commands>;

// Create a matcher using Effect's pattern matching
const commandMatcher = (command: Commands): Effect.Effect<CommandResult, never, never> =>
  pipe(
    Match.value(command),
    Match.when({ name: 'CreateUser' }, (cmd) =>
      Effect.succeed({
        _tag: 'Success' as const,
        position: { streamId: cmd.target, eventNumber: 1 } as EventStreamPosition,
      })
    ),
    Match.when({ name: 'UpdateEmail' }, (cmd) =>
      Effect.succeed({
        _tag: 'Success' as const,
        position: { streamId: cmd.target, eventNumber: 2 } as EventStreamPosition,
      })
    ),
    Match.exhaustive // TypeScript ensures all commands are handled!
  );

3. Build the Typed Registry

import {
  makeCommandRegistry,
  makeCommandRegistryLayer,
  CommandFromDefinitions,
  CommandDefinition,
  CommandResult,
} from '@codeforbreakfast/eventsourcing-commands';
import { Effect } from 'effect';

// Declare from previous examples
declare const commands: readonly [
  CommandDefinition<'CreateUser', { name: string; email: string }>,
  CommandDefinition<'UpdateEmail', { newEmail: string }>,
];
declare const commandMatcher: (
  command: CommandFromDefinitions<typeof commands>
) => Effect.Effect<CommandResult, never, never>;

// Create the registry with commands and matcher
const registry = makeCommandRegistry(commands, commandMatcher);

// Or create as an Effect Layer
const registryLayer = makeCommandRegistryLayer(commands, commandMatcher);

4. Dispatch Commands

import {
  WireCommand,
  CommandRegistry,
  CommandResult,
} from '@codeforbreakfast/eventsourcing-commands';
import { Effect, Context } from 'effect';

// Declare registry from previous example
declare const registry: Context.Tag.Service<typeof CommandRegistry>;

const wireCommand: WireCommand = {
  id: 'cmd-123',
  target: 'user-456',
  name: 'CreateUser',
  payload: {
    name: 'John Doe',
    email: '[email protected]',
  },
};

// The registry automatically validates the command against the appropriate schema
const result: CommandResult = await Effect.runPromise(registry.dispatch(wireCommand));

if (result._tag === 'Success') {
  console.log('Command executed successfully:', result.position);
} else {
  console.error('Command failed:', result.error);
}

API Reference

Core Types

WireCommand

Wire commands are used for transport/serialization (APIs, message queues, etc.):

interface WireCommand {
  readonly id: string;
  readonly target: string; // Usually the aggregate ID
  readonly name: string; // Command name
  readonly payload: unknown; // Unvalidated payload
}

DomainCommand

Domain commands are the validated internal representation:

interface DomainCommand<TPayload> {
  readonly id: string;
  readonly target: string;
  readonly name: string;
  readonly payload: TPayload; // Validated payload
}

CommandResult

All command processing results follow this discriminated union:

import { EventStreamPosition } from '@codeforbreakfast/eventsourcing-store';

type CommandResult =
  | { _tag: 'Success'; position: EventStreamPosition }
  | {
      _tag: 'Failure';
      error: {
        _tag:
          | 'ValidationError'
          | 'HandlerNotFound'
          | 'ExecutionError'
          | 'AggregateNotFound'
          | 'ConcurrencyConflict'
          | 'UnknownError';
        commandId: string;
        [key: string]: unknown;
      };
    };

Command Definition API

defineCommand(name, payloadSchema)

Creates a strongly typed command definition that pairs a command name with its payload schema:

import { Schema } from 'effect';
import { defineCommand } from '@codeforbreakfast/eventsourcing-commands';

const userCommand = defineCommand(
  'CreateUser',
  Schema.Struct({
    name: Schema.String,
    email: Schema.String,
  })
);

buildCommandSchema(commands)

Builds a discriminated union schema from multiple command definitions. This creates an exhaustive schema that validates any registered command:

import { buildCommandSchema, CommandDefinition } from '@codeforbreakfast/eventsourcing-commands';
import { Schema } from 'effect';

// Declare commands from previous examples
declare const createUserCommand: CommandDefinition<'CreateUser', { name: string; email: string }>;
declare const updateEmailCommand: CommandDefinition<'UpdateEmail', { newEmail: string }>;

const commands = [createUserCommand, updateEmailCommand];
const exhaustiveSchema = buildCommandSchema(commands);

Registry API

makeCommandRegistry(commands, matcher)

Creates a command registry using Effect's pattern matching. Features:

  • Exhaustive validation: All commands are validated against discriminated union schema
  • Compile-time safety: TypeScript ensures all command types are handled in matcher
  • Pattern matching: Uses Effect's Match.exhaustive for comprehensive command handling
  • Type inference: Full TypeScript support throughout the dispatch pipeline
import { Match, pipe, Effect } from 'effect';
import {
  makeCommandRegistry,
  CommandFromDefinitions,
  CommandResult,
  CommandDefinition,
} from '@codeforbreakfast/eventsourcing-commands';
import { EventStreamPosition } from '@codeforbreakfast/eventsourcing-store';

// Declare commands from previous examples
declare const createUserCommand: CommandDefinition<'CreateUser', { name: string; email: string }>;
declare const updateEmailCommand: CommandDefinition<'UpdateEmail', { newEmail: string }>;

const commands = [createUserCommand, updateEmailCommand] as const;
type Commands = CommandFromDefinitions<typeof commands>;

const commandMatcher = (command: Commands): Effect.Effect<CommandResult, never, never> =>
  pipe(
    Match.value(command),
    Match.when({ name: 'CreateUser' }, (cmd) =>
      Effect.succeed({
        _tag: 'Success' as const,
        position: { streamId: cmd.target, eventNumber: 1 } as EventStreamPosition,
      })
    ),
    Match.when({ name: 'UpdateEmail' }, (cmd) =>
      Effect.succeed({
        _tag: 'Success' as const,
        position: { streamId: cmd.target, eventNumber: 2 } as EventStreamPosition,
      })
    ),
    Match.exhaustive // Compile-time exhaustiveness checking
  );

const registry = makeCommandRegistry(commands, commandMatcher);

makeCommandRegistryLayer(commands, matcher)

Creates an Effect Layer containing the command registry:

import { Effect, pipe } from 'effect';
import {
  makeCommandRegistryLayer,
  CommandRegistry,
  CommandFromDefinitions,
  CommandDefinition,
  CommandResult,
  WireCommand,
} from '@codeforbreakfast/eventsourcing-commands';

// Declare from previous examples
declare const commands: readonly [
  CommandDefinition<'CreateUser', { name: string; email: string }>,
  CommandDefinition<'UpdateEmail', { newEmail: string }>,
];
declare const commandMatcher: (
  command: CommandFromDefinitions<typeof commands>
) => Effect.Effect<CommandResult, never, never>;
declare const wireCommand: WireCommand;

const layer = makeCommandRegistryLayer(commands, commandMatcher);

// Use in your Effect program
const program = pipe(
  CommandRegistry,
  Effect.flatMap((registry) => registry.dispatch(wireCommand)),
  Effect.provide(layer)
);

Key Benefits

Compile-Time Safety

The matcher-based registry system provides several compile-time guarantees:

  • Exhaustive command handling: Match.exhaustive ensures all command types are handled
  • Schema consistency: Each command name maps to exactly one payload schema
  • Type inference: Full TypeScript support with exact command types in each match arm
  • No runtime lookups: Pattern matching eliminates the need for handler maps

Runtime Validation

  • Exhaustive validation: Commands are validated against a discriminated union of all registered schemas
  • Early failure: Invalid commands fail fast with detailed error messages
  • Pattern matching: Effect's matchers provide functional command dispatch
  • Type-safe handling: Each match arm receives the exact command type

Example Error Handling

import { Effect, Context } from 'effect';
import {
  WireCommand,
  CommandRegistry,
  CommandResult,
} from '@codeforbreakfast/eventsourcing-commands';

// Declare from previous examples
declare const registry: Context.Tag.Service<typeof CommandRegistry>;
declare const wireCommand: WireCommand;

const result: CommandResult = await Effect.runPromise(registry.dispatch(wireCommand));

switch (result._tag) {
  case 'Success':
    console.log('Command processed:', result.position);
    break;

  case 'Failure':
    switch (result.error._tag) {
      case 'ValidationError':
        console.error('Invalid payload:', result.error.validationErrors);
        break;

      case 'HandlerNotFound':
        console.error('Unknown command:', result.error.commandName);
        console.log('Available commands:', result.error.availableHandlers);
        break;

      case 'ExecutionError':
        console.error('Handler failed:', result.error.message);
        break;

      default:
        console.error('Unknown error:', result.error);
    }
}

Architecture

This package sits between the domain layer (aggregates) and infrastructure layers (protocols, transports):

  • Wire Layer → External APIs use WireCommand for transport
  • Validation Layer → Registry validates and transforms to DomainCommand
  • Domain Layer → Handlers process validated DomainCommands
  • Result Layer → Standardized CommandResult responses

Related Packages

  • @codeforbreakfast/eventsourcing-store - Event storage and streaming
  • @codeforbreakfast/eventsourcing-aggregates - Domain aggregate patterns
  • @codeforbreakfast/eventsourcing-protocol - Protocol implementation