testando ui otimista
Deploy / Deploy (push) Successful in 1m27s
Details
Deploy / Deploy (push) Successful in 1m27s
Details
This commit is contained in:
parent
e08d80c223
commit
ba26dc1fe3
|
|
@ -12,12 +12,14 @@ const pino = require("pino");
|
||||||
const os = require("os");
|
const os = require("os");
|
||||||
const crypto = require("crypto");
|
const crypto = require("crypto");
|
||||||
const { spawn } = require("child_process");
|
const { spawn } = require("child_process");
|
||||||
|
const pendingSaves = new Set();
|
||||||
|
|
||||||
// --- RASTREAMENTO DE USUÁRIOS ---
|
// --- RASTREAMENTO DE USUÁRIOS ---
|
||||||
const activeUsers = {}; // Mapeia socket.id -> { username, room }
|
const activeUsers = {}; // Mapeia socket.id -> { username, room }
|
||||||
|
|
||||||
//import { LOG_SERVER } from "../utils.js"
|
//import { LOG_SERVER } from "../utils.js"
|
||||||
const LOG_SERVER = `/var/www/html/trens/src_mmpSearch/logs/creation_logs/server`
|
const LOG_SERVER = `/var/www/html/trens/src_mmpSearch/logs/creation_logs/server`;
|
||||||
const SESSION_JSON = `/var/www/html/trens/src_mmpSearch/logs/creation_logs/sessions`
|
const SESSION_JSON = `/var/www/html/trens/src_mmpSearch/logs/creation_logs/sessions`;
|
||||||
|
|
||||||
const DEFAULT_PROJECT_XML = `<?xml version="1.0"?>
|
const DEFAULT_PROJECT_XML = `<?xml version="1.0"?>
|
||||||
<!DOCTYPE lmms-project>
|
<!DOCTYPE lmms-project>
|
||||||
|
|
@ -99,7 +101,6 @@ const DEFAULT_PROJECT_XML = `<?xml version="1.0"?>
|
||||||
</song>
|
</song>
|
||||||
</lmms-project>`;
|
</lmms-project>`;
|
||||||
|
|
||||||
|
|
||||||
// -------------------------
|
// -------------------------
|
||||||
// LOGGER DINÂMICO (GERENCIADOR)
|
// LOGGER DINÂMICO (GERENCIADOR)
|
||||||
// -------------------------
|
// -------------------------
|
||||||
|
|
@ -153,14 +154,20 @@ const app = express();
|
||||||
const PORT = process.env.PORT || 33001;
|
const PORT = process.env.PORT || 33001;
|
||||||
|
|
||||||
// ====== HTTPS (Opção B) ======
|
// ====== HTTPS (Opção B) ======
|
||||||
const CERT_FULLCHAIN = process.env.SSL_FULLCHAIN || "/etc/letsencrypt/live/alice.ufsj.edu.br/fullchain.pem";
|
const CERT_FULLCHAIN =
|
||||||
const CERT_PRIVKEY = process.env.SSL_PRIVKEY || "/etc/letsencrypt/live/alice.ufsj.edu.br/privkey.pem";
|
process.env.SSL_FULLCHAIN ||
|
||||||
|
"/etc/letsencrypt/live/alice.ufsj.edu.br/fullchain.pem";
|
||||||
|
const CERT_PRIVKEY =
|
||||||
|
process.env.SSL_PRIVKEY ||
|
||||||
|
"/etc/letsencrypt/live/alice.ufsj.edu.br/privkey.pem";
|
||||||
|
|
||||||
if (!fs.existsSync(CERT_FULLCHAIN) || !fs.existsSync(CERT_PRIVKEY)) {
|
if (!fs.existsSync(CERT_FULLCHAIN) || !fs.existsSync(CERT_PRIVKEY)) {
|
||||||
console.error("[HTTPS] Certificados não encontrados.\n" +
|
console.error(
|
||||||
|
"[HTTPS] Certificados não encontrados.\n" +
|
||||||
` fullchain: ${CERT_FULLCHAIN}\n` +
|
` fullchain: ${CERT_FULLCHAIN}\n` +
|
||||||
` privkey : ${CERT_PRIVKEY}\n` +
|
` privkey : ${CERT_PRIVKEY}\n` +
|
||||||
"Defina SSL_FULLCHAIN/SSL_PRIVKEY ou instale os certificados no caminho padrão.");
|
"Defina SSL_FULLCHAIN/SSL_PRIVKEY ou instale os certificados no caminho padrão.",
|
||||||
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -194,14 +201,14 @@ function dataFile(roomName) {
|
||||||
function ensureRoom(roomName) {
|
function ensureRoom(roomName) {
|
||||||
if (roomStates[roomName]) return roomStates[roomName];
|
if (roomStates[roomName]) return roomStates[roomName];
|
||||||
|
|
||||||
// tenta carregar do disco
|
|
||||||
try {
|
try {
|
||||||
const p = dataFile(roomName);
|
const p = dataFile(roomName);
|
||||||
if (fs.existsSync(p)) {
|
if (fs.existsSync(p)) {
|
||||||
const j = JSON.parse(fs.readFileSync(p, "utf8"));
|
const j = JSON.parse(fs.readFileSync(p, "utf8"));
|
||||||
roomStates[roomName] = {
|
roomStates[roomName] = {
|
||||||
// 🔥 CORREÇÃO: Fallback para DEFAULT_PROJECT_XML se o JSON salvo estiver sem XML
|
// ✅ Mantém o patternState em vez do projectXml gigante
|
||||||
projectXml: j.projectXml || DEFAULT_PROJECT_XML,
|
patternState: j.patternState || null,
|
||||||
|
projectXml: j.projectXml || DEFAULT_PROJECT_XML, // Fallback para renderização
|
||||||
audio: j.audio || { tracks: [], clips: [] },
|
audio: j.audio || { tracks: [], clips: [] },
|
||||||
seq: j.seq || 0,
|
seq: j.seq || 0,
|
||||||
tokensSeen: new Set(),
|
tokensSeen: new Set(),
|
||||||
|
|
@ -212,9 +219,8 @@ function ensureRoom(roomName) {
|
||||||
console.warn(`[persist] falha ao carregar estado da sala ${roomName}:`, e);
|
console.warn(`[persist] falha ao carregar estado da sala ${roomName}:`, e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cria sala NOVA na memória
|
|
||||||
roomStates[roomName] = {
|
roomStates[roomName] = {
|
||||||
// 🔥 CORREÇÃO: Inicia com o XML do LMMS completo, não mais null
|
patternState: null,
|
||||||
projectXml: DEFAULT_PROJECT_XML,
|
projectXml: DEFAULT_PROJECT_XML,
|
||||||
audio: { tracks: [], clips: [] },
|
audio: { tracks: [], clips: [] },
|
||||||
seq: 0,
|
seq: 0,
|
||||||
|
|
@ -225,15 +231,39 @@ function ensureRoom(roomName) {
|
||||||
|
|
||||||
function saveRoom(roomName) {
|
function saveRoom(roomName) {
|
||||||
const r = ensureRoom(roomName);
|
const r = ensureRoom(roomName);
|
||||||
|
|
||||||
|
// Se já tem um salvamento agendado para esta sala, ignora
|
||||||
|
if (pendingSaves.has(roomName)) return;
|
||||||
|
pendingSaves.add(roomName);
|
||||||
|
|
||||||
|
// Aguarda 2 segundos antes de salvar (Debounce)
|
||||||
|
setTimeout(() => {
|
||||||
try {
|
try {
|
||||||
fs.mkdirSync(SESSION_JSON, { recursive: true });
|
fs.mkdirSync(SESSION_JSON, { recursive: true });
|
||||||
fs.writeFileSync(
|
// Extraia a stringificação do Event Loop principal usando promessas ou apenas aceite o custo mitigado pelo debounce
|
||||||
dataFile(roomName),
|
const payload = JSON.stringify({
|
||||||
JSON.stringify({ projectXml: r.projectXml, audio: r.audio, seq: r.seq })
|
patternState: r.patternState,
|
||||||
|
projectXml: r.projectXml, // Mantemos o backup antigo se existir
|
||||||
|
audio: r.audio,
|
||||||
|
seq: r.seq,
|
||||||
|
});
|
||||||
|
|
||||||
|
fs.writeFile(dataFile(roomName), payload, (err) => {
|
||||||
|
pendingSaves.delete(roomName); // Libera para futuros salvamentos
|
||||||
|
if (err)
|
||||||
|
console.warn(
|
||||||
|
`[persist] Erro assíncrono ao salvar sala ${roomName}:`,
|
||||||
|
err,
|
||||||
);
|
);
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(`[persist] falha ao salvar estado da sala ${roomName}:`, e);
|
pendingSaves.delete(roomName);
|
||||||
|
console.warn(
|
||||||
|
`[persist] falha ao preparar salvamento da sala ${roomName}:`,
|
||||||
|
e,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
}, 2000); // 2 segundos de respiro para o servidor
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------
|
// -------------------------
|
||||||
|
|
@ -247,6 +277,8 @@ const io = new Server(httpsServer, {
|
||||||
methods: ["GET", "POST"],
|
methods: ["GET", "POST"],
|
||||||
credentials: true,
|
credentials: true,
|
||||||
},
|
},
|
||||||
|
// Evita que o servidor chute o usuário ao receber XMLs pesados!
|
||||||
|
maxHttpBufferSize: 5e7,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("Servidor Backend do MMP-Search está no ar!");
|
console.log("Servidor Backend do MMP-Search está no ar!");
|
||||||
|
|
@ -269,15 +301,10 @@ function basenameNoExt(p) {
|
||||||
function applyAuthoritativeAction(roomName, action) {
|
function applyAuthoritativeAction(roomName, action) {
|
||||||
const room = ensureRoom(roomName);
|
const room = ensureRoom(roomName);
|
||||||
const state = room.audio;
|
const state = room.audio;
|
||||||
|
|
||||||
// idempotência por token (se fornecido)
|
|
||||||
if (action.__token && room.tokensSeen.has(action.__token)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (action.__token) room.tokensSeen.add(action.__token);
|
|
||||||
|
|
||||||
let mutated = false;
|
let mutated = false;
|
||||||
|
|
||||||
|
// REMOVIDO: checagem de tokensSeen e if (action.__token)
|
||||||
|
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case "ADD_AUDIO_LANE": {
|
case "ADD_AUDIO_LANE": {
|
||||||
// Usa o trackId enviado pelo cliente, se existir
|
// Usa o trackId enviado pelo cliente, se existir
|
||||||
|
|
@ -304,7 +331,7 @@ function applyAuthoritativeAction(roomName, action) {
|
||||||
) {
|
) {
|
||||||
console.warn(
|
console.warn(
|
||||||
`[Server] Ação ADD_AUDIO_CLIP rejeitada (dados inválidos):`,
|
`[Server] Ação ADD_AUDIO_CLIP rejeitada (dados inválidos):`,
|
||||||
action
|
action,
|
||||||
);
|
);
|
||||||
return null; // Rejeita a ação, não faz broadcast
|
return null; // Rejeita a ação, não faz broadcast
|
||||||
}
|
}
|
||||||
|
|
@ -340,7 +367,7 @@ function applyAuthoritativeAction(roomName, action) {
|
||||||
if (!action.clipId || !action.props) {
|
if (!action.clipId || !action.props) {
|
||||||
console.warn(
|
console.warn(
|
||||||
`[Server] Ação UPDATE_AUDIO_CLIP rejeitada (dados base inválidos):`,
|
`[Server] Ação UPDATE_AUDIO_CLIP rejeitada (dados base inválidos):`,
|
||||||
action
|
action,
|
||||||
);
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -352,7 +379,7 @@ function applyAuthoritativeAction(roomName, action) {
|
||||||
) {
|
) {
|
||||||
console.warn(
|
console.warn(
|
||||||
`[Server] Ação UPDATE_AUDIO_CLIP rejeitada (trackId inválido):`,
|
`[Server] Ação UPDATE_AUDIO_CLIP rejeitada (trackId inválido):`,
|
||||||
action
|
action,
|
||||||
);
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -362,7 +389,7 @@ function applyAuthoritativeAction(roomName, action) {
|
||||||
) {
|
) {
|
||||||
console.warn(
|
console.warn(
|
||||||
`[Server] Ação UPDATE_AUDIO_CLIP rejeitada (startTimeInSeconds inválido):`,
|
`[Server] Ação UPDATE_AUDIO_CLIP rejeitada (startTimeInSeconds inválido):`,
|
||||||
action
|
action,
|
||||||
);
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -377,7 +404,7 @@ function applyAuthoritativeAction(roomName, action) {
|
||||||
|
|
||||||
case "REMOVE_AUDIO_CLIP": {
|
case "REMOVE_AUDIO_CLIP": {
|
||||||
const i = state.clips.findIndex(
|
const i = state.clips.findIndex(
|
||||||
(x) => String(x.id) === String(action.clipId)
|
(x) => String(x.id) === String(action.clipId),
|
||||||
);
|
);
|
||||||
if (i >= 0) {
|
if (i >= 0) {
|
||||||
state.clips.splice(i, 1);
|
state.clips.splice(i, 1);
|
||||||
|
|
@ -399,7 +426,7 @@ function applyAuthoritativeAction(roomName, action) {
|
||||||
const prevCount = state.clips.length;
|
const prevCount = state.clips.length;
|
||||||
state.clips = state.clips.filter((c) => {
|
state.clips = state.clips.filter((c) => {
|
||||||
// Mantém apenas se tiver trackId E se esse trackId ainda existir na lista de tracks
|
// Mantém apenas se tiver trackId E se esse trackId ainda existir na lista de tracks
|
||||||
const parentTrackExists = state.tracks.some(t => t.id === c.trackId);
|
const parentTrackExists = state.tracks.some((t) => t.id === c.trackId);
|
||||||
return c.trackId && parentTrackExists;
|
return c.trackId && parentTrackExists;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -416,9 +443,9 @@ function applyAuthoritativeAction(roomName, action) {
|
||||||
|
|
||||||
if (!mutated) return null;
|
if (!mutated) return null;
|
||||||
|
|
||||||
room.seq += 1; // numeração autoritativa
|
// REMOVIDO: room.seq += 1; e atribuição de __seq
|
||||||
saveRoom(roomName); // persistência em disco
|
saveRoom(roomName);
|
||||||
return { ...action, __seq: room.seq }; // devolve ação com seq para broadcast
|
return action; // Apenas retorna a ação modificada
|
||||||
}
|
}
|
||||||
|
|
||||||
io.on("connection", (socket) => {
|
io.on("connection", (socket) => {
|
||||||
|
|
@ -439,35 +466,57 @@ io.on("connection", (socket) => {
|
||||||
activeUsers[socket.id] = { username: finalUserName, room: roomName };
|
activeUsers[socket.id] = { username: finalUserName, room: roomName };
|
||||||
|
|
||||||
await socket.join(roomName);
|
await socket.join(roomName);
|
||||||
console.log(`[ROOM] 🟢 Usuário ${finalUserName} (${socket.id}) entrou na sala: ${roomName}`);
|
console.log(
|
||||||
|
`[ROOM] 🟢 Usuário ${finalUserName} (${socket.id}) entrou na sala: ${roomName}`,
|
||||||
|
);
|
||||||
|
|
||||||
// Avisa os outros para mostrar o Popup Verde
|
// Avisa os outros para mostrar o Popup Verde
|
||||||
socket.to(roomName).emit("user_joined", { username: finalUserName, id: socket.id });
|
socket
|
||||||
|
.to(roomName)
|
||||||
|
.emit("user_joined", { username: finalUserName, id: socket.id });
|
||||||
|
|
||||||
// Atualiza o monitor de console
|
// Atualiza o monitor de console
|
||||||
const clientsInRoom = Array.from(io.sockets.adapter.rooms.get(roomName) || []);
|
const clientsInRoom = Array.from(
|
||||||
|
io.sockets.adapter.rooms.get(roomName) || [],
|
||||||
|
);
|
||||||
io.to(roomName).emit("room_status", {
|
io.to(roomName).emit("room_status", {
|
||||||
room: roomName, count: clientsInRoom.length, users: clientsInRoom
|
room: roomName,
|
||||||
|
count: clientsInRoom.length,
|
||||||
|
users: clientsInRoom,
|
||||||
});
|
});
|
||||||
logMyRooms(socket, "JOIN");
|
logMyRooms(socket, "JOIN");
|
||||||
|
|
||||||
const room = ensureRoom(roomName);
|
const room = ensureRoom(roomName);
|
||||||
if (room.projectXml) {
|
// Envia o JSON se existir, senão envia o XML de fallback
|
||||||
socket.emit("load_project_state", room.projectXml);
|
if (room.patternState) {
|
||||||
|
socket.emit("load_project_state", { patternState: room.patternState });
|
||||||
|
} else if (room.projectXml) {
|
||||||
|
socket.emit("load_project_state", { xml: room.projectXml });
|
||||||
}
|
}
|
||||||
|
|
||||||
socket.emit("action_broadcast", {
|
socket.emit("action_broadcast", {
|
||||||
action: { type: "AUDIO_SNAPSHOT", snapshot: room.audio, __seq: room.seq, __target: socket.id },
|
action: {
|
||||||
|
type: "AUDIO_SNAPSHOT",
|
||||||
|
snapshot: room.audio,
|
||||||
|
__seq: room.seq,
|
||||||
|
__target: socket.id,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
socket.to(roomName).emit("feedback", `Usuário ${finalUserName} entrou na sala.`);
|
socket
|
||||||
|
.to(roomName)
|
||||||
|
.emit("feedback", `Usuário ${finalUserName} entrou na sala.`);
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- GATILHO DE DESPERTAR ÚNICO ---
|
// --- GATILHO DE DESPERTAR ÚNICO ---
|
||||||
socket.on("request_full_sync", ({ room }) => {
|
socket.on("request_full_sync", ({ room }) => {
|
||||||
const targetRoom = ensureRoom(room);
|
const targetRoom = ensureRoom(room);
|
||||||
if (targetRoom && targetRoom.projectXml) {
|
if (targetRoom && targetRoom.projectXml) {
|
||||||
console.log(`[SYNC] Cliente ${socket.id} acordou e pediu Full Sync da sala ${room}`);
|
console.log(`[SYNC] Cliente ${socket.id} acordou e pediu Full Sync`);
|
||||||
socket.emit("load_project_state", targetRoom.projectXml);
|
// 🔥 Manda o XML dentro de um objeto avisando que é um pedido FORÇADO
|
||||||
|
socket.emit("load_project_state", {
|
||||||
|
xml: targetRoom.projectXml,
|
||||||
|
forceSync: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -511,7 +560,7 @@ socket.on("disconnect", () => {
|
||||||
cb && cb({ ok: false, error: "invalid_payload" });
|
cb && cb({ ok: false, error: "invalid_payload" });
|
||||||
console.warn(
|
console.warn(
|
||||||
`[broadcast_action] payload inválido de ${socket.id}:`,
|
`[broadcast_action] payload inválido de ${socket.id}:`,
|
||||||
payload
|
payload,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -528,7 +577,7 @@ socket.on("disconnect", () => {
|
||||||
socketId: socket.id,
|
socketId: socket.id,
|
||||||
action: action,
|
action: action,
|
||||||
},
|
},
|
||||||
"action_received"
|
"action_received",
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(`[Logger] Falha ao logar ação para sala ${roomName}:`, e);
|
console.warn(`[Logger] Falha ao logar ação para sala ${roomName}:`, e);
|
||||||
|
|
@ -541,7 +590,7 @@ socket.on("disconnect", () => {
|
||||||
// Garante que o emissor está na sala (caso o join tenha falhado/atrasado)
|
// Garante que o emissor está na sala (caso o join tenha falhado/atrasado)
|
||||||
if (!socket.rooms.has(roomName)) {
|
if (!socket.rooms.has(roomName)) {
|
||||||
console.warn(
|
console.warn(
|
||||||
`[broadcast_action] ${socket.id} NÃO estava na sala ${roomName}. Forçando join...`
|
`[broadcast_action] ${socket.id} NÃO estava na sala ${roomName}. Forçando join...`,
|
||||||
);
|
);
|
||||||
await socket.join(roomName);
|
await socket.join(roomName);
|
||||||
logMyRooms(socket, "FORCED_JOIN");
|
logMyRooms(socket, "FORCED_JOIN");
|
||||||
|
|
@ -550,18 +599,48 @@ socket.on("disconnect", () => {
|
||||||
// Carrega estado da sala
|
// Carrega estado da sala
|
||||||
const room = ensureRoom(roomName);
|
const room = ensureRoom(roomName);
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 1. PREVENÇÃO DE ECO DUPLO E MEMORY LEAK
|
||||||
|
// ==========================================
|
||||||
|
if (action.__token) {
|
||||||
|
if (room.tokensSeen.has(action.__token)) {
|
||||||
|
console.log(`[Deduplicação] Ação repetida ignorada: ${action.type}`);
|
||||||
|
return; // Mata a ação aqui, não faz broadcast!
|
||||||
|
}
|
||||||
|
room.tokensSeen.add(action.__token);
|
||||||
|
|
||||||
|
// Limpa tokens antigos (mantém apenas os últimos 1000)
|
||||||
|
if (room.tokensSeen.size > 1000) {
|
||||||
|
const oldestToken = room.tokensSeen.values().next().value;
|
||||||
|
room.tokensSeen.delete(oldestToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 2. ORDEM AUTORITATIVA GLOBAL (JUIZ)
|
||||||
|
// ==========================================
|
||||||
|
room.seq += 1;
|
||||||
|
action.__seq = room.seq; // TODAS as ações agora ganham um número de sequência
|
||||||
|
|
||||||
// Persiste estado do Pattern (Notas/Sequenciador)
|
// Persiste estado do Pattern (Notas/Sequenciador)
|
||||||
// Aceita tanto carregamento total quanto atualização de notas
|
if (
|
||||||
if ((action.type === "LOAD_PROJECT" || action.type === "SYNC_PATTERN_STATE") && action.xml) {
|
(action.type === "LOAD_PROJECT" ||
|
||||||
// Proteção: Não salva se o XML for vazio (evita corromper a sala com o bug antigo)
|
action.type === "SYNC_PATTERN_STATE") &&
|
||||||
if (action.xml.trim().length > 0) {
|
action.xml
|
||||||
|
) {
|
||||||
|
const xmlTrimmed = action.xml.trim();
|
||||||
|
|
||||||
|
// Validação de Integridade: O XML chegou inteiro?
|
||||||
|
if (xmlTrimmed.length > 0 && xmlTrimmed.endsWith("</lmms-project>")) {
|
||||||
room.projectXml = action.xml;
|
room.projectXml = action.xml;
|
||||||
saveRoom(roomName);
|
saveRoom(roomName);
|
||||||
console.log(
|
console.log(
|
||||||
`[broadcast_action] XML da sala ${roomName} atualizado via ${action.type}.`
|
`[broadcast_action] XML da sala ${roomName} salvo com segurança (${action.type}).`,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
console.warn(`[Server] Ignorando XML vazio vindo de ${action.type}`);
|
console.warn(
|
||||||
|
`[Server] ALERTA: XML corrompido/incompleto rejeitado de ${socket.id} na ação ${action.type}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -573,18 +652,18 @@ socket.on("disconnect", () => {
|
||||||
const idsInRoom = socketsInRoom.map((s) => s.id);
|
const idsInRoom = socketsInRoom.map((s) => s.id);
|
||||||
console.log(
|
console.log(
|
||||||
`[broadcast_action] ${action.type} (token=${action.__token}) para sala "${roomName}" com ${idsInRoom.length} cliente(s):`,
|
`[broadcast_action] ${action.type} (token=${action.__token}) para sala "${roomName}" com ${idsInRoom.length} cliente(s):`,
|
||||||
idsInRoom
|
idsInRoom,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Se foi uma ação do editor de áudio e foi aplicada, rebroadcast com __seq
|
// Se foi uma ação do editor de áudio e foi aplicada, rebroadcast com __seq
|
||||||
if (maybeApplied) {
|
if (maybeApplied) {
|
||||||
io.to(roomName).emit("action_broadcast", { action: maybeApplied });
|
socket.to(roomName).emit("action_broadcast", { action: maybeApplied });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Caso contrário, apenas repercute a ação como antes (sem __seq)
|
// Caso contrário, repercute a ação UMA ÚNICA VEZ para todo mundo na sala
|
||||||
io.to(roomName).emit("action_broadcast", { action });
|
socket.to(roomName).emit("action_broadcast", { action });
|
||||||
});
|
}); // <--- Fim do socket.on("broadcast_action")
|
||||||
|
|
||||||
// =======================================
|
// =======================================
|
||||||
// Retrocompat (daw_action)
|
// Retrocompat (daw_action)
|
||||||
|
|
@ -594,7 +673,7 @@ socket.on("disconnect", () => {
|
||||||
if (!room || !action) return;
|
if (!room || !action) return;
|
||||||
|
|
||||||
console.warn(
|
console.warn(
|
||||||
"[DEPRECATION] Recebido daw_action. Migre para broadcast_action com ACK."
|
"[DEPRECATION] Recebido daw_action. Migre para broadcast_action com ACK.",
|
||||||
);
|
);
|
||||||
|
|
||||||
// (Opcional, mas recomendado) Adicionando log dinâmico aqui também
|
// (Opcional, mas recomendado) Adicionando log dinâmico aqui também
|
||||||
|
|
@ -606,7 +685,7 @@ socket.on("disconnect", () => {
|
||||||
socketId: socket.id,
|
socketId: socket.id,
|
||||||
action: action,
|
action: action,
|
||||||
},
|
},
|
||||||
"action_received (deprecated daw_action)"
|
"action_received (deprecated daw_action)",
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(`[Logger] Falha ao logar daw_action para sala ${room}:`, e);
|
console.warn(`[Logger] Falha ao logar daw_action para sala ${room}:`, e);
|
||||||
|
|
@ -614,7 +693,7 @@ socket.on("disconnect", () => {
|
||||||
|
|
||||||
if (!socket.rooms.has(room)) {
|
if (!socket.rooms.has(room)) {
|
||||||
console.warn(
|
console.warn(
|
||||||
`[daw_action] ${socket.id} NÃO estava na sala ${room}. Forçando join...`
|
`[daw_action] ${socket.id} NÃO estava na sala ${room}. Forçando join...`,
|
||||||
);
|
);
|
||||||
await socket.join(room);
|
await socket.join(room);
|
||||||
logMyRooms(socket, "FORCED_JOIN(daw)");
|
logMyRooms(socket, "FORCED_JOIN(daw)");
|
||||||
|
|
@ -625,7 +704,7 @@ socket.on("disconnect", () => {
|
||||||
r.projectXml = action.xml;
|
r.projectXml = action.xml;
|
||||||
saveRoom(room);
|
saveRoom(room);
|
||||||
console.log(
|
console.log(
|
||||||
`[daw_action] Estado da sala ${room} atualizado (LOAD_PROJECT).`
|
`[daw_action] Estado da sala ${room} atualizado (LOAD_PROJECT).`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -633,7 +712,7 @@ socket.on("disconnect", () => {
|
||||||
const idsInRoom = socketsInRoom.map((s) => s.id);
|
const idsInRoom = socketsInRoom.map((s) => s.id);
|
||||||
console.log(
|
console.log(
|
||||||
`[daw_action] ${action.type} para sala "${room}" (${idsInRoom.length} cliente(s)):`,
|
`[daw_action] ${action.type} para sala "${room}" (${idsInRoom.length} cliente(s)):`,
|
||||||
idsInRoom
|
idsInRoom,
|
||||||
);
|
);
|
||||||
|
|
||||||
io.in(room).emit("action_broadcast", { action });
|
io.in(room).emit("action_broadcast", { action });
|
||||||
|
|
@ -668,9 +747,7 @@ app.use(express.json({ limit: "250mb" }));
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
const origin = req.headers.origin;
|
const origin = req.headers.origin;
|
||||||
|
|
||||||
const allowed = new Set([
|
const allowed = new Set(["https://alice.ufsj.edu.br"]);
|
||||||
"https://alice.ufsj.edu.br",
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (origin && allowed.has(origin)) {
|
if (origin && allowed.has(origin)) {
|
||||||
res.setHeader("Access-Control-Allow-Origin", origin);
|
res.setHeader("Access-Control-Allow-Origin", origin);
|
||||||
|
|
@ -700,14 +777,15 @@ function buildLmmsCommand(inputMmp, outputAudio, ext) {
|
||||||
return { cmd: LMMS_BIN, args: lmmsArgs };
|
return { cmd: LMMS_BIN, args: lmmsArgs };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function sanitizeFileName(name) {
|
function sanitizeFileName(name) {
|
||||||
return String(name || "projeto")
|
return (
|
||||||
|
String(name || "projeto")
|
||||||
.normalize("NFKD")
|
.normalize("NFKD")
|
||||||
.replace(/[^\w\s.-]/g, "")
|
.replace(/[^\w\s.-]/g, "")
|
||||||
.trim()
|
.trim()
|
||||||
.replace(/\s+/g, "_")
|
.replace(/\s+/g, "_")
|
||||||
.slice(0, 120) || "projeto";
|
.slice(0, 120) || "projeto"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isSafeZipEntry(name) {
|
function isSafeZipEntry(name) {
|
||||||
|
|
@ -721,12 +799,17 @@ function isSafeZipEntry(name) {
|
||||||
async function safeUnzip(zipPath, destDir) {
|
async function safeUnzip(zipPath, destDir) {
|
||||||
// lista entradas
|
// lista entradas
|
||||||
const list = await run("unzip", ["-Z1", zipPath], { timeoutMs: 60_000 });
|
const list = await run("unzip", ["-Z1", zipPath], { timeoutMs: 60_000 });
|
||||||
const entries = String(list.out || "").split("\n").map(x => x.trim()).filter(Boolean);
|
const entries = String(list.out || "")
|
||||||
|
.split("\n")
|
||||||
|
.map((x) => x.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
if (!entries.length) throw new Error("zip sem entradas (ou unzip falhou ao listar)");
|
if (!entries.length)
|
||||||
|
throw new Error("zip sem entradas (ou unzip falhou ao listar)");
|
||||||
|
|
||||||
for (const e of entries) {
|
for (const e of entries) {
|
||||||
if (!isSafeZipEntry(e)) throw new Error(`zip contém caminho inseguro: ${e}`);
|
if (!isSafeZipEntry(e))
|
||||||
|
throw new Error(`zip contém caminho inseguro: ${e}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// extrai
|
// extrai
|
||||||
|
|
@ -747,7 +830,6 @@ async function findFirstMmp(dir) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function run(cmd, args, { timeoutMs, cwd } = {}) {
|
function run(cmd, args, { timeoutMs, cwd } = {}) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const p = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"], cwd });
|
const p = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"], cwd });
|
||||||
|
|
@ -758,7 +840,9 @@ function run(cmd, args, { timeoutMs, cwd } = {}) {
|
||||||
p.stderr.on("data", (d) => (err += d.toString()));
|
p.stderr.on("data", (d) => (err += d.toString()));
|
||||||
|
|
||||||
const t = setTimeout(() => {
|
const t = setTimeout(() => {
|
||||||
try { p.kill("SIGKILL"); } catch {}
|
try {
|
||||||
|
p.kill("SIGKILL");
|
||||||
|
} catch {}
|
||||||
reject(new Error(`${cmd} timeout (${timeoutMs}ms)\n${err || out}`));
|
reject(new Error(`${cmd} timeout (${timeoutMs}ms)\n${err || out}`));
|
||||||
}, timeoutMs || LMMS_TIMEOUT_MS);
|
}, timeoutMs || LMMS_TIMEOUT_MS);
|
||||||
|
|
||||||
|
|
@ -786,18 +870,32 @@ app.get("/api/admin/salas", (req, res) => {
|
||||||
if (!io.sockets.sockets.has(key)) {
|
if (!io.sockets.sockets.has(key)) {
|
||||||
roomsData[key] = {
|
roomsData[key] = {
|
||||||
total_usuarios: value.size,
|
total_usuarios: value.size,
|
||||||
ids_conectados: Array.from(value)
|
ids_conectados: Array.from(value),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
salas_ativas: Object.keys(roomsData).length,
|
salas_ativas: Object.keys(roomsData).length,
|
||||||
detalhes: roomsData
|
detalhes: roomsData,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
// --------------------------------------------------
|
// --------------------------------------------------
|
||||||
|
|
||||||
|
// Rota para salvar o backup do JSON sem usar o WebSocket
|
||||||
|
app.post("/api/save_room_state", (req, res) => {
|
||||||
|
const { roomName, patternState } = req.body;
|
||||||
|
|
||||||
|
if (!roomName || !patternState) {
|
||||||
|
return res.status(400).json({ error: "Faltam dados" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const room = ensureRoom(roomName);
|
||||||
|
room.patternState = patternState;
|
||||||
|
saveRoom(roomName);
|
||||||
|
return res.status(200).json({ ok: true });
|
||||||
|
});
|
||||||
|
|
||||||
// trava simples pra não renderizar a mesma sala em paralelo
|
// trava simples pra não renderizar a mesma sala em paralelo
|
||||||
const renderLocks = new Map();
|
const renderLocks = new Map();
|
||||||
|
|
||||||
|
|
@ -807,7 +905,9 @@ app.post("/render", async (req, res) => {
|
||||||
const ext = String(format || "wav").toLowerCase();
|
const ext = String(format || "wav").toLowerCase();
|
||||||
const allowed = new Set(["wav", "ogg", "flac", "mp3"]);
|
const allowed = new Set(["wav", "ogg", "flac", "mp3"]);
|
||||||
if (!allowed.has(ext)) {
|
if (!allowed.has(ext)) {
|
||||||
return res.status(400).json({ ok: false, error: "invalid_format", allowed: [...allowed] });
|
return res
|
||||||
|
.status(400)
|
||||||
|
.json({ ok: false, error: "invalid_format", allowed: [...allowed] });
|
||||||
}
|
}
|
||||||
|
|
||||||
// trava sala (se aplicável)
|
// trava sala (se aplicável)
|
||||||
|
|
@ -833,7 +933,8 @@ app.post("/render", async (req, res) => {
|
||||||
await safeUnzip(zipPath, tmpDir);
|
await safeUnzip(zipPath, tmpDir);
|
||||||
|
|
||||||
inputMmpPath = await findFirstMmp(tmpDir);
|
inputMmpPath = await findFirstMmp(tmpDir);
|
||||||
if (!inputMmpPath) throw new Error("nenhum .mmp encontrado após extrair o pacote");
|
if (!inputMmpPath)
|
||||||
|
throw new Error("nenhum .mmp encontrado após extrair o pacote");
|
||||||
|
|
||||||
cwdForLmms = path.dirname(inputMmpPath); // isso faz o LMMS resolver samples/...
|
cwdForLmms = path.dirname(inputMmpPath); // isso faz o LMMS resolver samples/...
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -847,7 +948,9 @@ app.post("/render", async (req, res) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!projectXml || String(projectXml).trim().length === 0) {
|
if (!projectXml || String(projectXml).trim().length === 0) {
|
||||||
return res.status(400).json({ ok: false, error: "missing_xml_or_room" });
|
return res
|
||||||
|
.status(400)
|
||||||
|
.json({ ok: false, error: "missing_xml_or_room" });
|
||||||
}
|
}
|
||||||
|
|
||||||
inputMmpPath = path.join(tmpDir, "project.mmp");
|
inputMmpPath = path.join(tmpDir, "project.mmp");
|
||||||
|
|
@ -862,15 +965,25 @@ app.post("/render", async (req, res) => {
|
||||||
const downloadName = `${fileBase}.${ext}`;
|
const downloadName = `${fileBase}.${ext}`;
|
||||||
|
|
||||||
return res.download(outputPath, downloadName, (err) => {
|
return res.download(outputPath, downloadName, (err) => {
|
||||||
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
|
try {
|
||||||
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||||
|
} catch {}
|
||||||
if (roomName) renderLocks.delete(roomName);
|
if (roomName) renderLocks.delete(roomName);
|
||||||
if (err) console.error("[/render] download error:", err);
|
if (err) console.error("[/render] download error:", err);
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
|
try {
|
||||||
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||||
|
} catch {}
|
||||||
if (roomName) renderLocks.delete(roomName);
|
if (roomName) renderLocks.delete(roomName);
|
||||||
console.error("[/render] fail:", e);
|
console.error("[/render] fail:", e);
|
||||||
return res.status(500).json({ ok: false, error: "render_failed", details: String(e?.message || e) });
|
return res
|
||||||
|
.status(500)
|
||||||
|
.json({
|
||||||
|
ok: false,
|
||||||
|
error: "render_failed",
|
||||||
|
details: String(e?.message || e),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -883,22 +996,26 @@ app.post("/notify-update", express.json(), (req, res) => {
|
||||||
// O evento deve ser algo que ui.js entenda.
|
// O evento deve ser algo que ui.js entenda.
|
||||||
io.emit("system_update", {
|
io.emit("system_update", {
|
||||||
type: "RELOAD_SAMPLES",
|
type: "RELOAD_SAMPLES",
|
||||||
message: "Novo Sample/Project adicionado. Recarregando o navegador de arquivos...",
|
message:
|
||||||
|
"Novo Sample/Project adicionado. Recarregando o navegador de arquivos...",
|
||||||
});
|
});
|
||||||
console.log(
|
console.log(
|
||||||
"[Notificação] Evento 'RELOAD_SAMPLES' emitido para todos os clientes."
|
"[Notificação] Evento 'RELOAD_SAMPLES' emitido para todos os clientes.",
|
||||||
);
|
);
|
||||||
|
|
||||||
return res.status(200).send({ success: true, message: "Notificação de Samples/Projetos enviada." });
|
return res
|
||||||
|
.status(200)
|
||||||
|
.send({
|
||||||
|
success: true,
|
||||||
|
message: "Notificação de Samples/Projetos enviada.",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(400).send({ success: false, message: "updateType inválido." });
|
res.status(400).send({ success: false, message: "updateType inválido." });
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get("/", (req, res) => {
|
app.get("/", (req, res) => {
|
||||||
res.send(
|
res.send("Servidor Backend do MMP-Search está no ar novamente!");
|
||||||
"Servidor Backend do MMP-Search está no ar novamente!"
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// ====== START ======
|
// ====== START ======
|
||||||
|
|
|
||||||
|
|
@ -48,13 +48,42 @@ import {
|
||||||
import { renderAll, showToast } from "./ui.js"; // showToast()
|
import { renderAll, showToast } from "./ui.js"; // showToast()
|
||||||
import { updateStepUI, renderPatternEditor } from "./pattern/pattern_ui.js";
|
import { updateStepUI, renderPatternEditor } from "./pattern/pattern_ui.js";
|
||||||
import { PORT_SOCK } from "./config.js";
|
import { PORT_SOCK } from "./config.js";
|
||||||
import { DEFAULT_PROJECT_XML } from "./utils.js"
|
import { DEFAULT_PROJECT_XML } from "./utils.js";
|
||||||
|
|
||||||
|
export let hasLoadedInitialState = false;
|
||||||
export let USER_NAME = null;
|
export let USER_NAME = null;
|
||||||
export let currentRoom = null;
|
export let currentRoom = null;
|
||||||
let roomSeq = 0;
|
let roomSeq = 0;
|
||||||
let lastActionToken = 0;
|
let lastActionToken = 0;
|
||||||
let isOfflineMode = false;
|
let isOfflineMode = false;
|
||||||
|
let syncInterval = null;
|
||||||
|
|
||||||
|
// --- DEBOUNCER DO XML (Impede o flood de rede) ---
|
||||||
|
let syncXmlTimeout = null;
|
||||||
|
|
||||||
|
export function debouncedSyncPatternState() {
|
||||||
|
if (!currentRoom) return;
|
||||||
|
if (syncXmlTimeout) clearTimeout(syncXmlTimeout);
|
||||||
|
|
||||||
|
syncXmlTimeout = setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
// Pegamos apenas o JSON do estado atual
|
||||||
|
const patternState = appState.pattern;
|
||||||
|
|
||||||
|
// Faz o upload silencioso do JSON
|
||||||
|
await fetch("/api/save_room_state", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ roomName: currentRoom, patternState }),
|
||||||
|
});
|
||||||
|
console.log("[Backup] Estado JSON salvo com segurança via HTTP.");
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Erro ao salvar JSON para sync:", err);
|
||||||
|
}
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------
|
||||||
|
|
||||||
// --- FUNÇÃO DE AUTENTICAÇÃO INTELIGENTE ---
|
// --- FUNÇÃO DE AUTENTICAÇÃO INTELIGENTE ---
|
||||||
export async function getOrFetchUsername() {
|
export async function getOrFetchUsername() {
|
||||||
|
|
@ -62,7 +91,7 @@ export async function getOrFetchUsername() {
|
||||||
if (USER_NAME) return USER_NAME;
|
if (USER_NAME) return USER_NAME;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/check_auth', { credentials: 'include' });
|
const res = await fetch("/api/check_auth", { credentials: "include" });
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (data.logged_in) {
|
if (data.logged_in) {
|
||||||
USER_NAME = data.user;
|
USER_NAME = data.user;
|
||||||
|
|
@ -73,7 +102,9 @@ export async function getOrFetchUsername() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Se não estiver logado, barra a tela e pede apelido
|
// Se não estiver logado, barra a tela e pede apelido
|
||||||
let name = prompt("Você não está logado. Digite seu apelido para entrar na DAW Online:");
|
let name = prompt(
|
||||||
|
"Você não está logado. Digite seu apelido para entrar na DAW Online:",
|
||||||
|
);
|
||||||
if (!name || name.trim() === "") {
|
if (!name || name.trim() === "") {
|
||||||
name = "Produtor_" + Math.floor(Math.random() * 1000);
|
name = "Produtor_" + Math.floor(Math.random() * 1000);
|
||||||
}
|
}
|
||||||
|
|
@ -88,13 +119,13 @@ let lastReceivedSeq = 0;
|
||||||
function _getRackIdFallback() {
|
function _getRackIdFallback() {
|
||||||
// 1) pega do primeiro bassline que já tenha instrumentSourceId
|
// 1) pega do primeiro bassline que já tenha instrumentSourceId
|
||||||
const b = (appState.pattern.tracks || []).find(
|
const b = (appState.pattern.tracks || []).find(
|
||||||
(t) => t.type === "bassline" && t.instrumentSourceId
|
(t) => t.type === "bassline" && t.instrumentSourceId,
|
||||||
);
|
);
|
||||||
if (b?.instrumentSourceId) return b.instrumentSourceId;
|
if (b?.instrumentSourceId) return b.instrumentSourceId;
|
||||||
|
|
||||||
// 2) pega do primeiro instrumento do rack (parentBasslineId)
|
// 2) pega do primeiro instrumento do rack (parentBasslineId)
|
||||||
const child = (appState.pattern.tracks || []).find(
|
const child = (appState.pattern.tracks || []).find(
|
||||||
(t) => t.type !== "bassline" && t.parentBasslineId
|
(t) => t.type !== "bassline" && t.parentBasslineId,
|
||||||
);
|
);
|
||||||
if (child?.parentBasslineId) return child.parentBasslineId;
|
if (child?.parentBasslineId) return child.parentBasslineId;
|
||||||
|
|
||||||
|
|
@ -105,15 +136,17 @@ function _nextPatternIndex() {
|
||||||
const bassMax = Math.max(
|
const bassMax = Math.max(
|
||||||
-1,
|
-1,
|
||||||
...(appState.pattern.tracks || [])
|
...(appState.pattern.tracks || [])
|
||||||
.filter((t) => t.type === "bassline" && Number.isFinite(Number(t.patternIndex)))
|
.filter(
|
||||||
.map((t) => Number(t.patternIndex))
|
(t) => t.type === "bassline" && Number.isFinite(Number(t.patternIndex)),
|
||||||
|
)
|
||||||
|
.map((t) => Number(t.patternIndex)),
|
||||||
);
|
);
|
||||||
|
|
||||||
const nonBassMax = Math.max(
|
const nonBassMax = Math.max(
|
||||||
-1,
|
-1,
|
||||||
...(appState.pattern.tracks || [])
|
...(appState.pattern.tracks || [])
|
||||||
.filter((t) => t.type !== "bassline")
|
.filter((t) => t.type !== "bassline")
|
||||||
.map((t) => (t.patterns?.length || 0) - 1)
|
.map((t) => (t.patterns?.length || 0) - 1),
|
||||||
);
|
);
|
||||||
|
|
||||||
return Math.max(bassMax, nonBassMax) + 1;
|
return Math.max(bassMax, nonBassMax) + 1;
|
||||||
|
|
@ -215,7 +248,7 @@ export function sendActionSafe(action) {
|
||||||
socket.on("room_status", (data) => {
|
socket.on("room_status", (data) => {
|
||||||
console.log(
|
console.log(
|
||||||
`%c[MONITOR DE SALA] 👥 ${data.count} pessoa(s) na sala '${data.room}'`,
|
`%c[MONITOR DE SALA] 👥 ${data.count} pessoa(s) na sala '${data.room}'`,
|
||||||
'background: #222; color: #bada55; font-size: 16px; font-weight: bold; padding: 4px;'
|
"background: #222; color: #bada55; font-size: 16px; font-weight: bold; padding: 4px;",
|
||||||
);
|
);
|
||||||
console.log("IDs conectados:", data.users);
|
console.log("IDs conectados:", data.users);
|
||||||
|
|
||||||
|
|
@ -226,7 +259,6 @@ socket.on("room_status", (data) => {
|
||||||
});
|
});
|
||||||
// ---------------------------------
|
// ---------------------------------
|
||||||
|
|
||||||
// --- SUBSTITUA SEU socket.on("connect") POR ESTE ---
|
|
||||||
socket.on("connect", async () => {
|
socket.on("connect", async () => {
|
||||||
console.log(`Conectado ao servidor com ID: ${socket.id}`);
|
console.log(`Conectado ao servidor com ID: ${socket.id}`);
|
||||||
|
|
||||||
|
|
@ -235,16 +267,15 @@ socket.on("connect", async () => {
|
||||||
if (urlRoom) {
|
if (urlRoom) {
|
||||||
currentRoom = urlRoom;
|
currentRoom = urlRoom;
|
||||||
console.log(`Modo Online. Sala detectada na URL: ${currentRoom}`);
|
console.log(`Modo Online. Sala detectada na URL: ${currentRoom}`);
|
||||||
showToast(`🎧 Conectado à sala ${currentRoom}`, "info");
|
//showToast(`🎧 Conectado à sala ${currentRoom}`, "info");
|
||||||
|
|
||||||
// Como tem sala na URL, EXIGE o login/apelido agora
|
// Como tem sala na URL, EXIGE o login/apelido agora
|
||||||
const myName = await getOrFetchUsername();
|
const myName = await getOrFetchUsername();
|
||||||
|
|
||||||
socket.emit("join_room", {
|
socket.emit("join_room", {
|
||||||
roomName: currentRoom,
|
roomName: currentRoom,
|
||||||
userName: myName
|
userName: myName,
|
||||||
});
|
});
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// Entrou na DAW pura, não pede nada ainda.
|
// Entrou na DAW pura, não pede nada ainda.
|
||||||
console.log("Modo Local. Aguardando criação de sala.");
|
console.log("Modo Local. Aguardando criação de sala.");
|
||||||
|
|
@ -253,6 +284,9 @@ socket.on("connect", async () => {
|
||||||
|
|
||||||
syncServerTime();
|
syncServerTime();
|
||||||
setInterval(syncServerTime, 10000);
|
setInterval(syncServerTime, 10000);
|
||||||
|
// Mata o relógio antigo antes de criar um novo para evitar DDoS no servidor
|
||||||
|
if (syncInterval) clearInterval(syncInterval);
|
||||||
|
syncInterval = setInterval(syncServerTime, 10000);
|
||||||
});
|
});
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
@ -261,12 +295,12 @@ socket.on("connect", async () => {
|
||||||
socket.on("connect_error", (err) => {
|
socket.on("connect_error", (err) => {
|
||||||
console.error("Falha crítica na conexão do Socket:", err.message);
|
console.error("Falha crítica na conexão do Socket:", err.message);
|
||||||
alert(
|
alert(
|
||||||
"🚧 FALHA NA CONEXÃO 🚧\n\n❌Não foi possível conectar ao servidor em tempo real. ❌\n\n😅 Causa provável: Um Ad Blocker ou Firewall está bloqueando a conexão.\n\n😎 Por favor, desative seu Ad Blocker para este site e recarregue a página."
|
"🚧 FALHA NA CONEXÃO 🚧\n\n❌Não foi possível conectar ao servidor em tempo real. ❌\n\n😅 Causa provável: Um Ad Blocker ou Firewall está bloqueando a conexão.\n\n😎 Por favor, desative seu Ad Blocker para este site e recarregue a página.",
|
||||||
);
|
);
|
||||||
showToast(
|
showToast(
|
||||||
"❌ Falha grave de conexão. Desativa o Ad Blocker? 😅",
|
"❌ Falha grave de conexão. Desativa o Ad Blocker? 😅",
|
||||||
"error",
|
"error",
|
||||||
10000
|
10000,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -276,7 +310,7 @@ socket.on("connect_error", (err) => {
|
||||||
socket.on("system_update", (data) => {
|
socket.on("system_update", (data) => {
|
||||||
if (data.type === "RELOAD_SAMPLES") {
|
if (data.type === "RELOAD_SAMPLES") {
|
||||||
console.log(
|
console.log(
|
||||||
`[System Update] Recebida ordem para recarregar samples: ${data.message}`
|
`[System Update] Recebida ordem para recarregar samples: ${data.message}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
loadAndRenderSampleBrowser();
|
loadAndRenderSampleBrowser();
|
||||||
|
|
@ -286,61 +320,42 @@ socket.on("system_update", (data) => {
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// RECEBER ESTADO SALVO DA SALA
|
// RECEBER ESTADO SALVO DA SALA
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
socket.on("load_project_state", async (projectXml) => {
|
socket.on("load_project_state", async (payload) => {
|
||||||
console.log("Recebendo estado salvo da sala...");
|
// Ignora se já carregou (proteção que você já fez)
|
||||||
showToast("🔄 Recebendo estado atual da sala...", "info", 4000);
|
if (hasLoadedInitialState && !payload.forceSync) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Recebendo estado salvo da sala...");
|
||||||
if (isLoadingProject) return;
|
if (isLoadingProject) return;
|
||||||
isLoadingProject = true;
|
isLoadingProject = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. Carrega o XML que veio do servidor (pode estar desatualizado)
|
// Se vier com "patternState" (JSON), carrega direto!
|
||||||
await parseMmpContent(projectXml);
|
if (payload.patternState) {
|
||||||
|
console.log("Carregando estado via JSON direto (Ultra Rápido) 🚀");
|
||||||
// 🔥 CORREÇÃO: Força a restauração da sessão LOCAL logo após carregar o XML
|
appState.pattern = payload.patternState;
|
||||||
// Isso garante que suas alterações locais (BPM, steps) "ganhem" do servidor
|
|
||||||
if (currentRoom) {
|
|
||||||
const raw = sessionStorage.getItem(`temp_state_${currentRoom}`);
|
|
||||||
if (raw) {
|
|
||||||
// Se existe 'raw', confiamos que é o estado mais recente do usuário.
|
|
||||||
console.log("Re-aplicando sessão local (mesmo se vazia)...");
|
|
||||||
await loadStateFromSession();
|
|
||||||
}
|
}
|
||||||
|
// Fallback: Se ainda for um XML antigo do banco, faz o parse pesado
|
||||||
|
else if (payload.xml) {
|
||||||
|
console.log("Fazendo parse de projeto legado em XML...");
|
||||||
|
await parseMmpContent(payload.xml);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentRoom && !payload.forceSync) {
|
||||||
|
sessionStorage.removeItem(`temp_state_${currentRoom}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderAll();
|
renderAll();
|
||||||
saveStateToSession();
|
saveStateToSession();
|
||||||
|
hasLoadedInitialState = true;
|
||||||
showToast("🎵 Projeto carregado com sucesso", "success");
|
showToast("🎵 Projeto carregado com sucesso", "success");
|
||||||
|
|
||||||
const hasAudio =
|
|
||||||
(appState.audio?.clips?.length || 0) > 0 ||
|
|
||||||
(appState.audio?.tracks?.length || 0) > 0;
|
|
||||||
|
|
||||||
if (!hasAudio && currentRoom) {
|
|
||||||
console.log(
|
|
||||||
"Projeto XML carregado, sem áudio. Pedindo snapshot de áudio..."
|
|
||||||
);
|
|
||||||
|
|
||||||
// Libera a trava IMEDIATAMENTE antes de pedir
|
|
||||||
// Caso contrário, a resposta chega muito rápido e cai no bloqueio "justReset=true"
|
|
||||||
appState.global.justReset = false;
|
|
||||||
|
|
||||||
sendAction({ type: "AUDIO_SNAPSHOT_REQUEST" });
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Erro ao carregar projeto:", e);
|
console.error("Erro crítico ao carregar projeto:", e);
|
||||||
showToast("❌ Erro ao carregar projeto", "error");
|
showToast("❌ Erro ao carregar projeto", "error");
|
||||||
}
|
} finally {
|
||||||
isLoadingProject = false;
|
isLoadingProject = false;
|
||||||
|
|
||||||
// Mantemos o timeout apenas como segurança extra para outros casos
|
|
||||||
setTimeout(() => {
|
|
||||||
if (appState.global.justReset) {
|
|
||||||
// console.log("Socket: Limpando flag 'justReset' (timeout).");
|
|
||||||
appState.global.justReset = false;
|
|
||||||
}
|
}
|
||||||
}, 250);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
@ -403,19 +418,29 @@ export function sendAction(action) {
|
||||||
) {
|
) {
|
||||||
console.error(
|
console.error(
|
||||||
"[SOCKET] UPDATE_AUDIO_CLIP bloqueada (startTime):",
|
"[SOCKET] UPDATE_AUDIO_CLIP bloqueada (startTime):",
|
||||||
action
|
action,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = (++lastActionToken).toString();
|
// Usa o ID do socket + contador para garantir que nunca haverá colisão
|
||||||
|
// com sessões antigas, outros usuários ou F5.
|
||||||
|
const token = `${socket.id}_${++lastActionToken}`;
|
||||||
action.__token = token;
|
action.__token = token;
|
||||||
// Usa o relógio sincronizado com o servidor
|
// Usa o relógio sincronizado com o servidor
|
||||||
action.__send_time = Date.now() + serverOffsetMs;
|
action.__send_time = Date.now() + serverOffsetMs;
|
||||||
action.__senderId = socket.id;
|
action.__senderId = socket.id;
|
||||||
action.__senderName = USER_NAME;
|
action.__senderName = USER_NAME;
|
||||||
|
|
||||||
|
// --- 🚀 A MÁGICA: UI OTIMISTA (Zero Latência) ---
|
||||||
|
// 1. Já marca o token como processado para não rodar duplicado
|
||||||
|
processedTokens.add(token);
|
||||||
|
|
||||||
|
// 2. Aplica na SUA máquina IMEDIATAMENTE na hora do clique!
|
||||||
|
handleActionBroadcast(action);
|
||||||
|
// ----------------------------------------------
|
||||||
|
|
||||||
const isTransport =
|
const isTransport =
|
||||||
action.type === "TOGGLE_PLAYBACK" ||
|
action.type === "TOGGLE_PLAYBACK" ||
|
||||||
action.type === "STOP_PLAYBACK" ||
|
action.type === "STOP_PLAYBACK" ||
|
||||||
|
|
@ -438,52 +463,39 @@ export function sendAction(action) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// (Lógica Global/Local)
|
// (Lógica Global/Local)
|
||||||
|
// Se for modo local, ou não tiver sala, não precisa fazer o socket.emit
|
||||||
if (!inRoom || (isTransport && appState.global.syncMode === "local")) {
|
if (!inRoom || (isTransport && appState.global.syncMode === "local")) {
|
||||||
console.log("[SOCKET] (local) executando ação:", action.type);
|
|
||||||
handleActionBroadcast(action);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isTransport && appState.global.syncMode === "global") {
|
if (isTransport && appState.global.syncMode === "global") {
|
||||||
action.__syncMode = "global";
|
action.__syncMode = "global";
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(
|
// 🔥 Agora sim envia para o servidor (GZIP habilitado)
|
||||||
"[SOCKET] Enviando broadcast_action:",
|
socket.emit("broadcast_action", { roomName: currentRoom, action }, (ack) => {
|
||||||
action.type,
|
|
||||||
"para",
|
|
||||||
currentRoom,
|
|
||||||
"token=",
|
|
||||||
token
|
|
||||||
);
|
|
||||||
|
|
||||||
socket
|
|
||||||
.compress(false)
|
|
||||||
.emit("broadcast_action", { roomName: currentRoom, action }, (ack) => {
|
|
||||||
if (ack && ack.ok && ack.token === token) {
|
if (ack && ack.ok && ack.token === token) {
|
||||||
if (lastActionTimeout) {
|
if (lastActionTimeout) clearTimeout(lastActionTimeout);
|
||||||
clearTimeout(lastActionTimeout);
|
if (lastBroadcastTimeout) clearTimeout(lastBroadcastTimeout);
|
||||||
lastActionTimeout = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (lastActionTimeout) clearTimeout(lastActionTimeout);
|
if (lastActionTimeout) clearTimeout(lastActionTimeout);
|
||||||
lastActionTimeout = setTimeout(() => {
|
lastActionTimeout = setTimeout(() => {
|
||||||
console.warn("[SOCKET] ACK não recebido:", action.type, token);
|
if (action.type !== "SYNC_PATTERN_STATE") {
|
||||||
showToast(`⚠️ ACK (${action.type})`, "warning");
|
showToast(`⚠️ Rede lenta (${action.type})`, "warning");
|
||||||
}, 500);
|
}
|
||||||
|
}, 2500);
|
||||||
|
|
||||||
if (lastBroadcastTimeout) clearTimeout(lastBroadcastTimeout);
|
if (lastBroadcastTimeout) clearTimeout(lastBroadcastTimeout);
|
||||||
pendingToken = token;
|
pendingToken = token;
|
||||||
lastBroadcastTimeout = setTimeout(() => {
|
|
||||||
if (processedTokens.has(token)) return;
|
|
||||||
console.warn("[SOCKET] Eco não recebido, fallback:", action.type, token);
|
|
||||||
showToast(`⚠️ Eco (${action.type}), fallback`, "warning");
|
|
||||||
processedTokens.add(token);
|
|
||||||
handleActionBroadcast(action);
|
|
||||||
}, 900);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
lastBroadcastTimeout = setTimeout(() => {
|
||||||
|
// ⚠️ ATENÇÃO: Removemos o handleActionBroadcast(action) daqui!
|
||||||
|
// Ele não precisa mais rodar no fallback porque você já rodou ele ali em cima na hora do clique!
|
||||||
|
console.warn("[SOCKET] Falha de sincronia na rede para:", action.type);
|
||||||
|
}, 4000);
|
||||||
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// POP-UPS
|
// POP-UPS
|
||||||
|
|
@ -542,7 +554,8 @@ function _genPlaylistClipId() {
|
||||||
|
|
||||||
function _ensureBasslineForPatternIndex(patternIndex) {
|
function _ensureBasslineForPatternIndex(patternIndex) {
|
||||||
let b = appState.pattern.tracks.find(
|
let b = appState.pattern.tracks.find(
|
||||||
(t) => t.type === "bassline" && Number(t.patternIndex) === Number(patternIndex)
|
(t) =>
|
||||||
|
t.type === "bassline" && Number(t.patternIndex) === Number(patternIndex),
|
||||||
);
|
);
|
||||||
|
|
||||||
// fallback: cria se não existir (não quebra nada)
|
// fallback: cria se não existir (não quebra nada)
|
||||||
|
|
@ -578,7 +591,7 @@ function _ensureBasslineForPatternIndex(patternIndex) {
|
||||||
|
|
||||||
function _getDefaultRackId() {
|
function _getDefaultRackId() {
|
||||||
const inst = (appState.pattern.tracks || []).find(
|
const inst = (appState.pattern.tracks || []).find(
|
||||||
(t) => t.type !== "bassline" && t.parentBasslineId
|
(t) => t.type !== "bassline" && t.parentBasslineId,
|
||||||
);
|
);
|
||||||
return inst?.parentBasslineId ?? null;
|
return inst?.parentBasslineId ?? null;
|
||||||
}
|
}
|
||||||
|
|
@ -624,7 +637,7 @@ socket.on("action_broadcast", (payload) => {
|
||||||
latency_ms: latencyMs.toFixed(2),
|
latency_ms: latencyMs.toFixed(2),
|
||||||
out_of_order: isOutOfOrder,
|
out_of_order: isOutOfOrder,
|
||||||
missing_packets: missingPackets,
|
missing_packets: missingPackets,
|
||||||
timestamp: new Date().toISOString()
|
timestamp: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// --- FIM DA COLETA DE DADOS ---
|
// --- FIM DA COLETA DE DADOS ---
|
||||||
|
|
@ -667,7 +680,12 @@ function _syncNotesWithStepToggle(pattern, stepIndex, isActive) {
|
||||||
if (pattern.notes.some(samePos)) return;
|
if (pattern.notes.some(samePos)) return;
|
||||||
|
|
||||||
// tenta reaproveitar "template" de nota pra manter key/vol/pan coerentes
|
// tenta reaproveitar "template" de nota pra manter key/vol/pan coerentes
|
||||||
const tpl = pattern.notes[0] || { vol: 100, len: TICKS_PER_STEP, pan: 0, key: 57 };
|
const tpl = pattern.notes[0] || {
|
||||||
|
vol: 100,
|
||||||
|
len: TICKS_PER_STEP,
|
||||||
|
pan: 0,
|
||||||
|
key: 57,
|
||||||
|
};
|
||||||
|
|
||||||
pattern.notes.push({
|
pattern.notes.push({
|
||||||
vol: tpl.vol ?? 100,
|
vol: tpl.vol ?? 100,
|
||||||
|
|
@ -757,17 +775,19 @@ async function handleActionBroadcast(action) {
|
||||||
}
|
}
|
||||||
|
|
||||||
case "CREATE_NEW_PATTERN": {
|
case "CREATE_NEW_PATTERN": {
|
||||||
const patternIndex =
|
const patternIndex = Number.isFinite(Number(action.patternIndex))
|
||||||
Number.isFinite(Number(action.patternIndex)) ? Number(action.patternIndex) : _nextPatternIndex();
|
? Number(action.patternIndex)
|
||||||
|
: _nextPatternIndex();
|
||||||
|
|
||||||
const name = (action.name || "").trim() || `Beat/Bassline ${patternIndex}`;
|
const name =
|
||||||
|
(action.name || "").trim() || `Beat/Bassline ${patternIndex}`;
|
||||||
|
|
||||||
// 1) garante patterns em todas as tracks reais
|
// 1) garante patterns em todas as tracks reais
|
||||||
_ensurePatternsUpTo(patternIndex);
|
_ensurePatternsUpTo(patternIndex);
|
||||||
|
|
||||||
// 2) cria bassline track container (pattern “da playlist”)
|
// 2) cria bassline track container (pattern “da playlist”)
|
||||||
let b = (appState.pattern.tracks || []).find(
|
let b = (appState.pattern.tracks || []).find(
|
||||||
(t) => t.type === "bassline" && Number(t.patternIndex) === patternIndex
|
(t) => t.type === "bassline" && Number(t.patternIndex) === patternIndex,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!b) {
|
if (!b) {
|
||||||
|
|
@ -798,17 +818,20 @@ async function handleActionBroadcast(action) {
|
||||||
|
|
||||||
// 4) já seleciona essa pattern (opcional, mas fica UX boa)
|
// 4) já seleciona essa pattern (opcional, mas fica UX boa)
|
||||||
appState.pattern.activePatternIndex = patternIndex;
|
appState.pattern.activePatternIndex = patternIndex;
|
||||||
(appState.pattern.tracks || []).forEach((t) => (t.activePatternIndex = patternIndex));
|
(appState.pattern.tracks || []).forEach(
|
||||||
|
(t) => (t.activePatternIndex = patternIndex),
|
||||||
|
);
|
||||||
|
|
||||||
try { schedulePatternRerender(); } catch {}
|
try {
|
||||||
|
schedulePatternRerender();
|
||||||
|
} catch {}
|
||||||
renderAll();
|
renderAll();
|
||||||
saveStateToSession();
|
saveStateToSession();
|
||||||
|
|
||||||
// 5) persiste no servidor (igual você já faz em outros updates)
|
// 5) persiste no servidor (igual você já faz em outros updates)
|
||||||
const isFromSelf = action.__senderId === socket.id;
|
const isFromSelf = action.__senderId === socket.id;
|
||||||
if (window.ROOM_NAME && isFromSelf) {
|
if (currentRoom && isFromSelf) {
|
||||||
const xml = generateXmlFromStateExported();
|
debouncedSyncPatternState();
|
||||||
sendAction({ type: "SYNC_PATTERN_STATE", xml });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
@ -821,17 +844,25 @@ async function handleActionBroadcast(action) {
|
||||||
? Number(action.patternIndex)
|
? Number(action.patternIndex)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const nonBass = (appState.pattern.tracks || []).filter(t => t.type !== "bassline");
|
const nonBass = (appState.pattern.tracks || []).filter(
|
||||||
const currentCount = nonBass.reduce((m, t) => Math.max(m, t.patterns?.length ?? 0), 0);
|
(t) => t.type !== "bassline",
|
||||||
|
);
|
||||||
|
const currentCount = nonBass.reduce(
|
||||||
|
(m, t) => Math.max(m, t.patterns?.length ?? 0),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
|
||||||
// Próximo índice livre (fim da lista)
|
// Próximo índice livre (fim da lista)
|
||||||
const maxFromNonBass = nonBass.reduce(
|
const maxFromNonBass = nonBass.reduce(
|
||||||
(m, t) => Math.max(m, (t.patterns?.length || 0) - 1),
|
(m, t) => Math.max(m, (t.patterns?.length || 0) - 1),
|
||||||
-1
|
-1,
|
||||||
);
|
);
|
||||||
|
|
||||||
const maxFromBass = (appState.pattern.tracks || [])
|
const maxFromBass = (appState.pattern.tracks || [])
|
||||||
.filter(t => t.type === "bassline" && Number.isFinite(Number(t.patternIndex)))
|
.filter(
|
||||||
|
(t) =>
|
||||||
|
t.type === "bassline" && Number.isFinite(Number(t.patternIndex)),
|
||||||
|
)
|
||||||
.reduce((m, t) => Math.max(m, Number(t.patternIndex)), -1);
|
.reduce((m, t) => Math.max(m, Number(t.patternIndex)), -1);
|
||||||
|
|
||||||
const nextFree = Math.max(maxFromNonBass, maxFromBass) + 1;
|
const nextFree = Math.max(maxFromNonBass, maxFromBass) + 1;
|
||||||
|
|
@ -842,7 +873,7 @@ async function handleActionBroadcast(action) {
|
||||||
const idxAlreadyExists =
|
const idxAlreadyExists =
|
||||||
idx <= maxFromNonBass ||
|
idx <= maxFromNonBass ||
|
||||||
(appState.pattern.tracks || []).some(
|
(appState.pattern.tracks || []).some(
|
||||||
t => t.type === "bassline" && Number(t.patternIndex) === idx
|
(t) => t.type === "bassline" && Number(t.patternIndex) === idx,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (idxAlreadyExists) idx = nextFree;
|
if (idxAlreadyExists) idx = nextFree;
|
||||||
|
|
@ -856,7 +887,8 @@ async function handleActionBroadcast(action) {
|
||||||
for (const t of nonBass) {
|
for (const t of nonBass) {
|
||||||
t.patterns[idx] = t.patterns[idx] || _makeEmptyPattern(idx);
|
t.patterns[idx] = t.patterns[idx] || _makeEmptyPattern(idx);
|
||||||
t.patterns[idx].name = finalName;
|
t.patterns[idx].name = finalName;
|
||||||
if (t.patterns[idx].pos == null) t.patterns[idx].pos = idx * LMMS_BAR_TICKS;
|
if (t.patterns[idx].pos == null)
|
||||||
|
t.patterns[idx].pos = idx * LMMS_BAR_TICKS;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3) cria a lane "bassline" (a coluna do pattern)
|
// 3) cria a lane "bassline" (a coluna do pattern)
|
||||||
|
|
@ -868,7 +900,9 @@ async function handleActionBroadcast(action) {
|
||||||
// 4) opcional: já selecionar
|
// 4) opcional: já selecionar
|
||||||
if (action.select) {
|
if (action.select) {
|
||||||
appState.pattern.activePatternIndex = idx;
|
appState.pattern.activePatternIndex = idx;
|
||||||
appState.pattern.tracks.forEach((track) => (track.activePatternIndex = idx));
|
appState.pattern.tracks.forEach(
|
||||||
|
(track) => (track.activePatternIndex = idx),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderAll();
|
renderAll();
|
||||||
|
|
@ -878,8 +912,13 @@ async function handleActionBroadcast(action) {
|
||||||
}
|
}
|
||||||
|
|
||||||
case "REMOVE_LAST_PATTERN": {
|
case "REMOVE_LAST_PATTERN": {
|
||||||
const nonBass = (appState.pattern.tracks || []).filter(t => t.type !== "bassline");
|
const nonBass = (appState.pattern.tracks || []).filter(
|
||||||
const count = nonBass.reduce((m, t) => Math.max(m, t.patterns?.length ?? 0), 0);
|
(t) => t.type !== "bassline",
|
||||||
|
);
|
||||||
|
const count = nonBass.reduce(
|
||||||
|
(m, t) => Math.max(m, t.patterns?.length ?? 0),
|
||||||
|
0,
|
||||||
|
);
|
||||||
const last = count - 1;
|
const last = count - 1;
|
||||||
if (last <= 0) break;
|
if (last <= 0) break;
|
||||||
|
|
||||||
|
|
@ -888,7 +927,7 @@ async function handleActionBroadcast(action) {
|
||||||
|
|
||||||
// remove a lane bassline correspondente
|
// remove a lane bassline correspondente
|
||||||
appState.pattern.tracks = appState.pattern.tracks.filter(
|
appState.pattern.tracks = appState.pattern.tracks.filter(
|
||||||
t => !(t.type === "bassline" && Number(t.patternIndex) === last)
|
(t) => !(t.type === "bassline" && Number(t.patternIndex) === last),
|
||||||
);
|
);
|
||||||
|
|
||||||
// ajusta seleção
|
// ajusta seleção
|
||||||
|
|
@ -923,10 +962,9 @@ async function handleActionBroadcast(action) {
|
||||||
saveStateToSession();
|
saveStateToSession();
|
||||||
|
|
||||||
// ✅ sync XML (inclui bbtco depois do patch no file.js)
|
// ✅ sync XML (inclui bbtco depois do patch no file.js)
|
||||||
if (window.ROOM_NAME && isFromSelf) {
|
if (currentRoom && isFromSelf) {
|
||||||
try {
|
try {
|
||||||
const xml = generateXmlFromStateExported();
|
debouncedSyncPatternState();
|
||||||
sendAction({ type: "SYNC_PATTERN_STATE", xml });
|
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
@ -947,10 +985,9 @@ async function handleActionBroadcast(action) {
|
||||||
renderAll();
|
renderAll();
|
||||||
saveStateToSession();
|
saveStateToSession();
|
||||||
|
|
||||||
if (window.ROOM_NAME && isFromSelf) {
|
if (currentRoom && isFromSelf) {
|
||||||
try {
|
try {
|
||||||
const xml = generateXmlFromStateExported();
|
debouncedSyncPatternState();
|
||||||
sendAction({ type: "SYNC_PATTERN_STATE", xml });
|
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
@ -960,7 +997,9 @@ async function handleActionBroadcast(action) {
|
||||||
const { patternIndex, clipId } = action;
|
const { patternIndex, clipId } = action;
|
||||||
const b = _ensureBasslineForPatternIndex(patternIndex);
|
const b = _ensureBasslineForPatternIndex(patternIndex);
|
||||||
|
|
||||||
b.playlist_clips = b.playlist_clips.filter((c) => String(c.id) !== String(clipId));
|
b.playlist_clips = b.playlist_clips.filter(
|
||||||
|
(c) => String(c.id) !== String(clipId),
|
||||||
|
);
|
||||||
|
|
||||||
// limpa seleção se era o selecionado
|
// limpa seleção se era o selecionado
|
||||||
if (appState.global.selectedPlaylistClipId === clipId) {
|
if (appState.global.selectedPlaylistClipId === clipId) {
|
||||||
|
|
@ -971,10 +1010,9 @@ async function handleActionBroadcast(action) {
|
||||||
renderAll();
|
renderAll();
|
||||||
saveStateToSession();
|
saveStateToSession();
|
||||||
|
|
||||||
if (window.ROOM_NAME && isFromSelf) {
|
if (currentRoom && isFromSelf) {
|
||||||
try {
|
try {
|
||||||
const xml = generateXmlFromStateExported();
|
debouncedSyncPatternState();
|
||||||
sendAction({ type: "SYNC_PATTERN_STATE", xml });
|
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
@ -983,8 +1021,7 @@ async function handleActionBroadcast(action) {
|
||||||
case "SELECT_PLAYLIST_PATTERN_CLIP": {
|
case "SELECT_PLAYLIST_PATTERN_CLIP": {
|
||||||
const { patternIndex, clipId } = action;
|
const { patternIndex, clipId } = action;
|
||||||
appState.global.selectedPlaylistClipId = clipId ?? null;
|
appState.global.selectedPlaylistClipId = clipId ?? null;
|
||||||
appState.global.selectedPlaylistPatternIndex =
|
appState.global.selectedPlaylistPatternIndex = patternIndex ?? null;
|
||||||
patternIndex ?? null;
|
|
||||||
renderAll();
|
renderAll();
|
||||||
saveStateToSession();
|
saveStateToSession();
|
||||||
break;
|
break;
|
||||||
|
|
@ -1016,12 +1053,8 @@ async function handleActionBroadcast(action) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const isFromSelf = action.__senderId === socket.id;
|
const isFromSelf = action.__senderId === socket.id;
|
||||||
if (window.ROOM_NAME && isFromSelf) {
|
if (currentRoom && isFromSelf) {
|
||||||
const xml = generateXmlFromStateExported();
|
debouncedSyncPatternState();
|
||||||
sendAction({
|
|
||||||
type: "SYNC_PATTERN_STATE",
|
|
||||||
xml,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
saveStateToSession();
|
saveStateToSession();
|
||||||
|
|
@ -1062,7 +1095,7 @@ async function handleActionBroadcast(action) {
|
||||||
case "STOP_AUDIO_PLAYBACK":
|
case "STOP_AUDIO_PLAYBACK":
|
||||||
setTimeout(
|
setTimeout(
|
||||||
() => stopAudioEditorPlayback(action.rewind || false),
|
() => stopAudioEditorPlayback(action.rewind || false),
|
||||||
delayMs
|
delayMs,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|
@ -1133,11 +1166,11 @@ async function handleActionBroadcast(action) {
|
||||||
showToast("📂 Carregando...", "info");
|
showToast("📂 Carregando...", "info");
|
||||||
|
|
||||||
// Esta parte está CORRETA. Sempre limpa o sessionStorage.
|
// Esta parte está CORRETA. Sempre limpa o sessionStorage.
|
||||||
if (window.ROOM_NAME) {
|
if (currentRoom) {
|
||||||
try {
|
try {
|
||||||
sessionStorage.removeItem(`temp_state_${window.ROOM_NAME}`);
|
sessionStorage.removeItem(`temp_state_${currentRoom}`);
|
||||||
console.log(
|
console.log(
|
||||||
"Socket: Estado da sessão local limpo para LOAD_PROJECT."
|
"Socket: Estado da sessão local limpo para LOAD_PROJECT.",
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Socket: Falha ao limpar estado da sessão:", e);
|
console.error("Socket: Falha ao limpar estado da sessão:", e);
|
||||||
|
|
@ -1156,25 +1189,6 @@ async function handleActionBroadcast(action) {
|
||||||
isLoadingProject = false;
|
isLoadingProject = false;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "SYNC_PATTERN_STATE":
|
|
||||||
// 🔥 CORREÇÃO CRÍTICA:
|
|
||||||
// Esta ação serve apenas para o SERVIDOR atualizar o arquivo .json no disco.
|
|
||||||
// Os clientes online NÃO devem recarregar o XML, pois eles já atualizaram
|
|
||||||
// o estado via ações atômicas (TOGGLE_NOTE, ADD_TRACK, etc).
|
|
||||||
// Recarregar aqui causa o "resetProjectState" que mata o áudio.
|
|
||||||
|
|
||||||
console.log("Socket: XML salvo no servidor (Sync silencioso).");
|
|
||||||
|
|
||||||
// Se quiser garantir, salvamos apenas na sessão local do navegador
|
|
||||||
// sem mexer na engine de áudio ou na tela.
|
|
||||||
if (action.xml) {
|
|
||||||
// Atualiza apenas a string do XML na memória global para referência futura
|
|
||||||
const parser = new DOMParser();
|
|
||||||
appState.global.originalXmlDoc = parser.parseFromString(action.xml, "application/xml");
|
|
||||||
saveStateToSession();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "RESET_ROOM":
|
case "RESET_ROOM":
|
||||||
console.log("Socket: Recebendo comando de RESET_ROOM do servidor."); //
|
console.log("Socket: Recebendo comando de RESET_ROOM do servidor."); //
|
||||||
const who = actorOf(action);
|
const who = actorOf(action);
|
||||||
|
|
@ -1230,9 +1244,8 @@ async function handleActionBroadcast(action) {
|
||||||
|
|
||||||
// 🔥 CORREÇÃO: Avisa o servidor que o XML mudou!
|
// 🔥 CORREÇÃO: Avisa o servidor que o XML mudou!
|
||||||
const isFromSelf = action.__senderId === socket.id;
|
const isFromSelf = action.__senderId === socket.id;
|
||||||
if (window.ROOM_NAME && isFromSelf) {
|
if (currentRoom && isFromSelf) {
|
||||||
const xml = generateXmlFromStateExported();
|
debouncedSyncPatternState();
|
||||||
sendAction({ type: "SYNC_PATTERN_STATE", xml });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
saveStateToSession();
|
saveStateToSession();
|
||||||
|
|
@ -1248,16 +1261,15 @@ async function handleActionBroadcast(action) {
|
||||||
const who = actorOf(action);
|
const who = actorOf(action);
|
||||||
showToast(`❌ Faixa removida por ${who}`, "warning");
|
showToast(`❌ Faixa removida por ${who}`, "warning");
|
||||||
|
|
||||||
// Sincroniza o XML com o servidor para persistir a remoção
|
|
||||||
const isFromSelf = action.__senderId === socket.id;
|
const isFromSelf = action.__senderId === socket.id;
|
||||||
if (window.ROOM_NAME && isFromSelf) {
|
if (currentRoom && isFromSelf) {
|
||||||
// Importante: gerar o XML atualizado (sem a faixa)
|
debouncedSyncPatternState();
|
||||||
const xml = generateXmlFromStateExported();
|
|
||||||
sendAction({ type: "SYNC_PATTERN_STATE", xml });
|
|
||||||
}
|
}
|
||||||
saveStateToSession();
|
saveStateToSession();
|
||||||
} else {
|
} else {
|
||||||
console.warn(`Tentativa de remover track inexistente/fantasma: ${trackId}`);
|
console.warn(
|
||||||
|
`Tentativa de remover track inexistente/fantasma: ${trackId}`,
|
||||||
|
);
|
||||||
// Não faz nada, protegendo as faixas locais!
|
// Não faz nada, protegendo as faixas locais!
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
@ -1271,9 +1283,8 @@ async function handleActionBroadcast(action) {
|
||||||
|
|
||||||
// 🔥 CORREÇÃO: Avisa o servidor que o XML mudou (removeu o TripleOscillator, por exemplo)
|
// 🔥 CORREÇÃO: Avisa o servidor que o XML mudou (removeu o TripleOscillator, por exemplo)
|
||||||
const isFromSelf = action.__senderId === socket.id;
|
const isFromSelf = action.__senderId === socket.id;
|
||||||
if (window.ROOM_NAME && isFromSelf) {
|
if (currentRoom && isFromSelf) {
|
||||||
const xml = generateXmlFromStateExported();
|
debouncedSyncPatternState();
|
||||||
sendAction({ type: "SYNC_PATTERN_STATE", xml });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
saveStateToSession();
|
saveStateToSession();
|
||||||
|
|
@ -1313,7 +1324,9 @@ async function handleActionBroadcast(action) {
|
||||||
// 3. Remove os clips associados a essa track (Limpeza)
|
// 3. Remove os clips associados a essa track (Limpeza)
|
||||||
if (lastTrack && lastTrack.id) {
|
if (lastTrack && lastTrack.id) {
|
||||||
// Filtra mantendo apenas os clips que NÃO pertencem à track removida
|
// Filtra mantendo apenas os clips que NÃO pertencem à track removida
|
||||||
appState.audio.clips = appState.audio.clips.filter(clip => clip.trackId !== lastTrack.id);
|
appState.audio.clips = appState.audio.clips.filter(
|
||||||
|
(clip) => clip.trackId !== lastTrack.id,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Atualiza a tela
|
// 4. Atualiza a tela
|
||||||
|
|
@ -1341,7 +1354,12 @@ async function handleActionBroadcast(action) {
|
||||||
|
|
||||||
// Notes
|
// Notes
|
||||||
case "TOGGLE_NOTE": {
|
case "TOGGLE_NOTE": {
|
||||||
const { trackIndex: ti, patternIndex: pi, stepIndex: si, isActive } = action;
|
const {
|
||||||
|
trackIndex: ti,
|
||||||
|
patternIndex: pi,
|
||||||
|
stepIndex: si,
|
||||||
|
isActive,
|
||||||
|
} = action;
|
||||||
|
|
||||||
// ✅ garante que o índice exista em todas as tracks (evita “silêncio” na playlist)
|
// ✅ garante que o índice exista em todas as tracks (evita “silêncio” na playlist)
|
||||||
_ensurePatternsUpTo(pi);
|
_ensurePatternsUpTo(pi);
|
||||||
|
|
@ -1351,7 +1369,8 @@ async function handleActionBroadcast(action) {
|
||||||
t.patterns[pi] = t.patterns[pi] || _makeEmptyPattern(pi);
|
t.patterns[pi] = t.patterns[pi] || _makeEmptyPattern(pi);
|
||||||
|
|
||||||
// mantém metadados estáveis p/ export/sync
|
// mantém metadados estáveis p/ export/sync
|
||||||
if (t.patterns[pi].pos == null) t.patterns[pi].pos = pi * LMMS_BAR_TICKS;
|
if (t.patterns[pi].pos == null)
|
||||||
|
t.patterns[pi].pos = pi * LMMS_BAR_TICKS;
|
||||||
if (!t.patterns[pi].name) t.patterns[pi].name = `Pattern ${pi + 1}`;
|
if (!t.patterns[pi].name) t.patterns[pi].name = `Pattern ${pi + 1}`;
|
||||||
if (!Array.isArray(t.patterns[pi].notes)) t.patterns[pi].notes = [];
|
if (!Array.isArray(t.patterns[pi].notes)) t.patterns[pi].notes = [];
|
||||||
|
|
||||||
|
|
@ -1362,13 +1381,14 @@ async function handleActionBroadcast(action) {
|
||||||
// importante: se o Audio Editor estiver tocando, precisa re-schedulear
|
// importante: se o Audio Editor estiver tocando, precisa re-schedulear
|
||||||
restartAudioEditorIfPlaying();
|
restartAudioEditorIfPlaying();
|
||||||
|
|
||||||
try { updateStepUI(ti, pi, si, isActive); } catch {}
|
try {
|
||||||
|
updateStepUI(ti, pi, si, isActive);
|
||||||
|
} catch {}
|
||||||
if (!isFromSelf) schedulePatternRerender();
|
if (!isFromSelf) schedulePatternRerender();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (window.ROOM_NAME && isFromSelf) {
|
if (currentRoom && isFromSelf) {
|
||||||
const xml = generateXmlFromStateExported();
|
debouncedSyncPatternState();
|
||||||
sendAction({ type: "SYNC_PATTERN_STATE", xml });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
saveStateToSession();
|
saveStateToSession();
|
||||||
|
|
@ -1406,12 +1426,11 @@ async function handleActionBroadcast(action) {
|
||||||
|
|
||||||
// 4) Sync online somente se fui EU quem enviei
|
// 4) Sync online somente se fui EU quem enviei
|
||||||
const isFromSelf = action.__senderId === socket.id;
|
const isFromSelf = action.__senderId === socket.id;
|
||||||
if (window.ROOM_NAME && isFromSelf) {
|
if (currentRoom && isFromSelf) {
|
||||||
try {
|
try {
|
||||||
const xml = generateXmlFromStateExported(); // ← CORREÇÃO
|
|
||||||
sendAction({
|
sendAction({
|
||||||
type: "SYNC_PATTERN_STATE",
|
type: "SYNC_PATTERN_STATE",
|
||||||
xml,
|
patternState: appState.pattern, // Envia o objeto puro
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Erro gerando XML UPDATE_PATTERN_NOTES", e);
|
console.error("Erro gerando XML UPDATE_PATTERN_NOTES", e);
|
||||||
|
|
@ -1503,8 +1522,8 @@ async function handleActionBroadcast(action) {
|
||||||
const stepsPerBar = 16; // 4/4 em 1/16
|
const stepsPerBar = 16; // 4/4 em 1/16
|
||||||
|
|
||||||
const maxSteps = appState.pattern.tracks
|
const maxSteps = appState.pattern.tracks
|
||||||
.filter(t => t.type !== "bassline")
|
.filter((t) => t.type !== "bassline")
|
||||||
.map(t => t.patterns?.[patternIndex]?.steps?.length || 0)
|
.map((t) => t.patterns?.[patternIndex]?.steps?.length || 0)
|
||||||
.reduce((a, b) => Math.max(a, b), 0);
|
.reduce((a, b) => Math.max(a, b), 0);
|
||||||
|
|
||||||
const barsEl = document.getElementById("bars-input");
|
const barsEl = document.getElementById("bars-input");
|
||||||
|
|
@ -1513,7 +1532,6 @@ async function handleActionBroadcast(action) {
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
|
|
||||||
// Mostra o toast (só uma vez)
|
// Mostra o toast (só uma vez)
|
||||||
if (!isFromSelf) {
|
if (!isFromSelf) {
|
||||||
const who = actorOf(action);
|
const who = actorOf(action);
|
||||||
|
|
@ -1535,7 +1553,9 @@ async function handleActionBroadcast(action) {
|
||||||
case "AUDIO_SNAPSHOT_REQUEST": {
|
case "AUDIO_SNAPSHOT_REQUEST": {
|
||||||
// 🛑 BLOQUEADO: Agora o Servidor (backend) é quem manda o snapshot autoritativo.
|
// 🛑 BLOQUEADO: Agora o Servidor (backend) é quem manda o snapshot autoritativo.
|
||||||
// Se deixarmos os clientes responderem, eles vão enviar estados antigos ("zumbis").
|
// Se deixarmos os clientes responderem, eles vão enviar estados antigos ("zumbis").
|
||||||
console.log("Socket: Ignorando pedido de snapshot (Server Authoritative Mode).");
|
console.log(
|
||||||
|
"Socket: Ignorando pedido de snapshot (Server Authoritative Mode).",
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1584,11 +1604,11 @@ async function handleActionBroadcast(action) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
const trackExists = appState.audio?.tracks?.some(
|
const trackExists = appState.audio?.tracks?.some(
|
||||||
(t) => t.id === trackId
|
(t) => t.id === trackId,
|
||||||
);
|
);
|
||||||
if (!trackExists) {
|
if (!trackExists) {
|
||||||
console.warn(
|
console.warn(
|
||||||
`ADD_AUDIO_CLIP Pista ${trackId} não existe, criando...`
|
`ADD_AUDIO_CLIP Pista ${trackId} não existe, criando...`,
|
||||||
);
|
);
|
||||||
appState.audio.tracks.push({
|
appState.audio.tracks.push({
|
||||||
id: trackId,
|
id: trackId,
|
||||||
|
|
@ -1605,7 +1625,7 @@ async function handleActionBroadcast(action) {
|
||||||
startTimeInSeconds,
|
startTimeInSeconds,
|
||||||
clipId,
|
clipId,
|
||||||
name,
|
name,
|
||||||
patternData // <-- AQUI ESTÁ A "PARTITURA"
|
patternData, // <-- AQUI ESTÁ A "PARTITURA"
|
||||||
);
|
);
|
||||||
// --- FIM DA MODIFICAÇÃO ---
|
// --- FIM DA MODIFICAÇÃO ---
|
||||||
|
|
||||||
|
|
@ -1617,7 +1637,7 @@ async function handleActionBroadcast(action) {
|
||||||
`🎧 ${who} add “${(name || filePath || "")
|
`🎧 ${who} add “${(name || filePath || "")
|
||||||
.split(/[\\/]/)
|
.split(/[\\/]/)
|
||||||
.pop()}” em ${pista}`,
|
.pop()}” em ${pista}`,
|
||||||
"success"
|
"success",
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Erro ADD_AUDIO_CLIP:", e);
|
console.error("Erro ADD_AUDIO_CLIP:", e);
|
||||||
|
|
@ -1632,8 +1652,8 @@ async function handleActionBroadcast(action) {
|
||||||
isLoadingProject = true;
|
isLoadingProject = true;
|
||||||
showToast("📂 Carregando beat index...", "info");
|
showToast("📂 Carregando beat index...", "info");
|
||||||
|
|
||||||
if (window.ROOM_NAME) {
|
if (currentRoom) {
|
||||||
sessionStorage.removeItem(`temp_state_${window.ROOM_NAME}`);
|
sessionStorage.removeItem(`temp_state_${currentRoom}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -1669,18 +1689,23 @@ async function handleActionBroadcast(action) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case "REMOVE_AUDIO_LANE_BY_ID": { //
|
case "REMOVE_AUDIO_LANE_BY_ID": {
|
||||||
|
//
|
||||||
const { trackId } = action;
|
const { trackId } = action;
|
||||||
|
|
||||||
// 1. Encontra o índice da faixa com esse ID específico
|
// 1. Encontra o índice da faixa com esse ID específico
|
||||||
const trackIndex = appState.audio.tracks.findIndex(t => t.id === trackId);
|
const trackIndex = appState.audio.tracks.findIndex(
|
||||||
|
(t) => t.id === trackId,
|
||||||
|
);
|
||||||
|
|
||||||
if (trackIndex !== -1) {
|
if (trackIndex !== -1) {
|
||||||
// 2. Remove a faixa exata do array local
|
// 2. Remove a faixa exata do array local
|
||||||
appState.audio.tracks.splice(trackIndex, 1);
|
appState.audio.tracks.splice(trackIndex, 1);
|
||||||
|
|
||||||
// 3. Remove os clips associados a este ID (Limpeza local)
|
// 3. Remove os clips associados a este ID (Limpeza local)
|
||||||
appState.audio.clips = appState.audio.clips.filter(c => c.trackId !== trackId);
|
appState.audio.clips = appState.audio.clips.filter(
|
||||||
|
(c) => c.trackId !== trackId,
|
||||||
|
);
|
||||||
|
|
||||||
renderAll(); // Atualiza a tela
|
renderAll(); // Atualiza a tela
|
||||||
|
|
||||||
|
|
@ -1703,21 +1728,31 @@ async function handleActionBroadcast(action) {
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// RECUPERAÇÃO DE ESTADO (TAB SLEEP / WAKE UP)
|
// RECUPERAÇÃO DE ESTADO (TAB SLEEP / WAKE UP)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
let lastHiddenTime = Date.now();
|
||||||
|
/*
|
||||||
document.addEventListener("visibilitychange", () => {
|
document.addEventListener("visibilitychange", () => {
|
||||||
if (document.visibilityState === "visible") {
|
if (document.visibilityState === "hidden") {
|
||||||
console.log("%c[SISTEMA] Aba reativada! Solicitando estado...", "color: #00ff00; background: #222; padding: 4px;");
|
lastHiddenTime = Date.now(); // Marca a hora que você saiu da aba
|
||||||
|
}
|
||||||
|
else if (document.visibilityState === "visible") {
|
||||||
|
const sleepTimeMs = Date.now() - lastHiddenTime;
|
||||||
|
|
||||||
|
// Só força o Full Sync se a aba ficou escondida por MAIS DE 5 SEGUNDOS.
|
||||||
|
// Isso evita o loop infinito ao abrir o DevTools (F12) ou dar Alt-Tab rápido.
|
||||||
|
if (sleepTimeMs > 5000) {
|
||||||
|
console.log(`%c[SISTEMA] Aba reativada após ${Math.round(sleepTimeMs/1000)}s! Solicitando estado...`, "color: #00ff00; background: #222; padding: 4px;");
|
||||||
|
|
||||||
// Usa o currentRoom oficial
|
|
||||||
if (socket.connected && currentRoom) {
|
if (socket.connected && currentRoom) {
|
||||||
// Deleta o cache local velho
|
|
||||||
sessionStorage.removeItem(`temp_state_${currentRoom}`);
|
sessionStorage.removeItem(`temp_state_${currentRoom}`);
|
||||||
|
|
||||||
// Pede o estado fresquinho
|
|
||||||
socket.emit("request_full_sync", { room: currentRoom });
|
socket.emit("request_full_sync", { room: currentRoom });
|
||||||
showToast("🔄 Sincronizando com a sala...", "info");
|
showToast("🔄 Sincronizando aba inativa...", "info");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log("[SISTEMA] Aba reativada rapidamente. Ignorando Full Sync para poupar processamento.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
*/
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// NOTIFICAÇÕES DE PRESENÇA NA SALA
|
// NOTIFICAÇÕES DE PRESENÇA NA SALA
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue