Jumentix
Produto

Uma fábrica de software que suas equipes podem evoluir

O Jumentix empacota arquitetura, automação, adaptadores de runtime, documentação e governança em uma plataforma aberta.

Mascote open source do Jumentix

Capacidades da plataforma

Todo o ciclo de entrega, conectado

Gerenciamento de serviços

Modele domínios, entidades, relacionamentos, interfaces, ambientes e perfis de deploy antes da geração de código.

Runtimes orientados a contratos

Contratos OpenAPI 3.1 e AsyncAPI alinham validação de requests, handlers, clientes gerados, docs e checagens de rota.

Ecossistema de pacotes reutilizáveis

SDKs, contratos de persistência, mediação de mensagens, bootstrap de runtime, Cana e adaptadores vivem como pacotes compartilháveis entre serviços.

Persistência portável

Ports de repository e store mantêm casos de uso estáveis enquanto adaptadores PostgreSQL, MySQL, MongoDB, DynamoDB, SQLite, Firebase ou in-memory mudam.

Comunicação entre serviços

O Message Mediator suporta fluxos request/response e eventos para módulos ficarem em processo hoje e migrarem para workers ou brokers depois.

Entrega governada

CI por branch, scan de segurança, checagens de limites arquiteturais, sincronização de docs, checagens de rotas e evidências protegem cada release.

12adaptadores HTTP/functions
12alvos de banco de dados
3estilos de API contratados
99/90padrão de statements/branches

Tooling movido a Bun

O caminho rápido é o caminho padrão

O Jumentix padroniza Bun como runtime, gerenciador de pacotes, executor de scripts, test runner e bundler das specs de browser do monorepo. Isso mantém trabalho local, gates de CI, checagens de pacote e publicação do site em uma ferramenta só.

Por que Bun importa aqui

O repositório usa Bun onde ele realmente reduz atrito: installs rápidos com workspaces, execução direta de TypeScript, scripts repetíveis, gates focados por branch e bundling das specs de browser antes de o Cypress rodar contra IndexedDB e DOM reais.

  • Uma versão pinada, `bun@1.3.13`, protege todos os workspaces contra drift de ambiente.
  • `bun run --filter` mantém checagens de pacote focadas enquanto gates completos seguem disponíveis para release.
  • Bun empacota specs browser do Cana antes do Cypress, evitando fragilidade do webpack do Cypress sem perder evidência em browser real.
  • A mesma CLI move dev local, sync de docs, dry-runs de pacote, checagens de segurança e publicação do site em produção.
bun install
bun run website:dev
bun run ci:affected
1ferramenta de runtime/pacote/teste/bundle
1.3.13+versão Bun pinada
3raízes: apps, packages, tooling
30xteto oficial de velocidade de install vs npm

Playgrounds in-memory no browser

Os contratos dos pacotes rodam sem servidores

Estes exemplos executáveis usam registros Category e Task em adaptadores in-memory compatíveis com contrato e armazenamento local nativo do browser. Tudo roda no browser, para o time inspecionar o comportamento dos pacotes antes de adicionar Redis, RabbitMQ, bancos, processos PM2 ou runtime backend.

100%execução no browser
0servidores exigidos no lab
35code playgrounds disponíveis
2apps representados: Service Management e Backend Template
Conceito primeiro

1. Leia o limite

Comece pelo caminho do request e pela matriz de camadas para cada playground ter lugar na arquitetura.

Caminho MVP

2. Rode a menor fatia

Use o browser lab para ver Category e Task passarem por controller, caso de uso, adapter e estado do cliente.

Contratos

3. Componha domínios

Vá para Message Mediator, SDKs e ports de storage quando o exemplo precisar de dados entre domínios ou contrato consumidor.

Heavy data

4. Estresse o sistema

Finalize com o playground bulk mutex + DLQ para observar rejeição real de lock, replay e commits IndexedDB no canvas.

Service Management UI

O lab completo parte de um modelo visual de serviço, valida o formato do domínio e alimenta contratos de runtime com o mesmo vocabulário Category e Task.

Superfície de app

Backend Template

O caminho de request espelha limites de controller, caso de uso, repository, mediator e client sem expor usuários a setup de infraestrutura.

Runtime de app

Persistência e cache

Stores in-memory e estado chave/valor mantêm filtros de Category, registros Task e preferências de UI no mesmo formato de resposta dos adaptadores externos.

@jumentix/key-value-storage

Mensageria e locks

Message Mediator, Mutex Service e Dead Letter Queue mostram request/response, eventos, proteção de escrita e rejeição recuperável antes de infraestrutura durável entrar.

@jumentix/message-mediator

Recuperação de escrita em massa

O playground de DLQ envia requests Task rejeitados em massa de volta pelo fluxo do controller, então o replay ainda readquire o mutex da Category antes de escrever.

@jumentix/dead-letter-queue

Diretório de playgrounds

Navegue para qualquer exemplo executável

Todos os code playgrounds registrados aparecem aqui com link direto para o widget executável abaixo. Use como mapa entre Cana, mensageria, persistência, mutex, SDK e exemplos de design.

Lab MVP no browser

Fatias completas de produto com Category e Task, do primeiro MVP à recuperação heavy-data.

Caminho MVP

App Jumentix completo no browser

Uma fatia completa de produto 100% browser usando Category e Task.

Jumentix browser lab · Do zero ao primeiro MVP
Caminho de stress

Criação em massa com mutex + DLQ

Escritas concorrentes, locks, replay DLQ e persistência browser em um cenário heavy-data.

Jumentix browser lab · Mutex + Dead Letter Queue + Cana
Caminho MVP

MVP REST Dia 1 — use-case no adaptador in-memory

Uma fatia completa de produto 100% browser usando Category e Task.

Jumentix browser lab · Do zero ao primeiro MVP
Caminho MVP

MVP REST Dia 2 — client tipado sobre o mesmo adaptador

Uma fatia completa de produto 100% browser usando Category e Task.

Jumentix browser lab · Do zero ao primeiro MVP
Caminho MVP

MVP realtime Dia 1 — comando live com ack e broadcast

Uma fatia completa de produto 100% browser usando Category e Task.

Jumentix browser lab · Do zero ao primeiro MVP
Caminho MVP

MVP realtime Dia 2 — drill de paridade do fallback REST

Uma fatia completa de produto 100% browser usando Category e Task.

Jumentix browser lab · Do zero ao primeiro MVP
Caminho MVP

MVP SaaS Dia 1 — policy de tenant no adaptador in-memory

Uma fatia completa de produto 100% browser usando Category e Task.

Jumentix browser lab · Do zero ao primeiro MVP
Caminho MVP

MVP microsserviços Dia 2 — worker persistindo notificações reais

Uma fatia completa de produto 100% browser usando Category e Task.

Jumentix browser lab · Do zero ao primeiro MVP
Caminho MVP

MVP microsserviços Release — falha explícita com replay via DLQ

Uma fatia completa de produto 100% browser usando Category e Task.

Jumentix browser lab · Do zero ao primeiro MVP

Dados frontend com Cana

Dados relacionais no browser, workers, listeners de eventos, IndexedDB e integração com estado de UI.

Dados frontend

Primeiros passos

Estado relacional local, workers, eventos e exemplos de UI com IndexedDB.

Cana · Arquitetura Cana
Dados frontend

Upgrade de schema

Estado relacional local, workers, eventos e exemplos de UI com IndexedDB.

Cana · Arquitetura Cana
Dados frontend

Chaves

Estado relacional local, workers, eventos e exemplos de UI com IndexedDB.

Cana · Arquitetura Cana
Dados frontend

CRUD

Estado relacional local, workers, eventos e exemplos de UI com IndexedDB.

Cana · Arquitetura Cana
Dados frontend

Operacoes em lote

Estado relacional local, workers, eventos e exemplos de UI com IndexedDB.

Cana · Arquitetura Cana
Dados frontend

Query + explain

Estado relacional local, workers, eventos e exemplos de UI com IndexedDB.

Cana · Arquitetura Cana
Dados frontend

Transacoes

Estado relacional local, workers, eventos e exemplos de UI com IndexedDB.

Cana · Arquitetura Cana
Dados frontend

Eventos de mudanca

Estado relacional local, workers, eventos e exemplos de UI com IndexedDB.

Cana · Arquitetura Cana
Dados frontend

Hooks

Estado relacional local, workers, eventos e exemplos de UI com IndexedDB.

Cana · Arquitetura Cana
Dados frontend

Erros

Estado relacional local, workers, eventos e exemplos de UI com IndexedDB.

Cana · Arquitetura Cana
Dados frontend

Avaliacao de storage

Estado relacional local, workers, eventos e exemplos de UI com IndexedDB.

Cana · Arquitetura Cana
Dados frontend

Operation ledger

Estado relacional local, workers, eventos e exemplos de UI com IndexedDB.

Cana · Arquitetura Cana
Dados frontend

Export

Estado relacional local, workers, eventos e exemplos de UI com IndexedDB.

Cana · Arquitetura Cana
Dados frontend

Selecao de backend

Estado relacional local, workers, eventos e exemplos de UI com IndexedDB.

Cana · Arquitetura Cana
Dados frontend

Adapter de factory

Estado relacional local, workers, eventos e exemplos de UI com IndexedDB.

Cana · Arquitetura Cana
Dados frontend

Fluxo com worker client

Estado relacional local, workers, eventos e exemplos de UI com IndexedDB.

Cana · Arquitetura Cana
Dados frontend

MVP SPA Dia 1 — registros offline no Cana

Estado relacional local, workers, eventos e exemplos de UI com IndexedDB.

Cana · Arquitetura Cana
Dados frontend

MVP SPA Dia 2 — estado da UI via eventos do Cana e queries indexadas

Estado relacional local, workers, eventos e exemplos de UI com IndexedDB.

Cana · Arquitetura Cana
Dados frontend

MVP SPA Release — durabilidade ao reabrir

Estado relacional local, workers, eventos e exemplos de UI com IndexedDB.

Cana · Arquitetura Cana

Coordenação e ports de estado

Exemplos de mediator, mutex e key/value que explicam os limites arquiteturais em volta de trabalho compartilhado.

Consumidores e fonte de design

Exemplos de SDK clients e Designer Core que conectam modelagem de UI a consumidores executáveis.

Caminho MVP

App Jumentix completo no browser

Uma fatia completa de produto 100% browser usando Category e Task.

Prova
A mesma arquitetura pode começar com adaptadores in-memory antes da infraestrutura externa existir.
Observe
Limites de controller, service results, mensagens via mediator e atualizações de estado no cliente.

App Jumentix completo no browser

Execute Category e Task como domínios separados que trocam mensagens pelo mediator para compor um task board sem servidor.

const database = api.createInMemoryDatabase({
  stores: ['categories', 'tasks']
});
const keyValue = api.createKeyValueStorage();
const mutex = api.createMutex(keyValue);
const mediator = api.createMessageMediator();
const emittedEvents = [];

await database.connect();
await keyValue.connect();

const model = api.createServiceModel({
  app: 'service-management',
  domain: 'Tasks'
});
const designReport = api.validateDesign(model);

await database.stores.categories.create('work', {
  id: 'work',
  name: 'Work',
  color: '#2563eb'
});
await database.stores.categories.create('home', {
  id: 'home',
  name: 'Home',
  color: '#16a34a'
});

mediator.registerHandler('categories.get.v1', async (message) => {
  const category = await database.stores.categories.getOneById(message.payload.id);
  return {
    ok: Boolean(category.result),
    result: category.result,
    metadata: {
      domain: 'Categories',
      servedBy: 'categories.get.v1'
    }
  };
});

await mediator.subscribe('tasks.created', async (event) => {
  emittedEvents.push({
    title: event.payload.title,
    categoryId: event.payload.categoryId
  });
});

mediator.registerHandler('tasks.create.v1', async (message) => {
  const task = {
    ...message.payload,
    completed: false,
    createdAt: Date.now()
  };
  const lock = await mutex.lock('category', task.categoryId);
  if (!lock.result.locked) {
    return { ok: false, error: 'category is busy' };
  }
  try {
    await database.stores.tasks.create(task.id, task);
    await keyValue.set(`category:${task.categoryId}:lastTask`, task.id);
    await mediator.publish({ name: 'tasks.created', payload: task });
    return { ok: true, result: task };
  } finally {
    await mutex.unlock('category', task.categoryId);
  }
});

