Sofya Developers

Como declaro e troco eventos tipados de app?

Como declaro e troco eventos tipados de app?

Pergunta: "como declaro e troco eventos tipados de app?"

defineContract — forma é dado, tipo deriva

import { defineContract } from '@sofya-sdk/event-room';

const contract = defineContract({
  transcription: { text: 'string', partial: 'boolean' },
  control: { action: 'string' },
});

// contract.events.transcription.validate(value) -> {success, data} | {success:false, issues?}
// O tipo do payload é inferido da forma, sem repetir a declaração em TypeScript à mão.

A mini-linguagem de forma aceita 'string', 'number', 'boolean' (e as variantes opcionais 'string?'/'number?'/'boolean?'), objetos aninhados, e arrays ([T] = array de T; [A, B] = união de alternativas).

Escape hatch: Standard Schema V1

Se você já tem um validador Standard Schema (Zod, Valibot, etc.), passe-o direto no lugar da forma:

import { z } from 'zod';

const contract = defineContract({
  transcription: z.object({ text: z.string(), partial: z.boolean() }),
});

O validador precisa ser síncronodefineContract lança usage (contract_async_validator) se detectar um validador que retorna Promise.

Opt-out total: payload<T>()

Para um evento cujo formato você não quer validar em runtime, mas ainda quer o tipo em TypeScript:

import { payload } from '@sofya-sdk/event-room';

const contract = defineContract({
  raw: payload<{ anything: unknown }>(),
});

Sem validação de runtime — e um custo específico de rede de proteção, ver a nota abaixo.

Lado do host: send/on, do mesmo contrato

Passe o mesmo contract para createHostroom.send/room.on trocam eventos de app pelo lado do host, simétrico ao guest.sendEvent acima:

const { room } = await createHost({
  url: 'ws://localhost:8080/ws',
  credential: 'dev-host-credential',
  guestUrl: 'https://guest.example.com/join',
  contract,
  room: { accessMode: 'approval_and_code' },
});

room.on('transcription', (payload) => {
  console.log(payload.text, payload.partial);
});

room.send('control', { action: 'pause' });

Sem contract em createHost, room.send/room.on lançam usage (contract_required) — os dois verbos só existem de verdade quando o host declarou o que fala.

Limite: 16 KiB de payload

O limite é do runtime do servidor, não do contrato — MAX_EVENT_PAYLOAD_BYTES (16 KiB), medido no JSON compacto do payload sozinho. getEventPayloadBudgetBytes(name) devolve o orçamento exato já descontando a moldura do envelope para aquele nome de evento; guest.sendEvent/room.send medem o envelope serializado exatamente como o servidor mede e lançam usage (payload_too_large_client) síncrono, antes de tocar o socket, se passar do limite. A lib nunca fatia o payload — encolher é responsabilidade do app.

O handshake carrega {protocolVersion, contractFingerprint}

Antes de qualquer evento de app trafegar, host e guest trocam um handshake com a versão de protocolo (ver versionamento-e-deploy.md) e o fingerprint do contrato — um mapa nome do evento → digest da forma (EventContract['digestMap']). O algoritmo: FNV-1a rodado duas vezes com seeds diferentes sobre a forma canonicalizada (chaves de objeto ordenadas, tokens primitivos já codificando opcionalidade e aninhamento) — determinístico entre engines, sem depender de uma API de crypto. Eventos declarados via Standard Schema ou payload<T>() entram no mapa como 'opaque' em vez de um digest real.

A comparação (compareFingerprints) resolve cada nome em uma de cinco categorias — só meu contrato, só do peer, comparado por forma, comparado só por nome (quando um dos lados é opaco), ou divergente (mesmo nome, formas concretas diferentes — fatal, ver maquina-de-estados.md). O resultado do handshake é dado consultável, não log — é assim que se descobre, em runtime, que um evento perdeu a rede de proteção do fingerprint (ver seguranca.md para o custo da forma opaca).

Guarda de envio: nome que o peer não conhece, fora do contrato, ou payload inválido

sendEvent/send lançam, sempre síncrono, antes de tocar o socket:

  • event_unknown_to_peer (usage) — o fingerprint do peer não declara esse nome. Só existe depois do handshake fechar (ver erros.md para o porquê disso não contradizer "usage é erro de programação").
  • event_not_in_contract (usage, só do lado do host) — o nome não está no contract local.
  • event_payload_invalid (usage) — o payload não bate com a forma declarada para esse nome.
  • contract_required (usage, só do lado do host) — room.send/room.on chamados sem contract em createHost.

Do lado do recebimento, um evento que o contrato local não conhece (ou cujo payload não bate) nunca é entregue a um handler — vira observação não-terminal event_out_of_contract (infra) via onObservation, nunca silenciada.

Last updated on

On this page