@leparadoxhd/angular-hermes
v1.0.6
Published
Angular 20+ Socket.IO client for Hermes — auto-connect, channels, events$
Readme
@leparadoxhd/angular-hermes
Cliente Socket.IO para aplicaciones Angular 20+ (standalone) que se conectan a Hermes Server.
Hermes expone namespaces dinámicos (/${project}). Este paquete:
- Conecta automáticamente al arrancar la app.
- Gestiona
join/leavede rooms con prefijosuser:ygroup:. - Re-une la sesión de usuario tras reconexión.
- Expone streams RxJS (
connected$,events$,on<T>()).
Para encolar notificaciones desde el backend (BullMQ), usa @leparadoxhd/nestjs-hermes o @leparadoxhd/hermes-client. Este paquete es solo el cliente en el navegador.
Requisitos
| Dependencia | Versión |
|-------------|---------|
| @angular/core | ^20 o ^21 |
| @angular/common | ^20 o ^21 |
| rxjs | ^7.8 |
| socket.io-client | ^4.8 |
| Hermes Server | Socket.IO en el mismo project que configuras |
Instalación
npm install @leparadoxhd/angular-hermes socket.io-clientsocket.io-client es peer obligatorio (no se empaqueta dentro de la librería).
Configuración rápida
1. Registrar el provider
En app.config.ts (sin NgModule):
import { ApplicationConfig } from "@angular/core";
import { provideHermesSocket } from "@leparadoxhd/angular-hermes";
export const appConfig: ApplicationConfig = {
providers: [
provideHermesSocket({
project: "mi-app",
socketUrl: "https://hermes.example.com",
authToken: "secreto-en-claro", // opcional; el servidor guarda bcrypt en JSON
}),
],
};| Opción | Descripción |
|--------|-------------|
| project | Nombre del namespace. Debe coincidir con el project de Hermes / NestJS (forRoot({ project })). |
| socketUrl | Origen del servidor Hermes (protocolo + host + puerto). Ej. http://localhost:3000 en dev. |
| authToken? | Secreto en claro para namespaces protegidos (auth.token en handshake). No envíes el hash bcrypt. |
| autoConnect? | Conectar al instanciar el servicio. Por defecto true. Usa false si conectarás tras login con connect(token). |
Fijos en la librería (no configurables):
- Ruta Socket.IO:
path: "/"(mismo default que el servidor). - Transporte: solo
websocket.
URL efectiva de conexión:
{socketUrl}/{project}Ejemplo: https://hermes.example.com/mi-app → namespace /mi-app.
2. Inyectar el servicio
import { inject, Injectable } from "@angular/core";
import { HermesSocketService } from "@leparadoxhd/angular-hermes";
@Injectable({ providedIn: "root" })
export class NotificacionesService {
private readonly hermes = inject(HermesSocketService);
}El servicio se instancia al resolver el injector (normalmente al primer inject). Por defecto conecta al namespace en el constructor; no hace join hasta que llames a joinUserChannel o joinGroupChannel.
Para namespaces protegidos cuando el token solo existe tras login:
provideHermesSocket({
project: "mi-app",
socketUrl: environment.hermesUrl,
autoConnect: false,
});
// auth.service.ts — tras login
await this.hermes.connect(authToken);
await this.hermes.joinUserChannel(userHash);Si el token ya está en el config, connect() sin argumentos reconecta con ese valor. connect(nuevoToken) tiene prioridad y fuerza reconexión si cambia el secreto.
Flujo recomendado
sequenceDiagram
participant App as App_Angular
participant HS as HermesSocketService
participant Srv as Hermes_Server
App->>HS: provideHermesSocket (bootstrap)
HS->>Srv: connect /project
Note over App,Srv: Usuario anónimo — solo conectado al namespace
App->>HS: joinUserChannel(userHash)
HS->>Srv: join user:hash + group:authenticated
Srv-->>HS: emit notification (vía cola BullMQ)
HS-->>App: on notification
App->>HS: leaveUserChannel (logout)
HS->>Srv: leave user + authenticated- Bootstrap: conexión al namespace (sin rooms).
- Tras login:
await joinUserChannel(userHash). - Escuchar:
on<T>('nombre-evento')oevents$. - Logout:
await leaveUserChannel(userHash). - Grupos opcionales:
joinGroupChannel/leaveGroupChannelcuando haga falta.
Protocolo de rooms (Hermes)
Hermes solo acepta suscripciones con rooms user:<id> o group:<id> (id: letras, números, _, -, ., máx. 128 caracteres).
| Acción cliente | Evento Socket.IO | Payload |
|----------------|------------------|---------|
| Confirmación namespace | connected (servidor → cliente) | { success: true, namespace, socketId } |
| Suscribirse | join | "user:abc" o ["user:abc", "group:ventas"] → ack { success, ok, rooms } |
| Desuscribirse | leave | Igual formato |
Este paquete construye esos nombres por ti en los métodos públicos; no envíes el prefijo en joinUserChannel (solo el hash).
Canal de usuario autenticado
joinUserChannel(hash) une dos rooms en un solo join:
| Room | Uso |
|------|-----|
| user:<hash> | Mensajes dirigidos a un usuario (backend: user: hash en la cola). |
| group:authenticated | Broadcast a todos los usuarios logueados (backend: group: "authenticated"). |
Constante exportada: AUTHENTICATED_GROUP_ID === "authenticated".
No hay auto-join a group:public. Si lo necesitas, llama explícitamente joinGroupChannel("public").
HermesSocketService — API
Conexión
connect(authToken?: string): Promise<void>;
disconnect(): void;Conecta o reconecta al namespace. authToken opcional; si se omite, usa authToken del config. La Promise se resuelve cuando el WebSocket conecta; se rechaza en connect_error. Idempotente si ya está conectado con el mismo token.
disconnect() cierra la conexión y limpia la sesión activa (no re-join automático al reconectar hasta que vuelvas a llamar joinUserChannel).
Estado de conexión
readonly connected$: Observable<boolean>;truetrasconnect.falseendisconnectoconnect_error.- Emite el valor actual al suscribirse (
BehaviorSubjectinterno).
Eventos entrantes
readonly events$: Observable<HermesSocketMessage>;
// HermesSocketMessage = { event: string; payload: unknown }
on<T>(eventName: string): Observable<T>;Todos los eventos que envía el servidor (vía worker websocket) llegan a events$. on<T>() filtra por nombre y tipa el payload.
// Preferido: tipado
hermes.on<{ msg: string }>("notification").subscribe((data) => {
console.log(data.msg);
});
// Todos los eventos
hermes.events$.subscribe(({ event, payload }) => {
console.log(event, payload);
});Recuerda desuscribirte (takeUntilDestroyed, async pipe, etc.) para evitar fugas.
Canales de usuario
await hermes.joinUserChannel(userHash); // Promise<void>
await hermes.leaveUserChannel(userHash);- Guarda
userHashcomo sesión activa (activeUserHash). - Valida el id con
ID_PATTERN; si no es válido, lanzaError. - Si el socket no está conectado:
Error: Hermes socket is not connected. - Si el servidor rechaza el join:
Errorcon el mensaje del ack (invalid room, etc.).
leaveUserChannel hace leave de user:<hash> y group:authenticated. Si el hash coincide con la sesión activa, limpia activeUserHash.
Canales de grupo
await hermes.joinGroupChannel(groupHash);
await hermes.leaveGroupChannel(groupHash);Solo une group:<hash>. No se re-unen automáticamente tras reconexión (a diferencia de la sesión de usuario).
Reconexión
Si el socket se cae y vuelve a conectar:
- Si había
joinUserChannelprevio → reintentajoindeuser:<hash>+group:authenticated(silencioso si falla; puedes volver a llamarjoinUserChannel). - Los grupos unidos con
joinGroupChannelno se restauran solos.
Ejemplo completo (auth + notificaciones)
// app.config.ts
export const appConfig: ApplicationConfig = {
providers: [
provideHermesSocket({
project: environment.hermesProject,
socketUrl: environment.hermesUrl,
}),
],
};
// auth.service.ts
@Injectable({ providedIn: "root" })
export class AuthService {
private readonly hermes = inject(HermesSocketService);
async onLogin(userHash: string, authToken: string): Promise<void> {
await this.hermes.connect(authToken);
await this.hermes.joinUserChannel(userHash);
}
async onLogout(userHash: string): Promise<void> {
await this.hermes.leaveUserChannel(userHash);
this.hermes.disconnect();
}
}
// notificaciones.service.ts
@Injectable({ providedIn: "root" })
export class NotificacionesService {
private readonly hermes = inject(HermesSocketService);
private readonly destroyRef = inject(DestroyRef);
iniciarEscucha(): void {
this.hermes
.on<{ title: string; body: string }>("notification")
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((n) => {
// mostrar toast, etc.
});
}
}Enviar eventos desde el backend (NestJS)
El frontend escucha el nombre de evento que encolas en el servidor. El targeting usa hash sin prefijo en el job; Hermes añade user: / group: al emitir.
// NestJS — tras login del usuario
await this.hermesWs.add({
event: "notification",
payload: { title: "Hola", body: "Mundo" },
user: userHash, // → sockets en room user:<hash>
});
// Solo usuarios autenticados (todos los que hicieron joinUserChannel)
await this.hermesWs.add({
event: "system-announcement",
payload: { msg: "Mantenimiento en 5 min" },
group: "authenticated",
});
// Varios grupos
await this.hermesWs.add({
event: "team-update",
payload: { teamId: 1 },
group: ["ventas", "admins"],
});
// Todo el namespace (sin user ni group)
await this.hermesWs.add({
event: "global-ping",
payload: { t: Date.now() },
});Instalación Nest: @leparadoxhd/nestjs-hermes.
Variables de entorno (referencia)
En la app Angular (build-time):
# environment.ts
hermesUrl: 'http://localhost:3000',
hermesProject: 'mi-app',En Hermes Server (debe coincidir el project):
PORT=3000
# SOCKETIO_PATH=/socket.io # opcional; el servidor ya usa el estándarDetalle del servidor: README del monorepo.
API exportada
| Símbolo | Tipo | Descripción |
|---------|------|-------------|
| provideHermesSocket | función | Registra config + HermesSocketService |
| HermesSocketService | clase | Cliente Socket.IO inyectable |
| HermesSocketConfig | interface | { project, socketUrl, authToken?, autoConnect? } |
| HermesSocketMessage | interface | { event, payload } |
| InboundAck | type | Respuesta ack de join/leave (uso interno) |
| HERMES_SOCKET_OPTIONS | InjectionToken | Token de configuración |
| AUTHENTICATED_GROUP_ID | string | "authenticated" |
| userRoom(id) | función | "user:" + id |
| groupRoom(id) | función | "group:" + id |
Errores frecuentes
| Síntoma | Causa probable |
|---------|----------------|
| No llegan eventos | No llamaste joinUserChannel o el backend usa otro user / namespace. |
| Hermes socket is not connected | join* antes de connected$ === true. Espera conexión o reintenta. |
| Invalid user hash | Caracteres no permitidos en el hash (espacios, : en el id, etc.). |
| Conecta pero 404 / CORS | socketUrl incorrecta o proxy que no deja pasar WebSocket. |
| Namespace vacío | project distinto entre Angular y NestJS / Hermes. |
Desarrollo en el monorepo
bun run build:packages # incluye angular-hermes (ng-packagr)
bun test packages/angular-hermes/testsPublicación: ver PUBLISHING.md.
Relación con otros paquetes
| Paquete | Rol | |---------|-----| | angular-hermes (este) | Cliente Socket.IO en el navegador | | nestjs-hermes | Encolar jobs desde API Nest | | hermes-client | Cliente BullMQ sin framework | | hermes-server | Workers + Socket.IO |