mediator.registerHandler('tasks.board.v1', async (message) => {
  const taskList = await database.stores.tasks.getAll(
    { completed: message.payload.completed },
    { page: 1, size: 20 }
  );
  const cards = await Promise.all(taskList.result.map(async (task) => {
    const category = await mediator.request({
      contract: 'categories.get.v1',
      payload: { id: task.categoryId },
      metadata: {
        sourceDomain: 'Tasks',
        reason: 'compose task board'
      }
    });
    return {
      id: task.id,
      title: task.title,
      completed: task.completed,
      category: category.result
        ? {
          id: category.result.id,
          name: category.result.name,
          color: category.result.color
        }
        : null
    };
  }));
  return {
    ok: true,
    result: {
      view: 'task-board',
      composedBy: ['Tasks', 'Categories'],
      cards
    }
  };
});

const restClient = api.createRestClient((request) => mediator.request({
  contract: 'tasks.create.v1',
  payload: request.body,
  metadata: { transport: 'rest' }
}));
const websocketClient = api.createWebSocketClient((request) => mediator.request({
  contract: 'tasks.create.v1',
  payload: request.input,
  metadata: { transport: 'websocket' }
}));

const firstTask = await restClient.request({
  operationId: 'createTask',
  method: 'POST',
  path: '/tasks',
  body: {
    id: 'task-1',
    title: 'Publish in-memory playgrounds',
    categoryId: 'work'
  }
});
await websocketClient.connect();
const secondTask = await websocketClient.request({
  operationId: 'tasks.create',
  input: {
    id: 'task-2',
    title: 'Review browser contract flow',
    categoryId: 'home'
  }
});
await websocketClient.disconnect();

const tasks = await database.stores.tasks.getAll({}, { page: 1, size: 10 });
const lastWorkTask = await keyValue.get('category:work:lastTask');
const taskBoard = await mediator.request({
  contract: 'tasks.board.v1',
  payload: { completed: false },
  metadata: { source: 'browser-playground' }
});

return {
  designOk: designReport.ok,
  taskCount: tasks.total,
  createdByRest: firstTask.result.title,
  createdByWebSocket: secondTask.result.title,
  lastWorkTask: lastWorkTask.result,
  composedDomains: taskBoard.result.composedBy,
  taskBoard: taskBoard.result.cards,
  emittedEvents
};
Caminho de stress

Criação em massa com mutex + DLQ

Escritas concorrentes, locks, replay DLQ e persistência browser em um cenário heavy-data.

Prova
Requests rejeitados são trabalho recuperável, não trabalho perdido.
Observe
Rejeições vermelhas do lock, entrada na DLQ, replay pelo Message Mediator, commits dos workers e eventos IndexedDB enquanto o stream ainda roda.

Criação em massa com mutex + DLQ

Crie muitos registros Task para uma Category, force contenção de lock, envie requests rejeitados pelo controller para uma dead-letter queue e reprocesse tudo pelo fluxo do controller.

const taskSchema = {
  version: 1,
  stores: [
    {
      name: 'categories',
      keyPath: 'id',
      indexes: [{ name: 'byName', keyPath: 'name', unique: true }]
    },
    {
      name: 'tasks',
      keyPath: 'id',
      indexes: [
        { name: 'byCategory', keyPath: 'categoryId' },
        { name: 'byUpdatedAt', keyPath: 'updatedAt' },
        { name: 'bySource', keyPath: 'source' },
        { name: 'byClient', keyPath: 'clientId' },
        { name: 'byWorker', keyPath: 'workerId' }
      ]
    }
  ]
};
const database = api.createCanaDatabaseClient({
  name: api.createCanaDatabaseName('bulk-mutex-dlq-workers'),
  schema: taskSchema,
  operationLedger: true
});
const keyValue = api.createKeyValueStorage();
const mutex = api.createMutex(keyValue);
const mediator = api.createMessageMediator();
const deadLetterQueue = api.createDeadLetterQueue({ maxAttempts: 3 });
const replayInbox = [];
const timeline = [];
let totalTimelineEvents = 0;
const canaEvents = [];
const storageUsageSamples = [];
const workerShards = [];
const React = api.React;
const BulkTaskImportContext = React.createContext(null);
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const realtimeMetrics = {
  attempted: 0,
  processed: 0,
  rejected: 0,
  replayed: 0
};
const criticalTimelineSteps = new Set([
  'client-ingestion-stopped',
  'controller-replay-batch',
  'controller-replay',
  'react-component-render'
]);
function publishRealtimeMetrics(phase, request = {}) {
  if (typeof reportPlaygroundProgress === 'function') {
    reportPlaygroundProgress({
      ...realtimeMetrics,
      taskId: request.taskId,
      clientId: request.clientId,
      workerId: request.workerId,
      phase,
      timestamp: Date.now()
    });
  }
}
function recordTimeline(entry) {
  totalTimelineEvents += 1;
  if (timeline.length < 320) {
    timeline.push(entry);
  } else if (criticalTimelineSteps.has(entry.step)) {
    timeline.shift();
    timeline.push(entry);
  }
}
async function recordIndexedDbQuota(label, tasks = 0) {
  const estimate = navigator.storage && navigator.storage.estimate
    ? await navigator.storage.estimate()
    : {};
  const usage = Number(estimate.usage ?? 0);
  const quota = Number(estimate.quota ?? 0);
  const percent = quota > 0 ? (usage / quota) * 100 : 0;
  storageUsageSamples.push({ label, usage, quota, percent, tasks });
  return storageUsageSamples[storageUsageSamples.length - 1];
}
const stopCanaEvents = database.subscribe((event) => {
  canaEvents.push({
    cursor: event.cursor,
    type: event.type,
    store: event.store,
    key: event.key,
    taskId: event.store === 'tasks' && event.record ? event.record.id : undefined,
    categoryId: event.record && event.record.categoryId ? event.record.categoryId : event.key,
    clientId: event.record && event.record.clientId ? event.record.clientId : undefined,
    workerId: event.record && event.record.workerId ? event.record.workerId : undefined,
    source: event.record && event.record.source ? event.record.source : 'category-seed'
  });
});

function createCanaWorkerShard(id) {
  const channel = new MessageChannel();
  channel.port1.start?.();
  channel.port2.start?.();
  const shard = {
    id,
    status: 'starting',
    handledRequests: 0,
    events: 0,
    database: database.cana.name,
    channel,
    router: null,
    host: null,
    client: null
  };
  shard.router = api.createCanaRouter({
    port: channel.port1,
    timeoutMs: 5000,
    onBroadcast(event) {
      shard.events += 1;
      canaEvents.push({
        cursor: event.cursor,
        type: event.type,
        store: event.store,
        key: event.key,
        taskId: event.store === 'tasks' && event.record ? event.record.id : undefined,
        categoryId: event.record && event.record.categoryId ? event.record.categoryId : event.key,
        clientId: event.record && event.record.clientId ? event.record.clientId : undefined,
        workerId: id,
        source: event.record && event.record.source ? event.record.source : 'worker-broadcast'
      });
    }
  });
  shard.host = api.createCanaWorkerHost({
    port: channel.port2,
    name: database.cana.name,
    schema: taskSchema,
    originId: `docs-${id}`,
    retainedEvents: 50,
    operationLedger: true
  });
  shard.client = api.createCanaWorkerClient(shard.router);
  return shard;
}

function closeWorkerShard(shard) {
  return Promise.resolve()
    .then(() => shard.client.close())
    .catch(() => undefined)
    .then(() => {
      shard.router.dispose();
      shard.channel.port1.close();
      shard.channel.port2.close();
      return shard.host.dispose();
    });
}

function createBulkTaskImportProvider({ actorId, clientId }) {
  const state = {
    actorId,
    clientId,
    submittedBatches: 0,
    lastBatchSize: 0
  };
  const contextValue = {
    actorId,
    clientId,
    getState: () => ({ ...state }),
    submitBulkImport: async (tasks, options = {}) => {
      state.submittedBatches += 1;
      state.lastBatchSize = tasks.length;
      recordTimeline({
        step: 'react-context-submit',
        component: 'BulkTaskImportProvider',
        actorId,
        clientId,
        taskCount: tasks.length
      });
      const responses = await bulkImportController({
        body: { tasks },
        actorId,
        client: clientId,
        stopNewRequestsAfterMs: options.stopNewRequestsAfterMs ?? Number.POSITIVE_INFINITY
      });
      state.accepted = (state.accepted ?? 0) + responses.filter((response) => response.ok).length;
      state.rejected = (state.rejected ?? 0) + responses.filter((response) => response.deadLetterId).length;
      state.interrupted = (state.interrupted ?? 0) + responses.filter((response) => response.interrupted).length;
      recordTimeline({
        step: 'react-context-complete',
        component: 'BulkTaskImportProvider',
        clientId,
        accepted: state.accepted,
        rejected: state.rejected,
        interrupted: state.interrupted
      });
      return responses;
    }
  };
  return {
    Context: BulkTaskImportContext,
    value: contextValue
  };
}

function BulkImportPanel({ provider, createNextTask, streamConfig }) {
  const previewTree = React.createElement(
    provider.Context.Provider,
    { value: provider.value },
    'BulkImportButton'
  );
  return {
    component: 'BulkImportPanel',
    previewElementType: previewTree.type === provider.Context.Provider
      ? 'BulkTaskImportContext.Provider'
      : 'unknown',
    startStream: async () => {
      recordTimeline({
        step: 'react-component-click',
        component: 'BulkImportPanel',
        clientId: provider.value.clientId,
        mode: 'concurrent-30s-stream',
        durationMs: streamConfig.durationMs,
        maxConcurrentRequests: streamConfig.maxConcurrentRequests
      });
      const responses = [];
      let stopped = false;
      let inFlight = 0;
      const startedAt = Date.now();

      await new Promise((resolve) => {
        const launchNext = () => {
          if (Date.now() - startedAt >= streamConfig.durationMs) {
            if (!stopped) {
              stopped = true;
              recordTimeline({
                step: 'client-ingestion-stopped',
                component: 'BulkImportPanel',
                clientId: provider.value.clientId,
                elapsedMs: Date.now() - startedAt,
                reason: '30 second stream window completed'
              });
            }
            if (inFlight === 0) resolve();
            return;
          }

          while (inFlight < streamConfig.maxConcurrentRequests && Date.now() - startedAt < streamConfig.durationMs) {
            const task = createNextTask(provider.value.clientId);
            inFlight += 1;
            provider.value.submitBulkImport([task])
              .then((batchResponses) => {
                responses.push(...batchResponses);
              })
              .finally(() => {
                inFlight -= 1;
                launchNext();
              });
          }
        };
        launchNext();
      });

      recordTimeline({
        step: 'react-component-render',
        component: 'BulkImportPanel',
        clientId: provider.value.clientId,
        state: provider.value.getState()
      });
      return responses;
    }
  };
}

await database.connect();
await keyValue.connect();
workerShards.push(
  createCanaWorkerShard('worker-a'),
  createCanaWorkerShard('worker-b'),
  createCanaWorkerShard('worker-c')
);
await Promise.all(workerShards.map(async (shard) => {
  await shard.client.open();
  shard.status = 'ready';
}));
await recordIndexedDbQuota('opened', 0);

await database.stores.categories.add({
  id: 'work',
  name: 'Work',
  color: '#2563eb',
  createdAt: Date.now(),
  updatedAt: Date.now()
});
await recordIndexedDbQuota('category seeded', 0);

await mediator.subscribe('dead-letter.enqueued', async (event) => {
  replayInbox.push(event.payload.recordId);
  recordTimeline({
    step: 'dead-letter-listener-received',
    taskId: event.payload.taskId,
    recordId: event.payload.recordId
  });
});

await mediator.subscribe('tasks.created', async (event) => {
  recordTimeline({
    step: 'task-created-event',
    taskId: event.payload.id,
    source: event.payload.source
  });
});

let committedTaskCount = 0;

async function recordWriteQuota(taskId) {
  committedTaskCount += 1;
  if (committedTaskCount <= 3 || committedTaskCount % 500 === 0) {
    await recordIndexedDbQuota(`${taskId}: ${committedTaskCount} tasks`, committedTaskCount);
  }
}

function createTaskRecord(input, source) {
  return {
    id: input.id,
    title: input.title,
    categoryId: input.categoryId,
    completed: false,
    clientId: input.clientId,
    workerId: input.workerId,
    source,
    createdAt: new Date().toISOString(),
    updatedAt: Date.now()
  };
}

async function writeTasksWithCanaWorkers(tasks, source) {
  const groups = new Map();
  for (const task of tasks) {
    const selectedWorker = workerShards.find((shard) => shard.id === task.workerId)
      ?? workerShards[groups.size % workerShards.length];
    const record = {
      ...task,
      workerId: selectedWorker.id,
      source
    };
    const current = groups.get(selectedWorker) ?? [];
    current.push(record);
    groups.set(selectedWorker, current);
  }

  const reports = [];
  for (const [worker, records] of groups.entries()) {
    worker.status = records.length > 1 ? 'bulk-writing' : 'writing';
    const write = records.length === 1
      ? await worker.client.add('tasks', records[0])
      : await worker.client.bulkAdd('tasks', records);
    worker.handledRequests += records.length;
    worker.status = 'ready';
    for (const task of records) {
      await recordWriteQuota(task.id);
    }
    const lastTask = records[records.length - 1];
    await keyValue.set(`category:${lastTask.categoryId}:lastTask`, lastTask.id);
    reports.push({
      workerId: worker.id,
      count: records.length,
      emittedEvents: write.events ? write.events.length : records.length
    });
  }
  return reports;
}

async function createTaskUseCase(input) {
  const lock = await mutex.lock('category', input.categoryId);
  if (!lock.result.locked) {
    const record = await deadLetterQueue.enqueue({
      entityName: 'Task',
      resourceId: input.categoryId,
      operation: 'tasks.create.v1',
      payload: input,
      actorId: input.requestedBy
    });
    await mediator.publish({
      name: 'dead-letter.enqueued',
      payload: {
        recordId: record.id,
        taskId: input.id,
        categoryId: input.categoryId
      },
      metadata: {
        source: 'tasks.create.use-case',
        reason: 'category resource is already locked'
      }
    });
    realtimeMetrics.rejected += 1;
    publishRealtimeMetrics('rejected', {
      taskId: input.id,
      clientId: input.clientId,
      workerId: input.workerId
    });
    return {
      ok: false,
      status: 409,
      error: 'category is locked; request queued for replay',
      deadLetterId: record.id
    };
  }

  try {
    recordTimeline({
      step: 'lock-acquired',
      taskId: input.id,
      categoryId: input.categoryId
    });
    publishRealtimeMetrics('accepted', {
      taskId: input.id,
      clientId: input.clientId,
      workerId: input.workerId
    });
    await sleep(input.processingMs);
    const task = createTaskRecord(input, input.source);
    const [write] = await writeTasksWithCanaWorkers([task], input.source);
    recordTimeline({
      step: 'cana-task-written',
      taskId: task.id,
      categoryId: task.categoryId,
      clientId: task.clientId,
      workerId: write.workerId,
      source: task.source,
      emittedEvents: write.emittedEvents
    });
    await mediator.publish({ name: 'tasks.created', payload: task });
    realtimeMetrics.processed += 1;
    publishRealtimeMetrics('processed', {
      taskId: task.id,
      clientId: task.clientId,
      workerId: write.workerId
    });
    return { ok: true, status: 201, result: task };
  } finally {
    await mutex.unlock('category', input.categoryId);
    recordTimeline({
      step: 'lock-released',
      taskId: input.id,
      categoryId: input.categoryId
    });
  }
}

async function createTaskController(request) {
  if (!request.replay) {
    realtimeMetrics.attempted += 1;
    publishRealtimeMetrics('submitted', {
      taskId: request.body.id,
      clientId: request.body.clientId,
      workerId: request.body.workerId
    });
  }
  recordTimeline({
    step: request.replay ? 'controller-replay' : 'controller-create',
    taskId: request.body.id,
    clientId: request.body.clientId,
    workerId: request.body.workerId
  });
  return createTaskUseCase({
    ...request.body,
    requestedBy: request.actorId,
    source: request.replay ? 'dead-letter-replay' : 'bulk-import'
  });
}

async function bulkImportController(request) {
  const startedAt = Date.now();
  let stopRecorded = false;
  recordTimeline({
    step: 'bulk-import-controller',
    component: 'BulkImportController',
    actorId: request.actorId,
    client: request.client,
    clientId: request.client,
    taskCount: request.body.tasks.length,
    stopNewRequestsAfterMs: request.stopNewRequestsAfterMs
  });
  return Promise.all(request.body.tasks.map(async (task) => {
    if (task.clientDelayMs > 0) {
      await sleep(task.clientDelayMs);
    }
    const elapsedMs = Date.now() - startedAt;
    if (elapsedMs > request.stopNewRequestsAfterMs) {
      if (!stopRecorded) {
        stopRecorded = true;
        recordTimeline({
          step: 'client-ingestion-stopped',
          component: 'BulkImportController',
          clientId: request.client,
          elapsedMs,
          reason: 'stop accepting new client requests'
        });
      }
      recordTimeline({
        step: 'client-request-interrupted',
        component: 'BulkImportController',
        taskId: task.id,
        clientId: request.client,
        workerId: task.workerId,
        elapsedMs
      });
      publishRealtimeMetrics('interrupted', {
        taskId: task.id,
        clientId: request.client,
        workerId: task.workerId
      });
      return {
        ok: false,
        status: 202,
        interrupted: true,
        error: 'client stopped sending new requests before controller admission',
        taskId: task.id
      };
    }
    return createTaskController({
      body: task,
      actorId: request.actorId
    });
  }));
}

async function replayDeadLettersController() {
  const pendingBefore = await deadLetterQueue.pending();
  const replayableRecords = pendingBefore.filter((record) => record.operation === 'tasks.create.v1');
  const skippedRecords = pendingBefore.filter((record) => record.operation !== 'tasks.create.v1');
  recordTimeline({
    step: 'controller-replay-batch',
    component: 'ReplayDeadLettersController',
    taskCount: replayableRecords.length,
    mode: 'worker-bulk-add'
  });
  const replayTasks = replayableRecords.map((record) => createTaskRecord(
    {
      ...record.payload,
      processingMs: 0
    },
    'dead-letter-replay'
  ));
  const bulkReports = await writeTasksWithCanaWorkers(replayTasks, 'dead-letter-replay');
  await Promise.all(replayableRecords.map((record) => deadLetterQueue.settle(record.id, 'succeeded')));
  realtimeMetrics.replayed += replayableRecords.length;
  replayableRecords.slice(-900).forEach((record) => {
    publishRealtimeMetrics('replayed', {
      taskId: record.payload.id,
      clientId: record.payload.clientId,
      workerId: record.payload.workerId
    });
  });
  publishRealtimeMetrics('replayed');
  replayableRecords.slice(0, 80).forEach((record) => {
    recordTimeline({
      step: 'controller-replay',
      taskId: record.payload.id,
      clientId: record.payload.clientId,
      workerId: record.payload.workerId
    });
  });
  const report = {
    replayed: replayableRecords.map((record) => record.id),
    retried: [],
    abandoned: [],
    skipped: skippedRecords.map((record) => record.id)
  };
  const records = await deadLetterQueue.list();
  return {
    ok: true,
    status: 200,
    pendingBefore: pendingBefore.length,
    report,
    bulkReports,
    records: records.map((record) => ({
      id: record.id,
      taskId: record.payload.id,
      status: record.status,
      attempts: record.attempts
    }))
  };
}

const reactClientIds = ['react-client-a', 'react-client-b', 'react-client-c'];
const streamDurationMs = 30000;
const maxConcurrentRequestsPerClient = 12;
const requestPaceMs = 25;
let globalSequence = 0;
function createNextTask(clientId) {
  const sequence = globalSequence;
  globalSequence += 1;
  return {
    id: `task-${sequence + 1}`,
    title: `Concurrent Task ${sequence + 1}`,
    categoryId: 'work',
    clientId,
    workerId: workerShards[sequence % workerShards.length].id,
    sequence,
    processingMs: 12,
    clientDelayMs: requestPaceMs
  };
}

const reactClients = reactClientIds.map((clientId) => {
  const provider = createBulkTaskImportProvider({
    actorId: `${clientId}-controller`,
    clientId
  });
  return {
    id: clientId,
    provider,
    panel: BulkImportPanel({
      provider,
      createNextTask,
      streamConfig: {
        durationMs: streamDurationMs,
        maxConcurrentRequests: maxConcurrentRequestsPerClient
      }
    })
  };
});
const streamStartedAt = Date.now();
const bulkResponseGroups = await Promise.all(
  reactClients.map((client) => client.panel.startStream())
);
const actualRunDurationMs = Date.now() - streamStartedAt;
const bulkResponses = bulkResponseGroups.flat();
const pendingAfterBulk = await deadLetterQueue.pending();
const replay = await replayDeadLettersController();
const pendingAfterReplay = await deadLetterQueue.pending();
const taskRows = await database.stores.tasks.query({ index: 'byUpdatedAt' });
const categoryRows = await database.stores.categories.query({ index: 'byName' });
const lastTask = await keyValue.get('category:work:lastTask');
await recordIndexedDbQuota('final', taskRows.length);
const storage = await database.cana.storageState();
const workerShardSummary = workerShards.map((shard) => ({
  id: shard.id,
  database: shard.database,
  status: shard.status,
  handledRequests: shard.handledRequests,
  events: shard.events
}));
const databaseSnapshot = {
  adapter: 'Cana database adapter',
  backend: database.cana.backend,
  workerMode: 'createWorkerHost + createWorkerClient',
  storage,
  indexedDbDatabase: database.cana.name,
  stores: {
    categories: categoryRows.length,
    tasks: taskRows.length
  },
  categoryIds: categoryRows.map((category) => category.id),
  taskIds: taskRows.map((task) => task.id)
};

stopCanaEvents();
await Promise.all(workerShards.map(closeWorkerShard));
await database.disconnect();

const createdDuringBulk = bulkResponses.filter((response) => response.ok).length;
const submittedToController = bulkResponses.filter((response) => !response.interrupted).length;
const interruptedBeforeController = bulkResponses.filter((response) => response.interrupted).length;
const rejectedToDeadLetterQueue = bulkResponses.filter((response) => response.deadLetterId).length;
const deadLetterQueueFullyProcessed = pendingAfterReplay.length === 0
  && replay.records.every((record) => record.status === 'succeeded');
const jobsAccountedFor = taskRows.length + interruptedBeforeController;
publishRealtimeMetrics('complete');

return {
  databaseAdapter: 'Cana',
  databaseBackend: databaseSnapshot.backend,
  requestMode: 'concurrent-30s-stream',
  streamDurationMs,
  actualRunDurationMs,
  maxConcurrentRequestsPerClient,
  requestPaceMs,
  attemptedBulkCount: bulkResponses.length,
  createdDuringBulk,
  submittedToController,
  interruptedBeforeController,
  rejectedToDeadLetterQueue,
  shutdownReport: {
    stopNewRequestsAfterMs: streamDurationMs,
    pendingDeadLettersAfterReplay: pendingAfterReplay.length,
    deadLetterQueueFullyProcessed,
    jobsAccountedFor,
    noLostJobs: deadLetterQueueFullyProcessed && jobsAccountedFor === bulkResponses.length
  },
  pendingBeforeReplay: pendingAfterBulk.map((record) => ({
    id: record.id,
    taskId: record.payload.id,
    resourceId: record.resourceId,
    status: record.status
  })),
  replayInbox,
  replayReport: replay.report,
  finalTaskCount: taskRows.length,
  lastTaskInCategory: lastTask.result,
  reactClients: reactClients.map((client) => ({
    id: client.id,
    taskCount: client.provider.value.getState().accepted
      + client.provider.value.getState().rejected
      + client.provider.value.getState().interrupted,
    accepted: client.provider.value.getState().accepted,
    rejected: client.provider.value.getState().rejected,
    interrupted: client.provider.value.getState().interrupted
  })),
  workerShards: workerShardSummary,
  requestTimeline: timeline,
  totalTimelineEvents,
  canaEvents: canaEvents.slice(-500),
  totalCanaEvents: canaEvents.length,
  storageUsageSamples,
  databaseSnapshot,
  controllerLevelReplay: timeline
    .filter((entry) => entry.step.startsWith('controller'))
    .slice(0, 40)
    .map((entry) => entry.step),
  storedTasks: taskRows.slice(0, 20).map((task) => ({
    id: task.id,
    title: task.title,
    source: task.source,
    clientId: task.clientId,
    workerId: task.workerId
  })),
  omittedStoredTasks: Math.max(0, taskRows.length - 20),
  deadLetterRecords: replay.records
};
Fluxo em tempo real

