@jumentix/message-mediator — guia de uso
Responsabilidade no escopo
- Camada: messaging / adaptador de aplicação
- Responsável por: mediação pub/sub e request em processo ou via broker
- Usado com: use-cases de backend
- Não responsável por: rotas OpenAPI ou persistência de banco
O que é
@jumentix/message-mediator desacopla publishers de eventos de handlers
e suporta request/response sobre contratos nomeados. O adaptador in-memory
roda no browser e nos testes unitários; adaptadores RabbitMQ e BullMQ visam
servidores Node selecionados por variáveis de ambiente.
Por que existe
Times júnior precisam de um padrão para “algo aconteceu” (eventos) e “faça isso” (comandos/consultas) sem ligar cada módulo diretamente. O mediator permite prototipar in-memory numa SPA e depois deployar as mesmas assinaturas de handler atrás de um broker — sem reescrever lógica de negócio.
Pré-requisitos
- Runtime: browser (in-memory) ou Node/Bun (in-memory, RabbitMQ ou BullMQ).
- Leitura prévia: Começando e async/await básico em JavaScript.
- Para brokers: URL RabbitMQ ou env de conexão Redis (veja
compileMessageMediatorno pacote). - Opcional: shared-contracts para convenções de nomes de contrato em serviços maiores.
Glossário
| Termo | Significado |
|---|---|
| Evento de integração | Notificação fire-and-forget: { name, payload, occurredAt, metadata? }. |
| Message | Payload de request: { contract, version?, payload, metadata? }. |
| Handler | Função registrada para um contract — retorna IMessageResponse. |
| Mediator | Objeto com publish/subscribe e registerHandler/request. |
| Contrato | Id string estável de comando ou query (ex.: users.create). |
| Adaptador in-memory | InMemoryMessageMediatorAdapter — sem durabilidade, mesmo processo. |
| Adaptador broker | RabbitMQ ou BullMQ — multi-processo, sobrevive a restarts (só Node). |
Passos
1. Instalar (< 5 minutos)
bun add @jumentix/message-mediator2. Primeiro sucesso — publicar e assinar (< 15 minutos)
import { InMemoryMessageMediatorAdapter } from '@jumentix/message-mediator';
const mediator = InMemoryMessageMediatorAdapter.compile();
const seen: string[] = [];
mediator.subscribe('demo.ping', async (event) => {
seen.push(event.name);
});
await mediator.publish({
name: 'demo.ping',
payload: { hello: true },
occurredAt: new Date().toISOString()
});
console.log(seen); // ['demo.ping']Verifique o sucesso: seen.length === 1 após publish resolver.
3. Fluxo central — request/response
Registre handler e chame request:
mediator.registerHandler('users.create', async (message) => ({
result: { id: 'user-1', username: message.payload.username }
}));
const response = await mediator.request({
contract: 'users.create',
version: '1.0.0',
payload: { username: 'ana' }
});
if (response.error) throw response.error;
console.log(response.result);Handlers podem ser sync ou async. Erros em response.error não lançam exceção
a menos que seu wrapper escolha lançar.
4. Fluxo central — compilar por ambiente
import { compileMessageMediator } from '@jumentix/message-mediator';
// JUMENTIX_MESSAGE_MEDIATOR_ADAPTER=inmemory | rabbitmq | bullmq
const mediator = compileMessageMediator();| Env do adaptador | Quando usar |
|---|---|
inmemory (padrão) | Dev local, demos no browser, testes unitários |
rabbitmq / rabbit | Mensageria async multi-serviço |
bullmq / bull | Filas de jobs com Redis |
Adaptadores broker exigem env vars documentadas; URL ausente lança na compilação com mensagem clara.
5. Fluxo central — timeouts e opções de roteamento
const response = await mediator.request(
{ contract: 'billing.charge', payload: { amount: 10 } },
{ timeoutMs: 5000, routeKey: 'billing-primary' }
);Registre handlers com routeKey ou queueName correspondentes em
IMessageHandlerRegistrationOptions quando precisar de mais de um consumidor por
contrato.
6. Superfície completa — mapa da API
| Export | Papel |
|---|---|
IMessageMediator | Tipo da porta completa |
IEventBus | Só publish + subscribe |
InMemoryMessageMediatorAdapter.compile() | Mediator in-process |
RabbitMqMessageMediatorAdapter | RabbitMQ (Node) |
BullMqMessageMediatorAdapter | BullMQ (Node) |
compileMessageMediator() | Factory orientada a env |
Tipos: IMessage, IMessageResponse, IIntegrationEvent, MessageHandler | Tipagem de handlers e payloads |
Experimente no playground de docs
Mediator em memória
Troque mensagens entre os domínios Category e Task e componha um read model via request/response do mediator.
### Mediator em memória
```ts
const domainMessages = [];
const events = [];
const mediator = api.createInMemory();
const categories = new Map([
['work', { id: 'work', name: 'Work', color: '#2563eb' }],
['home', { id: 'home', name: 'Home', color: '#16a34a' }]
]);
const tasks = [];
await mediator.subscribe('tasks.created', async (event) => {
events.push({
title: event.payload.title,
categoryId: event.payload.categoryId
});
});
mediator.registerHandler('categories.get.v1', async (message) => {
domainMessages.push({
from: message.metadata.sourceDomain,
to: 'Categories',
contract: 'categories.get.v1',
categoryId: message.payload.id
});
return {
ok: true,
result: categories.get(message.payload.id) ?? null
};
});
mediator.registerHandler('tasks.create.v1', async (message) => {
const task = {
id: message.payload.id,
title: message.payload.title,
categoryId: message.payload.categoryId,
completed: false
};
tasks.push(task);
await mediator.publish({ name: 'tasks.created', payload: task });
return { ok: true, result: task };
});
mediator.registerHandler('tasks.board.v1', async () => {
const cards = await Promise.all(tasks.map(async (task) => {
const category = await mediator.request({
contract: 'categories.get.v1',
payload: { id: task.categoryId },
metadata: {
sourceDomain: 'Tasks',
reason: 'compose task board read model'
}
});
return {
id: task.id,
title: task.title,
categoryName: category.result?.name ?? 'Uncategorized',
categoryColor: category.result?.color ?? '#64748b'
};
}));
return {
ok: true,
result: {
readModel: 'TaskBoard',
composedFrom: ['Tasks', 'Categories'],
cards
}
};
});
const response = await mediator.request({
contract: 'tasks.create.v1',
payload: {
id: 'task-1',
title: 'Wire mediator events',
categoryId: 'work'
},
metadata: { source: 'browser-playground' }
});
const board = await mediator.request({
contract: 'tasks.board.v1',
payload: {},
metadata: { source: 'task-board-page' }
});
return {
createdTask: response.result,
board: board.result,
domainMessages,
events
};
```const domainMessages = [];
const events = [];
const mediator = api.createInMemory();
const categories = new Map([
['work', { id: 'work', name: 'Work', color: '#2563eb' }],
['home', { id: 'home', name: 'Home', color: '#16a34a' }]
]);
const tasks = [];
await mediator.subscribe('tasks.created', async (event) => {
events.push({
title: event.payload.title,
categoryId: event.payload.categoryId
});
});
mediator.registerHandler('categories.get.v1', async (message) => {
domainMessages.push({
from: message.metadata.sourceDomain,
to: 'Categories',
contract: 'categories.get.v1',
categoryId: message.payload.id
});
return {
ok: true,
result: categories.get(message.payload.id) ?? null
};
});
mediator.registerHandler('tasks.create.v1', async (message) => {
const task = {
id: message.payload.id,
title: message.payload.title,
categoryId: message.payload.categoryId,
completed: false
};
tasks.push(task);
await mediator.publish({ name: 'tasks.created', payload: task });
return { ok: true, result: task };
});
mediator.registerHandler('tasks.board.v1', async () => {
const cards = await Promise.all(tasks.map(async (task) => {
const category = await mediator.request({
contract: 'categories.get.v1',
payload: { id: task.categoryId },
metadata: {
sourceDomain: 'Tasks',
reason: 'compose task board read model'
}
});
return {
id: task.id,
title: task.title,
categoryName: category.result?.name ?? 'Uncategorized',
categoryColor: category.result?.color ?? '#64748b'
};
}));
return {
ok: true,
result: {
readModel: 'TaskBoard',
composedFrom: ['Tasks', 'Categories'],
cards
}
};
});
const response = await mediator.request({
contract: 'tasks.create.v1',
payload: {
id: 'task-1',
title: 'Wire mediator events',
categoryId: 'work'
},
metadata: { source: 'browser-playground' }
});
const board = await mediator.request({
contract: 'tasks.board.v1',
payload: {},
metadata: { source: 'task-board-page' }
});
return {
createdTask: response.result,
board: board.result,
domainMessages,
events
};O stub do playground usa subscribe/publish estilo tópico:
const seen = [];
const mediator = api.createInMemory();
await mediator.subscribe('demo.ping', async (msg) => { seen.push(msg); });
await mediator.publish('demo.ping', { hello: true });Em código de produção, prefira InMemoryMessageMediatorAdapter.compile() com
objetos de evento completos e registerHandler / request para comandos.
Quando não usar in-memory
- Workers multi-processo — cada processo tem memória própria; eventos não cruzam fronteiras de processo.
- Filas duráveis entre deploys — use adaptadores RabbitMQ ou BullMQ em Node.
Mantenha adaptadores broker fora de bundles de browser.
Erros comuns
| Sintoma | Causa | Correção | Verificar sucesso |
|---|---|---|---|
| Handler nunca roda | String errada de name ou contract | Igualdade exata; logue registros | seen ou response.result preenchido |
No handler registered for contract … | Falta registerHandler | Registre antes de request | response.error ausente |
| Request trava até timeout | Handler nunca resolve | Retorne ou rejeite no handler; use timeoutMs | Resposta dentro do timeout |
| Eventos perdidos após refresh | Adaptador in-memory | Troque para broker em Node | Evento sobrevive a restart |
JUMENTIX_RABBITMQ_URL is required | Adaptador Rabbit sem URL | Configure env ou use inmemory local | compileMessageMediator() funciona |
Checklist júnior (“Eu consigo …”)
- Publicar evento de integração e tratá-lo com
subscribe. - Registrar handler e completar round-trip
request/response. - Explicar por que in-memory serve em testes unitários mas não em produção multi-worker.
- Selecionar adaptador correto via
JUMENTIX_MESSAGE_MEDIATOR_ADAPTER. - Passar
timeoutMse tratarresponse.errorsem derrubar quem chamou. - Descrever diferença entre evento (
publish) e comando (request).
Próximo passo
Conecte clientes HTTP e realtime nos guias REST API e Realtime API, depois volte a Começando para a jornada completa de comunicação.