Elección de líder: el bloqueo que nunca se suelta
El patrón entero cabe en una idea: todas las pestañas piden el mismo bloqueo exclusivo, quien lo consigue es el líder mientras no lo suelte, y la cola de espera es la línea de sucesión que se ejecuta sola.
Con la garantía de liberación en la mano, la elección de líder deja de ser un algoritmo y pasa a ser un truco de tres líneas. No hay votaciones, no hay quórum, no hay mayorías ni identificadores que comparar: hay un nombre, una cola y un navegador que sabe quién está vivo. Lo interesante de este patrón no es su implementación, que verás entera en un minuto, sino todo lo que se sigue de ella —cómo se anuncia un líder, cómo lo descubre quien llega tarde, cómo abdica cuando conviene y qué significa exactamente ser el líder cuando nadie te ha dado un certificado.
- Implementar la elección de líder reteniendo un bloqueo exclusivo con una promesa que no resuelve.
- Entender la cola de espera como línea de sucesión automática y por qué no necesitas detectar la muerte del líder.
- Anunciar y descubrir el liderazgo por un canal de difusión sin robar el turno a nadie.
- Diseñar la abdicación voluntaria y saber en qué casos conviene y en cuáles hace daño.
El bloqueo que nunca se suelta
Recuerda la regla de la lección anterior: el bloqueo dura lo que tarde en asentarse la promesa que devuelve tu función. Si esa promesa no se asienta nunca, el bloqueo no se suelta nunca. Eso, que en cualquier otro contexto sería un error de programación, aquí es exactamente la declaración que buscas: yo soy el líder hasta que me muera.
// Todas las pestanas ejecutan esto al arrancar. Sin excepciones.
function competirPorElLiderazgo(alSerLider, nombre = 'app-lider-v1') {
let abdicar;
const abdicacion = new Promise((resolver) => { abdicar = resolver; });
const terminado = navigator.locks.request(nombre, { mode: 'exclusive' }, async () => {
const desmontar = await alSerLider(); // arrancar todo lo que solo hace el lider
await abdicacion; // aqui se retiene el bloqueo
await desmontar(); // salida ordenada si abdicamos
});
return { abdicar, terminado };
}
Fíjate en lo que no hay en ese código. No hay una rama para el caso de ser seguidor, porque el seguidor no ejecuta nada: su llamada simplemente no ha vuelto todavía. No hay temporizador que compruebe si el líder sigue ahí. No hay identificador de pestaña que comparar para desempatar. Y sobre todo, no hay ninguna lógica de detección de fallos, que es justo la parte que en cualquier otro sistema ocupa el noventa por ciento del código y produce el noventa por ciento de los errores.
La consecuencia es que todas las pestañas ejecutan literalmente la misma línea. La primera que llega entra y se queda; las que llegan después quedan suspendidas en un await que puede durar horas y que no consume nada. Un seguidor no es una pestaña que sepa que es seguidor: es una pestaña cuyo turno todavía no ha llegado.
Alguien objetará que mantener promesas colgadas para siempre en varias pestañas suena a fuga. No lo es. Una petición pendiente en la cola de bloqueos es una entrada en una estructura del navegador y una promesa sin resolver en tu montón: no hay temporizador corriendo, no hay evento repitiéndose, no hay tarea que despierte al hilo. El coste en energía es exactamente cero mientras nada cambie, que es precisamente lo contrario de lo que ocurre con un latido periódico o con un sondeo del estado. Este patrón no solo es más correcto que un sistema de latidos: además gasta menos batería, y en un portátil con doce pestañas abiertas eso se nota.
La cola es la línea de sucesión
El segundo regalo de la API es que la sucesión ya está escrita. Cuando la pestaña líder desaparece, el navegador libera el bloqueo y se lo concede a la siguiente petición de la cola, que estaba esperando desde el principio. Esa pestaña se despierta dentro de su propio callback, ejecuta el arranque del líder y ocupa el puesto. Nadie ha detectado nada, nadie ha votado y no ha hecho falta ningún acuerdo.
flowchart TB P[Todas las pestanas piden el nombre app-lider en modo exclusivo] --> A[Una lo obtiene y pasa a ser el lider] P --> B[Las demas quedan en cola sin ejecutar nada] A --> W[Arranca el worker y abre el almacenamiento] A --> R[Retiene el bloqueo con una promesa que no resuelve] R --> M[La pestana lider muere o abdica] M --> N[El navegador concede el bloqueo al primero de la cola] N --> A style A fill:#a6e3a1,color:#11111b style M fill:#f38ba8,color:#11111b style N fill:#89b4fa,color:#11111b
El orden de esa cola es el de llegada y se respeta por nombre, así que el relevo es determinista y no una carrera. Eso importa más de lo que parece cuando el navegador restaura una sesión con cuatro pestañas de tu aplicación a la vez: las cuatro piden, una entra y las otras tres quedan ordenadas, de modo que la sucesión ya está decidida antes de que ocurra nada.
Vale la pena comparar esto con lo que habría que escribir sin la API, porque solo así se aprecia lo que se ahorra. Un sistema de latidos necesita que el líder emita una señal periódica, que cada seguidor mantenga un reloj de vencimiento, que alguien decida cuánto silencio equivale a muerte, que la elección subsiguiente tenga desempate para cuando dos seguidores reaccionen a la vez, y que exista un procedimiento para el caso en que el supuesto muerto reaparezca. Son cinco piezas, cada una con sus parámetros y todas ellas fuente de errores intermitentes imposibles de reproducir. Aquí no hay ninguna: la muerte no se estima, se observa, y la observa quien tiene autoridad para hacerlo.
Hay dos matices que conviene tener presentes desde ya, aunque la última lección del nivel los desarrolla. El primero es que el relevo puede tardar: el navegador libera cuando destruye el contexto, y una pestaña suspendida no está destruida. El segundo es que el nuevo líder hereda el puesto pero no el estado: no sabe qué estaba haciendo el anterior, qué transacción quedó a medias ni qué peticiones se quedaron sin respuesta. La sucesión es automática; la recuperación, no.
La tentación inmediata es escribir let soyLider = true dentro del callback y consultarlo desde el resto de la aplicación. Es un error sutil y caro. Ese valor solo puede afirmar algo sobre el pasado: dice que en algún momento se te concedió el bloqueo, no que ahora lo tengas. Con el robo de bloqueos, con una suspensión larga o con un cierre desordenado del propio contexto, la variable puede seguir en cierto mientras otra pestaña ya está actuando como líder. Un liderazgo no es un estado que se guarda, es una concesión que se sostiene, y la única forma honesta de tratarlo es mantener toda la actividad del líder dentro del ámbito del callback y desmontarla cuando ese ámbito termina.
Anunciarse y ser descubierto
El líder no le sirve de nada a nadie si los demás no saben cómo hablarle. Aquí entra el segundo componente del patrón, que es un canal de difusión compartido por el origen. El líder se presenta cuando toma el mando y responde a quien pregunte; los seguidores preguntan al arrancar y escuchan los anuncios.
const canal = new BroadcastChannel('app-coordinacion');
const miId = crypto.randomUUID();
competirPorElLiderazgo(async () => {
const epoca = Date.now(); // marca de este mandato
canal.postMessage({ tipo: 'lider', id: miId, epoca });
const alMensaje = (ev) => {
if (ev.data.tipo === 'quien-manda') {
canal.postMessage({ tipo: 'lider', id: miId, epoca });
}
};
canal.addEventListener('message', alMensaje);
return () => canal.removeEventListener('message', alMensaje);
});
// Cualquier pestana que arranque y aun no tenga el mando
canal.postMessage({ tipo: 'quien-manda' });
El anuncio al tomar el mando cubre a las pestañas que ya estaban abiertas; la pregunta al arrancar cubre a las que llegan cuando el líder ya lleva rato ahí. Con esas dos piezas nadie se queda sin saber a quién dirigirse, y ninguna de las dos requiere sondear periódicamente.
Queda una pregunta que casi todo el mundo se hace y que conviene contestar con precisión: cómo averiguo si hay líder sin convertirme en uno. La respuesta corta es que hay dos maneras y solo una es aceptable. navigator.locks.query te devuelve una instantánea del origen y puedes mirar si el nombre aparece entre los tomados, pero esa lectura es una fotografía obsoleta en cuanto la recibes y jamás debe gobernar una decisión. La forma correcta es la de arriba: preguntar por el canal y esperar la respuesta del líder, porque quien contesta lo hace desde dentro de su callback, es decir, desde un sitio donde el liderazgo aún es real.
Anuncio al tomar el mando
En cuanto el callback arranca, el nuevo líder se presenta por el canal con su identificador y su época. Las pestañas que ya estaban abiertas se enteran del relevo sin haber tenido que vigilar nada.
Pregunta al arrancar
Quien acaba de abrirse difunde una petición de censo. Si hay líder, contesta; si nadie contesta en un plazo corto, es que la elección todavía se está resolviendo y basta con seguir esperando.
La época del mandato
Cada mandato lleva un número que crece. Sirve para descartar mensajes de un líder anterior que reaparece tarde, y es la base del mecanismo de protección que verás en la última lección.
Nunca decidir con la consulta
navigator.locks.query es un diagnóstico excelente y una fuente de verdad pésima. Entre que la lees y actúas, el líder puede haber cambiado dos veces. Úsala en tu panel de depuración y en ningún sitio más.
Abdicar a propósito
El líder puede soltar el bloqueo por voluntad propia: basta con resolver la promesa que lo retiene. La abdicación tiene usos legítimos y también una forma muy fácil de hacerse mal, así que conviene separar los casos.
const { abdicar } = competirPorElLiderazgo(arrancarComoLider);
// Caso legitimo: cerrar la sesion o desmontar la aplicacion incrustada
botonCerrarSesion.addEventListener('click', async () => {
abdicar(); // libera el bloqueo y desmonta el trabajo del lider
await terminado; // esperar a que la salida ordenada acabe
});
Los casos donde abdicar tiene sentido son pocos y todos comparten un rasgo: el líder sabe que va a dejar de ser útil antes de que el navegador se entere. Cerrar la sesión del usuario, desactivar un componente incrustado, aplicar una actualización de la aplicación que exige reabrir el almacenamiento con otro esquema. En todos ellos la abdicación es una salida ordenada que evita que la sucesión ocurra por la vía traumática.
Y hay un caso que parece razonable y no lo es: abdicar porque la pestaña ha pasado a segundo plano. Es tentador pensar que el líder debería ser siempre la pestaña que el usuario está mirando, pero encadenar el liderazgo a la visibilidad produce un baile continuo de relevos cada vez que alguien alterna entre dos pestañas, y cada relevo implica cerrar el almacenamiento, abrirlo de nuevo y rehacer el estado. El coste de mover el mando es alto; el de que el mando esté en una pestaña de fondo, casi siempre bajo. La lección cuatro del nivel se ocupa de cuándo esa intuición sí acierta y de cómo atenderla sin mover el liderazgo entero.
Conviene medir la magnitud de lo que acabas de conseguir, porque quien no viene de sistemas distribuidos tiende a subestimarlo y quien sí viene tiende a no creérselo. La elección de líder es uno de los problemas fundacionales de la disciplina y su solución honesta es célebre por su dificultad: Paxos y Raft existen precisamente para esto, y ambos necesitan una mayoría de participantes, un registro replicado, mandatos numerados, votaciones con desempate aleatorio y una prueba de corrección que ocupa un artículo entero, todo ello para acabar admitiendo que en presencia de particiones y relojes desincronizados lo único que pueden garantizar es que no habrá dos líderes al mismo tiempo, nunca que habrá uno. Tus pestañas son un sistema distribuido en casi todos los sentidos que importan: participantes independientes, sin memoria compartida, que fallan por separado, se suspenden sin avisar y se comunican por mensajes. Y sin embargo aquí el problema se disuelve, no se resuelve, y merece la pena entender exactamente por qué: porque existe un participante privilegiado, el navegador, que no comparte los fallos de los demás, que observa las muertes en lugar de inferirlas y que no puede quedar particionado respecto a sus propias pestañas. Un coordinador infalible convierte el consenso en un problema trivial; eso ya lo sabía la teoría, lo que no es habitual es tener uno. Extrae de aquí la moraleja general, porque vale para todo el track y para tu carrera entera: antes de implementar un algoritmo distribuido, busca si existe en tu sistema algún punto que ya sepa la verdad. Si lo hay, tu trabajo no es replicar el conocimiento sino canalizarlo, y la diferencia entre las dos actitudes son mil líneas de código y una clase entera de errores que nunca llegarás a tener. Y cuando no lo haya —que es lo que pasa entre dispositivos distintos, a partir del nivel veinte— entonces sí, sacas a Raft, o mejor aún, eliges una estructura de datos que no necesite ponerse de acuerdo con nadie.
- Implementa la función de competición y ejecútala en tres pestañas. Pinta en cada una si es líder o seguidora y comprueba que solo una lo es.
- Cierra la pestaña líder y mide el tiempo hasta que otra se anuncia. Repite matándola de golpe desde el gestor de tareas.
- Añade el canal de difusión con el anuncio y la pregunta de censo, y abre una cuarta pestaña para verificar que descubre al líder de inmediato.
- Numera cada mandato con una época creciente y muéstrala en pantalla. Observa cómo avanza a cada relevo.
- Implementa la abdicación voluntaria con un botón y comprueba que el desmontaje ordenado ocurre antes de que otra pestaña tome el mando.