Um canvas único acompanha requisições concorrentes por 30 segundos: cada request recente vira um token próprio atravessando cliente React, Context API, controller, mutex, DLQ, Message Mediator, replay, workers do Cana, commits no IndexedDB, eventos de subscribe e retorno ao cliente. Rejeições pelo lock aparecem em vermelho.

AceitoRejeitado pelo lockEntrada interrompidaReplayMessage MediatorCana workersEventos CanaIndexedDB quota
Clientes React: 0Workers Cana: 0Tentadas: 0Admitidas no controller: 0Aceitas no bulk: 0Rejeitadas pelo lock: 0Interrompidas antes do controller: 0Reprocessadas: 0DLQ drenada: noSem jobs perdidos: noTasks finais: 0Eventos reais: 0Eventos Cana: 0Tokens vivos de request: 0IndexedDB quota: 0.0000%Uso IndexedDB: 0 BAmostras quota: 0Janela: 30sConcorrência/cliente: 12
Fluxo realtime
processed 0rejected 0replayed 0
Comparação do pipeline
Admissão0.0%Pressão no lock0.0%DLQ recuperada0.0%Persistência final0.0%
aceitaslock rejeitouinterrompidasvia replay
Entrada total0 · 0.0%
Admitidas no controller0 · 0.0%
Criadas sem retry0 · 0.0%
Rejeitadas para DLQ0 · 0.0%
Recuperadas por replay0 · 0.0%
Persistidas no IndexedDB0 · 0.0%

0 requests foram recuperadas depois do lock; 0 chegaram ao banco.

Throughput
0 req/s0 requisições em 30.0s
Resultado das requisições
Criadas direto0
Lock -> DLQ0
Interrompidas0
Reprocessadas0
Clientes React
Aguardando execução0
Workers do Cana
Aguardando workers0
IndexedDB quota
0.0000% · 0 B
Saúde do encerramento
DLQ drenadano
Sem jobs perdidosno
Caminho MVP

MVP REST Dia 1 — use-case no adaptador in-memory

Uma fatia completa de produto 100% browser usando Category e Task.

Prova
A mesma arquitetura pode começar com adaptadores in-memory antes da infraestrutura externa existir.
Observe
Limites de controller, service results, mensagens via mediator e atualizações de estado no cliente.

MVP REST Dia 1 — use-case no adaptador in-memory

Execute o use-case de criar task contra o adaptador real in-memory e prove as regras 201/400/404.

const database = api.createInMemoryDatabase({ stores: ['categories', 'tasks'] });
await database.connect();
await database.stores.categories.create('work', { id: 'work', name: 'Work' });

async function createTaskUseCase(input) {
  if (!input.title || !String(input.title).trim()) {
    return { status: 400, body: { error: 'title is required' } };
  }
  const category = await database.stores.categories.getOneById(input.categoryId);
  if (!category.result) {
    return { status: 404, body: { error: 'category not found' } };
  }
  const task = {
    id: crypto.randomUUID(),
    title: input.title,
    categoryId: input.categoryId,
    completed: false
  };
  await database.stores.tasks.create(task.id, task);
  return { status: 201, body: task };
}

const created = await createTaskUseCase({ title: 'Ship the MVP', categoryId: 'work' });
const invalid = await createTaskUseCase({ title: ' ', categoryId: 'work' });
const orphan = await createTaskUseCase({ title: 'No owner', categoryId: 'missing' });
const listed = await database.stores.tasks.getAll({}, { page: 1, size: 10 });

return {
  created: created.status,
  invalid: invalid.status,
  unknownCategory: orphan.status,
  storedTotal: listed.total,
  firstTask: listed.result[0].title
};
Caminho MVP

MVP REST Dia 2 — client tipado sobre o mesmo adaptador

Uma fatia completa de produto 100% browser usando Category e Task.

Prova
A mesma arquitetura pode começar com adaptadores in-memory antes da infraestrutura externa existir.
Observe
Limites de controller, service results, mensagens via mediator e atualizações de estado no cliente.

MVP REST Dia 2 — client tipado sobre o mesmo adaptador

Roteie operationIds OpenAPI por um client REST para um handler apoiado no adaptador in-memory.

const database = api.createInMemoryDatabase({ stores: ['categories', 'tasks'] });
await database.connect();
await database.stores.categories.create('work', { id: 'work', name: 'Work' });

const client = api.createRestClient(async (request) => {
  if (request.operationId === 'createTask') {
    const input = request.body ?? {};
    if (!input.title || !String(input.title).trim()) {
      return { ok: false, status: 400, error: 'title is required' };
    }
    const category = await database.stores.categories.getOneById(input.categoryId);
    if (!category.result) {
      return { ok: false, status: 404, error: 'category not found' };
    }
    const task = { id: crypto.randomUUID(), ...input, completed: false };
    await database.stores.tasks.create(task.id, task);
    return { ok: true, status: 201, result: task };
  }
  if (request.operationId === 'listTasks') {
    const tasks = await database.stores.tasks.getAll({}, { page: 1, size: 20 });
    return { ok: true, status: 200, result: tasks.result };
  }
  return { ok: false, status: 404, error: 'unknown operationId' };
});

const created = await client.request({
  operationId: 'createTask',
  method: 'POST',
  path: '/tasks',
  body: { title: 'Publish first REST MVP', categoryId: 'work' }
});
const rejected = await client.request({
  operationId: 'createTask',
  method: 'POST',
  path: '/tasks',
  body: { title: '', categoryId: 'work' }
});
const listed = await client.request({ operationId: 'listTasks', method: 'GET', path: '/tasks' });

return {
  created: created.status,
  rejected: rejected.status,
  listed: listed.status,
  total: listed.result.length,
  firstTask: listed.result[0].title
};
Caminho MVP

MVP realtime Dia 1 — comando live com ack e broadcast

Uma fatia completa de produto 100% browser usando Category e Task.

Prova
A mesma arquitetura pode começar com adaptadores in-memory antes da infraestrutura externa existir.
Observe
Limites de controller, service results, mensagens via mediator e atualizações de estado no cliente.

MVP realtime Dia 1 — comando live com ack e broadcast

Ligue um client WebSocket a um handler do mediator que persiste pelo adaptador in-memory e transmite o evento de criação.

const database = api.createInMemoryDatabase({ stores: ['categories', 'tasks'] });
const mediator = api.createMessageMediator();
await database.connect();
await database.stores.categories.create('work', { id: 'work', name: 'Work' });

const liveCards = [];
await mediator.subscribe('tasks.created', async (event) => {
  liveCards.push(event.payload.title);
});

mediator.registerHandler('tasks.create.v1', async (message) => {
  const category = await database.stores.categories.getOneById(message.payload.categoryId);
  if (!category.result) {
    return { ok: false, error: 'category not found' };
  }
  const task = {
    id: crypto.randomUUID(),
    title: message.payload.title,
    categoryId: message.payload.categoryId,
    completed: false
  };
  await database.stores.tasks.create(task.id, task);
  await mediator.publish({ name: 'tasks.created', payload: task });
  return { ok: true, result: task };
});

const socket = api.createWebSocketClient((request) => mediator.request({
  contract: 'tasks.create.v1',
  payload: request.input,
  metadata: { transport: 'websocket' }
}));

await socket.connect();
const ack = await socket.request({
  operationId: 'tasks.create',
  input: { title: 'Show realtime status', categoryId: 'work' }
});
const rejected = await socket.request({
  operationId: 'tasks.create',
  input: { title: 'No owner', categoryId: 'missing' }
});
await socket.disconnect();

return {
  ack: ack.ok,
  createdTask: ack.result.title,
  rejectedError: rejected.error,
  broadcastedToUi: liveCards
};
Caminho MVP

MVP realtime Dia 2 — drill de paridade do fallback REST

Uma fatia completa de produto 100% browser usando Category e Task.

Prova
A mesma arquitetura pode começar com adaptadores in-memory antes da infraestrutura externa existir.
Observe
Limites de controller, service results, mensagens via mediator e atualizações de estado no cliente.

MVP realtime Dia 2 — drill de paridade do fallback REST

Derrube o handler do socket e prove que o fallback REST retorna o mesmo resultado de negócio pelo mesmo adaptador in-memory.

const database = api.createInMemoryDatabase({ stores: ['categories', 'tasks'] });
await database.connect();
await database.stores.categories.create('work', { id: 'work', name: 'Work' });

async function createTaskUseCase(input) {
  const task = {
    id: crypto.randomUUID(),
    title: input.title,
    categoryId: input.categoryId,
    completed: false
  };
  await database.stores.tasks.create(task.id, task);
  return { ok: true, result: task };
}

const downSocket = api.createWebSocketClient(async () => {
  throw new Error('socket unavailable');
});
const restFallback = api.createRestClient((request) => createTaskUseCase(request.body));

async function createTaskWithFallback(input) {
  try {
    const live = await downSocket.request({ operationId: 'tasks.create', input });
    return { transport: 'websocket', result: live };
  } catch {
    const fallback = await restFallback.request({
      operationId: 'createTask',
      method: 'POST',
      path: '/tasks',
      body: input
    });
    return { transport: 'rest', result: fallback };
  }
}

const response = await createTaskWithFallback({ title: 'Fallback parity task', categoryId: 'work' });
const stored = await database.stores.tasks.getAll({}, { page: 1, size: 10 });

return {
  transportUsed: response.transport,
  ok: response.result.ok,
  storedTotal: stored.total,
  storedTitle: response.result.result.title
};
Caminho MVP

MVP SaaS Dia 1 — policy de tenant no adaptador in-memory

Uma fatia completa de produto 100% browser usando Category e Task.

Prova
A mesma arquitetura pode começar com adaptadores in-memory antes da infraestrutura externa existir.
Observe
Limites de controller, service results, mensagens via mediator e atualizações de estado no cliente.

MVP SaaS Dia 1 — policy de tenant no adaptador in-memory

Prove a guarda de tenant: org-1 escreve a própria task, org-2 é negado e o store só guarda o registro legítimo.

const database = api.createInMemoryDatabase({ stores: ['categories', 'tasks'] });
await database.connect();
await database.stores.categories.create('work', { id: 'work', name: 'Work' });

async function createTenantTask(context, input) {
  if (context.organizationId !== input.organizationId) {
    return { ok: false, status: 403, error: 'tenant access denied' };
  }
  const task = {
    id: crypto.randomUUID(),
    organizationId: input.organizationId,
    title: input.title,
    categoryId: input.categoryId
  };
  await database.stores.tasks.create(task.id, task);
  return { ok: true, status: 201, result: task };
}

const own = await createTenantTask(
  { organizationId: 'org-1', userId: 'user-1' },
  { organizationId: 'org-1', title: 'Tenant scoped task', categoryId: 'work' }
);
const denied = await createTenantTask(
  { organizationId: 'org-2', userId: 'user-2' },
  { organizationId: 'org-1', title: 'Cross-tenant write', categoryId: 'work' }
);
const org1Tasks = await database.stores.tasks.getByRelation('organizationId', 'org-1');
const org2Tasks = await database.stores.tasks.getByRelation('organizationId', 'org-2');

return {
  ownWrite: own.status,
  crossTenantWrite: denied.status,
  org1Sees: org1Tasks.result.map((task) => task.title),
  org2Sees: org2Tasks.result.length
};
Caminho MVP

MVP microsserviços Dia 2 — worker persistindo notificações reais

Uma fatia completa de produto 100% browser usando Category e Task.

Prova
A mesma arquitetura pode começar com adaptadores in-memory antes da infraestrutura externa existir.
Observe
Limites de controller, service results, mensagens via mediator e atualizações de estado no cliente.

MVP microsserviços Dia 2 — worker persistindo notificações reais

Assine um worker de notificação no mediator e persista cada entrega no próprio store in-memory — sem endpoint HTTP falso.

const database = api.createInMemoryDatabase({ stores: ['tasks', 'notifications'] });
const mediator = api.createMessageMediator();
await database.connect();

await mediator.subscribe('tasks.created.v1', async (event) => {
  await database.stores.notifications.create(crypto.randomUUID(), {
    template: 'task-created',
    taskId: event.payload.id,
    title: event.payload.title
  });
});

