Bloqueios de Recursos
1 - Visão geral
O sistema Resource Lock oferece proteção colaborativa de edição ao impedir que múltiplos usuários editem simultaneamente a mesma tela ou composição. Isso garante a integridade dos dados e previne conflitos em ambientes multiusuário.
Principais Recursos
- Gerenciamento Automático de Bloqueios: Recursos são automaticamente bloqueados quando abertos para edição
- Indicadores Visuais: Feedback claro da interface mostrando quem está editando o quê
- Proteção contra Tempo Morto: Bloqueios expiram automaticamente após 5 minutos de inatividade
- Mecanismo de Batimentos Cardíacos: Sessões de edição ativa renovam automaticamente os bloqueios
- Liberação forçada: Administradores podem liberar bloqueios à força quando necessário
- Atualizações em tempo real: Atualizações de status do bloqueio via mecanismo de polling
- Desabilitação Abrangente: Todas as superfícies de edição são desativadas quando bloqueadas por outro usuário
Recursos Suportados
- Screens: Screens individuais de aplicação
- Composições: Composições componentes reutilizáveis
2 - Como Funciona
Ciclo de Vida da Trava

Fluxo de Aquisição de Locks
- Usuário Abre Recurso: O usuário navega até uma tela ou composição
- Solicitação de Bloqueio: A interface envia a solicitaçãoPOST /api/locks/acquire
- Verificação de Bloqueio: O backend verifica se o recurso já está bloqueado
- Conceda ou negue:
- Se desbloqueado: O bloqueio é concedido e armazenado no banco de dados
- Se bloqueado pelo mesmo usuário: O bloqueio é concedido (reentrada permitida)
- Se bloqueado por outro usuário: Solicitação negada com detalhes do bloqueio
- Batimentos cardíacos iniciados: O frontend começa a enviar batimentos cardíacos a cada 4 segundos
- Atualizações da interface: O estado travado é refletido em todos os componentes da interface
Mecanismo do Batimento Cardíaco
O mecanismo do batimento cardíaco mantém as travas ativas durante a edição ativa:
- Intervalo: A cada 4 segundos
- Ponto final: POST /api/locks/renew
- Propósito: Estende o tempo de expiração do bloqueio
- Automático: Executa em segundo plano enquanto o recurso está aberto
- Limpeza: Para quando o usuário navega para fora ou fecha o recurso
Expiração da Fechadura
Fechaduras expiram automaticamente após 5 minutos sem batimentos cardíacos:
- Tempo: 300 segundos (5 minutos)
- Propósito: Evitar que travas obsoletas bloqueiem recursos
- Limpeza Automática: O backend remove automaticamente fechaduras vencidas
- Período de Carência: Garante que bloqueios não persistam caso o usuário feche o navegador inesperadamente
3 - Implementação Backend
Esquema de Banco de Dados
CREATE TABLE lowcode_studio.resource_locks (
id TEXT PRIMARY KEY,
resource_type TEXT NOT NULL, -- 'screen' or 'composition'
resource_id TEXT NOT NULL,
locked_by TEXT NOT NULL, -- User ID
locked_at BIGINT NOT NULL, -- Unix timestamp in milliseconds
expires_at BIGINT NOT NULL, -- Unix timestamp in milliseconds
UNIQUE(resource_type, resource_id)
);
CREATE INDEX idx_resource_locks_resource
ON lowcode_studio.resource_locks(resource_type, resource_id);
CREATE INDEX idx_resource_locks_expires
ON lowcode_studio.resource_locks(expires_at);API Endpoints
Adquirir Lock
POST /api/locks/acquire
Content-Type: application/json
{
"resourceType": "screen",
"resourceId": "uuid-here",
"userId": "user-id-here"
}Resposta (Sucesso - 200):
{
"success": true,
"lock": {
"id": "lock-id",
"resourceType": "screen",
"resourceId": "uuid-here",
"lockedBy": "user-id-here",
"lockedByUser": {
"id": "user-id-here",
"firstName": "John",
"lastName": "Doe"
},
"lockedAt": 1737648000000,
"expiresAt": 1737648300000
}
}Resposta (Já Bloqueada - 409):
{
"success": false,
"error": "Resource is locked by another user",
"lock": {
"id": "lock-id",
"resourceType": "screen",
"resourceId": "uuid-here",
"lockedBy": "other-user-id",
"lockedByUser": {
"id": "other-user-id",
"firstName": "Jane",
"lastName": "Smith"
},
"lockedAt": 1737647900000,
"expiresAt": 1737648200000
}
}Renovar Trava
POST /api/locks/renew
Content-Type: application/json
{
"resourceType": "screen",
"resourceId": "uuid-here",
"userId": "user-id-here"
}Resposta (200):
{
"success": true,
"expiresAt": 1737648600000
}Trava de Liberação
POST /api/locks/release
Content-Type: application/json
{
"resourceType": "screen",
"resourceId": "uuid-here",
"userId": "user-id-here"
}Resposta (200):
{
"success": true
}Trava de Liberação Forçada
POST /api/locks/force
Content-Type: application/json
{
"resourceType": "screen",
"resourceId": "uuid-here"
}Resposta (200):
{
"success": true
}Conseguir Fechaduras por Recurso
GET /api/locks?resourceType=screen&resourceId=uuid-hereResposta (200):
{
"locks": [
{
"id": "lock-id",
"resourceType": "screen",
"resourceId": "uuid-here",
"lockedBy": "user-id",
"lockedByUser": {
"id": "user-id",
"firstName": "John",
"lastName": "Doe"
},
"lockedAt": 1737648000000,
"expiresAt": 1737648300000
}
]
}4 - Implementação Frontend
Arquitetura de Loja
O sistema de bloqueio utiliza sinais Preact para gerenciamento de estado reativo:
Arquivo: client/src/features/project/stores/resource-locks.ts
// Global signals
export const resourceLocks = signal<Map<string, ResourceLock>>(new Map());
export const lockHeartbeats = signal<Map<string, number>>(new Map());
// Computed signal for editing state
export const isEditingDisabled = computed(() => {
const currentActive = editMode.value === "composition"
? activeComposition.value
: activeScreen.value;
if (!currentActive) return false;
const lockStatus = getResourceLockStatus(
editMode.value === "composition" ? "composition" : "screen",
currentActive.uuid,
userProfile.value?.id || ""
);
return lockStatus.isLocked && !lockStatus.isOwnedByCurrentUser;
});Funções de Trava
adquireResourceLock
Tentativas de obter um bloqueio em um recurso:
const result = await acquireResourceLock('screen', screenUuid, userId);
if (result.success) {
// Lock acquired, start heartbeat
} else {
// Lock denied, show notification
}renewResourceLock
Renova uma fechadura existente (chamada por batimentos cardíacos):
await renewResourceLock('screen', screenUuid, userId);releaseResourceLock
Libera um bloqueio quando o usuário termina de editar:
await releaseResourceLock('screen', screenUuid, userId);getResourceLockStatus
Verifica o status atual do bloqueio de um recurso:
const status = getResourceLockStatus('screen', screenUuid, userId);
// Returns: { isLocked, isOwnedByCurrentUser, lock }Componentes da interface
Notificações do Canvas
Arquivo: client/src/features/project/details/components/layout/canvas-notifications.tsx
Exibe notificações de bloqueio e composições órfãs no topo da tela:
- Notificação de Bloqueio: Mostra quem está editando o recurso
- Avatar do Usuário: Avatar codificado por cores com base no ID do usuário
- Exibição de Horas: Mostra quando a fechadura foi adquirida
- Ícone de Cadeado: Indicador visual no final do banner
Lista de Tela
Arquivo: client/src/features/project/details/components/left-panel/screen-list.tsx
Mostra indicadores de bloqueio ao lado das telas bloqueadas:
- Ícone de Cadeado: Ícone pequeno de cadeado ao lado do nome de usuário
- Avatar do Usuário: Avatar minúsculo mostrando quem está com o bloqueio
- Dica de ferramenta: Passe o mouse para ver o nome completo do usuário
Árvore de Elementos
Arquivo: client/src/features/project/details/components/left-panel/elements-tree/
Desativa manipulação de elementos quando bloqueado:
- Botão de Adicionar: Oculto quando bloqueado
- Arrastar e Soltar: Desativado quando bloqueado
- Menu de Contexto: Vazio quando bloqueado
- Reordenamento: Impedido quando travado
Painel Direito
Arquivo: client/src/features/project/details/components/right-panel/index.tsx
Torna as abas legíveis, mas o conteúdo não editável:
- Abas : Permanecam clicáveis para navegação
- Conteúdo: Desativado com pointer-events-none opacity-60
- Todos os Painéis: Propriedades, Estilos, Encadernações, Documentação, Configurações
Ações de Tela
Arquivo: client/src/features/project/details/components/renderer/index.tsx
Ocultar botões de ação de elemento quando bloqueados:
- Mover-se para cima/baixo: Oculto
- Excluir: Oculto
- Cópia: Oculta
- Largura do Rótulo: Definido para 0 quando bloqueado
Estratégia de Pesquisa
O sistema utiliza polling adaptativo para verificar o status do bloqueio:
// With active locks: Poll every 5 seconds
// Without locks: Poll every 30 seconds
const refetchInterval = hasActiveLocks ? 5000 : 30000;
useQuery({
queryKey: ['resource-locks', projectId],
queryFn: () => fetchResourceLocks(projectId),
refetchInterval,
staleTime: 0,
});5 - Experiência do Usuário
Ao abrir um recurso bloqueado
- Navegação: Usuário clica em uma tela/composição bloqueada
- Checagem de Bloqueio: Sistema verifica se o recurso está bloqueado
- Notificação: Banner aparece no topo da tela
- UI desativada: Todos os controles de edição estão desativados
- Modo Somente Leitura: O usuário pode visualizar, mas não editar
Indicadores Visuais
Faixa de Fechadura (Tela)
Características: * Fundo vermelho () * Avatar do usuário com fundo codificado por cores * Exibição do nome completo * Tempo desde que a fechadura foi adquirida * Ícone de cadeado no final bg-red-50 border-red-200
Indicador de Lista de Tela
Características: * Ícone pequeno de cadeado ao lado do nome * Avatar de usuário pequeno (8px) * Dica de ferramenta ao passar o mouse mostrando o nome completo
Quando outro usuário adquire o bloqueio
- Detecção de Polling: Frontend detecta novo lock via polling
- Atualização da interface: Indicadores de trava aparecem imediatamente
- Edição desativada: Todos os controles se tornam não interativos
- Edições Atuais: As alterações locais do usuário permanecem, mas não podem ser salvas
- Notificação: Notificação opcional de toast (não implementada atualmente)
Quando a Tranca é Liberada
- Detecção de Liberação: O sondamento detecta remoção de trava
- Remoção de notificações: Banner de bloqueio desaparece
- UI Ativada: Todos os controles de edição ficam ativos
- Auto-Travamento: Se o usuário ainda estiver no recurso, ele pode adquirir o bloqueio
6 - Configuração
Tempo de Espera do Bloqueio
Altere o tempo de validade da fechadura:
Backend ():server/src/routes/locks/handlers.ts
const LOCK_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutesIntervalo de Batimentos cardíacos
Altere a frequência com que os cadeados são renovados:
Frontend ():client/src/features/project/stores/resource-locks.ts
const HEARTBEAT_INTERVAL = 4000; // 4 secondsIntervalo de votação
Alterem a frequência de pesquisa de status do bloqueio:
Frontend ():client/src/features/project/queries/resource-locks.ts
const WITH_LOCKS_INTERVAL = 5000; // 5 seconds
const WITHOUT_LOCKS_INTERVAL = 30000; // 30 seconds7 - Solução de problemas
Trava não soltando
Sintomas: Usuário fechou o navegador, mas o bloqueio persiste
Solução: Espere 5 minutos pela expiração automática ou use liberação forçada:
curl -X POST http://localhost:8000/api/locks/force \
-H "Content-Type: application/json" \
-d '{
"resourceType": "screen",
"resourceId": "uuid-here"
}'
Múltiplos usuários veem diferentes estados de bloqueio
Sintomas: Usuários veem informações conflitantes sobre bloqueios
Causa: Intervalo de pesquisa muito longo ou problemas de rede
Soluções:
- Reduzir o intervalo de votação na produção.
- Verifique a conectividade da rede.
- Verificar se o backend está respondendo.
- Verifique a conexão com o banco de dados.
Bloqueio adquirido, mas interface não atualizando
Sintomas: O bloqueio existe no banco de dados, mas a interface mostra desbloqueada
Causa: Problema com o cache de React Query ou sinal que não está sendo atualizado
Soluções:
- Verifique o console do navegador para erros.
- Verificar é chamado em componentes.
- Limpar o cache de Consultas React.
- Atualizar a página useSignals()
Batimentos cardíacos não enviando
Sintomas: O bloqueio expira enquanto o usuário está editando ativamente
Causa: Intervalo de batimentos cardíacos parou ou problemas na rede
Soluções:
- Verifique o console do navegador para erros de rede.
- Verificar se o usuário ainda está autenticado.
- Verifique se o intervalo está sendo liberado incorretamente.
- Verificar se o endpoint backend está funcionando /api/locks/renew
As cores dos avatares são idênticas
Sintomas: Usuários diferentes têm a mesma cor de avatar
Causa: Colisão de hash no algoritmo de geração de cor
Solução: Isso foi corrigido com o hash rolante polinomial. Se ainda ocorrer:
- Verificar se o código mais recente foi implantado
- Função de verificação
- Garantir que está sendo passado para o componente generateAvatarColor()userIdUserAvatar
8. Melhores Práticas
Para desenvolvedores
- Sempre Limpe: Garanta que as travas sejam liberadas quando o componente for desmontado
- Erros de Manipulação: Gerencie com elegância falhas de aquisição de travas
- Use Sinal Calculado: Referência em vez de recalcularisEditingDisabled
- Teste de Concorrência: Teste com múltiplos usuários editando simultaneamente
- Monitore o desempenho: Fique atento a excesso de polling ou chamadas API
Para usuários
- Fechar Trancas de Liberação: Feche recursos ao terminar a edição
- Não force o fechamento: Use a navegação adequada para garantir a limpeza
- Aguarde a expiração: Se a trava estiver presa, espere 5 minutos
- Comunique: Coordena com a equipe quem edita o quê
- Atualize se necessário: Se a interface parecer travada, atualize a página
Para Administradores
- Fechaduras de monitor: Verifique periodicamente se estão desgastadas
- Definir Tempos Razoáveis: Equilíbrio entre usabilidade e disponibilidade de recursos
- Liberação Forçada com Parcimónia: Use apenas quando necessário
- Manutenção do Banco de Dados: Limpeza regular de registros de fechaduras vencidas
- Análise de Log: Monitorar padrões de aquisição de bloqueios para detectar problemas
9 - Melhorias Futuras
Características Planejadas
- Suporte a WebSocket: Notificações de bloqueio em tempo real em vez de sondagem
- Roubo de Fechaduras: Permitir que administradores forcem a aquisição de travas
- Histórico de Bloqueios: Rastreie quem travou recursos e quando
- Análise de Fechaduras: Monitore os padrões de uso das fechaduras
- Cursores Colaborativos: Mostre onde outros usuários estão trabalhando
- Resolução de Conflitos: Mesclar mudanças conflitantes quando múltiplos usuários editam
- Solicitações de Trava: Solicita liberação do bloqueio do atual detentor
- Fila de Bloqueio: Usuários em fila aguardando um recurso travado
Otimizações de desempenho
- Redis Cache: Armazene bloqueios no Redis para acesso mais rápido
- Assinaturas GraphQL: Atualizações em tempo real via assinaturas
- Bloqueio Otimista: Permitir edição com detecção de conflitos
- Pool de Bloqueios: Agrupar múltiplos recursos sob um único bloqueio
- Pesquisa Inteligente: Ajuste a frequência das pesquisas com base na atividade
