Criou um grupo de tarefas. Algures, um utilizador está a olhar para um spinner e "a traduzir para 14 idiomas…" é verdade, mas inútil — nunca avança. O que quer é ver a contagem subir à frente dele: 3 prontos, depois 4, depois um idioma com falha, e no fim concluído.
Fazer polling ao grupo de tarefas leva-o até lá, mas é ruidoso, e cada pedido devolve-lhe um novo snapshot que tem de comparar com o anterior para perceber o que realmente mudou. O WebSocket inverte isso. Liga-se uma vez e o servidor envia um evento sempre que um idioma fica resolvido — e cada mensagem inclui o estado completo do grupo, por isso apresenta o snapshot e nunca precisa de reconciliar um delta. Se perder um frame, voltar a ligar-se ou reiniciar o separador, a mensagem seguinte volta a trazer a verdade completa.
GET /jobs/localization/groups/:groupId/wsAinda não conhece a localização assíncrona? Comece pela Visão geral. O groupId aqui é o que recebeu quando criou as tarefas.
Nesta página
- Tipos de mensagem
- Payloads das mensagens
- Como integrá-lo na sua interface
- Mantenha a sua chave de API no servidor
Tipos de mensagem#
Há quatro tipos de mensagem a circular no socket. Cada um indica o que acabou de acontecer e, ao mesmo tempo, entrega o estado atual do grupo inteiro.
| Tipo | Quando | Campos principais |
|---|---|---|
snapshot | Na ligação inicial | Estado completo do grupo |
job.completed | Um idioma termina com sucesso | jobId, locale, mais o estado completo do grupo |
job.failed | Um idioma falha | jobId, locale, error, mais o estado completo do grupo |
group.completed | Todas as tarefas foram resolvidas | groupId, status, mais o estado completo do grupo. O servidor fecha a ligação após esta mensagem. |
Cada mensagem contém um objeto snapshot com o estado atual do grupo: totalJobs, completedJobs, completedWithWarningsJobs, failedJobs e um mapa jobs indexado pelo ID da tarefa, cada um com o respetivo locale e status. Estas contagens são as mesmas que o endpoint do grupo de tarefas devolve — por isso, um snapshot vindo do socket e um poll ao endpoint REST concordam sobre até onde o grupo já avançou.
apresente o snapshot, nunca reconcilie
Nunca precisa de controlar que eventos já viu, reproduzir mensagens em falta ou fundir uma atualização parcial no estado local. Leia snapshot em cada mensagem e desenhe a sua interface a partir daí. Ao voltar a ligar-se, snapshot é reenviado primeiro, por isso um cliente que acabou de entrar e outro que esteve a ouvir o tempo todo convergem para o mesmo estado.
Payloads das mensagens#
Estes são os frames exatos que o servidor envia. Os IDs têm formatos reais (ljg_ para o grupo, ljb_ para cada tarefa); o snapshot aparece abreviado com "..." apenas onde repete a estrutura já mostrada.
Ao ligar-se, o servidor envia o estado atual:
{
"type": "snapshot",
"snapshot": {
"groupId": "ljg_A1b2C3d4E5f6G7h8",
"totalJobs": 3,
"completedJobs": 1,
"completedWithWarningsJobs": 0,
"failedJobs": 0,
"jobs": {
"ljb_A1b2C3d4E5f6G7h8": { "locale": "de", "status": "completed" },
"ljb_B2c3D4e5F6g7H8i9": { "locale": "fr", "status": "processing" },
"ljb_C3d4E5f6G7h8I9j0": { "locale": "ja", "status": "queued" }
}
}
}À medida que cada idioma termina, o evento identifica o idioma que mudou e inclui o snapshot atualizado:
{
"type": "job.completed",
"jobId": "ljb_B2c3D4e5F6g7H8i9",
"locale": "fr",
"snapshot": {
"groupId": "ljg_A1b2C3d4E5f6G7h8",
"totalJobs": 3,
"completedJobs": 2,
"completedWithWarningsJobs": 0,
"failedJobs": 0,
"jobs": {
"ljb_A1b2C3d4E5f6G7h8": { "locale": "de", "status": "completed" },
"ljb_B2c3D4e5F6g7H8i9": { "locale": "fr", "status": "completed" },
"ljb_C3d4E5f6G7h8I9j0": { "locale": "ja", "status": "processing" }
}
}
}Uma falha é uma mensagem normal, não uma ligação interrompida. job.failed inclui o idioma e um error, além do mesmo snapshot completo — o idioma com falha mostra status: "failed" no mapa jobs, todos os outros idiomas continuam a chegar, e o socket segue até group.completed:
{
"type": "job.failed",
"jobId": "ljb_C3d4E5f6G7h8I9j0",
"locale": "ja",
"error": "Model timeout after 30 seconds",
"snapshot": { "...": "..." }
}Quando todas as tarefas tiverem sido resolvidas, o servidor envia um evento final e fecha a ligação:
{
"type": "group.completed",
"groupId": "ljg_A1b2C3d4E5f6G7h8",
"status": "completed",
"snapshot": { "...": "..." }
}O status final é completed quando todos os idiomas foram concluídos com sucesso, completed_with_warnings quando todos os idiomas produziram resultado mas uma ou mais etapas opcionais do pipeline falharam em pelo menos um deles, partial quando alguns idiomas tiveram sucesso e outros falharam, e failed quando todos falharam. Para perceber o que cada um destes estados significa para o grupo como um todo, veja Acompanhar um grupo de tarefas.
Apresente a partir do snapshot sempre que encontrar algo que não reconhece
Faça switch sobre os tipos de mensagem que conhece e, para tudo o que não reconhecer, volte a apresentar a partir de snapshot. Cada mensagem inclui um snapshot completo, por isso um cliente que, por defeito, o desenha a partir daí mantém-se correto mesmo num frame para o qual não tem um ramo específico.
Como integrá-lo na sua interface#
O grupo é o seu modelo de progresso. Quando criou as tarefas, o 202 devolveu-lhe um groupId e um array jobs — uma entrada por idioma. Inicialize o seu registo de progresso a partir dessa resposta e terá a estrutura que o socket vai preencher: o total a contabilizar e um contador a começar em zero.
const { groupId, jobs } = await response.json();
await db.translationProgress.create({
contentId: content.id,
groupId,
totalLanguages: jobs.length,
completedLanguages: 0,
});Depois, abra o socket com esse groupId e, em cada mensagem, leia snapshot e volte a desenhar. Veja o contador subir à medida que os idiomas vão ficando concluídos, e pare quando chegar group.completed:
import WebSocket from "ws";
const groupId = "ljg_A1b2C3d4E5f6G7h8";
const ws = new WebSocket(
`wss://api.lingo.dev/jobs/localization/groups/${groupId}/ws`,
{ headers: { "X-API-Key": process.env.LINGO_API_KEY } }
);
ws.on("message", (raw) => {
const event = JSON.parse(raw);
const { snapshot } = event;
switch (event.type) {
case "snapshot":
console.log(`${snapshot.completedJobs}/${snapshot.totalJobs} complete`);
break;
case "job.completed":
console.log(`${event.locale} ready (${snapshot.completedJobs}/${snapshot.totalJobs})`);
break;
case "job.failed":
console.error(`${event.locale} failed: ${event.error}`);
break;
case "group.completed":
console.log(`All translations done: ${event.status}`);
ws.close();
break;
}
});Ao executar num grupo de três idiomas, isto imprime a execução à medida que acontece:
1/3 complete
fr ready (2/3)
ja failed: Model timeout after 30 seconds
All translations done: partialO contador avançou sozinho, um idioma falhou sem derrubar o stream, e partial disse-lhe onde a execução terminou — exatamente o que o seu spinner precisa para se transformar numa barra de progresso a sério. Repare que o loop nunca acumula estado: cada ramo lê a partir de snapshot na mensagem em mãos, por isso o mesmo código está correto na primeira ligação, em cada atualização e ao voltar a ligar-se.
Mantenha a sua chave de API no servidor#
O socket autentica-se com a sua chave de API, a mesma chave com âmbito de organização que os endpoints REST usam. Isto significa que o navegador é o lugar errado para o abrir — uma chave de API em JavaScript do cliente dá acesso a todos os motores da sua organização a qualquer pessoa que veja o código-fonte.
Ligue-se a partir do seu backend, não do navegador
Abra o WebSocket a partir do seu servidor, onde a chave já está, e depois distribua os eventos para o navegador através do seu próprio canal — um WebSocket ou stream de server-sent events que controla. O seu frontend recebe progresso em tempo real; a sua chave nunca sai da sua infraestrutura.
Isto espelha o modelo de webhook: a ligação que toca em Lingo.dev é do lado do servidor, e o que chega ao utilizador é aquilo que a sua própria aplicação decidir encaminhar.
Onde isto se enquadra#
O WebSocket é a vista em tempo real — está associado a um grupo e fecha quando esse grupo termina. Para uma entrega duradoura, de servidor para servidor, que sobreviva ao fecho de um separador ou a um deploy, combine-o com webhooks: o socket alimenta a interface enquanto a execução está no ecrã, o webhook regista cada resultado no momento em que chega. Ligue ambos a partir da mesma chamada de criação e os seus utilizadores veem o progresso à medida que acontece, enquanto o seu backend preserva o resultado independentemente de quem está a ver.