mediator.registerHandler('tasks.create.v1', async (message) => {
  const task = {
    id: crypto.randomUUID(),
    title: message.payload.title,
    categoryId: message.payload.categoryId,
    completed: false
  };
  await database.stores.tasks.create(task.id, task);
  await mediator.publish({ name: 'tasks.created.v1', payload: task });
  return { ok: true, result: task };
});

const created = await mediator.request({
  contract: 'tasks.create.v1',
  payload: { title: 'Notify assignee', categoryId: 'work' }
});
const sent = await database.stores.notifications.getAll({}, { page: 1, size: 10 });

return {
  task: created.result.title,
  notificationsDelivered: sent.total,
  firstNotification: sent.result[0]
};
Caminho MVP

MVP microsserviços Release — falha explícita com replay via DLQ

Uma fatia completa de produto 100% browser usando Category e Task.

Prova
A mesma arquitetura pode começar com adaptadores in-memory antes da infraestrutura externa existir.
Observe
Limites de controller, service results, mensagens via mediator e atualizações de estado no cliente.

MVP microsserviços Release — falha explícita com replay via DLQ

Envie uma mensagem venenosa para a dead-letter queue e reprocesse, provando que o modo de falha é explícito e medido.

const deadLetterQueue = api.createDeadLetterQueue({ maxAttempts: 2 });
const mediator = api.createMessageMediator();

mediator.registerHandler('tasks.create.v1', async (message) => {
  if (!message.payload.title) {
    const record = await deadLetterQueue.enqueue({
      entityName: 'Task',
      resourceId: message.payload.categoryId ?? 'unknown',
      operation: 'tasks.create.v1',
      payload: message.payload
    });
    return { ok: false, error: 'queued for replay', deadLetterId: record.id };
  }
  return { ok: true, result: { id: crypto.randomUUID(), ...message.payload } };
});

const failed = await mediator.request({
  contract: 'tasks.create.v1',
  payload: { categoryId: 'work' }
});
const report = await deadLetterQueue.replay({
  'tasks.create.v1': async (record) => {
    if (!record.payload.title) throw new Error('title is required');
    return record.id;
  }
});
const after = await deadLetterQueue.find(failed.deadLetterId);

return {
  firstAttempt: failed.error,
  replayReport: report,
  statusAfterReplay: after.status,
  lastError: after.lastError
};
Dados frontend

Primeiros passos

Estado relacional local, workers, eventos e exemplos de UI com IndexedDB.

Prova
Componentes frontend podem ouvir eventos de dados em vez de fazer polling ou duplicar lógica de storage.
Observe
Configuração de schema, operações de tabela, listeners de eventos, fluxo de workers e pontos de integração com state management.

Primeiros passos

Abra um client, crie registros Category e Task, depois leia de volta.

const client = cana.createClient({
  name: dbName,
  schema: {
  version: 1,
  stores: [
    { name: 'categories', keyPath: 'id', indexes: [{ name: 'byName', keyPath: 'name', unique: true }] },
    {
      name: 'tasks',
      keyPath: 'id',
      indexes: [
        { name: 'byCategory', keyPath: 'categoryId' },
        { name: 'byCompleted', keyPath: 'completed' },
        { name: 'byUpdatedAt', keyPath: 'updatedAt' }
      ]
    }
  ]
}
});
await client.open();
await client.table('categories').add({
  id: 'work',
  name: 'Work',
  color: '#2563eb',
  createdAt: Date.now(),
  updatedAt: Date.now()
});
await client.table('tasks').add({
  id: 'task-1',
  title: 'Write the Cana tutorial',
  categoryId: 'work',
  completed: false,
  priority: 'high',
  createdAt: Date.now(),
  updatedAt: Date.now()
});
return {
  backend: client.backend,
  category: await client.table('categories').get('work'),
  task: await client.table('tasks').get('task-1')
};
Dados frontend

Upgrade de schema

Estado relacional local, workers, eventos e exemplos de UI com IndexedDB.

Prova
Componentes frontend podem ouvir eventos de dados em vez de fazer polling ou duplicar lógica de storage.
Observe
Configuração de schema, operações de tabela, listeners de eventos, fluxo de workers e pontos de integração com state management.

Upgrade de schema

Comece com Category, depois suba a versao e adicione a tabela Task.

const v1 = cana.createClient({
  name: dbName,
  schema: {
    version: 1,
    stores: [{ name: 'categories', keyPath: 'id' }]
  }
});
await v1.open();
await v1.table('categories').add({ id: 'work', name: 'Work' });
await v1.close();

const v2 = cana.createClient({
  name: dbName,
  schema: {
  version: 1,
  stores: [
    { name: 'categories', keyPath: 'id', indexes: [{ name: 'byName', keyPath: 'name', unique: true }] },
    {
      name: 'tasks',
      keyPath: 'id',
      indexes: [
        { name: 'byCategory', keyPath: 'categoryId' },
        { name: 'byCompleted', keyPath: 'completed' },
        { name: 'byUpdatedAt', keyPath: 'updatedAt' }
      ]
    }
  ]
}
});
await v2.open();
await v2.table('tasks').add({
  id: 'task-1',
  title: 'Created after upgrade',
  categoryId: 'work',
  completed: false,
  priority: 'medium',
  createdAt: Date.now(),
  updatedAt: Date.now()
});
return {
  categories: await v2.table('categories').query(),
  tasks: await v2.table('tasks').query()
};
Dados frontend

Chaves

Estado relacional local, workers, eventos e exemplos de UI com IndexedDB.

Prova
Componentes frontend podem ouvir eventos de dados em vez de fazer polling ou duplicar lógica de storage.
Observe
Configuração de schema, operações de tabela, listeners de eventos, fluxo de workers e pontos de integração com state management.

Chaves

Use ids estaveis nos registros Category e Task.

const client = cana.createClient({
  name: dbName,
  schema: {
  version: 1,
  stores: [
    { name: 'categories', keyPath: 'id', indexes: [{ name: 'byName', keyPath: 'name', unique: true }] },
    {
      name: 'tasks',
      keyPath: 'id',
      indexes: [
        { name: 'byCategory', keyPath: 'categoryId' },
        { name: 'byCompleted', keyPath: 'completed' },
        { name: 'byUpdatedAt', keyPath: 'updatedAt' }
      ]
    }
  ]
}
});
await client.open();
await client.table('categories').add({
  id: 'docs',
  name: 'Docs',
  color: '#0f766e',
  createdAt: 1,
  updatedAt: 1
});
await client.table('tasks').add({
  id: 'docs-1',
  title: 'Document stable keys',
  categoryId: 'docs',
  completed: false,
  priority: 'medium',
  createdAt: 2,
  updatedAt: 2
});
return {
  categoryKey: 'docs',
  taskKey: 'docs-1',
  task: await client.table('tasks').get('docs-1')
};
Dados frontend

CRUD

Estado relacional local, workers, eventos e exemplos de UI com IndexedDB.

Prova
Componentes frontend podem ouvir eventos de dados em vez de fazer polling ou duplicar lógica de storage.
Observe
Configuração de schema, operações de tabela, listeners de eventos, fluxo de workers e pontos de integração com state management.

CRUD

Crie, leia, atualize e remova uma Task.

const client = cana.createClient({
  name: dbName,
  schema: {
  version: 1,
  stores: [
    { name: 'categories', keyPath: 'id', indexes: [{ name: 'byName', keyPath: 'name', unique: true }] },
    {
      name: 'tasks',
      keyPath: 'id',
      indexes: [
        { name: 'byCategory', keyPath: 'categoryId' },
        { name: 'byCompleted', keyPath: 'completed' },
        { name: 'byUpdatedAt', keyPath: 'updatedAt' }
      ]
    }
  ]
}
});
await client.open();
await client.table('categories').add({ id: 'work', name: 'Work', color: '#2563eb' });
const tasks = client.table('tasks');
await tasks.add({
  id: 'task-1',
  title: 'Draft the tutorial',
  categoryId: 'work',
  completed: false,
  priority: 'high',
  createdAt: 1,
  updatedAt: 1
});
await tasks.update('task-1', { completed: true, updatedAt: 2 });
const afterUpdate = await tasks.get('task-1');
await tasks.delete('task-1');
return { afterUpdate, afterDelete: await tasks.get('task-1') };
Dados frontend

Operacoes em lote

Estado relacional local, workers, eventos e exemplos de UI com IndexedDB.

Prova
Componentes frontend podem ouvir eventos de dados em vez de fazer polling ou duplicar lógica de storage.
Observe
Configuração de schema, operações de tabela, listeners de eventos, fluxo de workers e pontos de integração com state management.

Operacoes em lote

Popule Category e Task com operacoes em lote.

const client = cana.createClient({
  name: dbName,
  schema: {
  version: 1,
  stores: [
    { name: 'categories', keyPath: 'id', indexes: [{ name: 'byName', keyPath: 'name', unique: true }] },
    {
      name: 'tasks',
      keyPath: 'id',
      indexes: [
        { name: 'byCategory', keyPath: 'categoryId' },
        { name: 'byCompleted', keyPath: 'completed' },
        { name: 'byUpdatedAt', keyPath: 'updatedAt' }
      ]
    }
  ]
}
});
await client.open();
const now = Date.now();
await client.table('categories').bulkAdd([
  { id: 'work', name: 'Work', color: '#2563eb', createdAt: now, updatedAt: now },
  { id: 'home', name: 'Home', color: '#16a34a', createdAt: now, updatedAt: now }
]);
await client.table('tasks').bulkAdd([
  {
    id: 'task-1',
    title: 'Write the Cana tutorial',
    categoryId: 'work',
    completed: false,
    priority: 'high',
    createdAt: now,
    updatedAt: now
  },
  {
    id: 'task-2',
    title: 'Review category filters',
    categoryId: 'home',
    completed: true,
    priority: 'medium',
    createdAt: now,
    updatedAt: now + 1
  }
]);
const put = await client.table('tasks').bulkPut([
  {
    id: 'task-2',
    title: 'Review category filters',
    categoryId: 'home',
    completed: false,
    priority: 'high',
    createdAt: Date.now(),
    updatedAt: Date.now()
  },
  {
    id: 'task-3',
    title: 'Publish the example app',
    categoryId: 'work',
    completed: false,
    priority: 'medium',
    createdAt: Date.now(),
    updatedAt: Date.now()
  }
]);
return {
  put,
  categories: await client.table('categories').query({ index: 'byName' }),
  tasks: await client.table('tasks').query({ index: 'byUpdatedAt' })
};
Dados frontend

Query + explain

Estado relacional local, workers, eventos e exemplos de UI com IndexedDB.

Prova
Componentes frontend podem ouvir eventos de dados em vez de fazer polling ou duplicar lógica de storage.
Observe
Configuração de schema, operações de tabela, listeners de eventos, fluxo de workers e pontos de integração com state management.

Query + explain

Rode uma query indexada de Task por Category e inspecione o plano.

const client = cana.createClient({
  name: dbName,
  schema: {
  version: 1,
  stores: [
    { name: 'categories', keyPath: 'id', indexes: [{ name: 'byName', keyPath: 'name', unique: true }] },
    {
      name: 'tasks',
      keyPath: 'id',
      indexes: [
        { name: 'byCategory', keyPath: 'categoryId' },
        { name: 'byCompleted', keyPath: 'completed' },
        { name: 'byUpdatedAt', keyPath: 'updatedAt' }
      ]
    }
  ]
}
});
await client.open();
const now = Date.now();
await client.table('categories').bulkAdd([
  { id: 'work', name: 'Work', color: '#2563eb', createdAt: now, updatedAt: now },
  { id: 'home', name: 'Home', color: '#16a34a', createdAt: now, updatedAt: now }
]);
await client.table('tasks').bulkAdd([
  {
    id: 'task-1',
    title: 'Write the Cana tutorial',
    categoryId: 'work',
    completed: false,
    priority: 'high',
    createdAt: now,
    updatedAt: now
  },
  {
    id: 'task-2',
    title: 'Review category filters',
    categoryId: 'home',
    completed: true,
    priority: 'medium',
    createdAt: now,
    updatedAt: now + 1
  }
]);
const { records, plan } = await client.table('tasks').explain({
  index: 'byCategory',
  equals: 'work'
});
return { records, plan };
Dados frontend

Transacoes

Estado relacional local, workers, eventos e exemplos de UI com IndexedDB.

Prova
Componentes frontend podem ouvir eventos de dados em vez de fazer polling ou duplicar lógica de storage.
Observe
Configuração de schema, operações de tabela, listeners de eventos, fluxo de workers e pontos de integração com state management.

