npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@leparadoxhd/nestjs-hermes

v1.0.6

Published

NestJS module for Hermes — send tasks via BullMQ without exposing queue internals

Readme

@leparadoxhd/nestjs-hermes

Módulo NestJS 11 para enviar tareas a Hermes Server sin tocar BullMQ, carpetas en disco ni job.json. Tu aplicación solo llama a servicios inyectables; el plugin y el worker Hermes se encargan del resto.

Re-exporta los tipos de @leparadoxhd/hermes-client para que puedas importar todo desde un solo paquete.


Qué resuelve este plugin

| Sin Hermes (manual) | Con @leparadoxhd/nestjs-hermes | | --------------------------------------------- | ------------------------------------------------------ | | Crear colas BullMQ, backoff, reintentos | Política fija de Hermes (interna) | | Escribir SHARED_ROOT/<proyecto>/<jobId>/... | image.process() con input en tu disco | | Encolar con el mismo jobId que en disco | El client genera UUID, disco y Redis | | Gestionar conexiones Redis por cola | Una conexión Redis compartida creada por el módulo |

Hermes Server (Bun) consume las colas: WebSocket (Socket.IO), email (Gmail), push (Home Assistant) e imágenes (Sharp).


Requisitos

  • NestJS 11+
  • Redis accesible desde tu app y desde Hermes Server
  • Hermes Server en ejecución (workers + Socket.IO)
  • Volumen compartido montado en la misma ruta en productor y servidor (storage en el módulo = SHARED_ROOT en Hermes)
  • Misma versión de contrato que el servidor (paquete hermes-client publicado junto a este módulo)

Dependencias npm

npm install @leparadoxhd/nestjs-hermes bullmq ioredis

| Paquete | Rol | | ---------------------------- | ------------------------------------------------------------------------------- | | @leparadoxhd/nestjs-hermes | Módulo + servicios Nest | | bullmq | Peer — colas (usado por hermes-client dentro del plugin) | | ioredis | Peer — el plugin crea la instancia; no necesitas new Redis() en tu código | | @nestjs/common | Peer — Nest 11 |

No necesitas @nestjs/bullmq en tu aplicación.


Instalación rápida

// app.module.ts
import { Module } from "@nestjs/common";
import { HermesModule } from "@leparadoxhd/nestjs-hermes";

@Module({
  imports: [
    HermesModule.forRoot({
      connection: { host: "localhost", port: 6379 },
      storage: "/shared",
      project: "mi-app",
    }),
  ],
})
export class AppModule {}

El módulo es @Global() por defecto (isGlobal !== false): puedes inyectar HermesImageService, HermesEmailService, HermesPushService y HermesWebsocketService en cualquier feature sin reimportar el módulo.


Configuración del módulo

HermesModule.forRoot(options)

| Opción | Tipo | Obligatorio | Descripción | | ------------ | -------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | connection | string | RedisOptions | Sí | URL (redis://...) u opciones ioredis (host, port, password, db, …). No pases new Redis() — el plugin crea y destruye la conexión. | | storage | string | Sí | Raíz del volumen compartido (ej. /shared en Docker, misma ruta que ve Hermes Server). | | project | string | Sí | Slug del proyecto ([a-zA-Z0-9_-]+). Carpetas en disco y namespace WebSocket por defecto. Una app Nest = un proyecto Hermes. | | prefix | string | No | Prefijo BullMQ (default: hermes). | | isGlobal | boolean | No | Default true. Pon false si quieres importar HermesModule solo en un submódulo. |

HermesModule.forRootAsync(options)

Misma configuración, cargada desde ConfigService u otro provider:

import { Module } from "@nestjs/common";
import { ConfigModule, ConfigService } from "@nestjs/config";
import { HermesModule } from "@leparadoxhd/nestjs-hermes";

@Module({
  imports: [
    ConfigModule.forRoot(),
    HermesModule.forRootAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (config: ConfigService) => ({
        connection: config.getOrThrow<string>("REDIS_URL"),
        storage: config.getOrThrow<string>("SHARED_ROOT"),
        project: config.getOrThrow<string>("HERMES_PROJECT"),
      }),
    }),
  ],
})
export class AppModule {}

Variables de entorno sugeridas

REDIS_URL=redis://localhost:6379
SHARED_ROOT=/shared
HERMES_PROJECT=mi-app

Redis: gestión automática

  1. Al arrancar, el módulo crea una instancia ioredis con maxRetriesPerRequest: null (requerido por BullMQ).
  2. Esa instancia se pasa a las tres colas del HermesClient interno (BullMQ modo shared).
  3. Al apagar Nest (onApplicationShutdown), HermesLifecycle:
    • cierra colas y QueueEvents (hermesClient.close());
    • hace quit() en Redis.

No debes instanciar Redis en tu app para Hermes. Si pasas un objeto Redis en connection, el módulo lanza un error explicativo.

Caso avanzado: reutilizar la misma conexión

