Skip to content

Idempotência de Mutações

Toda mutação (POST, PATCH) na API do Neemias exige o header Idempotency-Key. Isso garante que uma mesma operação não seja processada mais de uma vez, mesmo que o cliente retente a requisição por timeout ou falha de rede — essencial para a arquitetura offline-first com sincronia assíncrona.

Header Obrigatório

Idempotency-Key: <UUID v4>

O cliente (frontend) gera uma chave UUID v4 única para cada operação de mutação e a envia no header. Se o header estiver ausente, o backend retorna:

json
{
  "error": "VALIDATION_FAILED",
  "message": "Idempotency-Key header is required"
}

Status: 400

Ledger de Idempotência

A tabela idempotency_ledger armazena o registro de cada requisição processada:

sql
CREATE TABLE idempotency_ledger (
  key TEXT NOT NULL,
  user_id TEXT NOT NULL,
  payload_hash TEXT NOT NULL,
  status_code INTEGER NOT NULL,
  response_body TEXT NOT NULL,      -- JSON serializado
  created_at TEXT NOT NULL,
  PRIMARY KEY(key, user_id)
);

A chave composta (key, user_id) garante que a mesma chave de idempotência pode ser reutilizada por usuários diferentes sem conflito.

Função idempotentMutation

Localizada em workers/src/db/queries.ts, esta função encapsula toda a lógica de idempotência:

typescript
async function idempotentMutation(
  idempotencyKey: string | undefined,
  userId: string,
  payload: unknown,
  execute: MutationExecutor, // () => Promise<MutationResult>
): Promise<{ statusCode: number; body: Record<string, unknown> }>;

Fluxo de Execução

1. Verifica se idempotencyKey está presente
   └─ Ausente → 400 VALIDATION_FAILED

2. Calcula hash do payload (payloadHash)
   └─ SHA-256 do JSON canônico

3. Consulta idempotency_ledger por (key, user_id)

4. Se encontrado:
   ├─ payload_hash diferente → 409 IDEMPOTENCY_KEY_REUSED_WITH_DIFFERENT_PAYLOAD
   └─ payload_hash igual → replay detectado, retorna resposta original (status_code + body)

5. Se NÃO encontrado:
   ├─ Executa a mutation (execute())
   ├─ Insere registro no ledger
   └─ Retorna resultado da mutation

Detecção de Replay

Se a mesma chave for usada com o mesmo payload, o backend reconhece como replay e retorna a resposta original — sem reexecutar a operação. Isso é seguro porque o payload é idêntico.

Rejeição de Reuso (409)

Se a mesma chave for usada com um payload diferente, o backend rejeita com 409 Conflict:

json
{
  "error": "IDEMPOTENCY_KEY_REUSED_WITH_DIFFERENT_PAYLOAD",
  "message": "Idempotency key already used with a different payload"
}

Isso impede que um cliente reutilize acidentalmente uma chave para operações diferentes.

Atomicidade com D1.batch()

A operação de negócio e a inserção no ledger são atômicas via D1.batch() (disponível a partir do D1 v0.26.0):

typescript
// Dentro de idempotentMutation
const { statusCode, body, statements = [] } = await execute();

const ledgerStmt = db
  .prepare(
    "INSERT INTO idempotency_ledger(key, user_id, payload_hash, status_code, response_body, created_at) VALUES(?1, ?2, ?3, ?4, ?5, ?6)",
  )
  .bind(idempotencyKey, userId, payloadHashValue, statusCode, JSON.stringify(body), nowISO());

await db.batch([...statements, ledgerStmt]);

O MutationExecutor pode retornar statements D1 adicionais (ex: INSERTs, UPDATEs) que são incluídos no mesmo batch(). Assim, ou tudo persiste (negócio + ledger) ou nada persiste.

Hash de Payload

A função payloadHash() em workers/src/modules/idempotency.ts gera um hash SHA-256 determinístico do payload:

typescript
async function payloadHash(payload: unknown): Promise<string>;

O payload é serializado como JSON canônico (ordenação estável de chaves) antes do hash, garantindo que dois payloads semanticamente idênticos produzam o mesmo hash independentemente da ordem das chaves no objeto original.

Uso no Frontend

No frontend, o EventWriter gera uma chave de idempotência (UUID v4) para cada evento e a armazena na entrada da syncQueue. Quando o syncQueueRepository drena a fila, cada requisição POST /api/v1/sync/events inclui o header Idempotency-Key correspondente.


Fonte: workers/CONTEXT.md + queries.ts

Distribuído sob licença MIT.