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íncrono — defineContract 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 createHost — room.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á nocontractlocal.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.onchamados semcontractemcreateHost.
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