Si otro servicio necesita el mismo Redis que Hermes:

import { Inject, Injectable } from "@nestjs/common";
import { HERMES_SHARED_REDIS } from "@leparadoxhd/nestjs-hermes";
import type { Redis } from "ioredis";

@Injectable()
export class OtroServicio {
  constructor(@Inject(HERMES_SHARED_REDIS) private readonly redis: Redis) {}
}

Esa conexión la posee el módulo Hermes; no hagas quit() manual salvo que sepas lo que haces.


Servicios inyectables

HermesWebsocketService

Encola un evento Socket.IO en Hermes Server.

import { Injectable } from "@nestjs/common";
import { HermesWebsocketService } from "@leparadoxhd/nestjs-hermes";

@Injectable()
export class NotificacionesService {
  constructor(private readonly hermesWs: HermesWebsocketService) {}

  async avisar(usuarioId: string) {
    await this.hermesWs.add(
      {
        event: "notification",
        payload: { userId: usuarioId, msg: "Hola" },
        // namespace opcional; si omites → usa `project` del módulo
        // namespace: "otro-namespace",
        user: usuarioHash, // opcional → `user:<hash>`; sin user/group → todo el namespace
      },
      { wait: false },
    );
  }
}

| Campo payload | Descripción | | ------------- | ---------------------------------------------------------------------- | | event | Nombre del evento Socket.IO | | payload | Datos serializables | | namespace? | Namespace Socket.IO (/${namespace}). Default: project de forRoot | | user? | Hash de usuario → room user:<hash> | | group? | Hash o lista de hashes → rooms group:<hash> | | (ninguno) | Sin user ni group → broadcast a todo el namespace |


HermesEmailService

Envía correo vía Gmail configurado en Hermes Server (GMAIL_USER / GMAIL_APP_PASSWORD).

const result = await this.hermesEmail.send(
  {
    to: "[email protected]",
    subject: "Asunto",
    html: "<p>Cuerpo</p>",
    text: "Texto plano opcional",
    replyTo: "[email protected]",
  },
  { wait: true },
);

if (result.waited && result.success) {
  console.log(result.data.messageId);
}

Requiere html o text (al menos uno).


HermesPushService

Notificación móvil vía Home Assistant (HA_URL, HA_TOKEN, HA_NOTIFY_DEVICE en el servidor Hermes).

await this.hermesPush.notify(
  { title: "Aviso", message: "Proceso listo" },
  { wait: false },
);

Si HA no está configurado, el job termina con { sent: false, skipped: true }. Errores de red/HA no fallan el job.


HermesImageService

Procesa una imagen en el worker Sharp de Hermes. No prepares carpetas ni job.json: solo rutas en tu servidor y operaciones.

import { Injectable } from "@nestjs/common";
import { HermesImageService } from "@leparadoxhd/nestjs-hermes";

@Injectable()
export class AvatarService {
  constructor(private readonly hermesImage: HermesImageService) {}

  async generarThumbnail(inputPath: string, destinoWeb: string) {
    return this.hermesImage.process(
      {
        input: inputPath,
        // o: { folder: "/tmp/uploads", file: "foto.jpg" },
        operations: {
          resize: { width: 400, fit: "inside", withoutEnlargement: true },
          format: "webp",
          quality: 80,
          stripMetadata: true,
        },
        output: destinoWeb, // opcional — ver tabla abajo
      },
      { wait: true, waitTimeout: 120_000 },
    );
  }
}

input / output

type ImageInput =
  | string // ruta absoluta o relativa en el host de tu app
  | { folder: string; file: string };
  • En storage Hermes los nombres son siempre UUID (el client copia tu archivo ahí).
  • El worker escribe el resultado en storage/<project>/<jobId>/output/<uuid>.<ext>.
  • Si declaras output en el payload, tras un job exitoso el client copia del volumen compartido a tu ruta (copiedTo en la respuesta). El worker nunca escribe directo en tu filesystem.

| output en payload | Tras wait: true y éxito | | ------------------- | ------------------------------------------------------ | | No | Usa result.data.storagePath en el volumen compartido | | Sí | storagePath + copiedTo (ruta en tu host) |

ImageOperations (contrato worker)

interface ImageOperations {
  format?: "jpeg" | "jpg" | "png" | "webp" | "avif" | "gif" | "tiff";
  quality?: number;
  resize?: {
    width?: number;
    height?: number;
    fit?: "cover" | "contain" | "inside" | "outside" | "fill";
    withoutEnlargement?: boolean;
  };
  shrink?: { quality?: number; effort?: number };
  rotate?: number;
  flip?: boolean;
  flop?: boolean;
  grayscale?: boolean;
  blur?: number;
  sharpen?: number | { sigma?: number };
  trim?: boolean | { threshold?: number };
  stripMetadata?: boolean;
  whiteBackground?: {
    transparentUntil?: number;
    opaqueFrom?: number;
  };
}

