import { CAPABILITIES } from '@setu-ts/common';import type { ICommandBus, IQueryBus, IServiceRegistry } from '@setu-ts/common';import type { CqrsPluginOptions } from '@setu-ts/cqrs-plugin';import { ADD_NOTE, addNoteHandler } from './notes.commands.ts';import type { AddNote } from './notes.commands.ts';import { READ_NOTES, readNotesHandler } from './notes.queries.ts';import type { ReadNotes } from './notes.queries.ts';import { createNoteStore } from './notes.store.ts';/** The handlers the notes feature contributes to `CqrsPlugin`. */export function notesHandlers(): Required< Pick<CqrsPluginOptions, 'commandHandlers' | 'queryHandlers'>> { const store = createNoteStore(); return { commandHandlers: [addNoteHandler(store)], queryHandlers: [readNotesHandler(store)] };}/** Sends an `add-note` command through the command bus. */export async function addNote(services: IServiceRegistry, text: string): Promise<void> { const command: AddNote = { type: ADD_NOTE, data: { text } }; await services.get<ICommandBus>(CAPABILITIES.COMMAND_BUS).execute<void>(command);}/** Asks the query bus, which is independent of the command bus, for every note. */export async function readNotes(services: IServiceRegistry): Promise<readonly string[]> { const query: ReadNotes = { type: READ_NOTES, data: {} }; return await services.get<IQueryBus>(CAPABILITIES.QUERY_BUS).execute<readonly string[]>(query);}
src/modules/notes/notes.commands.ts
import { CAPABILITIES } from '@setu-ts/common';import type { CqrsCommand, ICommandHandler, IRuntimeServices } from '@setu-ts/common';import type { CommandHandlerRegistration } from '@setu-ts/cqrs-plugin';import type { NoteStore } from './notes.store.ts';export const ADD_NOTE = 'add-note';/** Records a note, stamped with the time it was added. */export interface AddNote extends CqrsCommand<{ readonly text: string }> { readonly type: typeof ADD_NOTE;}/** * Registers the `add-note` handler as a factory. * * A factory runs once every plugin has registered, so it can resolve a * capability — here the runtime clock — that the command itself cannot carry. */export function addNoteHandler(store: NoteStore): CommandHandlerRegistration { return { type: ADD_NOTE, handler: (services): ICommandHandler<AddNote, void> => { const runtime = services.get<IRuntimeServices>(CAPABILITIES.RUNTIME); return { handle: (command) => store.add(`${command.data.text} @ ${runtime.now()}`), }; }, };}
src/modules/notes/notes.queries.ts
import type { CqrsQuery } from '@setu-ts/common';import type { QueryHandlerRegistration } from '@setu-ts/cqrs-plugin';import type { NoteStore } from './notes.store.ts';export const READ_NOTES = 'read-notes';/** Reads every recorded note. */export interface ReadNotes extends CqrsQuery<Record<string, never>> { readonly type: typeof READ_NOTES;}/** Registers the `read-notes` handler as a plain instance, which needs no capability. */export function readNotesHandler(store: NoteStore): QueryHandlerRegistration { return { type: READ_NOTES, handler: { handle: (): readonly string[] => store.all() } };}
src/modules/notes/notes.store.ts
/** The notes the command side writes and the query side reads. */export interface NoteStore { add(note: string): void; all(): readonly string[];}/** Creates an in-memory note store owned by one application. */export function createNoteStore(): NoteStore { const notes: string[] = []; return { add: (note) => { notes.push(note); }, all: () => [...notes], };}
smoke.ts
import assert from 'node:assert/strict';import { after, before, describe, it } from 'node:test';import type { IKernelApplication } from '@setu-ts/kernel';import { addNote, readNotes } from './src/modules/notes/index.ts';import { createApp } from './setu.config.ts';describe('cqrs example', () => { let app: IKernelApplication; before(async () => { app = createApp(); await app.start(); }); after(async () => { await app.stop(); }); it('reads through the query bus what the command bus wrote', async () => { await addNote(app.services, 'separate buses'); const notes = await readNotes(app.services); assert.equal(notes.length, 1); // The factory-built handler stamps each note with the runtime clock it resolved. assert.match(notes[0] ?? '', /^separate buses @ \d+$/); });});