Transacoes

Crie uma Category e sua primeira Task em um unico commit.

const client = cana.createClient({
  name: dbName,
  schema: {
  version: 1,
  stores: [
    { name: 'categories', keyPath: 'id', indexes: [{ name: 'byName', keyPath: 'name', unique: true }] },
    {
      name: 'tasks',
      keyPath: 'id',
      indexes: [
        { name: 'byCategory', keyPath: 'categoryId' },
        { name: 'byCompleted', keyPath: 'completed' },
        { name: 'byUpdatedAt', keyPath: 'updatedAt' }
      ]
    }
  ]
}
});
await client.open();
const tx = await client.transaction('readwrite', ['categories', 'tasks'], async (scope) => {
  const now = Date.now();
  await scope.table('categories').put({
    id: 'ops',
    name: 'Operations',
    color: '#f97316',
    createdAt: now,
    updatedAt: now
  });
  await scope.table('tasks').put({
    id: 'ops-1',
    title: 'Created with the category',
    categoryId: 'ops',
    completed: false,
    priority: 'high',
    createdAt: now,
    updatedAt: now
  });
  return 'ok';
});
return {
  outcome: tx.outcome,
  result: tx.result,
  categories: await client.table('categories').query(),
  tasks: await client.table('tasks').query()
};
Dados frontend

Eventos de mudanca

Estado relacional local, workers, eventos e exemplos de UI com IndexedDB.

Prova
Componentes frontend podem ouvir eventos de dados em vez de fazer polling ou duplicar lógica de storage.
Observe
Configuração de schema, operações de tabela, listeners de eventos, fluxo de workers e pontos de integração com state management.

Eventos de mudanca

Assine e colete eventos confirmados de Task.

const client = cana.createClient({
  name: dbName,
  schema: {
  version: 1,
  stores: [
    { name: 'categories', keyPath: 'id', indexes: [{ name: 'byName', keyPath: 'name', unique: true }] },
    {
      name: 'tasks',
      keyPath: 'id',
      indexes: [
        { name: 'byCategory', keyPath: 'categoryId' },
        { name: 'byCompleted', keyPath: 'completed' },
        { name: 'byUpdatedAt', keyPath: 'updatedAt' }
      ]
    }
  ]
}
});
await client.open();
const seen = [];
const stop = client.subscribe((event) => {
  seen.push({ cursor: event.cursor, type: event.type, store: event.store, key: event.key });
});
await client.table('categories').add({ id: 'docs', name: 'Docs', color: '#0f766e' });
await client.table('tasks').add({
  id: 'docs-1',
  title: 'Listen to Cana events',
  categoryId: 'docs',
  completed: false,
  priority: 'medium',
  createdAt: 1,
  updatedAt: 1
});
await client.table('tasks').update('docs-1', { completed: true, updatedAt: 2 });
stop();
return { events: seen };
Dados frontend

Hooks

Estado relacional local, workers, eventos e exemplos de UI com IndexedDB.

Prova
Componentes frontend podem ouvir eventos de dados em vez de fazer polling ou duplicar lógica de storage.
Observe
Configuração de schema, operações de tabela, listeners de eventos, fluxo de workers e pontos de integração com state management.

Hooks

Rode hooks beforeWrite e afterCommit ao redor de escritas de Task.

const trail = [];
const client = cana.createClient({
  name: dbName,
  schema: {
  version: 1,
  stores: [
    { name: 'categories', keyPath: 'id', indexes: [{ name: 'byName', keyPath: 'name', unique: true }] },
    {
      name: 'tasks',
      keyPath: 'id',
      indexes: [
        { name: 'byCategory', keyPath: 'categoryId' },
        { name: 'byCompleted', keyPath: 'completed' },
        { name: 'byUpdatedAt', keyPath: 'updatedAt' }
      ]
    }
  ]
},
  hooks: {
    beforeWrite: (ctx) => { trail.push('before:' + ctx.store + ':' + ctx.type); },
    afterCommit: (events) => { trail.push('commit:' + events.length); }
  }
});
await client.open();
await client.table('categories').add({ id: 'docs', name: 'Docs', color: '#0f766e' });
await client.table('tasks').add({
  id: 'docs-1',
  title: 'Passes through hooks',
  categoryId: 'docs',
  completed: false,
  priority: 'medium',
  createdAt: 1,
  updatedAt: 1
});
return { trail, task: await client.table('tasks').get('docs-1') };
Dados frontend

Erros

Estado relacional local, workers, eventos e exemplos de UI com IndexedDB.

Prova
Componentes frontend podem ouvir eventos de dados em vez de fazer polling ou duplicar lógica de storage.
Observe
Configuração de schema, operações de tabela, listeners de eventos, fluxo de workers e pontos de integração com state management.

Erros

Detecte ids duplicados de Category com isCanaErrorCode.

const client = cana.createClient({
  name: dbName,
  schema: {
  version: 1,
  stores: [
    { name: 'categories', keyPath: 'id', indexes: [{ name: 'byName', keyPath: 'name', unique: true }] },
    {
      name: 'tasks',
      keyPath: 'id',
      indexes: [
        { name: 'byCategory', keyPath: 'categoryId' },
        { name: 'byCompleted', keyPath: 'completed' },
        { name: 'byUpdatedAt', keyPath: 'updatedAt' }
      ]
    }
  ]
}
});
await client.open();
await client.table('categories').add({ id: 'docs', name: 'Docs', color: '#0f766e' });
try {
  await client.table('categories').add({ id: 'docs', name: 'Duplicada', color: '#dc2626' });
  return { unexpected: 'no error' };
} catch (error) {
  return {
    isCanaError: cana.isCanaError(error),
    constraint: cana.isCanaErrorCode(error, 'ConstraintViolation'),
    code: error && error.code
  };
}
Dados frontend

Avaliacao de storage

Estado relacional local, workers, eventos e exemplos de UI com IndexedDB.

Prova
Componentes frontend podem ouvir eventos de dados em vez de fazer polling ou duplicar lógica de storage.
Observe
Configuração de schema, operações de tabela, listeners de eventos, fluxo de workers e pontos de integração com state management.

Avaliacao de storage

Leia storageState e durabilityAssessment depois de gravar Task.

const client = cana.createClient({
  name: dbName,
  schema: {
  version: 1,
  stores: [
    { name: 'categories', keyPath: 'id', indexes: [{ name: 'byName', keyPath: 'name', unique: true }] },
    {
      name: 'tasks',
      keyPath: 'id',
      indexes: [
        { name: 'byCategory', keyPath: 'categoryId' },
        { name: 'byCompleted', keyPath: 'completed' },
        { name: 'byUpdatedAt', keyPath: 'updatedAt' }
      ]
    }
  ]
}
});
await client.open();
await client.table('categories').add({ id: 'docs', name: 'Docs', color: '#0f766e' });
await client.table('tasks').add({
  id: 'docs-1',
  title: 'Durable data',
  categoryId: 'docs',
  completed: false,
  priority: 'high',
  createdAt: 1,
  updatedAt: 1
});
const storage = await client.storageState();
const durability = await client.durabilityAssessment();
return { backend: client.backend, storage, durability };
Dados frontend

Operation ledger

Estado relacional local, workers, eventos e exemplos de UI com IndexedDB.

Prova
Componentes frontend podem ouvir eventos de dados em vez de fazer polling ou duplicar lógica de storage.
Observe
Configuração de schema, operações de tabela, listeners de eventos, fluxo de workers e pontos de integração com state management.

Operation ledger

Resolva uma escrita de Task commitada com operation ledger ligado.

const client = cana.createClient({
  name: dbName,
  schema: {
  version: 1,
  stores: [
    { name: 'categories', keyPath: 'id', indexes: [{ name: 'byName', keyPath: 'name', unique: true }] },
    {
      name: 'tasks',
      keyPath: 'id',
      indexes: [
        { name: 'byCategory', keyPath: 'categoryId' },
        { name: 'byCompleted', keyPath: 'completed' },
        { name: 'byUpdatedAt', keyPath: 'updatedAt' }
      ]
    }
  ]
},
  operationLedger: true
});
await client.open();
const tx = await client.transaction('readwrite', ['categories', 'tasks'], async (scope) => {
  const now = Date.now();
  await scope.table('categories').put({ id: 'docs', name: 'Docs', color: '#0f766e' });
  await scope.table('tasks').put({
    id: 'docs-1',
    title: 'Reconcile uncertain write',
    categoryId: 'docs',
    completed: false,
    priority: 'high',
    createdAt: now,
    updatedAt: now
  });
  return 'wrote';
});
const resolved = await client.resolveWrite(tx.correlationId, tx.attemptedAt);
return { outcome: tx.outcome, resolved, task: await client.table('tasks').get('docs-1') };
Dados frontend

Export

Estado relacional local, workers, eventos e exemplos de UI com IndexedDB.

Prova
Componentes frontend podem ouvir eventos de dados em vez de fazer polling ou duplicar lógica de storage.
Observe
Configuração de schema, operações de tabela, listeners de eventos, fluxo de workers e pontos de integração com state management.

Export

Exporte as stores Category e Task como dados puros.

const client = cana.createClient({
  name: dbName,
  schema: {
  version: 1,
  stores: [
    { name: 'categories', keyPath: 'id', indexes: [{ name: 'byName', keyPath: 'name', unique: true }] },
    {
      name: 'tasks',
      keyPath: 'id',
      indexes: [
        { name: 'byCategory', keyPath: 'categoryId' },
        { name: 'byCompleted', keyPath: 'completed' },
        { name: 'byUpdatedAt', keyPath: 'updatedAt' }
      ]
    }
  ]
}
});
await client.open();
const now = Date.now();
await client.table('categories').bulkAdd([
  { id: 'work', name: 'Work', color: '#2563eb', createdAt: now, updatedAt: now },
  { id: 'home', name: 'Home', color: '#16a34a', createdAt: now, updatedAt: now }
]);
await client.table('tasks').bulkAdd([
  {
    id: 'task-1',
    title: 'Write the Cana tutorial',
    categoryId: 'work',
    completed: false,
    priority: 'high',
    createdAt: now,
    updatedAt: now
  },
  {
    id: 'task-2',
    title: 'Review category filters',
    categoryId: 'home',
    completed: true,
    priority: 'medium',
    createdAt: now,
    updatedAt: now + 1
  }
]);
const dump = await client.exportAll();
return dump;
Dados frontend

Selecao de backend

Estado relacional local, workers, eventos e exemplos de UI com IndexedDB.

Prova
Componentes frontend podem ouvir eventos de dados em vez de fazer polling ou duplicar lógica de storage.
Observe
Configuração de schema, operações de tabela, listeners de eventos, fluxo de workers e pontos de integração com state management.

Selecao de backend

Mostre client.backend apos abrir o banco de tasks.

const client = cana.createClient({
  name: dbName,
  schema: {
  version: 1,
  stores: [
    { name: 'categories', keyPath: 'id', indexes: [{ name: 'byName', keyPath: 'name', unique: true }] },
    {
      name: 'tasks',
      keyPath: 'id',
      indexes: [
        { name: 'byCategory', keyPath: 'categoryId' },
        { name: 'byCompleted', keyPath: 'completed' },
        { name: 'byUpdatedAt', keyPath: 'updatedAt' }
      ]
    }
  ]
},
  fallback: 'localStorage'
});
await client.open();
await client.table('categories').add({ id: 'docs', name: 'Docs', color: '#0f766e' });
return { backend: client.backend, categories: await client.table('categories').query() };
Dados frontend

Adapter de factory

Estado relacional local, workers, eventos e exemplos de UI com IndexedDB.

Prova
Componentes frontend podem ouvir eventos de dados em vez de fazer polling ou duplicar lógica de storage.
Observe
Configuração de schema, operações de tabela, listeners de eventos, fluxo de workers e pontos de integração com state management.

Adapter de factory

Use createCanaDatabaseClient com stores Category e Task.