Ejemplo con fondo blanco → transparencia (útil en miniaturas de comida):

await this.hermesImage.process(
  {
    input: "/data/foto.jpg",
    output: "/static/thumb.webp",
    operations: {
      whiteBackground: { transparentUntil: 18, opaqueFrom: 55 },
      resize: { width: 100, fit: "inside", withoutEnlargement: true },
      format: "webp",
      quality: 80,
      stripMetadata: true,
    },
  },
  { wait: true },
);

Opciones de envío: wait

Segundo argumento común en add, send y process:

interface HermesSendOptions {
  wait?: boolean; // default: false
  waitTimeout?: number; // ms cuando wait=true; default: 120_000
}

Sin wait (fire-and-forget)

const ref = await this.hermesImage.process(payload);
// ref.jobId — string
// ref.waited === false

Con wait: true

Bloquea hasta que el worker termine (o falle por timeout):

type HermesSendResult<TData> =
  | { jobId: string; waited: false }
  | { jobId: string; waited: true; success: true; data: TData }
  | {
      jobId: string;
      waited: true;
      success: false;
      error: { message: string; name?: string };
    };

Tipos de data según cola:

| Servicio | TData (éxito) | | ---------------------------- | ------------------------------------------------------------------- | | HermesWebsocketService.add | { namespace?, event, payload, user?, group? } | | HermesEmailService | { messageId } | | HermesPushService | { sent, skipped?, error? } | | HermesImageService | { storagePath, copiedTo?, width, height, size, format, filename } |

Siempre comprueba result.waited y result.success antes de leer data.


Flujo de una imagen (resumen)

sequenceDiagram
  participant App as Tu servicio Nest
  participant Plugin as nestjs-hermes
  participant Disk as SHARED_ROOT
  participant Redis as Redis
  participant Worker as Hermes Server

  App->>Plugin: process(input, operations, wait?)
  Plugin->>Disk: jobId UUID + input + job.json
  Plugin->>Redis: add process jobId
  Worker->>Disk: lee input, escribe output UUID
  Worker->>Redis: completed + returnvalue
  alt wait true
    Plugin->>App: HermesSendResult con data
    opt output en payload
      Plugin->>Disk: copia output → ruta host
    end
  else wait false
    Plugin->>App: solo jobId
  end

Política de colas (no configurable)

Reintentos y limpieza los define Hermes en el client (no los sobrescribas en tu app):

  • attempts: 3
  • backoff: { type: "exponential", delay: 1000 }
  • removeOnComplete: true
  • removeOnFail: { age: 30 días }

Colas BullMQ: prefijo hermes, nombres websocket, email, image.


Tipos e imports

Todo lo del contrato se puede importar desde @leparadoxhd/nestjs-hermes:

import {
  HermesModule,
  HermesImageService,
  HermesEmailService,
  HermesWebsocketService,
  type HermesModuleOptions,
  type HermesSendOptions,
  type HermesSendResult,
  type ImageProcessPayload,
  type ImageOperations,
  type ImageInput,
  type EmailSendJob,
} from "@leparadoxhd/nestjs-hermes";

Checklist de despliegue

  1. Hermes Server corriendo con acceso a Redis y SHARED_ROOT.
  2. Tu app Nest con storage apuntando al mismo volumen (bind mount / NFS).
  3. project alineado con el slug que esperas en Socket.IO y en rutas de disco.
  4. Redis accesible desde ambos (misma URL que en Hermes).
  5. Versión de @leparadoxhd/nestjs-hermes compatible con la del servidor (misma línea que @leparadoxhd/hermes-client publicada).

Errores frecuentes

| Síntoma | Causa probable | | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | | Unsupported URL Type "workspace:" | Versión antigua de nestjs-hermes en npm; usa ≥ 1.0.1 con dependencia semver en hermes-client. | | ERR_PACKAGE_PATH_NOT_EXPORTED | Versión ESM-only antigua; usa ≥ 1.0.2 (build CommonJS). | | UnknownDependenciesException + HERMES_MODULE_OPTIONS | Versión sin fix DI async; usa ≥ 1.0.3. | | Job imagen falla / input not found | storage no compartido o archivo no existe antes de process(). | | WebSocket no llega | namespace distinto al del cliente Socket.IO; por defecto es project. | | Timeout con wait: true | Subir waitTimeout o optimizar operaciones Sharp. |


Qué no debes hacer

  • Pasar new Redis() en connection (el plugin lo gestiona).
  • Crear manualmente SHARED_ROOT/<project>/<jobId>/ ni job.json.
  • Usar @InjectQueue de BullMQ para Hermes (este módulo no expone esas colas).
  • Mezclar varios project en una misma app (configura un project en forRoot).
  • Asumir que el worker escribe en output de tu host (solo el client copia tras wait).

Relacionados


Versión

Publica @leparadoxhd/nestjs-hermes con la misma versión que @leparadoxhd/hermes-client cuando cambie el contrato de jobs o ImageOperations.