const adapter = cana.createCanaDatabaseClient({
  name: dbName,
  schema: {
  version: 1,
  stores: [
    { name: 'categories', keyPath: 'id', indexes: [{ name: 'byName', keyPath: 'name', unique: true }] },
    {
      name: 'tasks',
      keyPath: 'id',
      indexes: [
        { name: 'byCategory', keyPath: 'categoryId' },
        { name: 'byCompleted', keyPath: 'completed' },
        { name: 'byUpdatedAt', keyPath: 'updatedAt' }
      ]
    }
  ]
}
});
await adapter.connect();
await adapter.stores.categories.add({ id: 'docs', name: 'Docs', color: '#0f766e' });
await adapter.stores.tasks.add({
  id: 'docs-1',
  title: 'Created through the adapter',
  categoryId: 'docs',
  completed: false,
  priority: 'medium',
  createdAt: 1,
  updatedAt: 1
});
const task = await adapter.stores.tasks.get('docs-1');
await adapter.disconnect();
return {
  backend: adapter.cana.backend,
  stores: Object.keys(adapter.stores),
  task
};
Dados frontend

Fluxo com worker client

Estado relacional local, workers, eventos e exemplos de UI com IndexedDB.

Prova
Componentes frontend podem ouvir eventos de dados em vez de fazer polling ou duplicar lógica de storage.
Observe
Configuração de schema, operações de tabela, listeners de eventos, fluxo de workers e pontos de integração com state management.

Fluxo com worker client

Use createWorkerHost, createRouter e createWorkerClient com MessageChannel.

const channel = new MessageChannel();
channel.port1.start?.();
channel.port2.start?.();

const broadcasts = [];
const router = cana.createRouter({
  port: channel.port1,
  timeoutMs: 5000,
  onBroadcast(event) {
    broadcasts.push({
      cursor: event.cursor,
      type: event.type,
      store: event.store,
      key: event.key
    });
  }
});

const host = cana.createWorkerHost({
  port: channel.port2,
  name: dbName,
  schema: {
  version: 1,
  stores: [
    { name: 'categories', keyPath: 'id', indexes: [{ name: 'byName', keyPath: 'name', unique: true }] },
    {
      name: 'tasks',
      keyPath: 'id',
      indexes: [
        { name: 'byCategory', keyPath: 'categoryId' },
        { name: 'byCompleted', keyPath: 'completed' },
        { name: 'byUpdatedAt', keyPath: 'updatedAt' }
      ]
    }
  ]
},
  originId: 'docs-worker-host',
  retainedEvents: 20,
  operationLedger: true
});

const workerClient = cana.createWorkerClient(router);

try {
  await workerClient.open();
  const now = Date.now();
  await workerClient.put('categories', {
    id: 'work',
    name: 'Work',
    color: '#2563eb',
    createdAt: now,
    updatedAt: now
  });
  await workerClient.add('tasks', {
    id: 'task-worker-1',
    title: 'Persist through the worker boundary',
    categoryId: 'work',
    completed: false,
    priority: 'medium',
    createdAt: now,
    updatedAt: now
  });
  const tasks = await workerClient.query('tasks', {
    index: 'byCategory',
    equals: 'work'
  });
  const count = await workerClient.count('tasks');
  return {
    ping: await workerClient.ping(),
    count,
    tasks,
    broadcasts
  };
} finally {
  await workerClient.close();
  router.dispose();
  await host.dispose();
  channel.port1.close();
  channel.port2.close();
}
Dados frontend

MVP SPA Dia 1 — registros offline no Cana

Estado relacional local, workers, eventos e exemplos de UI com IndexedDB.

Prova
Componentes frontend podem ouvir eventos de dados em vez de fazer polling ou duplicar lógica de storage.
Observe
Configuração de schema, operações de tabela, listeners de eventos, fluxo de workers e pontos de integração com state management.

MVP SPA Dia 1 — registros offline no Cana

Abra um client Cana IndexedDB real, semeie Category e crie a primeira Task — o fluxo inteiro roda no browser.

const client = cana.createClient({
  name: dbName,
  schema: {
  version: 1,
  stores: [
    { name: 'categories', keyPath: 'id', indexes: [{ name: 'byName', keyPath: 'name', unique: true }] },
    {
      name: 'tasks',
      keyPath: 'id',
      indexes: [
        { name: 'byCategory', keyPath: 'categoryId' },
        { name: 'byCompleted', keyPath: 'completed' },
        { name: 'byUpdatedAt', keyPath: 'updatedAt' }
      ]
    }
  ]
}
});
await client.open();

const now = Date.now();
await client.table('categories').add({
  id: 'work',
  name: 'Work',
  color: '#2563eb',
  createdAt: now,
  updatedAt: now
});
await client.table('tasks').add({
  id: 'task-1',
  title: 'Offline task',
  categoryId: 'work',
  completed: false,
  priority: 'high',
  createdAt: now,
  updatedAt: now
});

return {
  backend: client.backend,
  category: await client.table('categories').get('work'),
  task: await client.table('tasks').get('task-1')
};
Dados frontend

MVP SPA Dia 2 — estado da UI via eventos do Cana e queries indexadas

Estado relacional local, workers, eventos e exemplos de UI com IndexedDB.

Prova
Componentes frontend podem ouvir eventos de dados em vez de fazer polling ou duplicar lógica de storage.
Observe
Configuração de schema, operações de tabela, listeners de eventos, fluxo de workers e pontos de integração com state management.

MVP SPA Dia 2 — estado da UI via eventos do Cana e queries indexadas

Assine eventos de mudança confirmados, escreva e atualize uma Task, depois leia de volta pelo índice byCategory.

const client = cana.createClient({
  name: dbName,
  schema: {
  version: 1,
  stores: [
    { name: 'categories', keyPath: 'id', indexes: [{ name: 'byName', keyPath: 'name', unique: true }] },
    {
      name: 'tasks',
      keyPath: 'id',
      indexes: [
        { name: 'byCategory', keyPath: 'categoryId' },
        { name: 'byCompleted', keyPath: 'completed' },
        { name: 'byUpdatedAt', keyPath: 'updatedAt' }
      ]
    }
  ]
}
});
await client.open();

const seen = [];
const stop = client.subscribe((event) => {
  seen.push({ type: event.type, store: event.store, key: event.key });
});

const now = Date.now();
await client.table('categories').add({
  id: 'work',
  name: 'Work',
  color: '#2563eb',
  createdAt: now,
  updatedAt: now
});
await client.table('tasks').add({
  id: 'task-1',
  title: 'Listen to local changes',
  categoryId: 'work',
  completed: false,
  priority: 'medium',
  createdAt: now,
  updatedAt: now
});
await client.table('tasks').update('task-1', { completed: true, updatedAt: now + 1 });
stop();

const workTasks = await client.table('tasks').query({
  index: 'byCategory',
  equals: 'work'
});

return {
  events: seen,
  workTasks: workTasks.map((task) => ({ title: task.title, completed: task.completed }))
};
Dados frontend

MVP SPA Release — durabilidade ao reabrir

Estado relacional local, workers, eventos e exemplos de UI com IndexedDB.

Prova
Componentes frontend podem ouvir eventos de dados em vez de fazer polling ou duplicar lógica de storage.
Observe
Configuração de schema, operações de tabela, listeners de eventos, fluxo de workers e pontos de integração com state management.

MVP SPA Release — durabilidade ao reabrir

Feche o client e reabra o mesmo banco: os registros offline sobrevivem, provando estado local durável.

const client = cana.createClient({
  name: dbName,
  schema: {
  version: 1,
  stores: [
    { name: 'categories', keyPath: 'id', indexes: [{ name: 'byName', keyPath: 'name', unique: true }] },
    {
      name: 'tasks',
      keyPath: 'id',
      indexes: [
        { name: 'byCategory', keyPath: 'categoryId' },
        { name: 'byCompleted', keyPath: 'completed' },
        { name: 'byUpdatedAt', keyPath: 'updatedAt' }
      ]
    }
  ]
}
});
await client.open();

const now = Date.now();
await client.table('categories').add({
  id: 'work',
  name: 'Work',
  color: '#2563eb',
  createdAt: now,
  updatedAt: now
});
await client.table('tasks').add({
  id: 'task-1',
  title: 'Survives reload',
  categoryId: 'work',
  completed: false,
  priority: 'high',
  createdAt: now,
  updatedAt: now
});
await client.close();

const reopened = cana.createClient({
  name: dbName,
  schema: {
  version: 1,
  stores: [
    { name: 'categories', keyPath: 'id', indexes: [{ name: 'byName', keyPath: 'name', unique: true }] },
    {
      name: 'tasks',
      keyPath: 'id',
      indexes: [
        { name: 'byCategory', keyPath: 'categoryId' },
        { name: 'byCompleted', keyPath: 'completed' },
        { name: 'byUpdatedAt', keyPath: 'updatedAt' }
      ]
    }
  ]
}
});
await reopened.open();
const tasksAfterReopen = await reopened.table('tasks').query();
const categoriesAfterReopen = await reopened.table('categories').query();

return {
  backend: reopened.backend,
  categoriesAfterReopen: categoriesAfterReopen.length,
  tasksAfterReopen: tasksAfterReopen.map((task) => task.title)
};
Mensageria de domínio

Mediator em memória

Composição request/response e eventos entre módulos de domínio independentes.

Prova
Módulos podem trocar dados por contratos sem importar um ao outro diretamente.
Observe
Subjects, payloads, handlers, leituras compostas e caminho de retorno ao chamador.

Mediator em memória

Troque mensagens entre os domínios Category e Task e componha um read model via request/response do mediator.

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
};
Mensageria de domínio

MVP microsserviços Dia 1 — contrato sobre o mediator in-memory

Composição request/response e eventos entre módulos de domínio independentes.

Prova
Módulos podem trocar dados por contratos sem importar um ao outro diretamente.
Observe
Subjects, payloads, handlers, leituras compostas e caminho de retorno ao chamador.

MVP microsserviços Dia 1 — contrato sobre o mediator in-memory

Prove request/response e publish/listen no mediator in-memory real, incluindo o erro explícito para contrato desconhecido.

const mediator = api.createInMemory();
const receivedEvents = [];

await mediator.subscribe('tasks.created.v1', async (event) => {
  receivedEvents.push(event.payload.title);
});

mediator.registerHandler('tasks.create.v1', async (message) => {
  const task = {
    id: crypto.randomUUID(),
    title: message.payload.title,
    categoryId: message.payload.categoryId,
    completed: false
  };
  await mediator.publish({ name: 'tasks.created.v1', payload: task });
  return { ok: true, result: task };
});

const created = await mediator.request({
  contract: 'tasks.create.v1',
  payload: { title: 'Notify assignee', categoryId: 'work' }
});
const unknown = await mediator.request({
  contract: 'tasks.unknown.v1',
  payload: {}
});

return {
  createdTask: created.result.title,
  eventDelivered: receivedEvents,
  unknownContractError: unknown.error
};
Port de estado local

Chave/valor em memória

Pequenos registros de estado com o mesmo formato contratual usado por adaptadores substituíveis.

Prova
A infraestrutura continua substituível enquanto consumidores mantêm service results estáveis.
Observe
Comportamento create/read/update/delete e envelopes de erro previsíveis.

Chave/valor em memória

Guarde preferências da lista de Task com o mesmo formato de resposta usado pelos adaptadores do pacote.

const client = api.createInMemory();
await client.connect();

await client.set('ui:selected-category', {
  id: 'work',
  name: 'Work',
  visibleTaskIds: ['task-1', 'task-3']
});
await client.set('ui:last-sort', 'priority-desc');

const selectedCategory = await client.get('ui:selected-category');
const lastSort = await client.get('ui:last-sort');
await client.del('ui:last-sort');
const deletedSort = await client.get('ui:last-sort');
await client.disconnect();

return {
  selectedCategory: selectedCategory.result,
  lastSort: lastSort.result,
  deletedSort: deletedSort.result
};
Guarda de concorrência

Mutex com KV em memória

Locks por Category em volta dos fluxos de Task.

Prova
Um caso de uso pode proteger trabalho compartilhado sem acoplar o domínio ao storage.
Observe
Aquisição de lock, rejeição, release e service-result retornado ao controller.

Mutex com KV em memória

Proteja uma atualização de Category enquanto dois escritores de Task competem pelo mesmo recurso.

const keyValue = api.createKeyValueStorage();
const mutex = api.create(keyValue);

const firstWriter = await mutex.lock('category', 'work');
const secondWriter = await mutex.lock('category', 'work');
const lockedBeforeRelease = await mutex.isLocked('category', 'work');
await mutex.unlock('category', 'work');
const lockedAfterRelease = await mutex.isLocked('category', 'work');

return {
  firstWriter: firstWriter.result,
  secondWriter: secondWriter.result,
  lockedBeforeRelease: lockedBeforeRelease.result,
  lockedAfterRelease: lockedAfterRelease.result
};
Contrato consumidor

Cliente REST com fetch mock

Um consumidor client-side chamando o mesmo comportamento de Task por uma superfície de API estável.

Prova
Consumidores podem depender de contratos gerados em vez de detalhes de transporte escritos à mão.
Observe
Formato de request, formato de response, tratamento de erro e pontos de integração tipados.

Cliente REST com fetch mock

Chame operações OpenAPI de Task com um cliente mock seguro para browser.

const client = api.createMockClient();

const created = await client.request({
  operationId: 'createTask',
  method: 'POST',
  path: '/tasks',
  body: {
    id: 'task-1',
    title: 'Generate REST SDK example',
    categoryId: 'work',
    completed: false
  }
});
const listed = await client.request({
  operationId: 'listTasks',
  method: 'GET',
  path: '/tasks?categoryId=work'
});

return {
  created,
  listed
};
Contrato consumidor

Cliente WS com socket fake

Um consumidor client-side chamando o mesmo comportamento de Task por uma superfície de API estável.

Prova
Consumidores podem depender de contratos gerados em vez de detalhes de transporte escritos à mão.
Observe
Formato de request, formato de response, tratamento de erro e pontos de integração tipados.

Cliente WS com socket fake

Use o contrato do cliente realtime para criar e listar registros Task no browser.

const client = api.createFakeClient();
const status = await client.connect();

const created = await client.request({
  operationId: 'tasks.create',
  input: {
    id: 'task-1',
    title: 'Render realtime updates',
    categoryId: 'home',
    completed: false
  }
});
const listed = await client.request({
  operationId: 'tasks.list',
  input: { categoryId: 'home' }
});
const closed = await client.disconnect();

return {
  status,
  created,
  listed,
  closed
};
Modelo de design

Validar um design

Formato do domínio, entidades e relações antes do código runtime.

Prova
O modelo da UI pode virar contratos e exemplos executáveis.
Observe
Modelagem de Category e Task, saída de validação e dados prontos para contrato.

Validar um design

Normalize o modelo de exemplo e colete issues de validação (API real do designer-core).

const raw = api.buildSampleModelPayload();
const state = api.normalizeStatePayload(raw);
const issues = api.collectModelIssues(state);
const errors = issues.filter((issue) => issue.severity === 'error');
return {
  ok: errors.length === 0,
  issueCount: issues.length,
  errorCount: errors.length,
  sample: issues.slice(0, 3)
};

Superfícies do produto

O que o Jumentix entrega para cada equipe

O site não deve se esconder atrás de linguagem vaga de plataforma: o Jumentix é um conjunto conectado de ferramentas, pacotes, templates, contratos e regras de release.

Modelo

Domain Designer

Desenhe contextos delimitados, entidades, campos, relacionamentos, regras de validação, composição OpenAPI, exportações AsyncAPI e pacotes boilerplate em um workspace.

Construa

Backend Template

Comece com Users, Auth, RBAC, tenancy, controllers, casos de uso, ports, adaptadores, contratos de erro e perfis de runtime testáveis.

Consuma

SDKs gerados

Clientes REST, WebSocket e gRPC leem os contratos canônicos para frontend e backend compartilharem o mesmo vocabulário de API.

Opere

Governança e caminhos de deploy

Gates de qualidade, política de dependências, perfis de serviço, Docker, PM2, alvos serverless e evidências de release tornam a adoção auditável.

Qualidade como superfície do produto

Requisitos, testes e evidências não ficam escondidos

O Jumentix trata qualidade como parte da promessa visível do produto. Requisitos são checados, testes são mapeados, regras de arquitetura são executáveis e caminhos de publicação carregam evidências em vez de depender de cerimônia.

99/90padrão de statements/branches
reqsrequisitos ligados a checagens executáveis
0tolerância a drift oculto de docs
realtestes browser para APIs browser

Registro de requisitos

O trabalho de entrega conecta comportamento, docs, testes e evidência de release para que “pronto” seja auditável.

requirements:check

Testes confiáveis

Unit, integração, route sweep Cypress, specs de browser e smoke de Storybook cobrem a superfície real de cada camada.

test-map:check

Arquitetura executável

Scripts de limite rejeitam imports e atalhos que vazariam frameworks, bancos ou infraestrutura para o domínio.

arch:check-*

Disciplina de publicação

Sync de conteúdo, checagens de rota, dry-runs de pacote e governança de release rodam antes de artefatos públicos avançarem.

website:test:prepublish
bun run requirements:check
bun run docs:consumers:package-scripts
bun run website:test:prepublish

Plataforma pronta para AI

A UI entrega arquitetura com governança acoplada

O Jumentix é preparado para AI porque o produto não obriga agentes a inferir arquitetura a partir de código espalhado. A UI captura contextos delimitados, entidades, interfaces, perfis de deploy e requisitos; o repositório expõe docs legíveis por agentes e checagens executáveis que tornam trabalho gerado auditável.

UImodelo de domínio como input de primeira classe
llmscontexto do site legível por agentes
reqsrequisitos ligados a checks
gatesevidência de arquitetura e publish
1. Modele o serviço

A UI captura o domínio

Category, Task, relacionamentos, validações e superfícies de API viram dados explícitos da plataforma.

2. Fundamente o agente

Docs e pacotes nomeiam o caminho

O agente lê índice de docs, contratos de pacote e blueprint da UI antes de escolher arquivos para alterar.

3. Gere dentro dos limites

Ports e adapters moldam o código

Casos de uso, controllers, SDKs, exemplos Cana e perfis de runtime preservam seus limites de ownership.

4. Verifique evidências

Checks de governança capturam drift

Requisitos, test maps, scripts de arquitetura, checagens de rota e gates prepublish validam a mudança.

5. Publique com confiança

A PR carrega prova

Revisores veem o que mudou, por que cabe na arquitetura e quais checks provam que está pronto.

Instruções de baixo contexto

Agentes podem partir do índice de docs, páginas de pacote, metadados de rotas e snippets de código em vez de adivinhar qual arquivo possui um comportamento.

/llms-full.txt

UI como input de arquitetura

A UI de Service Management transforma conceitos de produto em contextos, interfaces e escolhas de deploy que geradores e revisores conseguem inspecionar.

service-management-ui

Geração governada

Mudanças geradas ou escritas por agentes ainda passam por requisitos, test maps, rotas, limites de pacote e governança de release antes de publicar.

requirements:check

Grafo de pacotes fundamentado

Pacotes reutilizáveis dão ao trabalho de AI nomes estáveis para persistência, mediação, clientes, Cana, bootstrap de runtime e limites arquiteturais.

packages/*

Trabalho de AI vira entrega governada

A UI entrega para a AI um ponto de partida restrito: vocabulário do serviço, limites, interfaces, perfil de runtime e obrigações de qualidade são explícitos antes de qualquer prompt. Isso transforma o agente de um gerador por chute em um contribuidor dentro das regras da plataforma.

  • Prompts referenciam contextos delimitados, entidades, requisitos e pacotes nomeados em vez de desejos amplos de implementação.
  • Código gerado tem destino conhecido: modelo da UI, contratos, SDKs, casos de uso, adaptadores, testes e docs.
  • Revisores podem rejeitar drift com checagens executáveis em vez de depender apenas de review arquitetural manual.
Ação de AIFonte da UIGuarda-corpo de governançaSaída útil
Desenhar com segurançaContexto delimitado, entidades, relacionamentos e requisitos.Validação do modelo de serviço e registro de requisitos.Uma spec de serviço que produto, arquitetura e engenharia revisam juntas.
Gerar com segurançaInterfaces, eventos, perfil de deploy e escolhas de pacote.OpenAPI, AsyncAPI, checagens de rota e limites de workspace.Contratos, SDKs, handlers e exemplos alinhados ao mesmo modelo.
Publicar com evidênciaExpectativas de qualidade, caminho de release e índice de docs.Test map, checagens de arquitetura, sync de docs e gates prepublish.Uma PR que revisores auditam com prova concreta em vez de confiança narrativa.
{
  "boundedContext": "Tasks",
  "entities": [
    { "name": "Category", "fields": ["id", "name", "color"] },
    { "name": "Task", "fields": ["id", "title", "categoryId", "completed"] }
  ],
  "interfaces": ["REST", "WebSocket"],
  "deploymentProfiles": ["dev", "staging", "production"],
  "requirements": ["REQ-TASK-CATEGORY", "REQ-TASK-LIVE-UPDATES"],
  "governanceChecks": [
    "requirements:check",
    "test-map:check",
    "arch:check-workspace-boundaries"
  ]
}

Componha o runtime em vez de ficar preso a ele

Escolha a interface e a infraestrutura adequadas a cada serviço. Domínio e casos de uso permanecem atrás de ports estáveis, então a equipe pode testar Express localmente, publicar um handler serverless depois ou mover um contexto delimitado para um worker sem reescrever comportamento de negócio.

  • Variáveis de ambiente selecionam adaptadores; casos de uso não importam tipos de framework.
  • Os mesmos contratos alimentam documentação, validação, SDKs e governança de rotas.
  • Cada pacote tem limite de ownership e expectativa de teste antes do release.
const database = await compileDatabaseClient({
  driver: process.env.JUMENTIX_DATABASE_DRIVER,
});

const taskStore = database.stores.Tasks;
await taskStore.create(task);

Modelo operacional

Quem usa o quê na plataforma

O Jumentix só é útil quando cada papel enxerga sua parte do sistema de entrega. Esta matriz conecta a superfície do produto ao trabalho diário.

PapelQuando usaSuperfície JumentixResultado
Product ownerUma nova capacidade SaaS precisa de escopo claro antes da implementação.Domain Designer, documentação exportada do modelo, blueprints de casos de uso.Itens de backlog mapeiam para limites de domínio em vez de tarefas técnicas soltas.
Engenharia backendUm serviço precisa de rotas, validação, persistência, auth ou comportamento realtime.Backend template, ports/adapters, contratos OpenAPI e AsyncAPI.Trabalho de feature fica dentro dos módulos de aplicação e domínio.
Engenharia frontendUma SPA/PWA precisa de acesso tipado a APIs e fluxos offline.SDKs gerados, Cana, pacotes de integração React/Vue, exemplos prontos para IndexedDB.Estado da UI e contratos do servidor permanecem sincronizados.
Time de plataformaVárias squads precisam dos mesmos padrões sem governança copiada à mão.Gates por branch, políticas de workspace, limites de pacotes, perfis de deploy.Golden paths reutilizáveis com evidência para revisões de segurança e qualidade.

Operação com PM2

Serviços Bun ganham perfis supervisionados

O Jumentix usa PM2 quando uma VM ou container precisa de supervisão de processo: perfis por ambiente, logs, status, métricas, reloads e recuperação no startup. Os arquivos ecosystem deixam explícitos os processos REST, WebSocket, gRPC e Service Management.

4famílias de processo nomeadas
3perfis dev/staging/production
buninterpreter PM2 nos ecosystem files
reloadcaminho zero-downtime para serviços compatíveis

Por que PM2 pertence à história do Jumentix

PM2 não é a única opção de deploy, mas é uma ponte operacional forte para times que rodam serviços Node/Bun em máquinas persistentes. Ele daemoniza apps, reinicia processos com falha, expõe logs e métricas, suporta cluster mode e preserva a lista de processos após restarts.

  • Perfis são específicos por ambiente em vez de ficarem escondidos no histórico do shell.
  • Fallback REST, realtime e gRPC podem iniciar, ser inspecionados e reiniciados de forma independente.
  • Bun permanece como interpreter, então a mesma escolha de runtime move scripts de dev e serviços supervisionados.
  • O modelo PM2 complementa Docker, serverless e cloud functions sem mudar código de domínio.
bun run pm2:start:dev:restapi
bun run pm2:start:staging:websocket-rest
bun run pm2:start:prod:grpc-rest

Fluxo de arquitetura

Mudanças permanecem próximas da feature

01Adaptador de interfaceValida e traduz requests externos.
02ControllerSeleciona o caso de uso da aplicação.
03Caso de uso e domínioAplica comportamento de negócio e contratos.
04Adaptador de saídaPersiste, publica ou integra.

Construa o produto. Preserve a arquitetura.

Explore o código, execute a fábrica localmente e transforme seu próximo serviço Node.js em uma capacidade repetível de plataforma.